返回

SQL查询优化:子查询 vs CTE (百万级数据实战)

mysql

SQL 查询优化:子查询 vs. CTE (针对特定场景)

这个问题比较的是在一个拥有660万行,14列的数据集中进行查询优化时,子查询和 CTE 哪种方法更好。原问题给出了两种查询语句, 并想知道在效率、清晰度和可维护性方面,哪个更好,我们来好好说道说道。

问题及目标

原问题的目标是从 service_requests_clean_v3 表中,找出每个季节请求数量排名前三的服务。 给出了两种 SQL 查询方案:使用子查询和使用 CTE(Common Table Expression,通用表表达式)。

数据表的部分示例如下:

service_request_id requested_date ... service_name ... season
... ... ... ... ... ...

原始查询及性能

查询 1: 使用子查询

SELECT season, service_name, request_count
FROM (
    SELECT season, service_name, COUNT(*) AS request_count,
           ROW_NUMBER() OVER (PARTITION BY season ORDER BY COUNT(*) DESC) AS row_num
    FROM service_requests_clean_v3
    GROUP BY season, service_name
) AS ranked_requests
WHERE row_num IN (1,2,3)
ORDER BY season;

执行时间:约 1 分 33 秒

查询 2: 使用 CTE

WITH ranked_requests AS (
    SELECT season, service_name, COUNT(*) AS request_count
    FROM service_requests_clean_v3
    GROUP BY season, service_name
),
top_requests AS (
    SELECT season, service_name, request_count,
           ROW_NUMBER() OVER (PARTITION BY season ORDER BY request_count DESC) AS row_num
    FROM ranked_requests
)
SELECT season, service_name, request_count
FROM top_requests
WHERE row_num IN (1,2,3)
ORDER BY season;

执行时间:约 1 分 24 秒

单从执行时间上看,似乎 CTE 稍快一些,但差别不明显。 咱不能只看表面,得深入分析分析。

问题分析

两种查询方式都能得到正确结果,但在效率和可读性上可能存在差异。 产生效率差异的主要原因在于数据库如何处理查询以及内部优化策略。 对于这两种方法,可能的原因:

  1. 查询执行计划: 数据库对子查询和 CTE 的处理方式可能不同, 产生的执行计划也会有区别。
  2. 物化(Materialization): 一些数据库在处理 CTE 时可能会将其结果物化(即临时存储),这在某些情况下能提升性能,但也可能增加开销。子查询是否被优化, 是否被"扁平化"(flatten),也会对最终结果有影响.
  3. 统计信息: 数据库使用统计信息来优化查询。如果统计信息过时或不准确, 可能会导致优化器选择了较差的执行计划。
  4. 数据分布和索引: 数据的实际情况,例如重复值的多少、有没有合适的索引,都会大大影响效率.

解决方案及优化

既然说到优化了,咱就不能只停留在表面,得给出一些实用的建议。

  1. 查看执行计划

    这是优化的第一步! 通过查看执行计划, 能了解数据库实际上是如何执行查询的,从而找出瓶颈。

    • MySQL/MariaDB:

      EXPLAIN SELECT ...; -- 把你的查询语句放在这里
      

      关注 type、possible_keys、key、rows 和 Extra 列, 看看有没有全表扫描(type 为 ALL)、有没有用到索引、扫描了多少行等信息。

    • PostgreSQL:

      EXPLAIN ANALYZE SELECT ...;  -- 加了 ANALYZE 会实际执行查询并显示更详细的信息
      

    通过执行计划能知道,查询慢在了哪。 是在分组计数慢?还是在排序取 Top N 慢?

  2. 添加索引

    在这个案例中, 原作者后续自己添加了一个联合索引, 就大幅度提高了 CTE 的性能, 可见索引的重要性. 原始表上可能没有针对 seasonservice_name 的索引。 根据查询的特点,创建合适的索引是优化性能的关键。

    CREATE INDEX idx_season_service_name ON service_requests_clean_v3 (season, service_name);
    

    这条语句创建了一个复合索引, 包含了seasonservice_name两列, 可以加速GROUP BYORDER BY操作. 对于ROW_NUMBER() OVER (PARTITION BY season ORDER BY COUNT(*) DESC)这个窗口函数,也有帮助.

    安全提示 : 虽然索引通常对读操作有好处,但是会降低写操作(INSERT, UPDATE, DELETE)的速度. 建立索引要权衡利弊。 索引也不是越多越好, 无用或重复的索引会浪费空间,降低性能.

  3. 优化 CTE/子查询写法 (通常用处不大,但有时也值得试试)

    虽然本次查询里,这两种写法性能差距不大, 但是了解他们内部原理总归是有好处的。

    • CTE: 在本例中,使用 CTE 并没有显著提升性能,但它提高了查询的可读性,更容易让人理解。 如果数据量巨大, 有些数据库会对CTE做"物化", 反复使用的时候不用重复计算, 可以有性能优势。

    • 子查询: 原查询中子查询被用作派生表。 有时候,数据库优化器能将子查询"扁平化",将其与外部查询合并, 进行整体优化. 有时候又不能优化,这样反而效率不高。

    改写子查询(如果数据库不能很好优化, 可以试试这样):

    一种可能的改写方法(不一定能变快,只是展示一个思路):

    SELECT
       s.season,
       s.service_name,
       s.request_count
    FROM
       (
           SELECT
               season,
               service_name,
               COUNT(*) AS request_count,
           ROW_NUMBER() OVER (PARTITION BY season ORDER BY COUNT(*) DESC) as rn
       FROM
               service_requests_clean_v3
       GROUP BY
           season,
           service_name
     ) s
    where s.rn <=3
    ORDER BY
      s.season;
    

    这里的关键是把筛选row_num的条件,直接放到内部,不用额外的AS. 这样做是否变快,要具体看执行计划。

  4. 其他优化技巧(进阶)

    • 数据类型优化: 确保使用了合适的数据类型。 例如, service_name 如果是较短的字符串,使用 VARCHAR 而不是 TEXT 能节省空间。如果season只有几个固定值,用ENUM可能比VARCHAR好。

    • 统计信息更新: 如果数据经常变动, 要定期更新统计信息。

      • MySQL/MariaDB:

        ANALYZE TABLE service_requests_clean_v3;
        
      • PostgreSQL:

        ANALYZE service_requests_clean_v3;
        
      • 分批处理(对于极大量数据)
        如果要处理的数据量非常大, 可以考虑分批处理, 一次只处理一部分season。

  5. 选择CTE 还是子查询?(最终建议)

    根据原问题的更新, 创建索引后, CTE 的查询速度显著提升。 再加上 CTE 通常具有更好的可读性, 所以在这种情况下, 推荐使用 CTE

    总结一下判断标准:

    • 可读性: CTE 通常更好。
    • 性能: 需要根据实际情况测试, 但通常在有合适索引的情况下,两者差别不大,甚至 CTE 更快。
    • 可维护性: 如果逻辑复杂, CTE更容易维护.

    因此,除非有非常特殊的原因,建议选择 CTE。 并且记得: 无论用哪种, 都要记得查看执行计划, 用索引优化。