优化 Oracle 中的慢速相关查询 SQL
Optimize slow correlated query in Oracle SQL
我进行了一个有效的查询,但我认为它有点慢。当我将输出抑制为 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个排名的答案。在我以前版本的代码中,子查询的 where 子句中没有处理代码、处理开始和结束日期要求。这导致子查询得出例如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'))
导致查询正确,之前不完全正确的地方。他们还导致查询需要几个小时,而不是 175 行大约 40 秒。
我现在的问题是,如何重写此查询以使其更快?我将 Oracle 11.2.40 与 Toad Data Point 3.5 结合使用,但不幸的是我看不到解释计划。
如果使用keep
关键字获取第一个值,就可以省去嵌套子查询。反过来,这允许您使用与外部查询相关的查询,因此您不必重新计算 all 行的结果来获取给定行的值.
查询如下所示:
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
关键字来表达日期常量。
这里有很多多余的结构,比如 TRUNC(to_date('31/12/2014', 'dd/mm/yyyy'), 'DDD')
。您正在调用 trunc
说 "strip off any time component",然后向其传递一个没有时间组件的构造日期。只需说 date '2014-01-01'
就可以了。
至于使用日期范围,如果您想 select 日期,例如 2014 年,最好的方法是这样比较:myDate >= date '2014-01-01' and myDate < date '2015-01-01'
。这样您就不必担心 myDate
有时间分量以及该时间值可能是多少。保存 between
用于具有离散值或日期的数据类型,您知道这些数据类型已经在您想要的离散组件中。
None 这些建议可以解决您的特定问题。但是养成首先编写 "skinny" 代码的习惯,如果它运行得太慢,它将简化您对问题的搜索。
一个可能会加快查询速度但即使不会显着简化查询从而提高可维护性的主要建议是从 select 列表中删除子查询。
一般来说,对于复杂的查询,不要试图一次写完。 Select a table(针对您的情况的治疗)和 select 您知道自己将需要的数据。检查结果。如果您还不知道,请了解它。确保它完整且准确。
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';
现在将下一个 table 加入其中,将您希望从 table 中看到的数据添加到 select 列表中,从第一个中删除您满意的数据有并且不需要。
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 列表中添加您需要的任何字段,以验证您是否获得了完全正确的结果(针对您目前指定的条件)。对其他每个 table 重复上述操作,一次添加一个,直到获得最终结果。这样,如果您突然开始得到错误的结果,您就会知道是哪个 table 引发了问题。
您的最终结果集可能包含许多您不需要的行。没关系,只要它包含您 do 需要的所有行。将过滤保存到最后,因为您希望能够看到查询生成的所有数据。当您知道数据包含您需要的一切时,最后一步是过滤掉不需要的结果,直到您只拥有您想要的结果。但是能够查看所有数据可以向您展示执行该过滤的多种方法,如果您及早过滤掉数据,这些方法可能并不明显。
我没有任何测试数据,所以我无法在下面测试我的候选人。但是,它应该相当接近,除非我完全错过了某些东西(一种明显的可能性)。如果不出意外,也许它可以为您指明解决方案。
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
作为 "there is no end date yet defined." 的指示符尝试将其设置为 NOT NULL 并使用Max Date 值的默认值 date '9999-12-31'
。这意味着同样的事情,并通过摆脱对 nvl
或其他处理 NULL 值的方式的需要来简化比较。
编辑:糟糕。我已经移动了窗口函数以将其移开,因为我只是在获得部分结果之后。当我 cut/pasted 代码时它被包含在内。
哦,好吧,不妨将其包含在最终答案中。
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。
我进行了一个有效的查询,但我认为它有点慢。当我将输出抑制为 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个排名的答案。在我以前版本的代码中,子查询的 where 子句中没有处理代码、处理开始和结束日期要求。这导致子查询得出例如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'))
导致查询正确,之前不完全正确的地方。他们还导致查询需要几个小时,而不是 175 行大约 40 秒。
我现在的问题是,如何重写此查询以使其更快?我将 Oracle 11.2.40 与 Toad Data Point 3.5 结合使用,但不幸的是我看不到解释计划。
如果使用keep
关键字获取第一个值,就可以省去嵌套子查询。反过来,这允许您使用与外部查询相关的查询,因此您不必重新计算 all 行的结果来获取给定行的值.
查询如下所示:
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
关键字来表达日期常量。
这里有很多多余的结构,比如 TRUNC(to_date('31/12/2014', 'dd/mm/yyyy'), 'DDD')
。您正在调用 trunc
说 "strip off any time component",然后向其传递一个没有时间组件的构造日期。只需说 date '2014-01-01'
就可以了。
至于使用日期范围,如果您想 select 日期,例如 2014 年,最好的方法是这样比较:myDate >= date '2014-01-01' and myDate < date '2015-01-01'
。这样您就不必担心 myDate
有时间分量以及该时间值可能是多少。保存 between
用于具有离散值或日期的数据类型,您知道这些数据类型已经在您想要的离散组件中。
None 这些建议可以解决您的特定问题。但是养成首先编写 "skinny" 代码的习惯,如果它运行得太慢,它将简化您对问题的搜索。
一个可能会加快查询速度但即使不会显着简化查询从而提高可维护性的主要建议是从 select 列表中删除子查询。
一般来说,对于复杂的查询,不要试图一次写完。 Select a table(针对您的情况的治疗)和 select 您知道自己将需要的数据。检查结果。如果您还不知道,请了解它。确保它完整且准确。
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';
现在将下一个 table 加入其中,将您希望从 table 中看到的数据添加到 select 列表中,从第一个中删除您满意的数据有并且不需要。
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 列表中添加您需要的任何字段,以验证您是否获得了完全正确的结果(针对您目前指定的条件)。对其他每个 table 重复上述操作,一次添加一个,直到获得最终结果。这样,如果您突然开始得到错误的结果,您就会知道是哪个 table 引发了问题。
您的最终结果集可能包含许多您不需要的行。没关系,只要它包含您 do 需要的所有行。将过滤保存到最后,因为您希望能够看到查询生成的所有数据。当您知道数据包含您需要的一切时,最后一步是过滤掉不需要的结果,直到您只拥有您想要的结果。但是能够查看所有数据可以向您展示执行该过滤的多种方法,如果您及早过滤掉数据,这些方法可能并不明显。
我没有任何测试数据,所以我无法在下面测试我的候选人。但是,它应该相当接近,除非我完全错过了某些东西(一种明显的可能性)。如果不出意外,也许它可以为您指明解决方案。
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
作为 "there is no end date yet defined." 的指示符尝试将其设置为 NOT NULL 并使用Max Date 值的默认值 date '9999-12-31'
。这意味着同样的事情,并通过摆脱对 nvl
或其他处理 NULL 值的方式的需要来简化比较。
编辑:糟糕。我已经移动了窗口函数以将其移开,因为我只是在获得部分结果之后。当我 cut/pasted 代码时它被包含在内。
哦,好吧,不妨将其包含在最终答案中。
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。