在数据库管理和开发过程中,面对海量数据集时,如何高效地执行复杂的查询操作一直是开发者关注的核心问题之一。关联子查询(Correlated Subquery)是指子查询中引用了外部查询的列,它的特别之处在于,其本身是不完整的,它的闭包中包含一些外层查询提供的参数。所以关联子查询的执行顺序为先执行外层查询,然后对所有通过过滤条件的记录执行内层查询。关联子查询作为实现基于主查询条件动态过滤逻辑的一种常见手段,虽然直观易用,但在处理大规模数据时往往面临性能挑战。未经过优化的关联子查询可能导致重复计算和不必要的资源消耗,成为系统性能的瓶颈。
在《关联子查询调优系列 | 如何使用通用表表达式(CTE)》一文中,我们提到了通过 CTE 提升查询可读性和执行效率的方法,介绍了如何将嵌套的子查询逻辑提取为中间结果以避免重复计算。然而,CTE 并非唯一的解决方案,也不是所有场景下的最优选择。为了更全面地应对各类关联子查询带来的性能问题,本文将继续深入该主题,介绍另外两种行之有效的优化策略:使用窗口函数进行复杂分析计算的重构、通过 JOIN 实现高效的表间连接以及替代传统子查询以减少执行次数。
这两种方法各自具有不同的作用机制和适用场景:
使用窗口函数来替代子查询,从而在主查询中执行聚合计算,从而减少了子查询的执行次数,提高了查询性能。
1、使用窗口函数替换子查询中原有的聚合函数,子查询中的相关列作为 Partition Key,新增临时列用于存储结果。
2、进行去关联化,将外部查询和子查询的相关表下推到子查询中,进行 Join,根据相关列是否唯一分为两种情况
3、此时子查询包含了所有共同相关表,因此可以去掉外部查询的相关表,剩余与子查询无关的非相关表,再与子查询经过窗口函数计算过的表进行 Join,从而去除相关查询
窗口函数通常用于执行分析型计算,例如排名、累积和分组比较。窗口函数通常用于类似以下情况的查询,其中需要在结果集中执行分析操作:
使用窗口函数改写关联子查询时,需要注意满足以下前提:
1、检查外部查询:
2、检查子查询:
3、检查聚合函数:
4、使用子查询匹配外部查询,生成一个带有外部查询也包含共同表的临时的查询 Block。 这样做是为了可以复用物化视图构建的函数,也就是使用物化视图将这个临时的 Query block 进行保存,作为中间的结果供后续使用,此时就可以删除这个临时的 Query Block 了。
5、外层在 join table/condition 上包含内层时,内层如果有其他非相关表,必须是无损连接(lesslose join),这样才能确保内层相关表的数据在 Join 过程中不会丢失或增加。 内层相关表可以添加更多的单表限定条件,转换时,需要在窗口聚合过程中借助于像 CASE WHEN 这样的条件判断,以确保数据能够按照原来的语义进行过滤。
原始关联子查询 查找销售金额高于平均销售金额的客户,并返回前 10 位符合条件的客户:
WITH tmp AS (
SLEECT c_customer_id, s.ss_sales_price
FROM customer, store_sales s
WHERE s.ss_customer_sk = c_customer_sk
)
SELECT c_customer_id, ss_sales_price
FROM tmp a
WHERE ss_sales_price > (SELECT 0.2 * AVG(ss_sales_price) FROM tmp b WHERE a.c_customer_id = b.c_customer_id)
LIMIT 10
进入 DBA Service 查看性能如下:该关联子查询用时 1.6m。
使用窗口函数进行改写
WITH tmp AS (
SELECT c.c_customer_id, s.ss_sales_price, AVG(s.ss_sales_price) OVER (PARTITION BY c.c_customer_sk ) AS avg_sales
FROM customer c, store_sales s
WHERE c.c_customer_sk = s.ss_customer_sk
)
SELECT c_customer_id, ss_sales_price
FROM tmp
WHERE ss_sales_price > 0.2 * avg_sales
LIMIT 10
可以发现运行时长为 30.9s,对比使用关联子查询的运行时间上优化了 2 倍。
将关联子查询改写为 INNER JOIN 操作,以避免多次执行子查询。INNER JOIN 会将两个表关联在一起,根据连接条件过滤不匹配的行,从而减少数据的处理量。
通常情况下,当出现以下类型的关联子查询时,考虑将其改写为 JOIN 操作是一个有效的优化手段:
关联子查询 SQL 示例 假设我们有两个表,分别为 store_sales 和 customer,其中 store_sales 表包含销售数据,customer 表包含客户信息。我们想查找购买产品 '98377' 的累计销售金额大于 10000 的客户。
首先给出原始关联子查询 SQL 语句如下:
SELECT c.c_customer_id
FROM customer c
WHERE (
SELECT SUM(ss.ss_ext_sales_price)
FROM store_sales ss
WHERE ss.ss_customer_sk = c.c_customer_sk
) > 500000
AND LEFT(c.c_first_name, 1) = 'M'
ORDER BY c.c_customer_id DESC;
进入 DBAService 界面查看该查询的性能:总耗时为 27s,Shuffle 读为 384.559MB 读速度为 606.76KB/s、Shuffle 写为 523.755MB 写速度为 826.39KB/s。
使用JOIN进行改写
可以将子查询的功能合并到主查询中,使用连接操作,以减少子查询的执行次数:
SELECT c.c_customer_id
FROM customer c
JOIN (
SELECT ss_customer_sk, SUM(ss_ext_sales_price) AS total_sales
FROM store_sales
GROUP BY ss_customer_sk
HAVING total_sales > 10000
) AS sales_summary
ON CONCAT(c.c_customer_sk, '') = CONCAT(sales_summary.ss_customer_sk, '')
ORDER BY c.c_customer_id
LIMIT 10;
使用连接(JOIN)来代替子查询的过滤条件,经过改写之后的 SQL 语句的执行时间为 7.2s,对于原始关联子查询语句优化了约 2.8 倍。另外 shuffle 读速度为 1.81MB/s,shuffle 写速度为 2.45MB/s,较原始关联子查询均加快了 2 倍。
在数据库管理和开发过程中,面对海量数据集时,如何高效地执行复杂的查询操作一直是开发者关注的核心问题之一。关联子查询(Correlated Subquery)是指子查询中引用了外部查询的列,它的特别之处在于,其本身是不完整的,它的闭包中包含一些外层查询提供的参数。所以关联子查询的执行顺序为先执行外层查询,然后对所有通过过滤条件的记录执行内层查询。关联子查询作为实现基于主查询条件动态过滤逻辑的一种常见手段,虽然直观易用,但在处理大规模数据时往往面临性能挑战。未经过优化的关联子查询可能导致重复计算和不必要的资源消耗,成为系统性能的瓶颈。
在《关联子查询调优系列 | 如何使用通用表表达式(CTE)》一文中,我们提到了通过 CTE 提升查询可读性和执行效率的方法,介绍了如何将嵌套的子查询逻辑提取为中间结果以避免重复计算。然而,CTE 并非唯一的解决方案,也不是所有场景下的最优选择。为了更全面地应对各类关联子查询带来的性能问题,本文将继续深入该主题,介绍另外两种行之有效的优化策略:使用窗口函数进行复杂分析计算的重构、通过 JOIN 实现高效的表间连接以及替代传统子查询以减少执行次数。
这两种方法各自具有不同的作用机制和适用场景:
使用窗口函数来替代子查询,从而在主查询中执行聚合计算,从而减少了子查询的执行次数,提高了查询性能。
1、使用窗口函数替换子查询中原有的聚合函数,子查询中的相关列作为 Partition Key,新增临时列用于存储结果。
2、进行去关联化,将外部查询和子查询的相关表下推到子查询中,进行 Join,根据相关列是否唯一分为两种情况
3、此时子查询包含了所有共同相关表,因此可以去掉外部查询的相关表,剩余与子查询无关的非相关表,再与子查询经过窗口函数计算过的表进行 Join,从而去除相关查询
窗口函数通常用于执行分析型计算,例如排名、累积和分组比较。窗口函数通常用于类似以下情况的查询,其中需要在结果集中执行分析操作:
使用窗口函数改写关联子查询时,需要注意满足以下前提:
1、检查外部查询:
2、检查子查询:
3、检查聚合函数:
4、使用子查询匹配外部查询,生成一个带有外部查询也包含共同表的临时的查询 Block。 这样做是为了可以复用物化视图构建的函数,也就是使用物化视图将这个临时的 Query block 进行保存,作为中间的结果供后续使用,此时就可以删除这个临时的 Query Block 了。
5、外层在 join table/condition 上包含内层时,内层如果有其他非相关表,必须是无损连接(lesslose join),这样才能确保内层相关表的数据在 Join 过程中不会丢失或增加。 内层相关表可以添加更多的单表限定条件,转换时,需要在窗口聚合过程中借助于像 CASE WHEN 这样的条件判断,以确保数据能够按照原来的语义进行过滤。
原始关联子查询 查找销售金额高于平均销售金额的客户,并返回前 10 位符合条件的客户:
WITH tmp AS (
SLEECT c_customer_id, s.ss_sales_price
FROM customer, store_sales s
WHERE s.ss_customer_sk = c_customer_sk
)
SELECT c_customer_id, ss_sales_price
FROM tmp a
WHERE ss_sales_price > (SELECT 0.2 * AVG(ss_sales_price) FROM tmp b WHERE a.c_customer_id = b.c_customer_id)
LIMIT 10
进入 DBA Service 查看性能如下:该关联子查询用时 1.6m。
使用窗口函数进行改写
WITH tmp AS (
SELECT c.c_customer_id, s.ss_sales_price, AVG(s.ss_sales_price) OVER (PARTITION BY c.c_customer_sk ) AS avg_sales
FROM customer c, store_sales s
WHERE c.c_customer_sk = s.ss_customer_sk
)
SELECT c_customer_id, ss_sales_price
FROM tmp
WHERE ss_sales_price > 0.2 * avg_sales
LIMIT 10
可以发现运行时长为 30.9s,对比使用关联子查询的运行时间上优化了 2 倍。
将关联子查询改写为 INNER JOIN 操作,以避免多次执行子查询。INNER JOIN 会将两个表关联在一起,根据连接条件过滤不匹配的行,从而减少数据的处理量。
通常情况下,当出现以下类型的关联子查询时,考虑将其改写为 JOIN 操作是一个有效的优化手段:
关联子查询 SQL 示例 假设我们有两个表,分别为 store_sales 和 customer,其中 store_sales 表包含销售数据,customer 表包含客户信息。我们想查找购买产品 '98377' 的累计销售金额大于 10000 的客户。
首先给出原始关联子查询 SQL 语句如下:
SELECT c.c_customer_id
FROM customer c
WHERE (
SELECT SUM(ss.ss_ext_sales_price)
FROM store_sales ss
WHERE ss.ss_customer_sk = c.c_customer_sk
) > 500000
AND LEFT(c.c_first_name, 1) = 'M'
ORDER BY c.c_customer_id DESC;
进入 DBAService 界面查看该查询的性能:总耗时为 27s,Shuffle 读为 384.559MB 读速度为 606.76KB/s、Shuffle 写为 523.755MB 写速度为 826.39KB/s。
使用JOIN进行改写
可以将子查询的功能合并到主查询中,使用连接操作,以减少子查询的执行次数:
SELECT c.c_customer_id
FROM customer c
JOIN (
SELECT ss_customer_sk, SUM(ss_ext_sales_price) AS total_sales
FROM store_sales
GROUP BY ss_customer_sk
HAVING total_sales > 10000
) AS sales_summary
ON CONCAT(c.c_customer_sk, '') = CONCAT(sales_summary.ss_customer_sk, '')
ORDER BY c.c_customer_id
LIMIT 10;
使用连接(JOIN)来代替子查询的过滤条件,经过改写之后的 SQL 语句的执行时间为 7.2s,对于原始关联子查询语句优化了约 2.8 倍。另外 shuffle 读速度为 1.81MB/s,shuffle 写速度为 2.45MB/s,较原始关联子查询均加快了 2 倍。