0

0

使用Common Table Expressions (CTE) 简化复杂查询

betcha

betcha

发布时间:2025-09-10 09:52:01

|

980人浏览过

|

来源于php中文网

原创

CTE通过模块化分解复杂查询,提升可读性与维护性,适用于多步骤处理、递归层级遍历等场景,相比子查询更易复用和调试,但性能优化仍依赖索引、过滤和执行计划合理性。

使用common table expressions (cte) 简化复杂查询

使用Common Table Expressions (CTE) 确实是简化复杂SQL查询的一剂良药。它允许我们将一个庞大、难以理解的查询分解成多个逻辑上独立的、命名的子查询,这些子查询可以被后续的CTE或主查询引用。这种模块化的方式极大地提升了查询的可读性、可维护性,甚至在某些特定场景下能帮助我们更清晰地构建逻辑,避免深层嵌套子查询的噩梦。

在我的日常工作中,尤其面对那些需要多步骤数据处理、聚合后再过滤,或者涉及层级结构数据的查询时,CTE几乎成了我的首选工具。它就像是给复杂查询搭建了一个清晰的脚手架,每一步操作都有其明确的名称和职责,让原本纠缠不清的逻辑变得一目了然。

-- 假设我们有一个Orders表和Customers表
-- 目标:找出过去一年中,购买总金额超过所有客户平均购买总金额的客户,
-- 并列出他们最近一笔订单的详情。

-- 没有CTE,你可能会看到这样的查询,或者更复杂的嵌套:
SELECT
    c.CustomerID,
    c.CustomerName,
    o_latest.OrderID,
    o_latest.OrderDate,
    o_latest.OrderTotal
FROM
    Customers c
JOIN
    (SELECT CustomerID, MAX(OrderDate) AS LatestOrderDate
     FROM Orders
     WHERE OrderDate >= DATEADD(year, -1, GETDATE())
     GROUP BY CustomerID) AS latest_orders_summary
ON c.CustomerID = latest_orders_summary.CustomerID
JOIN
    Orders o_latest ON latest_orders_summary.CustomerID = o_latest.CustomerID
                   AND latest_orders_summary.LatestOrderDate = o_latest.OrderDate
WHERE
    c.CustomerID IN (
        SELECT CustomerID
        FROM (
            SELECT CustomerID, SUM(OrderTotal) AS TotalSpent
            FROM Orders
            WHERE OrderDate >= DATEADD(year, -1, GETDATE())
            GROUP BY CustomerID
        ) AS customer_spending
        WHERE TotalSpent > (
            SELECT AVG(TotalSpent)
            FROM (
                SELECT SUM(OrderTotal) AS TotalSpent
                FROM Orders
                WHERE OrderDate >= DATEADD(year, -1, GETDATE())
                GROUP BY CustomerID
            ) AS all_customer_spending_avg
        )
    );

-- 使用CTE,同样的逻辑变得清晰得多:
WITH RecentOrders AS (
    -- 1. 筛选出过去一年的订单
    SELECT
        CustomerID,
        OrderID,
        OrderDate,
        OrderTotal
    FROM
        Orders
    WHERE
        OrderDate >= DATEADD(year, -1, GETDATE())
),
CustomerSpending AS (
    -- 2. 计算每个客户在过去一年的总消费
    SELECT
        CustomerID,
        SUM(OrderTotal) AS TotalSpent,
        MAX(OrderDate) AS LastOrderDate
    FROM
        RecentOrders
    GROUP BY
        CustomerID
),
AverageOverallSpending AS (
    -- 3. 计算所有客户的平均总消费
    SELECT
        AVG(TotalSpent) AS AvgTotalSpent
    FROM
        CustomerSpending
),
HighValueCustomers AS (
    -- 4. 找出总消费超过平均值的客户
    SELECT
        cs.CustomerID,
        cs.TotalSpent,
        cs.LastOrderDate
    FROM
        CustomerSpending cs
    CROSS JOIN
        AverageOverallSpending aos
    WHERE
        cs.TotalSpent > aos.AvgTotalSpent
)
-- 5. 最后,联结回原始订单和客户信息,获取这些高价值客户的最新订单详情
SELECT
    c.CustomerID,
    c.CustomerName,
    ro.OrderID,
    ro.OrderDate,
    ro.OrderTotal
FROM
    Customers c
JOIN
    HighValueCustomers hvc ON c.CustomerID = hvc.CustomerID
JOIN
    RecentOrders ro ON hvc.CustomerID = ro.CustomerID AND hvc.LastOrderDate = ro.OrderDate;

通过上述对比,不难看出CTE如何将一个复杂问题拆解成一系列逻辑步骤。每个CTE都专注于完成一个特定的任务,它的输出可以作为下一个CTE的输入,最终汇聚成我们想要的最终结果。这种“分而治之”的策略,在我看来,是编写高质量、易于理解和维护SQL代码的关键。

CTE与子查询(Subquery)有什么区别

这个问题经常被提起,因为它们在某种程度上都能实现查询的模块化。然而,它们之间存在几个关键的、值得深思的差异。子查询,特别是嵌套子查询,通常被视为查询的一部分,它的结果集在外部查询中被直接使用,并且通常是匿名且一次性的。当子查询层层嵌套时,代码的可读性会急剧下降,调试起来更是令人头疼。

CTE则不同,它通过

WITH
子句明确地命名一个临时结果集。这个命名不仅提升了可读性,更重要的是,一个CTE可以在同一个查询中被多次引用,这在某种程度上实现了代码的复用。想象一下,如果一个复杂的计算结果需要在查询的不同部分被多次使用,使用CTE可以避免重复编写相同的逻辑。此外,CTE的作用域仅限于紧随其后的那个
SELECT
,
INSERT
,
UPDATE
,
DELETE
CREATE VIEW
语句。这意味着它不会像临时表那样在整个会话中持续存在,也不会造成数据库对象的污染。还有一个子查询无法比拟的独特之处,就是CTE能够支持递归查询,这在处理层级结构数据时是不可或缺的利器。

从我个人的经验来看,当查询逻辑需要多于一个步骤,或者某个中间结果需要在查询的不同位置被引用时,CTE几乎总是比子查询更优的选择。它迫使你以一种更结构化、更清晰的方式思考你的数据处理流程。

CTE在递归查询中的应用是怎样的?

递归CTE是Common Table Expressions中最强大也最令人兴奋的特性之一,它让SQL在处理层级数据方面变得异常灵活和高效。例如,组织架构图、物料清单(BOM)、社交网络中的朋友关系链等,这些都属于典型的层级或图结构数据,而递归CTE正是解决这类问题的理想工具。

一个递归CTE通常由两个主要部分组成:

Pixlr
Pixlr

Pixlr是一款2008年推出的在线图片编辑和AI图片处理工具,目前已推出AI 图像生成器、AI 生成填充、AI 删除背景、AI 删除对象和 AI 图像扩展等现代 AI 工具。

下载
  1. 锚定成员(Anchor Member):这是递归的起点,它是一个非递归的查询,用于建立递归的基础。它定义了递归过程中的初始行集。
  2. 递归成员(Recursive Member):这是递归的主体,它引用了CTE自身,并基于锚定成员或上一次递归的结果来生成新的行。每次迭代都会处理上一次迭代产生的新行,直到不再有新的行产生。

这两个部分通过

UNION ALL
连接起来。数据库系统会先执行锚定成员,然后反复执行递归成员,每次都使用前一次迭代的结果作为输入,直到递归成员不再返回任何行。

举个例子,假设我们有一个

Employees
表,其中包含
EmployeeID
EmployeeName
ManagerID
,我们想找出某个员工的所有下属(包括直接和间接下属),并显示他们所在的层级。

WITH EmployeeHierarchy AS (
    -- 锚定成员:找到某个特定员工(例如,EmployeeID = 101 的员工)
    SELECT
        EmployeeID,
        EmployeeName,
        ManagerID,
        0 AS Level -- 初始层级为0
    FROM
        Employees
    WHERE
        EmployeeID = 101 -- 从这个员工开始向下查找

    UNION ALL

    -- 递归成员:找到上一层级员工的直接下属
    SELECT
        e.EmployeeID,
        e.EmployeeName,
        e.ManagerID,
        eh.Level + 1 AS Level -- 层级加1
    FROM
        Employees e
    JOIN
        EmployeeHierarchy eh ON e.ManagerID = eh.EmployeeID -- 联结到CTE自身
)
SELECT
    EmployeeID,
    EmployeeName,
    Level
FROM
    EmployeeHierarchy
ORDER BY
    Level, EmployeeName;

这个例子清晰地展示了递归CTE如何从一个起点出发,一步步地向下遍历层级结构。锚定成员提供了起始点,而递归成员则定义了如何从当前层级扩展到下一层级。这种能力是普通子查询或视图无法实现的,它赋予了SQL处理复杂数据关系以全新的维度。

CTE的性能优化策略有哪些?

谈到CTE的性能,这其实是一个比较微妙的话题。很多人会误以为使用CTE会天然地带来性能提升,但实际上,CTE本身更多是一种逻辑组织和代码可读性的工具,它并不能神奇地改变底层查询的执行计划。数据库优化器在处理CTE时,通常会将其“展开”或“内联”到主查询中,然后像处理普通查询一样进行优化。这意味着,一个编写糟糕的CTE查询,其性能可能和等效的糟糕子查询一样差。

然而,这不代表我们不能通过一些策略来“优化”CTE的性能:

  1. 确保内部查询高效:CTE的性能基础是其内部定义的查询是否高效。如果CTE内部的
    SELECT
    语句涉及大量数据扫描、复杂的联结或聚合,那么无论它是否被封装在CTE中,都会是性能瓶颈。所以,确保CTE内部使用的表有合适的索引,联结条件合理,过滤条件有效,这才是根本。
  2. 避免不必要的复杂性:不要为了使用CTE而使用CTE。如果一个简单的子查询就能完成任务,那么就没有必要将其封装成一个CTE。过度细分CTE有时反而会增加优化器的负担,因为它需要处理更多的逻辑单元。
  3. 理解优化器的行为:在某些数据库系统中(例如SQL Server),CTE默认情况下不会被“物化”(materialized),即不会在内存或磁盘上创建一个临时的物理表来存储CTE的结果。优化器会尝试将其与主查询合并处理。但有时,如果一个CTE非常复杂且被多次引用,数据库可能会选择物化它。了解你的数据库系统如何处理CTE,可以通过
    EXPLAIN
    SHOW PLAN
    等工具查看执行计划,这能帮助你判断CTE是否被有效利用。
  4. 谨慎处理大结果集:如果一个CTE会产生一个非常大的中间结果集,而这个结果集又被后续查询多次引用,那么考虑将其结果先存入一个临时表表变量,有时反而能获得更好的性能。这是因为临时表会被物理写入磁盘或内存,其上的索引可以被后续查询利用,避免了重复计算。
  5. 合理运用过滤:尽可能早地在CTE内部进行数据过滤。减少传递给下一个CTE或主查询的数据量,是提升性能的黄金法则。不要在最终的
    SELECT
    语句中才过滤掉大量中间结果,这会导致不必要的计算。

总的来说,CTE的性能优化更多地是关于如何编写高效的SQL查询,而不是CTE本身有什么特殊的魔法。它提供了一个清晰的框架,让我们能更好地组织和理解复杂逻辑,而一个清晰的逻辑往往是性能优化的第一步。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

673

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

319

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

344

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

1081

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

355

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

671

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

561

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

404

2024.04.29

苹果官网入口直接访问
苹果官网入口直接访问

苹果官网直接访问入口是https://www.apple.com/cn/,该页面具备0.8秒首屏渲染、HTTP/3与Brotli加速、WebP+AVIF双格式图片、免登录浏览全参数等特性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

10

2025.12.24

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 0.6万人学习

Node.js 教程
Node.js 教程

共57课时 | 7.2万人学习

CSS3 教程
CSS3 教程

共18课时 | 4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号