一、四大核心概念速览
| 概念 | 一句话解释 | 类比 |
|---|---|---|
WITH (CTE) |
给子查询起个临时名字,后面反复引用 | 临时变量 |
UNION ALL |
把多个查询结果纵向拼接在一起 | 多张表上下粘贴 |
EXISTS / NOT EXISTS |
判断子查询有没有数据,返回 TRUE/FALSE | if 判断 |
WHERE TRUE / FALSE |
控制查询是否返回数据的开关 | 开关按钮 |
二、WITH(CTE 公共表表达式)
2.1 基本语法
sql
WITH 临时表名 AS (
SELECT ... FROM 真实表 WHERE 条件
)
SELECT * FROM 临时表名;
2.2 核心用法
① 单个 CTE
sql
WITH dept_summary AS (
SELECT department, COUNT(*) AS cnt
FROM employee
GROUP BY department
)
SELECT * FROM dept_summary WHERE cnt > 10;
② 多个 CTE(后面的可以引用前面的
sql
WITH
cte1 AS (
SELECT * FROM 表A WHERE 条件
),
cte2 AS (
SELECT * FROM cte1 JOIN 表B ON ... -- 引用 cte1
)
SELECT * FROM cte2;
③ 递归 CTE(树形结构查询)
sql
WITH RECURSIVE tree AS (
-- 根节点
SELECT id, name, parent_id, 1 AS level
FROM menu WHERE parent_id IS NULL
UNION ALL
-- 递归子节点
SELECT m.id, m.name, m.parent_id, t.level + 1
FROM menu m JOIN tree t ON m.parent_id = t.id
)
SELECT * FROM tree;
2.3 注意事项
| 注意点 | 说明 |
|---|---|
| ⚠️ 作用域 | CTE 只在紧跟其后的一条 SQL 中有效,不能跨语句 |
| ⚠️ 不是临时表 | CTE 不会持久化存储,语句执行完就消失 |
| ⚠️ PG 版本差异 | PG 12 之前 CTE 是优化屏障(强制物化),12+ 可被内联优化 |
| ⚠️ 递归必须加关键字 | PostgreSQL 中必须写 WITH RECURSIVE,不能省略 |
| ⚠️ 紧跟使用 | WITH ... AS (...) 后面必须立刻跟 SELECT/INSERT/UPDATE/DELETE |
三、UNION ALL(纵向合并结果集)
3.1 基本语法
sql
SELECT col1, col2 FROM 表A
UNION ALL
SELECT col1, col2 FROM 表B;
3.2 UNION 与 UNION ALL 的区别
sql
-- UNION:合并 + 去重(慢)
SELECT name FROM 表A
UNION
SELECT name FROM 表B;
-- UNION ALL:合并 + 不去重(快)
SELECT name FROM 表A
UNION ALL
SELECT name FROM 表B;
| 对比项 | UNION | UNION ALL |
|---|---|---|
| 是否去重 | ✅ 去重 | ❌ 不去重 |
| 性能 | 慢(需要排序去重) | 快 |
| 使用场景 | 需要去重时 | 确定无重复或允许重复时 |
3.3 注意事项
| 注意点 | 说明 |
|---|---|
| ⚠️ 列数必须一致 | 两个 SELECT 的列数量必须相同 |
| ⚠️ 类型要兼容 | 对应位置的列数据类型要兼容(如不能一边是 int 一边是 text) |
| ⚠️ 列名取第一个 | 最终结果的列名以第一个 SELECT 的列名为准 |
| ⚠️ ORDER BY 放最后 | 排序只能放在整个 UNION ALL 的最后,不能放在中间 |
sql
-- ✅ 正确:ORDER BY 放最后
SELECT name, age FROM 表A
UNION ALL
SELECT name, age FROM 表B
ORDER BY name;
-- ❌ 错误:ORDER BY 放在中间
SELECT name, age FROM 表A ORDER BY name -- 报错!
UNION ALL
SELECT name, age FROM 表B;
如果需要对单个部分排序,需要用子查询包裹:
sql
(SELECT name, age FROM 表A ORDER BY name LIMIT 10)
UNION ALL
(SELECT name, age FROM 表B ORDER BY name LIMIT 10);
四、EXISTS / NOT EXISTS(存在性判断)
4.1 基本语法
sql
-- 子查询有数据 → TRUE
WHERE EXISTS (SELECT 1 FROM 表 WHERE 条件)
-- 子查询没数据 → TRUE
WHERE NOT EXISTS (SELECT 1 FROM 表 WHERE 条件)
4.2 SELECT 1 的含义
sql
EXISTS (SELECT 1 FROM ...)
EXISTS (SELECT * FROM ...)
EXISTS (SELECT 'abc' FROM ...)
三种写法效果完全一样! EXISTS 只关心有没有行返回 ,不关心返回的是什么值。SELECT 1 是惯用写法,语义最清晰。
4.3 EXISTS vs IN 的对比
sql
-- 写法1:EXISTS
SELECT * FROM 表A
WHERE EXISTS (
SELECT 1 FROM 表B WHERE 表B.id = 表A.id
);
-- 写法2:IN
SELECT * FROM 表A
WHERE 表A.id IN (
SELECT id FROM 表B
);
| 对比项 | EXISTS | IN |
|---|---|---|
| 大表驱动小表 | ✅ 更快 | 较慢 |
| 小表驱动大表 | 较慢 | ✅ 更快 |
| 能处理 NULL | ✅ 能 | ❌ 可能有问题 |
| 可读性 | 一般 | 更直观 |
4.4 注意事项
| 注意点 | 说明 |
|---|---|
| ⚠️ 不要 SELECT * | 虽然结果一样,但 SELECT 1 语义更清晰 |
| ⚠️ 关联条件 | 通常需要内外表的关联条件,否则只要子查询有数据就恒为 TRUE |
| ⚠️ NULL 处理 | EXISTS 对 NULL 友好,IN 遇到 NULL 可能返回意外结果 |
五、WHERE TRUE / FALSE(条件开关)
5.1 基本效果
sql
SELECT * FROM 表 WHERE TRUE; -- 返回全部数据(等于没有WHERE)
SELECT * FROM 表 WHERE FALSE; -- 返回0条(全部过滤掉)
5.2 实际应用场景
WHERE TRUE ------ 绿灯放行(等于没写)
当 SQL 变成 SELECT * FROM B表 WHERE TRUE AND (其他条件); 时:
- 含义 :
TRUE永远为真。 - 作用 :它对查询结果没有任何影响,完全等价于
SELECT * FROM B表 WHERE (其他条件);。 - 底层执行 :数据库优化器看到
TRUE AND,会自动把它忽略掉(常量折叠) ,然后老老实实去扫描 B 表,根据后面的(其他条件)过滤出数据,最后通过UNION ALL拼接到上面的结果集中。
WHERE FALSE ------ 拔掉电源(查询剪枝 / 短路)
当 SQL 变成 SELECT * FROM B表 WHERE FALSE AND (其他条件); 时,魔法就发生了:
-
含义 :
FALSE永远为假。这就意味着,无论后面的(其他条件)是什么,无论表里有多少数据,这行记录都不可能满足条件。 -
作用 :强制让这个查询返回 0 条记录(空结果集)。
-
底层执行(极度关键) :
现代关系型数据库(如 PostgreSQL、MySQL、Oracle)的查询优化器(Optimizer)非常聪明。
当它在解析 SQL 语法树时,只要看到WHERE FALSE或者WHERE 1=2这种恒假条件 ,它会触发一种叫做 "常量折叠 (Constant Folding)" 或 "查询剪枝 (Query Pruning)" 的优化机制。也就是说,数据库根本不会去触碰 B 表的磁盘文件!根本不会去读 B 表的索引! 它在生成执行计划的那一瞬间,就直接把对 B 表的访问操作给"砍掉"了,瞬间返回一个空壳。
① MyBatis 动态 SQL 拼接技巧
sql
<!-- 传统写法:用 <where> 标签 -->
<select id="query">
SELECT * FROM user
<where>
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</where>
</select>
<!-- 简洁写法:用 WHERE TRUE -->
<select id="query">
SELECT * FROM user
WHERE TRUE
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</select>
WHERE TRUE打底,后面所有条件都用AND开头,永远不会出现语法错误。
② 调试时只看表结构
sql
SELECT * FROM 超大表 WHERE FALSE;
-- 瞬间返回,0条数据,但能看到所有列名和类型
③ 作为查询开关
sql
SELECT * FROM 表A
UNION ALL
SELECT * FROM 表B WHERE TRUE; -- 表B全部数据都参与合并
SELECT * FROM 表A
UNION ALL
SELECT * FROM 表B WHERE FALSE; -- 表B不参与合并(0条)
-- A查到数据时,SQL 实际变成了:
SELECT * 表A -- 返回A的数据
UNION ALL
SELECT * FROM 表B WHERE FALSE; -- 返回0条,B被"关掉"了
-- A有数据时,SQL 实际变成了:
SELECT * FROM 表A -- 返回A的数据
UNION ALL
SELECT * FROM 表B WHERE TRUE; -- 返回B的全部数据
-- 返回A和B的全部数据
-- A没查到数据时,SQL 实际变成了:
SELECT * FROM a_results -- 返回0条
UNION ALL
SELECT * FROM 表B WHERE TRUE; -- 返回B的全部数据
5.3 注意事项
| 注意点 | 说明 |
|---|---|
| ⚠️ WHERE TRUE 不影响性能 | 数据库优化器会自动忽略 WHERE TRUE |
| ⚠️ WHERE FALSE 不会扫表 | 优化器直接返回空结果,不消耗资源 |
| ⚠️ 不是所有数据库都支持 | MySQL、PostgreSQL 支持;Oracle 需要用 WHERE 1=1 代替 |
六、四者联合使用:多表优先级查询实战
6.1 业务需求
查企业信息:先查危化品登记表(A表),A表有数据就用A的结果;A表没有数据,再查工商登记表(B表)兜底。
6.2 完整 SQL
sql
-- 第一步:WITH 把A表的查询结果缓存为临时表
WITH a_results AS (
SELECT
company_name AS companyName,
company_code AS companyCode,
'' AS uniscId,
address_registry AS addressRegistry,
representative_person AS representativePerson,
responsible_phone AS responsiblePhone
FROM bussdata.ds_whdjxt_v_tb_base_company_1
WHERE company_name LIKE '%某企业%'
)
-- 第二步:先返回A表的数据
SELECT * FROM a_results
-- 第三步:UNION ALL 合并B表数据
UNION ALL
-- 第四步:NOT EXISTS 判断A表是否为空,为空才查B表
SELECT
enterprise_name AS companyName,
'' AS companyCode,
unisc_id AS uniscId,
regis_addr AS addressRegistry,
legal_representative AS representativePerson,
contact_number AS responsiblePhone
FROM bussdata.company_mobile
WHERE NOT EXISTS (SELECT 1 FROM a_results) -- 关键开关!
AND enterprise_name LIKE '%某企业%';
6.3 执行流程图
sql
┌──────────────────────────────────────┐
│ ① WITH: 执行A表查询,缓存结果 │
│ a_results = SELECT ... FROM 表A │
└─────────────────┬────────────────────┘
│
a_results 有数据?
/ \
是/ \否
/ \
┌─────────────┐ ┌──────────────────┐
│ a_results │ │ a_results │
│ = 5条数据 │ │ = 0条(空) │
└──────┬──────┘ └────────┬─────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────────┐
│② SELECT * │ │② SELECT * │
│FROM a_results│ │FROM a_results │
│→ 返回5条 │ │→ 返回0条 │
├─────────────┤ ├──────────────────┤
│③ UNION ALL │ │③ UNION ALL │
├─────────────┤ ├──────────────────┤
│④ NOT EXISTS │ │④ NOT EXISTS │
│= FALSE │ │= TRUE │
│→ 相当于 │ │→ 相当于 │
│WHERE FALSE │ │WHERE TRUE │
│→ B返回0条 │ │→ 查B表,返回B数据 │
└──────┬──────┘ └────────┬─────────┘
│ │
▼ ▼
最终:A的5条数据 最终:B的数据
七、完整对照表
| 概念 | 作用 | 等价理解 | 在本例中的角色 |
|---|---|---|---|
WITH |
缓存A表查询结果 | 临时变量 | 让A的结果可被多次引用 |
UNION ALL |
拼接A和B的结果 | 上下粘贴两张表 | 合并两个数据源 |
NOT EXISTS |
判断A是否为空 | if 判断 | 控制B是否执行的开关 |
WHERE TRUE/FALSE |
NOT EXISTS 的返回值 | 开关的两个状态 | TRUE=查B,FALSE=不查B |
八、常见踩坑总结
| 坑 | 表现 | 解决方案 |
|---|---|---|
| CTE 后面没紧跟 SELECT | 语法报错 | WITH ... AS (...) SELECT ... 必须连写 |
| UNION ALL 两边列数不同 | 列数不匹配报错 | 用 '' 或 NULL 补齐缺少的列 |
| UNION ALL 中间加 ORDER BY | 语法报错 | ORDER BY 只能放最后面 |
MyBatis 中 <if> 前面缺 AND |
SQL 语法错误 | 用 WHERE TRUE 或 <where> 标签 |
| PG 递归 CTE 忘写 RECURSIVE | 语法报错 | WITH RECURSIVE tree AS (...) |
Docker 中导入 SQL 报 \N 错误 |
换行符问题 | sed -i 's/\r$//' file.sql |
九、一句话总结
WITH 负责缓存,UNION ALL 负责合并,NOT EXISTS 负责判断,WHERE TRUE/FALSE 是最终的开关 ------ 四者配合实现了"A表优先、B表兜底"的优雅查询模式,一条 SQL 搞定多表优先级逻辑,无需存储过程。