优化Oracle SQL中的慢相关查询

时间:2015-02-24 11:47:49

标签: sql oracle correlated-subquery

我提出了一个有效的查询,但在我看来有点慢。当我将输出抑制为10行时,执行查询需要13分钟。这是查询,从一些东西中删除:

SELECT
    (SELECT ANSWER
        FROM (
            SELECT to_number(fiit.ANSWER, '999') ANSWER,
                    foin.CLIENT_ID id,
                    foin.STARTDATE start_date,
                    row_number() over(PARTITION BY foin.CLIENT_ID ORDER BY foin.FORM_ID ASC) rnk
                FROM forms_filled foin, forms_items_filled fiit, treatment trtm
                WHERE foin.FORM_ID = fiit.FORM_ID
                AND foin.CLIENT_ID = trtm.CLIENT_ID
                AND fiit.FORM_NUMBER = 607
                AND fiit.FORM_ITEM_NUMBER = 3779
                AND length(fiit.ANSWER) >= 1
                AND trtm.TREATMENTCODE = 'K'
                AND trtm.ENDDATE BETWEEN TRUNC(to_date('01/01/2014', 'dd/mm/yyyy'), 'DDD') AND TRUNC(to_date('31/12/2014', 'dd/mm/yyyy'), 'DDD')
                AND foin.STARTDATE BETWEEN trtm.STARTDATUM AND NVL(trtm.ENDDATE, to_date('01/01/9999', 'dd/mm/yyyy'))
                ) inn
    WHERE rnk = 1
    AND inn.id = client.CLIENT_ID
    ) form1
FROM treatment trtm, CLIENT client
WHERE trtm.TREATMENTCODE = 'K'
AND client.CLIENT_ID = trtm.CLIENT_ID
AND trtm.ENDDATE BETWEEN TRUNC(to_date('01/01/2014', 'dd/mm/yyyy'), 'DDD') AND TRUNC(to_date('31/12/2014', 'dd/mm/yyyy'), 'DDD')

外部查询导致175个客户具有特定的治疗代码并且在2014年有治疗结束日期。现在,对于这些客户中的每一个,检索到许多其他数据(如姓名,年龄,治疗时间),这些不相关我现在离开了然后有大约30个类似的子查询,从表单中检索答案。我使用了相关查询,因为要从这些表单中检索答案,必须知道客户端ID。如果这是子查询唯一需要查找数据的东西,那就不会有问题,但还有另外一个要求:检索到的表格必须在治疗期内填写,因为我找不到办法将此数据从外部查询推送到子子查询,我在子查询中再次查询,这导致速度慢。

拥有子查询和子查询的原因是因为必须找到表单中的第N个排名答案。在我的代码的先前版本中,我没有subsubquery的where子句中的treatmentcode,treatment start和enddate要求。这导致子查询得出例如4个结果被排名1,2,3,4但不一定是在治疗期间形成的形式,这是错误的。

添加这些行:

AND trtm.TREATMENTCODE = 'K'
AND trtm.ENDDATE BETWEEN TRUNC(to_date('01/01/2014', 'dd/mm/yyyy'), 'DDD')
AND TRUNC(to_date('31/12/2014', 'dd/mm/yyyy'), 'DDD')
AND foin.STARTDATE BETWEEN trtm.STARTDATUM AND NVL(trtm.ENDDATE, to_date('01/01/9999', 'dd/mm/yyyy'))

导致查询正确,之前它不完全正确。它们还导致查询需要几个小时,而不是大约需要大约40秒。

我现在的问题是,如何重写此查询以使其更快?我将Oracle 11.2.40与Toad Data Point 3.5结合使用,但遗憾的是我无法看到解释计划。

2 个答案:

答案 0 :(得分:2)

如果使用keep关键字获取第一个值,则可以省略嵌套的子查询。反过来,这允许您使用与外部查询相关的查询,因此您不必重新计算所有行的结果以获取给定行的值。

查询看起来像:

SELECT (SELECT max(to_number(fiit.ANSWER, '999')) keep (dense_rank first order by foin.FORM_ID ASC)
        FROM forms_filled foin JOIN
             forms_items_filled fiit
             ON foin.FORM_ID = fiit.FORM_ID
        WHERE foin.CLIENT_ID = trtm.CLIENT_ID AND
              fiit.FORM_NUMBER = 607
              fiit.FORM_ITEM_NUMBER = 3779 AND
              length(fiit.ANSWER) >= 1 AND
              foin.STARTDATE BETWEEN trtm.STARTDATUM AND NVL(trtm.ENDDATE, DATE '1999-01-01')
       )

我还建议您使用现代显式join语法和date关键字来表示日期常量。

答案 1 :(得分:1)

你在这里有很多多余的结构,比如TRUNC(to_date('31/12/2014', 'dd/mm/yyyy'), 'DDD')。您正在呼叫trunc说"剥离任何时间组件"然后传递一个没有时间组件的构造日期。只需说出date '2014-01-01'并完成它。

至于使用日期范围,如果您想选择2014年的日期,最好的方法是比较如下:myDate >= date '2014-01-01' and myDate < date '2015-01-01'。这样您就不必担心myDate有时间组件以及时间值可能是多少。保存between数据类型,使用您知道的谨慎值或日期已包含在所需的谨慎组件中。

这些建议都不能解决您的特定问题。但要养成写作&#34; skinny&#34;首先是代码,如果问题运行得太慢,它将简化您对问题的搜索。

一个可能会加快查询速度的主要建议,但即使不是,也会大大简化它,从而提高可维护性,就是从选择列表中删除子查询。

一般来说,对于复杂的查询,不要试图一次性写出整个内容。选择一个表(在您的情况下处理)并选择您知道您将需要的数据。检查结果。如果你还没有了解它。确保它完整准确。

select  t.CLIENT_ID, t.TREATMENTCODE, t.ENDDATE
from    treatment t
where   t.TREATMENTCODE = 'K'
    and t.ENDDATE >= date '2014-01-01'
    and t.ENDDATE  < date '2015-01-01';

现在加入下一个表格,将选择列表添加到您希望从该表格中查看的数据,从第一个数据中删除您满意的数据,否则不需要。

select  t.CLIENT_ID, c.CLIENT_ID
from    treatment t
join    client    c
    on  c.CLIENT_ID = t.CLIENT_ID
where   t.TREATMENTCODE = 'K'
    and t.ENDDATE >= date '2014-01-01'
    and t.ENDDATE  < date '2015-01-01';

在选择列表中添加所需的任何字段,以验证您是否获得了正确的结果(对于您目前指定的条件)。对每个其他表重复一次,一次添加一个表,直到得到最终结果。这样,如果您突然开始得到错误的结果,您就会知道哪个表启动了问题。

您的最终结果集可能包含许多您不需要的行。只要它包含您 所需的所有行,那就没问题。将过滤保存为最后一个,因为您希望能够查看查询生成的所有数据。当您知道数据包含您需要的所有内容时,最后一步是过滤掉不需要的结果,直到您只拥有所需内容。但是能够看到所有数据可以向您展示执行过滤的许多方法,如果您尽早过滤数据,这些方法可能并不明显。

我没有任何测试数据,所以我无法在下面测试我的候选人。但是,它应该相当接近,除非我完全错过了某些东西(一种明显的可能性)。如果没有别的,也许它可以指向你一个解决方案。

SELECT  c.CLIENT_ID, to_number( fif.ANSWER, '999' ) form1
FROM    treatment   t
join    CLIENT      c
    on  c.CLIENT_ID     = t.CLIENT_ID
join    forms_filled ff
    on  ff.CLIENT_ID    = c.CLIENT_ID
join    forms_items_filled fif
    on  fif.FORM_ID     = ff.FORM_ID
WHERE   t.TREATMENTCODE = 'K'
    and fif.FORM_NUMBER = 607
    AND fif.FORM_ITEM_NUMBER = 3779
    AND length( fif.ANSWER ) >= 1
    AND t.ENDDATE >= date '2014-01-01'
    AND t.ENDDATE  < date '2015-01-01'
    AND ff.STARTDATE BETWEEN t.STARTDATUM AND NVL(t.ENDDATE, date '9999-12-31');

另一个建议:当你有一个像end_date这样的字段时,第一个冲动是使用NULL作为&#34的指示符;还没有定义结束日期。&#34;尝试将其设置为NOT NULL并使用默认的最大日期值date '9999-12-31'。这意味着同样的事情,并通过摆脱nvl或其他处理NULL值的方式的需要来简化比较。

修改:糟糕。我已经移动了窗口函数以使其不受影响,因为我只是在部分结果之后。剪切/粘贴代码时包含它。

哦,好吧,也可以在最终答案中加入。

with
Partial( CLIENT_ID, form1, rnk )as(
    SELECT  c.CLIENT_ID, to_number( fif.ANSWER, '999' ) form1,
            row_number() over(PARTITION BY ff.CLIENT_ID ORDER BY ff.FORM_ID ASC) rnk
    FROM    treatment   t
    join    CLIENT      c
        on  c.CLIENT_ID     = t.CLIENT_ID
    join    forms_filled ff
        on  ff.CLIENT_ID    = c.CLIENT_ID
    join    forms_items_filled fif
        on  fif.FORM_ID     = ff.FORM_ID
    WHERE   t.TREATMENTCODE = 'K'
        and fif.FORM_NUMBER = 607
        AND fif.FORM_ITEM_NUMBER = 3779
        AND fif.ANSWER is not null
        AND t.ENDDATE >= date '2014-01-01'
        AND t.ENDDATE  < date '2015-01-01'
        AND ff.STARTDATE BETWEEN t.STARTDATUM AND NVL(t.ENDDATE, date '9999-12-31')
)
select  CLIENT_ID, form1
from    Partial
where   rnk = 1;

假设这让你非常接近,如果你看一下这个和原版之间的执行计划,你会看到一个显着的改进。

另一个变化。您正在测试一个字符串,以确保它至少有一个字符。在Oracle中,不需要将空字符串视为NULL。只需检查NOT NULL。