This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/12-DQL SELECT 全解析.md
T
2026-05-17 00:06:11 +08:00

313 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, SELECT, 查询执行顺序, GROUP BY, 分页, 窗口函数, CTE]
create time: 2026-05-16 00:00
---
# DQL — SELECT 全解析
## 概述
SELECT 是 MySQL 中使用频率最高的语句,也是最容易被误解的语句。理解其内部执行顺序是写出高性能查询的第一步。
## SQL 书写顺序 vs 执行顺序
```mermaid
flowchart LR
W["⑥ SELECT"] --> A["① FROM"]
A --> B["② JOIN"]
B --> C["③ ON"]
C --> D["④ WHERE"]
D --> E["⑤ GROUP BY"]
E --> F["⑦ HAVING"]
F --> G["⑧ DISTINCT"]
G --> H["⑨ ORDER BY"]
H --> I["⑩ LIMIT / OFFSET"]
style W fill:#C44569,color:#fff
style A fill:#00B6BC,color:#fff
style I fill:#4FC08D,color:#fff
```
> [!QUESTION] 为什么理解这个很重要?
> 因为 SQL 不是按照你写的顺序执行的——而是按数字顺序!这意味着:
> - `WHERE` 在 `SELECT` 之前执行 → 不能在 WHERE 中使用 SELECT 定义的别名
> - `GROUP BY` 在 `HAVING` 之前 → WHERE 过滤行,HAVING 过滤分组
> - `ORDER BY` 在 `LIMIT` 之前 → 先排序再截断
### 一个完整的例子
```sql
SELECT
DATE(created_at) AS order_date, -- ⑥ 计算列
COUNT(*) AS cnt, -- 聚合函数
SUM(amount) AS total -- 聚合函数
FROM orders -- ① 确定数据源
WHERE status = 'paid' -- ④ 先过滤行
AND created_at >= '2026-01-01' -- 过滤条件
GROUP BY DATE(created_at) -- ⑤ 按日期分组
HAVING COUNT(*) > 5 -- ⑦ 过滤分组
ORDER BY total DESC -- ⑨ 排序
LIMIT 10; -- ⑩ 取前 10 页
```
## SELECT 关键字详解
### DISTINCT
```sql
-- 去重查询
SELECT DISTINCT department FROM employees;
-- DISTINCT 作用于所有选中的列组合
SELECT DISTINCT country, city FROM customers;
-- 返回的是 (country, city) 的唯一组合
```
> [!QUESTION] DISTINCT 一定快吗?
> 不一定。`DISTINCT` 本质上是 `GROUP BY` 的简化版——MySQL 内部可能通过 temporary table + filesort 去重。当数据量大时,它的代价不亚于一次普通的分组聚合。
>
> **替代方案**:如果去重目的是为下拉框提供选项,可以用 `SELECT DISTINCT department FROM employees LIMIT 50;` 限制返回量;更优的做法是在应用层缓存选项列表。
### GROUP BY 优化
```sql
-- ✅ 好:GROUP BY 走索引
-- idx_status_dept = (status, department),可以直接按 department 分组
SELECT department, COUNT(*)
FROM employees
WHERE status = 'active'
GROUP BY department;
-- ❌ 差:GROUP BY 无法利用索引(LIKE 左模糊导致索引失效)
SELECT department, COUNT(*)
FROM employees
WHERE name LIKE '%chen%' -- 左模糊导致索引失效
GROUP BY department;
-- EXPLAIN: Using where; Using temporary
```
### HAVING 与 WHERE 的选择
```sql
-- ✅ WHERE 过滤行(早过滤,减少数据量)
SELECT department, AVG(salary)
FROM employees
WHERE hire_date >= '2024-01-01' -- 先筛选最近入职的人
GROUP BY department
HAVING AVG(salary) > 15000; -- 再过滤平均工资
-- ❌ 把能放 WHERE 的条件放到 HAVING 里
-- 虽然结果一样,但效率更低
SELECT department, AVG(salary)
FROM employees
GROUP BY department
HAVING hire_date >= '2024-01-01' -- 错!HAVING 不能用非聚合列
AND AVG(salary) > 15000;
```
## 窗口函数 (WINDOW FUNCTIONS)
窗口函数是 MySQL 8.0+ 引入的分析利器——它能在**不减少行数**的前提下进行聚合计算。
### 排名函数
```sql
-- ROW_NUMBER() / RANK() / DENSE_RANK()
-- 按部门内工资排名(处理并列名次的三种方式)
SELECT name, department, salary,
ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS drnk
FROM employees;
-- 结果示例:
-- | name | dept | salary | rn | rnk | drnk |
-- | Alice | sales | 20000 | 1 | 1 | 1 |
-- | Bob | sales | 20000 | 2 | 1 | 1 |
-- | Carol | sales | 18000 | 3 | 3 | 2 |
```
> [!TIP] RANK vs DENSE_RANK 的区别
> - `RANK(20000, 20000, 18000)` → **1, 1, 3**(跳过第二名)
> - `DENSE_RANK(20000, 20000, 18000)` → **1, 1, 2**(不跳号)
> - `ROW_NUMBER` → **1, 2, 3**(永远无并列)
### 前后行访问 — LAG / LEAD
```sql
SELECT order_date, amount,
LAG(amount, 1) OVER(ORDER BY order_date) AS prev_amount, -- 前一天的金额
LEAD(amount, 1) OVER(ORDER BY order_date) AS next_amount -- 后一天的金额
FROM daily_sales;
-- 计算日环比增长率
SELECT order_date, amount,
ROUND((amount - LAG(amount) OVER(ORDER BY order_date)) / LAG(amount) OVER(ORDER BY order_date) * 100, 2) AS growth_pct
FROM daily_sales;
```
### 累计计算
```sql
-- 累计求和 (Running Total)
SELECT order_date, amount,
SUM(amount) OVER(ORDER BY order_date) AS running_total
FROM daily_sales;
-- 当前分区内占比
SELECT department, name, salary,
ROUND(salary * 100.0 / SUM(salary) OVER(PARTITION BY department), 2) AS dept_pct
FROM employees;
```
> [!NOTE] 窗口函数执行时机
> - 执行顺序在 WHERE、GROUP BY、HAVING **之后**,ORDER BY **之前**
> - 因此不能用 WHERE 直接过滤窗口函数的结果——需要套一层子查询:
> ```sql
> SELECT * FROM (
> SELECT *, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY created_at DESC) AS rn
> FROM orders
> ) ranked WHERE rn = 1;
> -- 作用:每个用户的最新一条订单记录
> ```
## 子查询与 CTE
### 标量子查询 (Scalar Subquery)
返回单一值,可以像普通列一样使用:
```sql
-- WHERE 中的标量子查询
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees); -- 工资高于公司平均的人
```
### 行子查询 (Row Subquery / IN)
```sql
-- IN 子查询
SELECT name, department
FROM employees
WHERE department IN (SELECT id FROM departments WHERE region = 'APAC');
-- EXISTS:关注"是否存在"而非具体数据,比 IN 更高效
SELECT d.name
FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);
```
### CTE (Common Table Expression) — WITH 语法
MySQL 8.0+ 推荐用法,比嵌套子查询可读性强很多:
```sql
-- 简单 CTE
WITH dept_stats AS (
SELECT department, COUNT(*) AS emp_count, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
)
SELECT * FROM dept_stats WHERE avg_salary > 15000;
-- 递归 CTE:处理层级数据(组织架构、分类树等)
WITH RECURSIVE org_chart AS (
-- 锚点成员:根节点
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 递归成员:逐层展开
SELECT e.id, e.name, e.manager_id, oc.level + 1
FROM employees e
INNER JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY level, name;
```
> [!QUESTION] CTE vs 派生表?
> - **可读性**:CTE 命名清晰,逻辑分层;派生表层层嵌套,括号匹配困难
> - **性能**:MySQL 会将非递归 CTE 优化为临时表或内联展开——多数情况下两者性能一致
> - **复用**:同一个 CTE 可在一个语句中多次引用(派生表不行)
---
## 深分页问题
这是 MySQL 最著名的性能陷阱之一。
```sql
-- ❌ 灾难级写法:扫描 100 万行后丢弃前 999,990 行
SELECT * FROM orders LIMIT 999990, 10;
-- MySQL 需要先定位到第 999990 行,才返回接下来的 10 行
-- ✅ 方案一:延迟关联(Deferred Join)
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 999990, 10
) AS tmp ON o.id = tmp.id;
-- 子查询只扫主键索引(极紧凑),外层再 JOIN 拿完整数据
```
```mermaid
flowchart LR
subgraph "传统 LIMIT 999990,10"
A1["扫描聚簇索引<br/>跳过 999990 行"] --> A2["取出 10 行数据"]
end
subgraph "延迟关联方案"
B1["扫描聚簇索引<br/>跳过 999990 行"] --> B2["仅提取 10 个主键"]
B2 --> B3["JOIN 回聚簇索引<br/>精确查找 10 个主键"]
B3 --> B4["返回结果"]
end
A1 --> A2
style B1 fill:#00B6BC,color:#fff
style B2 fill:#00D866,color:#fff
```
### 游标分页(推荐)
```sql
-- 上一页最后一条记录的 id = 999985
SELECT * FROM orders
WHERE id > 999985
ORDER BY id ASC
LIMIT 10;
```
> [!TIP] 为什么游标分页更优?
> - `WHERE id > ?` 走索引范围扫描,复杂度 O(log N) 而非 O(N)
> - 无论在第几页,查询时间恒定
> - 需要前端传「上一页最后一个 ID」作为下一页的游标
>
> **局限性**:不支持跳页(不能直接跳到第 100 页),但这对瀑布流场景足够。
## 性能小贴士
```sql
-- ❌ 避免 SELECT *
SELECT * FROM users WHERE status = 1;
-- ✅ 只查需要的列
SELECT id, username, email FROM users WHERE status = 1;
-- 好处:减少网络传输、提高 Buffer Pool 命中率、可能触发 Covering Index
-- ❌ 函数包裹索引列
SELECT * FROM users WHERE YEAR(created_at) = 2026;
-- ✅ 用范围替代函数
SELECT * FROM users
WHERE created_at >= '2026-01-01'
AND created_at < '2027-01-01';
```
## 关联笔记
- [[hhs/MySQL/12-JOIN 原理与优化]] — 深入理解 JOIN 的内部执行机制
- [[hhs/MySQL/13-子查询与派生表]] — 与 SELECT 密切相关的子查询技术
- [[hhs/GORM/06-排序与分页]] — GORM 分页 API 与原生 SQL 的差异