9.6 KiB
9.6 KiB
tags, create time
| tags | create time | |||||||
|---|---|---|---|---|---|---|---|---|
|
2026-05-16 00:00 |
DQL — SELECT 全解析
概述
SELECT 是 MySQL 中使用频率最高的语句,也是最容易被误解的语句。理解其内部执行顺序是写出高性能查询的第一步。
SQL 书写顺序 vs 执行顺序
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之前 → 先排序再截断
一个完整的例子
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
-- 去重查询
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 优化
-- ✅ 好: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 的选择
-- ✅ 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+ 引入的分析利器——它能在不减少行数的前提下进行聚合计算。
排名函数
-- 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
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;
累计计算
-- 累计求和 (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 直接过滤窗口函数的结果——需要套一层子查询:
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)
返回单一值,可以像普通列一样使用:
-- WHERE 中的标量子查询
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees); -- 工资高于公司平均的人
行子查询 (Row Subquery / IN)
-- 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+ 推荐用法,比嵌套子查询可读性强很多:
-- 简单 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 最著名的性能陷阱之一。
-- ❌ 灾难级写法:扫描 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 拿完整数据
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
游标分页(推荐)
-- 上一页最后一条记录的 id = 999985
SELECT * FROM orders
WHERE id > 999985
ORDER BY id ASC
LIMIT 10;
[!TIP] 为什么游标分页更优?
WHERE id > ?走索引范围扫描,复杂度 O(log N) 而非 O(N)- 无论在第几页,查询时间恒定
- 需要前端传「上一页最后一个 ID」作为下一页的游标
局限性:不支持跳页(不能直接跳到第 100 页),但这对瀑布流场景足够。
性能小贴士
-- ❌ 避免 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 的差异