1. CTE 到底是什么
CTE(Common Table Expression,公用表表达式)是SQL中一种强大的临时结果集定义方式。它允许我们在一个查询中定义临时命名的结果集,这个结果集仅在该查询的执行期间存在。简单来说,CTE就是:
先把一小段查询结果,起个名字,临时存起来,后面接着用。
它的标准语法结构如下:
sql复制WITH 临时表名 AS (
查询语句
)
主查询语句
举个实际例子,假设我们有一个电商订单表,需要计算各类商品的运费占比,然后再将这个占比信息关联回主表进行分析:
sql复制WITH log_Share AS (
SELECT
`category`,
SUM(COALESCE(`shipping_fee`,0)) / SUM(COALESCE(`payment_amount`,0)) AS `shipping_ratio`
FROM ecommerce.orders
WHERE `shipping_fee` > 0
GROUP BY `category`
)
SELECT
o.*,
ls.shipping_ratio
FROM ecommerce.orders o
LEFT JOIN log_Share ls
ON o.category = ls.category
1.1 为什么要用 CTE
使用CTE主要解决两个核心问题:
第一,SQL可读性问题
当我们需要执行多步复杂查询时,如果不使用CTE,所有逻辑都会揉在一个庞大的SQL语句中。比如上面的例子,如果不用CTE,我们需要在主查询中重复运费占比的计算逻辑,或者使用子查询,这都会使SQL变得难以阅读和维护。
第二,逻辑清晰度问题
CTE让SQL查询像分步骤编写:
- 先定义中间结果(运费占比计算)
- 再使用这个中间结果进行主查询
这种分步处理的方式,让SQL的逻辑层次更加清晰,就像写程序时先定义变量再使用一样自然。
1.2 CTE的本质特性
CTE有几个重要特性需要特别注意:
-
临时性:CTE只在当前查询执行期间存在,查询结束后自动消失。它不会像CREATE TABLE创建的表格那样持久存储在数据库中。
-
作用域限制:CTE只在定义它的WITH语句之后的查询中可见。不能在同一个WITH子句中定义两个CTE后,让第一个CTE引用第二个CTE。
-
可递归:CTE支持递归查询(使用RECURSIVE关键字),这是它比子查询更强大的地方之一,特别适合处理树形结构数据。
1.3 实际应用场景类比
可以把CTE想象成做数学题时的草稿纸:
- 草稿步骤:先计算中间结果(如全班平均分)
- 正式计算:使用中间结果完成最终计算(找出高于平均分的学生)
这种"先准备,后使用"的思维方式,正是CTE的核心价值所在。它让复杂的SQL查询变得像分步解题一样有条理。
1.4 使用CTE的注意事项
-
命名要有意义:CTE的名称应该清晰表达其内容,如log_Share、user_stats等,避免使用t1、t2这类无意义的名称。
-
避免过度嵌套:虽然CTE可以提高可读性,但过多的CTE也会让SQL变得冗长。通常3-5个CTE是比较合理的范围。
-
性能考量:在某些数据库中,CTE可能被优化为临时表,而在另一些数据库中可能只是逻辑上的视图。对于大数据量查询,需要考虑CTE对性能的影响。
经验分享:在实际项目中,我通常会先用CTE写出清晰易懂的查询,如果性能有问题再考虑优化。可读性优先于过早优化是一个很好的原则。
2. CURRENT_DATE与CURDATE()的深入解析
在MySQL中处理日期时,我们经常会遇到CURRENT_DATE和CURDATE()这两个函数。它们的功能相同,都返回当前日期,但在使用风格和兼容性上有一些差异值得探讨。
2.1 基本功能对比
这两个函数在MySQL中的表现完全一致:
sql复制SELECT CURRENT_DATE; -- 返回格式:YYYY-MM-DD
SELECT CURDATE(); -- 返回格式:YYYY-MM-DD
它们都会返回当前日期,不包含时间部分,格式为标准的'YYYY-MM-DD'。
2.2 风格差异分析
CURDATE()的特点:
- 是MySQL特有的函数语法
- 使用函数调用形式,带有括号
- 在MySQL社区中使用更广泛
- 对于熟悉MySQL的开发人员更直观
CURRENT_DATE的特点:
- 是标准SQL语法
- 可以不带括号使用(在MySQL中)
- 在其他数据库系统中兼容性更好
- 看起来更像关键字而非函数
2.3 跨数据库兼容性考虑
如果你编写的SQL需要在不同数据库系统中运行,建议使用CURRENT_DATE,因为:
- PostgreSQL、Oracle、SQL Server等主流数据库都支持
- 是SQL标准的一部分
- 行为在不同数据库中更一致
而在纯MySQL环境中,两者可以随意选择,取决于团队习惯。
2.4 实际使用建议
- 团队一致性:选择一种风格并在整个项目中保持一致
- 新项目推荐:建议使用CURRENT_DATE以获得更好的兼容性
- 括号使用:即使CURRENT_DATE可以不使用括号,也建议加上以保持一致性
踩坑记录:曾经在一个从MySQL迁移到PostgreSQL的项目中,因为使用了大量CURDATE()导致迁移时需要修改大量SQL。如果一开始就使用CURRENT_DATE可以避免这个问题。
3. SQL注释与缩进的最佳实践
良好的注释和缩进习惯可以显著提高SQL代码的可读性和可维护性。与Python等语言不同,SQL对缩进没有语法要求,但适当的格式化仍然非常重要。
3.1 注释规范详解
单行注释
SQL支持两种单行注释风格:
sql复制-- 这是标准的SQL单行注释(推荐)
# 这是MySQL扩展的单行注释(不推荐)
推荐使用--(双短划线加空格)的原因:
- 是SQL标准规定的注释方式
- 所有主流数据库都支持
- 比#更易读,因为#可能与其他符号混淆
多行注释
sql复制/*
* 这是多行注释
* 可以跨越多行
* 适合大段的解释说明
*/
多行注释适合:
- 文件头部的版权和作者信息
- 复杂逻辑的详细说明
- 临时禁用大段代码
3.2 缩进与格式化指南
虽然SQL不强制要求缩进,但良好的格式化习惯可以大大提高代码可读性:
基本原则
- 子查询应该缩进
- SELECT列表中的每个字段应该单独一行
- JOIN条件应该清晰对齐
- 复杂条件应该合理换行
好的例子:
sql复制SELECT
u.user_id,
u.username,
COUNT(o.order_id) AS order_count
FROM
users u
LEFT JOIN
orders o ON u.user_id = o.user_id
WHERE
u.register_date > '2023-01-01'
AND o.status = 'completed'
GROUP BY
u.user_id, u.username
HAVING
COUNT(o.order_id) > 5
ORDER BY
order_count DESC
格式化工具推荐:
- SQLFormat
- Poor SQL
- 各种IDE内置的SQL格式化功能
实用技巧:在团队项目中,制定统一的SQL风格指南并使用自动化格式化工具,可以避免很多格式争议,提高协作效率。
4. DATE_SUB函数深度解析
DATE_SUB是MySQL中用于日期减法运算的重要函数,它允许我们从指定日期中减去一个时间间隔。理解这个函数的细节对于处理各种日期计算场景非常关键。
4.1 函数语法结构
基本语法:
sql复制DATE_SUB(start_date, INTERVAL expr unit)
参数说明:
- start_date:基准日期,可以是日期/时间表达式或列名
- expr:要减去的时间数量
- unit:时间单位(DAY, MONTH, YEAR等)
4.2 实际应用示例
基本用法:
sql复制-- 获取30天前的日期
SELECT DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY);
-- 获取3个月前的日期
SELECT DATE_SUB('2023-06-15', INTERVAL 3 MONTH);
复杂场景:
sql复制-- 计算用户最后一次登录是否在90天前
SELECT
user_id,
last_login_date,
CASE WHEN last_login_date < DATE_SUB(CURRENT_DATE, INTERVAL 90 DAY)
THEN 'Inactive'
ELSE 'Active'
END AS status
FROM users;
4.3 支持的时间单位
DATE_SUB支持丰富的时间单位:
- MICROSECOND
- SECOND
- MINUTE
- HOUR
- DAY
- WEEK
- MONTH
- QUARTER
- YEAR
4.4 边界情况处理
使用DATE_SUB时需要注意的特殊情况:
- 闰年处理:减去年份时会自动处理闰年
- 月末日期:减去月份时如果目标月份没有足够的天数,会返回该月最后一天
- 无效日期:如果结果是无效日期(如2023-02-30),会返回NULL
4.5 性能优化建议
-
在WHERE条件中使用DATE_SUB时,注意它可能使索引失效:
sql复制-- 不推荐(索引可能失效) SELECT * FROM orders WHERE DATE_SUB(order_date, INTERVAL 7 DAY) > '2023-01-01'; -- 推荐(更好的索引使用) SELECT * FROM orders WHERE order_date > DATE_ADD('2023-01-01', INTERVAL 7 DAY); -
对于固定时间间隔的查询,考虑使用预计算的日期值而非函数调用
经验之谈:在处理报表查询时,我经常使用DATE_SUB来计算同比/环比时间段。例如,获取去年同期的数据:
DATE_SUB(CURRENT_DATE, INTERVAL 1 YEAR)。这种用法既直观又高效。