vault backup: 2026-05-21 12:20:54
This commit is contained in:
@@ -101,6 +101,45 @@ sequenceDiagram
|
||||
>
|
||||
> **优先用 `ON DUPLICATE KEY UPDATE`**,除非你确实需要完整的「删+插」语义。
|
||||
|
||||
### INSERT ... SELECT
|
||||
|
||||
从另一张表或查询结果批量插入数据——这是日常开发中比 `VALUES` 批量插入更高频的用法。
|
||||
|
||||
```sql
|
||||
-- 基础:从查询结果插入
|
||||
INSERT INTO user_archive (id, username, email, archived_at)
|
||||
SELECT id, username, email, NOW()
|
||||
FROM users
|
||||
WHERE status = 0 AND updated_at < '2025-01-01';
|
||||
|
||||
-- 搭配聚合:插入每日统计快照
|
||||
INSERT INTO daily_stats (stat_date, order_count, total_amount)
|
||||
SELECT CURDATE(), COUNT(*), SUM(amount)
|
||||
FROM orders
|
||||
WHERE DATE(created_at) = CURDATE();
|
||||
```
|
||||
|
||||
> [!QUESTION] INSERT ... SELECT 会锁表吗?
|
||||
> 这取决于事务隔离级别和索引情况:
|
||||
> - 在 **RC(Read Committed)** 下:只锁定被插入的目标表,对源表加短暂的共享锁
|
||||
> - 在 **RR(Repeatable Read)** 下:源表的读取走一致性快照,**不会阻塞源表的写入**
|
||||
> - 目标表:插入的行会加行锁,大批量插入时注意不要超长事务
|
||||
|
||||
> [!CAUTION] INSERT ... SELECT 的两个常见坑
|
||||
> 1. **SELECT 中不能引用正在被插入的目标表**(MySQL 会报 `Table is specified twice`)。解决办法:嵌套一层子查询
|
||||
> ```sql
|
||||
> -- ❌ 报错:直接引用目标表
|
||||
> INSERT INTO orders_backup SELECT * FROM orders WHERE id IN (SELECT id FROM orders WHERE ...);
|
||||
> -- ✅ 正确:用子查询隔离
|
||||
> INSERT INTO orders_backup SELECT * FROM orders WHERE id IN (SELECT t.id FROM (SELECT id FROM orders WHERE ...) t);
|
||||
> ```
|
||||
> 2. **大批量插入时分批执行**,避免长事务持有过多锁。可以用 `LIMIT` + 循环分批:
|
||||
> ```sql
|
||||
> -- 每次只插入 5000 条,循环直到 affected_rows = 0
|
||||
> INSERT INTO user_archive
|
||||
> SELECT * FROM users WHERE status = 0 LIMIT 5000;
|
||||
> ```
|
||||
|
||||
## UPDATE
|
||||
|
||||
UPDATE 的核心原则只有一条:**精准定位、最小影响**。先思考「哪些行需要改」,再写 SET 子句。
|
||||
|
||||
@@ -136,6 +136,80 @@ SELECT a.name, b.name FROM table_a CROSS JOIN table_b;
|
||||
-- 结果 = |a| × |b| 行
|
||||
```
|
||||
|
||||
## SELF JOIN(自连接)
|
||||
|
||||
自连接是**同一张表与自身进行 JOIN**——表没有变,只是用两个不同的别名把它"当成两张表"来用。这是处理**层次结构**和**行间比较**的经典手法。
|
||||
|
||||
### 场景一:组织架构树(上下级关系)
|
||||
|
||||
```sql
|
||||
-- 找出每个员工及其直属上级的名字
|
||||
SELECT e.name AS employee, m.name AS manager
|
||||
FROM employees e
|
||||
LEFT JOIN employees m ON e.manager_id = m.id;
|
||||
|
||||
-- 结果:
|
||||
-- | employee | manager |
|
||||
-- | Alice | NULL | ← CEO,无上级
|
||||
-- | Bob | Alice |
|
||||
-- | Carol | Alice |
|
||||
-- | David | Bob |
|
||||
```
|
||||
|
||||
> [!NOTE] SELF JOIN vs 递归 CTE 的选择
|
||||
> - **只查一层关系**(直接上级/下级):SELF JOIN 简洁高效
|
||||
> - **需要遍历整棵树**(所有层级的汇报线):必须用递归 CTE——详见 [[hhs/MySQL/02-SQL核心/10-子查询与派生表]]
|
||||
|
||||
### 场景二:查找同一组内的相邻行
|
||||
|
||||
```sql
|
||||
-- 找出连续两天都有订单的用户(日环比分析)
|
||||
SELECT DISTINCT a.user_id
|
||||
FROM orders a
|
||||
INNER JOIN orders b
|
||||
ON a.user_id = b.user_id
|
||||
AND DATEDIFF(b.created_at, a.created_at) = 1
|
||||
WHERE DATE(a.created_at) = '2026-05-20';
|
||||
```
|
||||
|
||||
### 场景三:去重——保留每组最新的一条
|
||||
|
||||
```sql
|
||||
-- 删除同一用户的历史记录,只保留最新一条
|
||||
DELETE old FROM orders old
|
||||
INNER JOIN orders new
|
||||
ON old.user_id = new.user_id
|
||||
AND old.created_at < new.created_at;
|
||||
-- old 是"要删的旧记录",new 是"用来比较的新记录"
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph "orders 表(别名 old)"
|
||||
O1["user=1, 05-18"]
|
||||
O2["user=1, 05-19"]
|
||||
O3["user=1, 05-20"]
|
||||
end
|
||||
subgraph "orders 表(别名 new)"
|
||||
N1["user=1, 05-18"]
|
||||
N2["user=1, 05-19"]
|
||||
N3["user=1, 05-20"]
|
||||
end
|
||||
O1 -->|"old.created_at < new.created_at"| N2
|
||||
O1 --> N3
|
||||
O2 --> N3
|
||||
O3 -.->|"无更早匹配"| N3
|
||||
|
||||
style O1 fill:#EE5A24,color:#fff
|
||||
style O2 fill:#EE5A24,color:#fff
|
||||
style O3 fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
> [!TIP] 自连接的性能注意事项
|
||||
> - 自连接本质是同一张表扫描两次,如果表很大且没有索引,代价等同于两张大表的 BNLJ
|
||||
> - **务必确保 JOIN 条件列有索引**(如 `manager_id`、`user_id + created_at`)
|
||||
> - 如果只是做"每组取最新一条",`ROW_NUMBER()` 窗口函数通常比自连接 DELETE 更安全(详见 [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]])
|
||||
|
||||
## JOIN 执行算法
|
||||
|
||||
MySQL InnoDB 在执行 JOIN 时,**本质上只有两种核心策略**:被驱动表有索引时用 **INLJ**(Index Nested-Loop Join),没有索引时退化到 **BNLJ**(Block Nested-Loop Join)。而 INLJ 内部又根据索引是否为唯一键进一步细分。
|
||||
|
||||
@@ -36,10 +36,13 @@ graph BT
|
||||
|
||||
```sql
|
||||
-- 用法 1:在 SELECT 中调用
|
||||
SELECT
|
||||
SELECT
|
||||
username,
|
||||
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
|
||||
FROM users;
|
||||
-- 副作用:这个查询其实也是"相关子查询"的典型例子
|
||||
-- 因为内层的 user_id = users.id 引用了外层表的列
|
||||
-- 每一行 users 的记录都会触发一次内层查询
|
||||
|
||||
-- 用法 2:在 WHERE 中等价于常量
|
||||
SELECT * FROM products
|
||||
@@ -50,6 +53,10 @@ INSERT INTO reports (month, order_total)
|
||||
VALUES ('2026-05', (SELECT SUM(amount) FROM orders WHERE MONTH(created_at) = 5));
|
||||
```
|
||||
|
||||
> [!QUESTION] 上面用法 1 的子查询,每一行都要重新执行吗?
|
||||
> 是的——但不是因为"放在 SELECT 里",而是因为它引用了外层的 `users.id`。
|
||||
> 这就引出了子查询最重要的分类维度:**相关 vs 无关**。
|
||||
|
||||
### 相关 vs 无关子查询
|
||||
|
||||
这是理解子查询性能的基石:**子查询是否引用了外层查询的列?**
|
||||
@@ -436,6 +443,14 @@ WITH RECURSIVE org_chain AS (
|
||||
SELECT * FROM org_chain ORDER BY level, name;
|
||||
```
|
||||
|
||||
> [!QUESTION] 为什么叫「自我引用」?递归 CTE 到底怎么执行的?
|
||||
> `org_chain` 这个名字在定义内部就被引用了——`FROM employees e INNER JOIN org_chain oc`。
|
||||
> 第一轮:只执行锚点成员,得到根节点(CEO);
|
||||
> 第二轮:用第一轮的结果去 JOIN employees,得到 CEO 的直接下属;
|
||||
> 第三轮:用第二轮的结果去 JOIN,得到下属的下属……
|
||||
> 直到某一轮 JOIN 结果为空,递归自动终止。
|
||||
> **关键:没有终止条件 → 无限循环 → 触发 `cte_max_recursion_depth` 报错。**
|
||||
|
||||
> [!WARNING] 递归 CTE 的注意事项
|
||||
> - MySQL 默认递归深度上限为 **1000**(`cte_max_recursion_depth`),超出会报错
|
||||
> - 务必设置合理的终止条件,否则会无限递归直到达到上限
|
||||
@@ -466,6 +481,16 @@ SELECT * FROM org_chain ORDER BY level, name;
|
||||
> **子查询不是万能的——它首先是为了表达清晰,其次才是性能。**
|
||||
> 写完后务必跑 `EXPLAIN`,确认没有意料之外的临时表或多表扫描。当数据量上去后,能改写成 JOIN 的子查询就尽量改写,因为 JOIN 的执行路径对 Optimizer 更加透明。
|
||||
|
||||
> [!TIP] 学习路径建议
|
||||
> 如果你刚接触子查询,建议按这个顺序学习:
|
||||
> 1. **标量子查询** → 先理解「子查询就是一次独立的 SELECT」
|
||||
> 2. **相关 vs 无关** → 理解执行次数差异(本文核心概念)
|
||||
> 3. **IN vs EXISTS** → 掌握两种过滤范式,重点理解 NOT IN 陷阱
|
||||
> 4. **派生表** → 子查询作为虚拟表,理解物化与 Flattening
|
||||
> 5. **CTE** → 用现代语法重写派生表,最终挑战递归 CTE
|
||||
>
|
||||
> 每一步都要跑 `EXPLAIN` 看执行计划——**看懂 EXPLAIN 比背语法重要十倍**。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]] — SELECT 执行顺序与子查询的关系
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
tags: [MySQL, UNION, UNION ALL, 集合运算]
|
||||
tags: [MySQL, UNION, UNION ALL, INTERSECT, EXCEPT, 集合运算]
|
||||
create time: 2026-05-16 00:00
|
||||
---
|
||||
|
||||
@@ -47,6 +47,114 @@ flowchart LR
|
||||
> [!TIP] 首选 UNION ALL
|
||||
> 如果你能通过业务逻辑保证各部分结果不重复(比如按日期区间划分),**一律用 UNION ALL**。UNION 的去重操作需要 Sort/Dedup 阶段,在大结果集上是昂贵操作。
|
||||
|
||||
## INTERSECT 与 EXCEPT(MySQL 8.0.22+)
|
||||
|
||||
MySQL 8.0.22 开始支持标准 SQL 的 `INTERSECT`(交集)和 `EXCEPT`(差集),补齐了集合运算的最后一块拼图。
|
||||
|
||||
### 基本语法
|
||||
|
||||
```sql
|
||||
-- INTERSECT:取两个查询的交集(两边都有的行)
|
||||
SELECT city FROM customers_china
|
||||
INTERSECT
|
||||
SELECT city FROM customers_japan;
|
||||
-- 结果:只返回同时存在于两个结果集中的城市
|
||||
|
||||
-- EXCEPT:取差集(左边有、右边没有的行)
|
||||
SELECT city FROM customers_china
|
||||
EXCEPT
|
||||
SELECT city FROM customers_japan;
|
||||
-- 结果:返回在中国有但日本没有的城市
|
||||
```
|
||||
|
||||
> [!QUESTION] INTERSECT vs IN / EXISTS?
|
||||
> 两者在语义上等价,但 `INTERSECT` 更直观地表达了"取交集"的意图。实际执行中,Optimizer 通常会将 INTERSECT 转换为 Semi Join 或 Anti Join——因此性能差异不大,选择哪种取决于**可读性**。
|
||||
|
||||
### INTERSECT / EXCEPT vs UNION 行为对比
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["查询 A: {1,2,3}"] --> OP
|
||||
B["查询 B: {2,3,4}"] --> OP
|
||||
|
||||
OP --> R1["UNION: {1,2,3,4}"]
|
||||
OP --> R2["INTERSECT: {2,3}"]
|
||||
OP --> R3["EXCEPT: {1}"]
|
||||
|
||||
style R1 fill:#74C0FC,color:#000
|
||||
style R2 fill:#00D866,color:#fff
|
||||
style R3 fill:#FF9F43,color:#000
|
||||
```
|
||||
|
||||
| 操作 | 语义 | 去重 | 对应集合论 |
|
||||
|------|------|------|-----------|
|
||||
| `UNION` | A ∪ B | ✅ 默认去重 | 并集 |
|
||||
| `UNION ALL` | A ∪ B(含重复) | ❌ | 多重集并 |
|
||||
| `INTERSECT` | A ∩ B | ✅ 默认去重 | 交集 |
|
||||
| `EXCEPT` | A − B | ✅ 默认去重 | 差集 |
|
||||
|
||||
> [!NOTE] 默认行为:去重
|
||||
> INTERSECT 和 EXCEPT 默认都会去重(等价于 DISTINCT)。如果想去重保留所有行,目前 **MySQL 不支持 `INTERSECT ALL` / `EXCEPT ALL`**,需要通过 UNION ALL + 计数的方式模拟。
|
||||
|
||||
### 实用场景
|
||||
|
||||
```sql
|
||||
-- 场景一:找出"既买了 A 又买了 B"的用户(交集)
|
||||
SELECT user_id FROM orders WHERE product_id = 'A'
|
||||
INTERSECT
|
||||
SELECT user_id FROM orders WHERE product_id = 'B';
|
||||
|
||||
-- 等价的 EXISTS 写法(对比可读性)
|
||||
SELECT DISTINCT o1.user_id
|
||||
FROM orders o1
|
||||
WHERE o1.product_id = 'A'
|
||||
AND EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = o1.user_id AND o2.product_id = 'B');
|
||||
|
||||
-- 场景二:找出"注册了但从未下单"的用户(差集)
|
||||
SELECT id FROM users
|
||||
EXCEPT
|
||||
SELECT DISTINCT user_id FROM orders;
|
||||
|
||||
-- 等价的 NOT EXISTS 写法
|
||||
SELECT u.id FROM users u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
|
||||
```
|
||||
|
||||
> [!TIP] 何时选 INTERSECT/EXCEPT?
|
||||
> - **逻辑简单、需要取交集/差集**:`INTERSECT` / `EXCEPT` 语法最清晰
|
||||
> - **需要返回关联表的字段**:用 `EXISTS` / `NOT EXISTS` 或 JOIN,因为 INTERSECT/EXCEPT 只能操作整行或指定列
|
||||
> - **需要高性能**:大表场景下 EXPLAIN 确认执行计划——Optimizer 通常会转换为 Semi/Anti Join,性能与手写子查询一致
|
||||
|
||||
### 优先级与括号
|
||||
|
||||
当混合使用多个集合运算时,默认**从左到右**执行。需要用括号改变优先级:
|
||||
|
||||
```sql
|
||||
-- 先 UNION 再 INTERSECT(左到右)
|
||||
SELECT id FROM t1
|
||||
UNION
|
||||
SELECT id FROM t2
|
||||
INTERSECT
|
||||
SELECT id FROM t3;
|
||||
-- 实际含义:t1 UNION (t2 INTERSECT t3) ← 错误!
|
||||
-- 实际执行:(t1 UNION t2) INTERSECT t3 ← 从左到右
|
||||
|
||||
-- ✅ 用括号明确意图
|
||||
(SELECT id FROM t1 UNION SELECT id FROM t2)
|
||||
INTERSECT
|
||||
SELECT id FROM t3;
|
||||
|
||||
-- 也可以用 ORDER BY + LIMIT 修饰每个子句(需括号)
|
||||
(SELECT id FROM t1 ORDER BY id LIMIT 10)
|
||||
INTERSECT
|
||||
(SELECT id FROM t2 ORDER BY id LIMIT 5);
|
||||
```
|
||||
|
||||
> [!WARNING] 注意括号的必要性
|
||||
> 当 INTERSECT/EXCEPT 与 ORDER BY/LIMIT 混用时,不加括号可能产生歧义。**养成给每个子查询加括号的习惯**,避免优先级意外。
|
||||
|
||||
---
|
||||
|
||||
## 实战场景
|
||||
|
||||
### 场景一:多表同构合并
|
||||
@@ -348,6 +456,7 @@ flowchart TD
|
||||
| 主题 | 一句话 |
|
||||
|------|--------|
|
||||
| UNION ALL vs UNION | 能用 ALL 就绝不用 UNION — 去重的临时表+文件排序代价远超想象 |
|
||||
| INTERSECT / EXCEPT | 8.0.22+ 原生支持交集与差集,语义比 IN/EXISTS 更直观,Optimizer 会转为 Semi/Anti Join |
|
||||
| ORDER BY/LIMIT 作用域 | 只在 UNION 最后一个 SELECT 生效;跨表排序必须外层包装子查询 |
|
||||
| 字段匹配规则 | 列数必须相同,类型应尽量兼容 — 列名取自第一个 SELECT |
|
||||
| Feed 流/多源合并 | UNION ALL 的经典场景,一次 DB 往返搞定多数据源混合 |
|
||||
|
||||
Reference in New Issue
Block a user