执行顺序
SQL查询的逻辑执行顺序是:FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY
CTE 公用表达式
WITH ranked_items AS
是 SQL 中的 公用表表达式(Common Table Expression,CTE),用于定义一个临时命名的结果集,供后续查询重复引用。其核心作用是 简化复杂查询的逻辑,提高代码可读性和维护性
语法解释
WITH 临时表名 AS (
-- 子查询定义临时表的数据
SELECT ... FROM ...
)
-- 主查询,可引用上述临时表
SELECT ... FROM 临时表名 WHERE ...;
WITH
:声明 CTE 的关键字。ranked_items
:临时表的名称。AS
:指定临时表的数据来源(子查询)。- 子查询:定义临时表的具体数据,可包含复杂的逻辑(如窗口函数、聚合等)。
示例中的具体含义
在用户提到的示例中:
WITH ranked_items AS (
SELECT
order_id,
product_name,
unit_price,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY unit_price DESC
) AS rank
FROM order_items
)
SELECT order_id, product_name, unit_price
FROM ranked_items
WHERE rank = 1;
- 子查询逻辑
- 对
order_items
表按order_id
分组(PARTITION BY
),每组内按unit_price
降序排序。 - 使用
ROW_NUMBER()
为每组生成行号(rank
列)。
- 对
- 主查询逻辑
- 从临时表
ranked_items
中筛选出rank = 1
的行,即每个订单中价格最高的商品。
- 从临时表
CTE 的优势
- 代码简洁性:将复杂逻辑拆解为多个步骤,避免嵌套子查询。
- 可重用性:临时表可在同一查询中多次引用(如递归查询)。
- 性能优化:部分数据库(如 SQL Server)会缓存 CTE 结果以减少重复计算。
其他典型场景
- 递归查询(如组织结构树遍历):
WITH RecursiveCTE AS (
-- 初始查询
SELECT id, name, manager_id
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 递归查询
SELECT e.id, e.name, e.manager_id
FROM employees e
INNER JOIN RecursiveCTE r ON e.manager_id = r.id
)
SELECT * FROM RecursiveCTE;
- 分步计算(如多步骤统计):
WITH
sales_data AS (SELECT * FROM orders WHERE year = 2025),
region_summary AS (SELECT region, SUM(amount) FROM sales_data GROUP BY region)
SELECT * FROM region_summary;
注意事项
- 作用域限制:CTE 仅在紧随其后的主查询中有效
- 数据库兼容性:主流数据库(如 PostgreSQL、MySQL 8.0+、SQL Server)均支持 CTE,但语法细节可能略有差异。
如需进一步了解递归 CTE 或性能优化技巧,可参考 SQL 官方文档
row_number窗口函数
ROW_NUMBER()
是 SQL 中常用的窗口函数,用于为结果集中的每一行分配唯一的序号。以下是详细语法、解释及示例:
语法与解释
ROW_NUMBER() OVER (
[PARTITION BY 列1, 列2, ...]
ORDER BY 列A [ASC|DESC], 列B [ASC|DESC], ...
) AS 别名
解释1:
在这个语法中,partition by 可选,表示按照指定的列或表达式对结果集进行分区。每个分区都会生成一个独立的行号序列,使得行号在每个分区内唯一。
order by 用于指定排序顺序,可以根据一个或多个列进行排序。可以使用 asc(升序,默认值)或 desc(降序)来指定排序方式。
需要注意的是:ROW_NUMBER() OVER()
函数中 OVER()
子句的分组(PARTITION BY
)和排序(ORDER BY
)的执行时机晚于 主查询的WHERE
、GROUP BY
和主查询的 ORDER BY
子句。
解释2:
-
PARTITION BY
(可选):将数据划分为多个分区,每个分区内行号从 1 开始重新计数:
-
ORDER BY
(必需):指定分区内的排序规则,决定行号的分配顺序
-
别名
:为生成的行号列命名,便于后续引用
示例(由简单到复杂)
示例 1:基本用法(无分区)
为员工按入职日期升序分配行号:
SELECT
employee_id,
employee_name,
hire_date,
ROW_NUMBER() OVER (ORDER BY hire_date) AS row_num
FROM employees;
结果:
employee_id | employee_name | hire_date | row_num
------------|---------------|------------|--------
1 | Alice | 2020-01-01 | 1
3 | Bob | 2021-03-15 | 2
2 | Charlie | 2022-06-20 | 3
说明:所有行按 hire_date
排序,行号连续且唯一
示例 2:按分区分配行号
为每个班级的学生按成绩降序排名:
SELECT
name,
class,
marks,
ROW_NUMBER() OVER (PARTITION BY class ORDER BY marks DESC) AS class_rank
FROM students;
结果:
name | class | marks | class_rank
--------------|-------|-------|-----------
Diya Sharma | 10 | 92 | 1
Aarav Patel | 10 | 85 | 2
Arjun Singh | 10 | 78 | 3
Rohan Verma | 11 | 95 | 1
Ananya Gupta | 11 | 88 | 2
Ishaan Kumar | 11 | 82 | 3
...
说明:每个班级(分区)内独立生成行号,按成绩从高到低排序
示例 3:复杂场景(分页与去重)
场景:筛选每个订单中价格最高的商品。
WITH ranked_items AS (
SELECT
order_id,
product_name,
unit_price,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY unit_price DESC
) AS rank
FROM order_items
)
SELECT order_id, product_name, unit_price
FROM ranked_items
WHERE rank = 1;
结果:
order_id | product_name | unit_price
---------|--------------|-----------
1 | Laptop | 1000.00
2 | Headphones | 100.00
3 | Laptop | 800.00
说明:通过 PARTITION BY order_id
按订单分组,每组按价格降序排列后取第一行
关键点总结
唯一性:ROW_NUMBER()
为每行生成唯一序号,即使排序字段值相同
应用场景:排名、分页、去重、分组内筛选等
兼容性:支持 MySQL 8+、PostgreSQL、Hive 等主流数据库
执行顺序
在 SQL 查询中,ROW_NUMBER() OVER()
函数中 OVER()
子句的分组(PARTITION BY
)和排序(ORDER BY
)的执行时机晚于 WHERE
、GROUP BY
和主查询的 ORDER BY
子句。
具体顺序如下:
WHERE
子句:先过滤原始数据,排除不满足条件的行。GROUP BY
子句:对过滤后的数据进行分组和聚合。- 主查询的
ORDER BY
子句(如果有):对最终结果集进行排序。 - 窗口函数的
PARTITION BY
和ORDER BY
:在以上步骤完成后,窗口函数基于最终结果集进行分组和排序,并为每行分配行号
示例与解释
假设有一个表 employees
,包含 department
(部门)、salary
(工资)字段:
场景 1:基础查询
SELECT
department,
salary,
ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS rank
FROM employees
WHERE department != 'HR'
ORDER BY department;
执行顺序:
-
WHERE department != 'HR'
:过滤掉部门为HR
的行。 -
对剩余数据执行
ROW_NUMBER()
的窗口操作:
- 按
department
分组(PARTITION BY
)。 - 组内按
salary
降序排序(ORDER BY salary DESC
)。
- 按
-
主查询的
ORDER BY department
:对最终结果按部门排序。
场景 2:结合 GROUP BY
SELECT
department,
AVG(salary) AS avg_salary,
ROW_NUMBER() OVER(ORDER BY AVG(salary) DESC) AS rank
FROM employees
GROUP BY department;
执行顺序:
GROUP BY department
:按部门分组并计算平均工资。- 窗口函数基于分组后的结果,按
avg_salary
降序排序并分配行号。
关键点
- 窗口函数的作用域:窗口函数操作的是
WHERE
和GROUP BY
处理后的结果集 - 主查询
ORDER BY
的影响:主查询的ORDER BY
仅影响最终输出顺序,不影响窗口函数内部的分组和排序逻辑 - 性能优化:若需对窗口函数的结果进一步筛选(如取排名前 N),需使用子查询或 CTE
Query-Dsl 支持row_number窗口函数
1. 支持的版本
-
QueryDSL SQL 模块(
querydsl-sql
)从 4.x 版本开始支持窗口函数。 -
如果你使用的是
JPA/Hibernate
,需注意:
- JPA 标准本身不直接支持窗口函数,但可以通过 原生 SQL 片段 或数据库特定的扩展来实现。
- QueryDSL 的 JPA 模块(
querydsl-jpa
)可以通过SQLExpressions
构造窗口函数。
2. 使用 ROW_NUMBER()
的示例
以下是一个基于 QueryDSL SQL 模块 的示例:
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.sql.SQLExpressions;
// 假设有一个表 QEmployee
QEmployee employee = QEmployee.employee;
// 构造 ROW_NUMBER() OVER (ORDER BY salary DESC)
Expression<Integer> rowNumber = SQLExpressions.rowNumber()
.over()
.orderBy(employee.salary.desc())
.as("row_num");
// 查询语句
List<Tuple> results = queryFactory.select(
employee.id,
employee.name,
employee.salary,
rowNumber
)
.from(employee)
.fetch();
3. JPA/Hibernate 中的限制
如果使用 JPA/Hibernate:
- 需要依赖数据库原生支持窗口函数(如 PostgreSQL、MySQL 8.0+、SQL Server、Oracle 等)。
- 可能需要通过
SQLExpressions
或自定义模板生成窗口函数:
// 使用 SQLExpressions 构造窗口函数
Expression<Integer> rowNumber = SQLExpressions.rowNumber()
.over()
.orderBy(employee.salary.desc())
.as("row_num");
// 添加到 SELECT 子句
JPAQuery<?> query = new JPAQuery<>(entityManager);
query.select(employee.id, employee.name, rowNumber)
.from(employee);
4. 注意事项
- 数据库兼容性:确保底层数据库支持窗口函数(如 MySQL 8.0+ 支持,但旧版本不支持)。
- QueryDSL 版本:推荐使用 QueryDSL 5.x 或更高版本以获得更好的窗口函数支持。
- 原生 SQL 回退:如果 QueryDSL 的抽象无法满足需求,可以直接使用原生 SQL:
String sql = "SELECT id, name, ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num FROM employee";
List<Tuple> results = queryFactory.nativeQuery(sql).fetch();
总结
- 支持:QueryDSL SQL 模块直接支持
ROW_NUMBER()
。 - JPA/Hibernate:需结合数据库原生支持,并通过
SQLExpressions
或原生 SQL 实现。 - 始终检查数据库文档和 QueryDSL 版本兼容性。