Administrator
发布于 2025-02-22 / 3 阅读
0
0

SqlAdvanceUse

执行顺序

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;
  1. 子查询逻辑
    • order_items 表按 order_id 分组(PARTITION BY),每组内按 unit_price 降序排序。
    • 使用 ROW_NUMBER() 为每组生成行号(rank 列)。
  2. 主查询逻辑
    • 从临时表 ranked_items 中筛选出 rank = 1 的行,即每个订单中价格最高的商品。

CTE 的优势

  1. 代码简洁性:将复杂逻辑拆解为多个步骤,避免嵌套子查询。
  2. 可重用性:临时表可在同一查询中多次引用(如递归查询)。
  3. 性能优化:部分数据库(如 SQL Server)会缓存 CTE 结果以减少重复计算。

其他典型场景

  1. 递归查询(如组织结构树遍历):
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;
  1. 分步计算(如多步骤统计):
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)的执行时机晚于 主查询的WHEREGROUP 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)的执行时机晚于 WHEREGROUP BY 和主查询的 ORDER BY 子句

具体顺序如下:

  1. WHERE 子句:先过滤原始数据,排除不满足条件的行。
  2. GROUP BY 子句:对过滤后的数据进行分组和聚合。
  3. 主查询的 ORDER BY 子句(如果有):对最终结果集进行排序。
  4. 窗口函数的 PARTITION BYORDER 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;

执行顺序

  1. WHERE department != 'HR':过滤掉部门为 HR 的行。

  2. 对剩余数据执行

    ROW_NUMBER()
    

    的窗口操作:

    • department 分组(PARTITION BY)。
    • 组内按 salary 降序排序(ORDER BY salary DESC)。
  3. 主查询的 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;

执行顺序

  1. GROUP BY department:按部门分组并计算平均工资。
  2. 窗口函数基于分组后的结果,按 avg_salary 降序排序并分配行号。

关键点

  • 窗口函数的作用域:窗口函数操作的是 WHEREGROUP 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 版本兼容性。

评论