【无标题】

SQL 学习笔记

阅读方式

  • 第 0 章是速查表,随时回来翻;
  • 第 1~3 章是基础(概念 / 数据类型 / 建表改表);
  • 第 4~7 章是查询与函数(日常写得最多);
  • 第 8~10 章是高级(窗口函数 / 索引 / 事务锁);
  • 第 11~15 章是设计、避坑、方言、面试、附录。
  • 难度用 ⭐ 标注,⭐ 越多越难。难点一律用表格呈现。

全文示例表统一(先看一眼,后面所有 SQL 都基于它们):

复制代码
employees(员工表)                          departments(部门表)
┌────────┬────────┬─────────┬────────┬────────────┬────────────┐   ┌─────────┬───────────┬──────────┐
│ emp_id │ name   │ dept_id │ salary │ hire_date  │ manager_id │   │ dept_id │ dept_name │ location │
├────────┼────────┼─────────┼────────┼────────────┼────────────┤   ├─────────┼───────────┼──────────┤
│  1     │ 张三   │   10    │ 8000   │ 2021-03-01 │     3      │   │   10    │ 技术部    │ 北京     │
│  2     │ 李四   │   10    │ 12000  │ 2020-07-15 │     3      │   │   20    │ 市场部    │ 上海     │
│  3     │ 王五   │   20    │ 15000  │ 2019-01-20 │    NULL    │   │   30    │ 财务部    │ 北京     │
│  4     │ 赵六   │   30    │ 9000   │ 2022-11-05 │     3      │   └─────────┴───────────┴──────────┘
│  5     │ 孙七   │   NULL  │ 7000   │ 2023-02-10 │    NULL    │
└────────┴────────┴─────────┴────────┴────────────┴────────────┘

orders(订单表,用于分组/窗口函数示例)
┌──────────┬─────────┬────────┬────────────┬────────┐
│ order_id │ user_id │ amount │ order_date │ status │
├──────────┼─────────┼────────┼────────────┼────────┤
│  1001    │    1    │ 120.50 │ 2024-01-05 │ 已付款 │
│  1002    │    1    │ 300.00 │ 2024-02-11 │ 已付款 │
│  1003    │    2    │  80.00 │ 2024-01-20 │ 已退款 │
└──────────┴─────────┴────────┴────────────┴────────┘

方言说明 :正文默认 MySQL 8.0。关键差异在第 13 章统一对比。


〇、常用命令速查表(先看这个)

0.1 六大基本动作

动作 语法模板 说明
查 DQL SELECT 列 FROM 表 WHERE 条件; 最高频,占日常 80%
增 DML INSERT INTO 表(列1,列2) VALUES(值1,值2); 批量:VALUES(...),(...);
改 DML UPDATE 表 SET 列=新值 WHERE 条件; 忘写 WHERE = 全表更新
删 DML DELETE FROM 表 WHERE 条件; 忘写 WHERE = 全表清空
建 DDL CREATE TABLE 表(列 类型 约束, ...); 见 §3
改结构 DDL ALTER TABLE 表 ADD/MODIFY/CHANGE/DROP 列; 加字段、改类型

0.2 查询语句标准骨架(背下来)

sql 复制代码
SELECT   [DISTINCT] 列, 聚合函数(列)
FROM     表 [别名]
[JOIN    另一张表 [别名] ON 连接条件]
WHERE    行级过滤条件
GROUP BY 分组列
HAVING   分组后过滤条件
ORDER BY 排序列 [ASC|DESC]
LIMIT    条数 [OFFSET 偏移];

0.3 一句话速查(按使用频率排序)

我想做... 写法 难度
查所有列 SELECT * FROM t; ⭐
去重 SELECT DISTINCT col FROM t; ⭐
模糊匹配 WHERE name LIKE '张%'; ⭐
多值匹配 WHERE dept_id IN (10,20); ⭐
区间 WHERE salary BETWEEN 8000 AND 15000; ⭐
空值判断 WHERE x IS NULL / IS NOT NULL ⭐
排序 ORDER BY salary DESC; ⭐
取前 N 行 LIMIT 10; ⭐
分页 LIMIT 10 OFFSET 20; ⭐⭐
建表 CREATE TABLE t(id INT PRIMARY KEY AUTO_INCREMENT, ...); ⭐⭐
加字段 ALTER TABLE t ADD COLUMN age INT DEFAULT 0; ⭐⭐
建索引 CREATE INDEX idx_name ON t(col); ⭐⭐
计数 COUNT(*)、COUNT(DISTINCT col) ⭐⭐
分组统计 GROUP BY dept_id ⭐⭐
分组后过滤 HAVING COUNT(*) > 2 ⭐⭐
两表关联 JOIN d ON e.dept_id = d.dept_id ⭐⭐
插入更新二选一 INSERT ... ON DUPLICATE KEY UPDATE ... ⭐⭐⭐
拼接字符串 CONCAT(a,'-',b) ⭐⭐
分组内合并成串 GROUP_CONCAT(name) ⭐⭐⭐
条件分支 CASE WHEN ... THEN ... ELSE ... END ⭐⭐⭐
组内排名 ROW_NUMBER() OVER(PARTITION BY x ORDER BY y) ⭐⭐⭐⭐
同比环比 LAG(x,1) OVER(ORDER BY y) ⭐⭐⭐⭐
复杂中间结果 WITH t AS (SELECT ...) SELECT ... FROM t ⭐⭐⭐⭐
看执行计划 EXPLAIN SELECT ...; ⭐⭐⭐⭐

0.4 常用函数速查

类别 函数 作用
聚合 COUNT SUM AVG MAX MIN 计数/求和/均值/最大/最小
字符串 CONCAT SUBSTRING TRIM UPPER LOWER REPLACE LENGTH LOCATE 拼接/截取/去空格/大小写/替换/长度/定位
数值 ROUND CEIL FLOOR ABS MOD TRUNCATE 四舍五入/上取整/下取整/绝对值/取余/截断
日期 NOW() CURDATE() DATE_ADD DATEDIFF DATE_FORMAT TIMESTAMPDIFF 当前时间/今天/加减/差值/格式化
条件 IFNULL(x,0) COALESCE(x,...) NULLIF(a,b) IF(c,a,b) CASE WHEN 空值兜底/分支
转换 CAST(x AS 类型) CONVERT(x, 类型) 显式类型转换
分组 GROUP_CONCAT 组内值合并成字符串
窗口 ROW_NUMBER RANK DENSE_RANK LAG LEAD SUM() OVER 见 §8.2
JSON JSON_EXTRACT JSON_OBJECT -> ->> 见 §7.6

0.5 数据类型速查表(建表时对照)⭐⭐

想存什么 推荐类型 理由
主键 ID BIGINT UNSIGNED AUTO_INCREMENT 或 INT 自增整数,索引效率高
姓名 / 标题 / 邮箱 VARCHAR(50~255) 长度可变且不长
手机号 / 身份证 / MD5 / UUID CHAR(11) / CHAR(18) / CHAR(32) 定长用 CHAR,省长度字节
状态 / 类型码(少量枚举) TINYINT 或 ENUM 占 1 字节
布尔 TINYINT(1) MySQL 无原生 BOOLEAN
金额 DECIMAL(10,2) 绝不能用 FLOAT/DOUBLE
百分比 / 比率 DECIMAL(5,4) 精度可控
计数 / 年龄 INT 范围足够
创建时间 DATETIME 或 TIMESTAMP 见 §2.4
纯日期(生日) DATE 3 字节
文章内容 / 长描述 TEXT 超过 VARCHAR 就上 TEXT
图片 / 文件 不建议存库 ,只存路径 VARCHAR 数据库只存 URL/路径
灵活结构 JSON MySQL 5.7+ 原生支持

0.6 DDL 一句话速查 ⭐⭐

操作 语句
建库 CREATE DATABASE db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
删库 DROP DATABASE db;
建表 CREATE TABLE t (...);
复制表结构 CREATE TABLE t2 LIKE t1;
复制表结构+数据 CREATE TABLE t2 AS SELECT * FROM t1;
加列 ALTER TABLE t ADD COLUMN c INT AFTER b;
改列类型 ALTER TABLE t MODIFY COLUMN c VARCHAR(100);
改列名+类型 ALTER TABLE t CHANGE COLUMN old new INT;
删列 ALTER TABLE t DROP COLUMN c;
改表名 RENAME TABLE old TO new;
清空表 TRUNCATE TABLE t;
删表 DROP TABLE IF EXISTS t;
建索引 CREATE INDEX idx ON t(c1,c2);
删索引 DROP INDEX idx ON t;
看建表语句 SHOW CREATE TABLE t;
看表结构 DESC t; / SHOW COLUMNS FROM t;

一、基础概念 ⭐

1.1 SQL 五类语句

分类 全称 作用 代表语句
DQL Data Query Language 查询 SELECT
DML Data Manipulation Language 增删改数据 INSERT UPDATE DELETE
DDL Data Definition Language 定义/修改表结构 CREATE ALTER DROP TRUNCATE RENAME
DCL Data Control Language 权限 GRANT REVOKE
TCL Transaction Control Language 事务 BEGIN COMMIT ROLLBACK SAVEPOINT

💡 面试常问:DDL 与 DML 最大区别是 DDL 会隐式提交事务,无法回滚。

1.2 DELETE / TRUNCATE / DROP 对比(高频考点)

对比项 DELETE TRUNCATE DROP
分类 DML DDL DDL
删除什么 按条件删行 清空所有行,保留表结构 删除整张表(结构+数据+索引)
能否加 WHERE ✅ ❌ ❌
能否回滚 ✅ 可回滚 ❌ 隐式提交,不可回滚 ❌ 不可回滚
是否记日志 ✅ 逐行记 undo/binlog(慢) ❌ 只记少量(快) ---
自增 ID 不重置 重置为 1 ---
触发器 ✅ 触发 ❌ 不触发 ❌
释放表空间 ❌ 需 OPTIMIZE TABLE ✅ 立即释放 ✅
速度 慢 快 快

1.3 术语对照表

术语 含义 类比
Database / Schema 数据库(MySQL 中二者基本等价) 一个 Excel 文件
Table 表 Excel 中的一个 sheet
Row / Record 行 / 记录 sheet 中的一行
Column / Field 列 / 字段 sheet 中的一列
Primary Key 主键,唯一标识一行 身份证号
Foreign Key 外键,指向另一表主键 引用别的 sheet 的 ID
Index 索引,加速查询 书的目录
View 视图,虚拟表 保存好的查询结果
Transaction 事务,一组原子操作 转账的"扣款+入账"

1.4 命名规范与注释

sql 复制代码
-- 单行注释(-- 后必须有空格)
# MySQL 特有的单行注释
/* 多行
   注释 */

-- 推荐命名:小写 + 下划线,见名知意
CREATE TABLE user_order (
    id         BIGINT PRIMARY KEY,
    user_id    BIGINT NOT NULL,
    created_at DATETIME NOT NULL
);
规范项 建议
大小写 关键字大写、表名列名小写(可读性)
分隔符 下划线 user_id,别用驼峰
布尔字段 用 is_ 前缀:is_deleted
时间字段 用 _at 后缀:created_at、updated_at
禁止 用保留字做列名(order、desc、status 需加反引号或改名)
长度 表名 ≤ 32 字符

二、数据类型(重点补充)⭐⭐

2.1 三大类概览

大类 具体类型 常见用途
数值 TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT、DECIMAL、FLOAT/DOUBLE、BIT ID、数量、金额、比率
字符串 CHAR、VARCHAR、TEXT 家族、ENUM、SET 姓名、地址、正文
日期时间 DATE、TIME、DATETIME、TIMESTAMP、YEAR 生日、下单时间
二进制 BINARY、VARBINARY、BLOB 家族 图片、文件、加密数据
特殊 JSON、GEOMETRY(空间) 灵活结构、地理位置

2.2 CHAR vs VARCHAR vs TEXT(★重点,最容易搞混)

sql 复制代码
CREATE TABLE demo (
    code    CHAR(6),        -- 定长 6 字符
    name    VARCHAR(50),    -- 变长,最多 50 字符
    content TEXT            -- 长文本
);
核心对比表
对比维度 CHAR(n) VARCHAR(n) TEXT
存储方式 定长,不足补空格到 n 变长,实际长度 + 1~2 字节长度前缀 变长,过长时存行外(溢出页),行内只留指针
最大长度 255 字符 受行总 65535 字节限制(utf8mb4 下约 16383 字符) TEXT 64KB / MEDIUMTEXT 16MB / LONGTEXT 4GB
空间占用 声明多少占多少(可能浪费) 按实际内容(省空间) 按实际内容
尾随空格 存入时补空格,读取时自动去掉 原样保留 原样保留
默认值 DEFAULT ✅ 支持 ✅ 支持 ❌ MySQL 中不支持
索引 整列可建索引 整列可建(过长需前缀索引) ⚠️ 必须指定前缀长度 INDEX(content(20))
排序比较 全列参与 全列参与 只比较前 max_sort_length(默认 1024)字节
内存临时表 定长,引擎友好 变长 大字段易导致内存临时表转磁盘临时表(慢)
WHERE / ORDER 性能 最快(定长、无碎片) 快 慢(尤其排序、去重)
UPDATE 碎片 无 变长更新可能产生行碎片 可能产生
能否整行存内存 ✅ ✅ 超阈值时只存指针,需额外 IO
选择建议表
场景 选什么 为什么
手机号(11 位)、身份证(18 位) CHAR(11) / CHAR(18) 长度固定,CHAR 省掉长度前缀字节,定长查找更快
国家代码、状态码、性别 CHAR(2) / CHAR(1) 定长且极短
MD5(32)、UUID(36) CHAR(32) / CHAR(36) 定长
姓名、邮箱、标题、地址 VARCHAR(50~255) 长度变化大
商品描述、短备注 VARCHAR(500~2000) 仍在单行舒适区内
文章内容、评论正文、日志、大 JSON TEXT / MEDIUMTEXT 超过 VARCHAR 舒适区
频繁被 WHERE/ORDER BY/GROUP BY 的字段 绝不用 TEXT 会拖垮性能
必须知道的 6 个细节

① VARCHAR(n) 里的 n 是「字符数」不是「字节数」(MySQL 5.0.3 之后)

sql 复制代码
-- utf8mb4 下一个汉字占 4 字节
VARCHAR(10)   -- 最多存 10 个汉字 或 10 个英文字母,但最多占 40 字节

② 长度前缀是 1 字节还是 2 字节?

列定义的最大字节数 长度前缀
≤ 255 字节(如 utf8mb4 的 VARCHAR(63)) 1 字节
> 255 字节 2 字节

所以能省则省:utf8mb4 下 VARCHAR(255) 比 VARCHAR(256) 每个值省 1 字节。

③ 行总长度限制 65535 字节

sql 复制代码
-- ❌ 报错:Row size too large
CREATE TABLE bad (a VARCHAR(20000), b VARCHAR(20000)) CHARSET utf8mb4;
-- utf8mb4: 20000*4 + 20000*4 = 160000 字节 > 65535

TEXT / BLOB 在行内只占 9~12 字节指针,不计入这个限制 ------ 这也是长文本必须用 TEXT 的原因之一。

④ CHAR 的尾随空格"吃掉"现象

sql 复制代码
CREATE TABLE t (c CHAR(5), v VARCHAR(5));
INSERT INTO t VALUES ('ab  ', 'ab  ');
SELECT CONCAT('[', c, ']'), CONCAT('[', v, ']') FROM t;
-- 结果:[ab]    [ab  ]     ← CHAR 的尾随空格被去掉了!

如果尾随空格有业务含义(如定长文件导入),改用 VARCHAR。

⑤ TEXT 家族容量表

类型 最大长度 约等于
TINYTEXT 255 字节 短句
TEXT 64 KB 一篇文章
MEDIUMTEXT 16 MB 一本书
LONGTEXT 4 GB 极大文本

⑥ TEXT vs BLOB

对比 TEXT BLOB
内容 文本,有字符集和排序规则 二进制字节流,无字符集
比较排序 按字符序 按字节
用途 文章、描述 图片、文件、加密数据

⑦ 性能最佳实践:TEXT 垂直拆分

sql 复制代码
-- ✅ 推荐:主表只放常用小字段,正文单独一张表
CREATE TABLE article (id BIGINT PRIMARY KEY, title VARCHAR(200), author_id BIGINT, created_at DATETIME);
CREATE TABLE article_content (article_id BIGINT PRIMARY KEY, content MEDIUMTEXT);

列表页只查 article 很快,点进详情才查 article_content。

2.3 数值类型

整数类型对照表
类型 字节 有符号范围 无符号范围 用途
TINYINT 1 -128 ~ 127 0 ~ 255 状态码、年龄、布尔
SMALLINT 2 ±32767 0 ~ 65535 小计数
MEDIUMINT 3 ±838 万 0 ~ 1677 万 中等计数
INT / INTEGER 4 ±21 亿 0 ~ 42 亿 最常用
BIGINT 8 ±922 亿亿 0 ~ 1844 亿亿 主键、大额、雪花 ID
sql 复制代码
id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT   -- 无符号,范围翻倍
age        TINYINT UNSIGNED                          -- 0~255,够用且省空间
is_deleted TINYINT(1) DEFAULT 0                      -- 当作布尔用

⚠️ INT(11) 里的 11 不是长度! 那是"显示宽度",配合 ZEROFILL 才有用,MySQL 8.0.17 起已废弃。存储范围只由类型本身(INT)决定。

DECIMAL vs FLOAT vs DOUBLE(★金额必考)
对比 DECIMAL(M,D) FLOAT DOUBLE
精度 精确定点数 单精度近似(约 7 位有效数字) 双精度近似(约 15 位)
是否有误差 ❌ 无 ✅ 有 ✅ 有
存储 变长(每 4 字节存 9 位) 4 字节 8 字节
计算速度 慢(十进制运算) 快 快
适用场景 金额、汇率、税率 科学计算 科学计算
sql 复制代码
-- 灾难演示
SELECT 0.1 + 0.2 = 0.3;    -- 结果 0(FALSE!浮点误差)
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2))
     = CAST(0.3 AS DECIMAL(10,2));    -- 结果 1(TRUE)
参数 含义 示例
M 总位数(精度),最大 65 DECIMAL(10,2) → 共 10 位
D 小数位数(标度),最大 30 → 小数 2 位,整数 8 位

铁律:凡是涉及钱、必须精确对账的,一律 DECIMAL。

2.4 日期时间类型(★重点)

类型 字节 格式 范围 时区敏感 用途
DATE 3 YYYY-MM-DD 1000-01-01 ~ 9999-12-31 ❌ 生日、纯日期
TIME 3 HH:MM:SS -838:59:59 ~ 838:59:59 ❌ 时长、营业时间
YEAR 1 YYYY 1901 ~ 2155 ❌ 年份
DATETIME 8 YYYY-MM-DD HH:MM:SS 1000 ~ 9999 年 ❌ 不随时区变 推荐:下单时间、创建时间
TIMESTAMP 4 YYYY-MM-DD HH:MM:SS 1970 ~ 2038-01-19 ✅ 随时区转换 需要自动更新 / 跨时区
DATETIME vs TIMESTAMP 深度对比
对比项 DATETIME TIMESTAMP
存储空间 8 字节 4 字节(省一半)
时间范围 1000~9999 年 1970~2038(2038 问题)
时区 存什么读什么,不转换 存时转 UTC,读时转回当前时区
NULL 可为 NULL 默认 NOT NULL,且自动填充当前时间
自动更新 需显式写 ON UPDATE CURRENT_TIMESTAMP 默认第一个 TIMESTAMP 列会自动设置
索引效率 略低 略高
sql 复制代码
CREATE TABLE t (
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    birthday   DATE,
    duration   TIME
);

2038 问题 :TIMESTAMP 用 32 位存秒,2038-01-19 03:14:07 后溢出。长期系统建议 DATETIME。

2.5 ENUM / SET

类型 含义 存储 示例
ENUM 单选,从预设值里选一个 1~2 字节(存下标) ENUM('男','女','未知')
SET 多选,可选多个 1~8 字节(位图) SET('read','write','delete')
sql 复制代码
CREATE TABLE user (
    gender ENUM('男','女','未知') DEFAULT '未知',
    perms  SET('read','write','delete')
);
INSERT INTO user VALUES ('男', 'read,write');
优点 缺点
省空间、自带约束(非法值插不进) 增删枚举值要 ALTER TABLE,线上有风险
可读性好 排序按下标而非字母序,反直觉
跨数据库移植差

实践建议 :变动频繁的状态码用 TINYINT + 字典表,而不是 ENUM。

2.6 JSON 类型(MySQL 5.7+)

sql 复制代码
CREATE TABLE config (id BIGINT PRIMARY KEY, data JSON);
INSERT INTO config VALUES (1, '{"theme":"dark","lang":"zh"}');

SELECT data->>'$.theme' FROM config;               -- 'dark'(去引号)
SELECT JSON_EXTRACT(data, '$.lang') FROM config;   -- "zh"(带引号)
SELECT * FROM config WHERE data->>'$.theme' = 'dark';
函数 作用
JSON_EXTRACT(j,path) / j->path 取值(带引号)
j->>path 取值(自动去引号)
JSON_OBJECT(k,v,...) 构造 JSON 对象
JSON_ARRAY(v,...) 构造 JSON 数组
JSON_SET / JSON_REPLACE / JSON_REMOVE 修改
JSON_CONTAINS(j,val,path) 是否包含
JSON_LENGTH(j) 元素个数

⚠️ JSON 列不能直接建索引,需借助生成列间接索引:

sql 复制代码
ALTER TABLE config ADD COLUMN theme VARCHAR(20)
    GENERATED ALWAYS AS (data->>'$.theme') STORED;
CREATE INDEX idx_theme ON config(theme);

2.7 隐式类型转换陷阱表 ⭐⭐⭐

场景 后果 例子
字符串列 vs 数字比较 索引失效,全表扫描 WHERE phone = 13800138000(phone 是 VARCHAR)
VARCHAR 列存数字后排序 按字典序:'10' < '9' ORDER BY varchar_col
日期列 vs 字符串 一般可转换,格式错则变 NULL '2024/01/01' 不被识别
+ 运算 字符串被强转数字,非数字变 0 '1abc' + 1 = 2(有 warning)

口诀:比较时两侧类型必须一致,否则索引白建。

2.8 类型选择总原则

原则 说明
越小越好 能用 TINYINT 就别用 INT,省磁盘、省内存、省索引空间
简单越好 整数 > 字符串;IP 用 INT UNSIGNED 而非 VARCHAR(15)
避免 NULL 尽量 NOT NULL DEFAULT '',NULL 让索引、统计、比较都变复杂
金额用 DECIMAL 绝不用浮点
定长用 CHAR 手机号、编码、MD5
长文本拆表 TEXT 独立出去

三、DDL 数据定义语言 ⭐⭐

3.1 CREATE TABLE 完整语法

sql 复制代码
CREATE TABLE IF NOT EXISTS employee (
    emp_id     BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '员工ID',
    name       VARCHAR(50)     NOT NULL                COMMENT '姓名',
    dept_id    INT                                     COMMENT '部门ID',
    salary     DECIMAL(10,2)   NOT NULL DEFAULT 0.00   COMMENT '月薪',
    hire_date  DATE            NOT NULL                COMMENT '入职日期',
    manager_id BIGINT UNSIGNED                         COMMENT '上级ID',
    PRIMARY KEY (emp_id),
    UNIQUE KEY uk_name_dept (name, dept_id),
    KEY        idx_dept (dept_id),
    CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='员工表';
组成部分 说明
IF NOT EXISTS 表已存在时不报错
列定义 列名 类型 [约束] [DEFAULT 默认值] [COMMENT 注释]
PRIMARY KEY 主键
UNIQUE KEY 唯一索引
KEY / INDEX 普通索引
FOREIGN KEY 外键
ENGINE=InnoDB 存储引擎(默认,支持事务 + 行锁)
CHARSET=utf8mb4 必须 utf8mb4 (utf8 是残缺的 3 字节,存不了 emoji)
COLLATE 排序规则,utf8mb4_unicode_ci 不区分大小写

3.2 约束详解表(★重点)

约束 关键字 作用 说明
主键 PRIMARY KEY 唯一标识一行 隐含 NOT NULL + UNIQUE;一表仅一个;InnoDB 中是聚簇索引
外键 FOREIGN KEY 引用另一表主键 保证引用完整性;大厂常禁用(性能与分库限制)
唯一 UNIQUE 值不可重复 允许多个 NULL;一表可多个
非空 NOT NULL 不能为空 强烈建议都加
默认 DEFAULT 没给值时的兜底 DEFAULT 0、DEFAULT CURRENT_TIMESTAMP
自增 AUTO_INCREMENT 自动递增 必须作用在索引列(通常主键)
检查 CHECK 自定义条件 CHECK (salary > 0);MySQL 8.0.16 才真正生效
sql 复制代码
-- 组合主键
PRIMARY KEY (order_id, product_id)

-- 建表后加/删约束
ALTER TABLE employee ADD CONSTRAINT uk_email UNIQUE (email);
ALTER TABLE employee ADD CONSTRAINT ck_salary CHECK (salary >= 0);
ALTER TABLE employee DROP CHECK ck_salary;
PRIMARY KEY vs UNIQUE
对比 PRIMARY KEY UNIQUE
数量 每表 1 个 可多个
NULL ❌ 不允许 ✅ 允许多个 NULL
索引类型 聚簇索引(InnoDB) 二级索引
语义 行的身份 业务唯一性

3.3 ALTER TABLE 常见操作

sql 复制代码
ALTER TABLE employee ADD COLUMN email VARCHAR(100) AFTER name;   -- 加列并指定位置
ALTER TABLE employee ADD COLUMN age TINYINT FIRST;               -- 加到最前
ALTER TABLE employee MODIFY COLUMN name VARCHAR(100) NOT NULL;   -- 改类型/约束
ALTER TABLE employee CHANGE COLUMN name full_name VARCHAR(100);  -- 改列名+类型
ALTER TABLE employee ALTER COLUMN salary SET DEFAULT 5000;       -- 设默认值
ALTER TABLE employee DROP COLUMN age;                            -- 删列
ALTER TABLE employee RENAME TO staff;                            -- 改表名
ALTER TABLE employee ADD INDEX idx_salary (salary);              -- 加索引
ALTER TABLE employee CHARACTER SET = utf8mb4;                    -- 改字符集

⚠️ 线上变更风险 :大表 ALTER 会锁表(MySQL 8.0 支持 ALGORITHM=INSTANT 秒级加列,但改类型仍需重建表)。生产建议用 pt-online-schema-change 或 gh-ost。

3.4 索引的 DDL

sql 复制代码
CREATE INDEX idx_dept_salary ON employee(dept_id, salary);   -- 联合索引
CREATE UNIQUE INDEX uk_code ON product(code);                -- 唯一索引
CREATE FULLTEXT INDEX ft_content ON article(content);        -- 全文索引
ALTER TABLE employee ADD INDEX idx_name(name(10));           -- 前缀索引
DROP INDEX idx_dept_salary ON employee;
SHOW INDEX FROM employee;

四、DML 数据操作 ⭐⭐

4.1 INSERT 插入

sql 复制代码
-- 单行全列
INSERT INTO employees VALUES (1,'张三',10,8000.00,'2021-03-01',3);

-- 指定列(推荐)
INSERT INTO employees (name, dept_id, salary, hire_date)
VALUES ('李四', 10, 12000.00, '2020-07-15');

-- 批量(效率高很多)
INSERT INTO employees (name, dept_id, salary) VALUES
  ('王五', 20, 15000),
  ('赵六', 30, 9000),
  ('孙七', NULL, 7000);

-- 从查询结果插入
INSERT INTO employee_bak (name, salary)
SELECT name, salary FROM employees WHERE dept_id = 10;
四种"冲突处理"写法对比(★重点)
写法 遇到唯一键冲突时 说明
INSERT INTO 直接报错,整条失败 默认行为
INSERT IGNORE INTO 静默跳过 有 warning,其他错误也会被忽略(慎用)
INSERT ... ON DUPLICATE KEY UPDATE 执行 UPDATE 最常用(upsert)
REPLACE INTO 先 DELETE 再 INSERT ⚠️ 会删旧行,自增 ID 变化、触发删除触发器
sql 复制代码
-- upsert:存在则累加,不存在则插入
INSERT INTO daily_stat (stat_date, pv)
VALUES ('2024-01-01', 1)
ON DUPLICATE KEY UPDATE pv = pv + 1;

4.2 UPDATE 更新

sql 复制代码
-- 单表
UPDATE employees SET salary = salary * 1.1 WHERE dept_id = 10;

-- 关联更新(JOIN)
UPDATE employees e
JOIN departments d ON e.dept_id = d.dept_id
SET e.salary = e.salary * 1.2
WHERE d.location = '北京';

-- 带 LIMIT,防止误伤全表
UPDATE employees SET salary = 8000 WHERE dept_id IS NULL LIMIT 10;

-- 按排序更新
UPDATE employees SET salary = 8000 ORDER BY hire_date LIMIT 5;

⚠️ 安全习惯 :先写 SELECT 验证 WHERE 条件命中行数,确认无误再改成 UPDATE。MySQL 开启 sql_safe_updates 后,不带主键条件的 UPDATE/DELETE 会被拒绝。

4.3 DELETE 删除

sql 复制代码
DELETE FROM employees WHERE emp_id = 5;
DELETE FROM employees WHERE dept_id IS NULL LIMIT 100;   -- 分批删,避免长事务

-- 多表删除
DELETE e
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE d.location = '上海';

4.4 外键级联行为表

动作 含义
CASCADE 主表删/改,从表跟着删/改
SET NULL 主表删/改,从表外键置 NULL(要求该列可为 NULL)
RESTRICT / NO ACTION 默认,从表还有引用则禁止主表删/改
SET DEFAULT 置为默认值(InnoDB 不支持)
sql 复制代码
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
  ON DELETE SET NULL
  ON UPDATE CASCADE

4.5 软删除 vs 硬删除

方式 做法 优点 缺点
硬删除 DELETE 干净、省空间 不可恢复,破坏外键
软删除(推荐) UPDATE SET is_deleted=1 可恢复、保留历史 表变大,查询都要带 is_deleted=0

五、DQL 基础查询 ⭐

5.1 SELECT / FROM

sql 复制代码
SELECT * FROM employees;                                 -- 查所有列
SELECT name, salary FROM employees;                      -- 指定列
SELECT name AS 姓名, salary * 12 AS 年薪 FROM employees; -- 别名 + 运算
SELECT DISTINCT dept_id FROM employees;                  -- 去重

SELECT * 的四宗罪 :① 多传无用列,浪费网络/IO;② 无法走覆盖索引;③ 表结构变更时程序易崩;④ 无法看清依赖。

5.2 WHERE 运算符表

运算符 含义 示例
= != <> 等于 / 不等于 dept_id <> 10
> < >= <= 比较 salary >= 8000
AND OR NOT 逻辑 a AND (b OR c)
BETWEEN a AND b 闭区间 a,b salary BETWEEN 8000 AND 15000
IN / NOT IN 属于集合 dept_id IN (10,20)
LIKE 模糊匹配 name LIKE '张%'
IS NULL / IS NOT NULL 空值判断 dept_id IS NULL
EXISTS 子查询是否有结果 EXISTS (SELECT 1 FROM ...)
REGEXP / RLIKE 正则匹配 name REGEXP '^张'

⚠️ 优先级陷阱 :AND 优先级 > OR。

WHERE a=1 OR b=2 AND c=3 ≡ a=1 OR (b=2 AND c=3)。混用时必须加括号。

5.3 LIKE 与 REGEXP

模式 含义 对 张三丰
'张%' 以张开头 ✅
'%丰' 以丰结尾 ✅
'张_丰' 张 + 任意 1 字符 + 丰 ✅
'%三%' 包含三 ✅
'\_' 转义下划线本身 匹配 _
sql 复制代码
SELECT * FROM employees WHERE name REGEXP '^[张李]';              -- 姓张或李
SELECT * FROM employees WHERE phone REGEXP '^1[3-9][0-9]{9}$';    -- 手机号校验

性能 :'%xx' 前置通配符 → 索引失效 ;'xx%' → 可以走索引。

5.4 NULL 的三值逻辑(★重点)

sql 复制代码
SELECT * FROM employees WHERE dept_id = NULL;    -- ❌ 永远返回空!
SELECT * FROM employees WHERE dept_id IS NULL;   -- ✅ 正确
表达式 结果
NULL = NULL UNKNOWN(不是 TRUE)
NULL <> NULL UNKNOWN
NULL AND TRUE UNKNOWN
NULL AND FALSE FALSE
NULL OR TRUE TRUE
NULL OR FALSE UNKNOWN
NOT NULL UNKNOWN

只有 WHERE 结果为 TRUE 的行才返回 ,UNKNOWN 被过滤 → 这就是 = NULL 查不到数据的原因。

另外:COUNT(col)、SUM()、AVG() 都自动忽略 NULL ;COUNT(*) 不忽略。

5.5 ORDER BY 排序

sql 复制代码
ORDER BY salary DESC, name ASC;                        -- 多列排序
ORDER BY ISNULL(dept_id), dept_id;                     -- MySQL 让 NULL 排最后
ORDER BY FIELD(status,'已付款','已退款','待付款');     -- 自定义顺序
场景 写法
升序 ASC(默认,可省略)
降序 DESC
NULL 排最后(MySQL) ORDER BY col IS NULL, col
NULL 排最前(MySQL) ORDER BY col IS NOT NULL, col
中文按拼音 字段使用 utf8mb4_unicode_ci 排序规则

5.6 LIMIT / OFFSET 分页

需求 写法 等价
前 10 条 LIMIT 10 LIMIT 0, 10
第 3 页(每页 10 条) LIMIT 10 OFFSET 20 LIMIT 20, 10

LIMIT a, b = 跳过 a 条、取 b 条,顺序别搞反。

5.7 SQL 逻辑执行顺序(★必背)

书写顺序 ≠ 执行顺序。理解这张表,很多"怪问题"就通了。

书写顺序 执行顺序 阶段 关键说明
1 SELECT 6 投影 选列,别名在此才生效
2 FROM 1 取表 加载数据源
3 JOIN ... ON 2 连接 生成中间结果集
4 WHERE 3 行过滤 不能用聚合、不能用 SELECT 别名
5 GROUP BY 4 分组 行被压成组
6 HAVING 5 组过滤 可以用聚合函数
7 DISTINCT 7 去重
8 ORDER BY 8 排序 可以用 SELECT 别名
9 LIMIT 9 截断 最后执行

由此推出 3 条铁律:

  1. WHERE 里不能 引用 SELECT 定义的别名(执行时别名还没生成)。
  2. WHERE 里不能用聚合函数(聚合发生在 GROUP BY 之后)。
  3. ORDER BY 可以用别名。
sql 复制代码
-- ❌ 错:WHERE 里用别名
SELECT salary * 12 AS 年薪 FROM employees WHERE 年薪 > 100000;
-- ✅ 对:重写表达式,或包一层子查询
SELECT * FROM (SELECT salary * 12 AS 年薪 FROM employees) t WHERE 年薪 > 100000;

六、进阶查询 ⭐⭐⭐

6.1 聚合函数

函数 作用 是否忽略 NULL
COUNT(*) 统计行数(含全 NULL 行) 否
COUNT(col) 统计该列非 NULL 值个数 是
COUNT(DISTINCT col) 非重复值个数 是
SUM(col) / AVG(col) 求和 / 平均 是
MAX(col) / MIN(col) 最大 / 最小 是
GROUP_CONCAT(col) 组内值拼成字符串 是
sql 复制代码
SELECT COUNT(*) AS 总人数,
       COUNT(dept_id) AS 有部门人数,     -- NULL 不计
       AVG(salary) AS 平均薪资,
       GROUP_CONCAT(name ORDER BY salary DESC SEPARATOR '、') AS 名单
FROM employees;
-- 名单示例:'王五、李四、赵六、张三'

COUNT(*) vs COUNT(1) vs COUNT(col):前两者都统计行数(现代引擎优化后性能一致);COUNT(col) 跳过 NULL,语义不同。

6.2 GROUP BY / HAVING

sql 复制代码
SELECT dept_id, COUNT(*) AS 人数, AVG(salary) AS 平均薪资
FROM employees
GROUP BY dept_id
HAVING COUNT(*) >= 2
ORDER BY 平均薪资 DESC;
WHERE vs HAVING 对比(★高频考点)
对比项 WHERE HAVING
执行时机 分组前过滤原始行 分组后过滤分组结果
能否用聚合函数 ❌ ✅
能否用 SELECT 别名 ❌ ✅(部分引擎支持)
能否用普通列 ✅ 只能用 GROUP BY 中出现的列
性能 更优(先缩小数据量) 较差

黄金法则:能放 WHERE 的条件绝不放 HAVING。

GROUP BY 铁律 :SELECT 里出现的非聚合列必须全部出现在 GROUP BY 中 ,否则结果不确定(MySQL 5.7+ 默认开启 ONLY_FULL_GROUP_BY,会直接报错)。

6.3 分组小计:WITH ROLLUP

sql 复制代码
SELECT dept_id, COUNT(*) AS 人数 FROM employees GROUP BY dept_id WITH ROLLUP;
dept_id 人数
10 2
20 1
30 1
NULL 4 ← 合计行

6.4 JOIN 多表连接(★重点)

sql 复制代码
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;
六种连接对比表
类型 关键字 结果集 集合表示
内连接 INNER JOIN / JOIN 只保留两边都匹配的行 A ∩ B
左连接 LEFT JOIN 左表全保留,右表无匹配补 NULL A 全部
右连接 RIGHT JOIN 右表全保留,左表无匹配补 NULL B 全部
全连接 FULL OUTER JOIN 两边全保留,各自无匹配补 NULL A ∪ B
交叉连接 CROSS JOIN 笛卡尔积(行数 = M×N) A × B
自连接 SELF JOIN 同一张表跟自己连 A ⋈ A
复制代码
   INNER JOIN          LEFT JOIN           RIGHT JOIN          FULL OUTER
  ┌───┐ ┌───┐        ┌───┐ ┌───┐        ┌───┐ ┌───┐        ┌───┐ ┌───┐
  │░░░│ │   │        │███│ │░░░│        │░░░│ │███│        │███│ │░░░│
  │ A │ │ B │        │ A │ │ B │        │ A │ │ B │        │ A │ │ B │
  └───┘ └───┘        └───┘ └───┘        └───┘ └───┘        └───┘ └───┘
  只取交集          左表全要           右表全要           两边全要
  ███ = 一定保留的部分   ░░░ = 匹配上才保留 / 补 NULL 保留

MySQL 不支持 FULL OUTER JOIN ,用 LEFT JOIN ... UNION ... RIGHT JOIN 模拟。

自连接示例(查员工及其上级):

sql 复制代码
SELECT e.name AS 员工, m.name AS 上级
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
ON vs WHERE 在 LEFT JOIN 中的差别(★经典陷阱)
写法 效果
LEFT JOIN b ON a.id=b.id AND b.status=1 保留左表全部行,右表只匹配 status=1 的
LEFT JOIN b ON a.id=b.id WHERE b.status=1 ⚠️ 退化成 INNER JOIN(NULL 行被 WHERE 过滤)

6.5 UNION / UNION ALL

关键字 去重 性能
UNION ✅ 排序去重 慢
UNION ALL ❌ 直接拼接 快,优先用
sql 复制代码
SELECT name FROM employees
UNION ALL
SELECT dept_name FROM departments;   -- 列数、类型需兼容

6.6 子查询

类型 位置 示例
标量子查询 SELECT 列 (SELECT AVG(salary) FROM employees)
列子查询 WHERE + IN WHERE dept_id IN (SELECT dept_id FROM ...)
相关子查询 WHERE 引用外层列 WHERE salary > (SELECT AVG(salary) FROM employees x WHERE x.dept_id = e.dept_id)
派生表 FROM FROM (SELECT ... GROUP BY ...) t
EXISTS WHERE WHERE EXISTS (SELECT 1 FROM ... WHERE ...)
IN vs EXISTS 对比
对比 IN EXISTS
执行方式 先算出子查询集合,再判断包含 对外层每行执行一次子查询
适合 子查询结果集小 子查询结果集大 / 有关联条件
NULL 陷阱 ⚠️ 子查询含 NULL 时 NOT IN 永远返回空 ✅ 不受 NULL 影响

⚠️ NOT IN + NULL 灾难:

sql 复制代码
SELECT * FROM employees WHERE dept_id NOT IN (SELECT dept_id FROM departments);
-- 若 departments 中有 dept_id 为 NULL 的行 → 结果永远为空!
-- 因为 x NOT IN (10,20,NULL) 恒为 UNKNOWN
-- ✅ 解决:改用 NOT EXISTS,或子查询加 WHERE dept_id IS NOT NULL

6.7 派生表必须起别名

sql 复制代码
-- ❌ MySQL 报错:Every derived table must have its own alias
SELECT * FROM (SELECT dept_id FROM employees GROUP BY dept_id);
-- ✅
SELECT * FROM (SELECT dept_id FROM employees GROUP BY dept_id) t;

七、函数大全 ⭐⭐⭐

7.1 字符串函数

函数 作用 示例 → 结果
CONCAT(a,b,...) 拼接(遇 NULL 整体为 NULL) CONCAT('A','-','B') → A-B
CONCAT_WS(sep,a,b) 带分隔符拼接(跳过 NULL) CONCAT_WS('-','A',NULL,'B') → A-B
SUBSTRING(s,pos,len) 截取(pos 从 1 开始) SUBSTRING('abcdef',2,3) → bcd
LEFT(s,n) / RIGHT(s,n) 取左/右 n 个字符 LEFT('abcdef',3) → abc
TRIM / LTRIM / RTRIM 去空格 TRIM(' x ') → x
UPPER(s) / LOWER(s) 大小写 UPPER('ab') → AB
REPLACE(s,old,new) 替换 REPLACE('a-b','-','_') → a_b
LENGTH(s) 字节长度 utf8mb4 下 '中文' → 6
CHAR_LENGTH(s) 字符个数 '中文' → 2
LOCATE(sub,s) 子串位置(从 1 起,0 = 没找到) LOCATE('b','abc') → 2
LPAD(s,n,pad) / RPAD 左/右补位 LPAD('7',3,'0') → 007
FIND_IN_SET(s,list) 在逗号串中找位置 FIND_IN_SET('b','a,b,c') → 2

⚠️ CONCAT 遇 NULL 结果就是 NULL,要兜底用 IFNULL 或改用 CONCAT_WS。

7.2 数值函数

函数 作用 示例 → 结果
ROUND(x,d) 四舍五入 ROUND(3.1415,2) → 3.14
TRUNCATE(x,d) 直接截断 TRUNCATE(3.149,2) → 3.14
CEIL(x) / FLOOR(x) 上/下取整 CEIL(3.2) → 4
ABS(x) 绝对值
MOD(a,b) / a % b 取余 MOD(7,3) → 1
RAND() 0~1 随机 ORDER BY RAND() LIMIT 1(大表慢)
GREATEST(a,b,...) / LEAST 最大 / 最小
FORMAT(x,d) 千分位格式化 FORMAT(1234567,2) → 1,234,567.00

7.3 日期函数

函数 作用
NOW() / CURDATE() / CURTIME() 当前日期时间 / 日期 / 时间
DATE(d) / TIME(d) 提取日期 / 时间部分
YEAR/MONTH/DAY(d) 取年 / 月 / 日
DATE_ADD(d, INTERVAL n unit) 日期加:DATE_ADD(NOW(), INTERVAL 7 DAY)
DATE_SUB(d, INTERVAL n unit) 日期减
DATEDIFF(d1,d2) 相差天数(d1 - d2)
TIMESTAMPDIFF(unit,d1,d2) 相差指定单位(YEAR/MONTH/DAY/HOUR...)
DATE_FORMAT(d,fmt) 格式化:DATE_FORMAT(NOW(),'%Y-%m-%d')
STR_TO_DATE(s,fmt) 字符串转日期
LAST_DAY(d) 当月最后一天
DAYOFWEEK(d) 星期几(1 = 周日)
UNIX_TIMESTAMP(d) / FROM_UNIXTIME(n) 时间戳互转

常用格式符 :%Y 四位年、%y 两位年、%m 月、%d 日、%H 24 小时、%i 分、%s 秒、%W 星期名。

7.4 条件函数

函数 作用 示例
IF(cond,a,b) 三元表达式 IF(salary>10000,'高','低')
IFNULL(x,default) x 为 NULL 返回 default IFNULL(dept_id,0)
COALESCE(a,b,c) 返回第一个非 NULL COALESCE(NULL,NULL,3) → 3
NULLIF(a,b) a=b 返回 NULL,否则返回 a NULLIF(b,0) 防除零
CASE WHEN 多分支 见 §8.1
sql 复制代码
SELECT salary / NULLIF(bonus_count, 0) FROM t;   -- 安全除法,避免除零报错

7.5 类型转换

sql 复制代码
SELECT CAST('123' AS UNSIGNED);     -- 字符串转数字
SELECT CAST(salary AS CHAR);        -- 数字转字符串
SELECT CONVERT('2024-01-01', DATE);
SELECT '123' + 0;                   -- 隐式转数字(小技巧)
目标类型 写法
整数 SIGNED / UNSIGNED
小数 DECIMAL(10,2)
字符串 CHAR / CHAR(20)
日期 DATE / DATETIME / TIME
二进制 BINARY

7.6 JSON 函数

sql 复制代码
SELECT JSON_OBJECT('id',1,'name','张三');      -- {"id": 1, "name": "张三"}
SELECT JSON_ARRAY(1,2,3);                      -- [1, 2, 3]
SELECT JSON_EXTRACT('{"a":1}', '$.a');         -- 1
SELECT JSON_SET('{"a":1}', '$.b', 2);          -- {"a": 1, "b": 2}
SELECT JSON_LENGTH('{"a":1,"b":2}');           -- 2
SELECT JSON_VALID('{"a":1}');                  -- 1

7.7 系统 / 元信息函数

函数 作用
DATABASE() 当前数据库名
VERSION() 数据库版本
USER() / CURRENT_USER() 当前用户
LAST_INSERT_ID() 最近插入的自增 ID
ROW_COUNT() 上条 DML 影响的行数
FOUND_ROWS() 配合 SQL_CALC_FOUND_ROWS 取总数

八、高级特性 ⭐⭐⭐⭐

8.1 CASE WHEN ------ SQL 里的 if-else

sql 复制代码
SELECT name, salary,
  CASE
    WHEN salary >= 15000 THEN '高'
    WHEN salary >= 10000 THEN '中'
    ELSE '低'
  END AS 薪资档位
FROM employees;
两种写法 语法 适用
简单 CASE CASE 列 WHEN 值1 THEN r1 WHEN 值2 THEN r2 END 只有等值判断
搜索 CASE CASE WHEN 条件1 THEN r1 WHEN 条件2 THEN r2 END 任意条件(更灵活)

⚠️ 忘记写 ELSE 时,不满足任何分支返回 NULL。

实战:行转列(透视表)

sql 复制代码
SELECT dept_id,
  SUM(CASE WHEN salary >= 15000 THEN 1 ELSE 0 END) AS 高薪人数,
  SUM(CASE WHEN salary <  15000 THEN 1 ELSE 0 END) AS 普通人数,
  SUM(CASE WHEN hire_date >= '2022-01-01' THEN 1 ELSE 0 END) AS 新人
FROM employees
GROUP BY dept_id;

8.2 窗口函数 ------ 高级 SQL 的分水岭 ⭐⭐⭐⭐

基本语法

sql 复制代码
函数名() OVER (
  [PARTITION BY 分组列]                 -- 类似 GROUP BY,但不合并行
  [ORDER BY 排序列 [ASC|DESC]]          -- 决定计算顺序
  [ROWS|RANGE BETWEEN ... AND ...]      -- 滑动窗口范围
)
窗口函数 vs GROUP BY(★核心区别)
对比 GROUP BY 窗口函数
输出行数 压缩(每组 1 行) 不变(每行都保留)
结果位置 直接作为列值 作为附加列挂在原行旁边
能否保留明细 ❌ ✅
sql 复制代码
SELECT dept_id, AVG(salary) FROM employees GROUP BY dept_id;   -- 3 行
SELECT name, dept_id, salary,
       AVG(salary) OVER (PARTITION BY dept_id) AS 本部门平均
FROM employees;                                                -- 5 行,明细都在
排名三兄弟对比(★超高频考点)
函数 并列时行为 序号是否跳跃 值为 100,100,90 时
ROW_NUMBER() 并列也强制不同号 不跳 1, 2, 3
RANK() 并列同号 跳跃 1, 1, 3
DENSE_RANK() 并列同号 不跳 1, 1, 2
NTILE(n) 分 n 桶 --- 分 2 桶:1, 1, 2
sql 复制代码
SELECT name, dept_id, salary,
  ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
  RANK()       OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk,
  DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drk
FROM employees;

取"每组唯一 Top1"用 ROW_NUMBER();"并列第一都要"用 RANK() / DENSE_RANK()。

其他常用窗口函数
函数 作用 典型用途
SUM/AVG/COUNT/MAX/MIN() OVER(...) 组内聚合 累计值、占比、组内基准
LAG(col, n, default) 取前 n 行的值 环比、上期对比
LEAD(col, n, default) 取后 n 行的值 下期、留存
FIRST_VALUE(col) 组内第一行的值 基准值
LAST_VALUE(col) 组内最后一行的值(注意 frame 默认到当前行)
NTH_VALUE(col, n) 组内第 n 行的值
NTILE(n) 分成 n 桶 分位数、Top N%
PERCENT_RANK() 相对排名 (rank-1)/(rows-1)
CUME_DIST() 累计分布
sql 复制代码
-- 环比
SELECT order_id, amount,
       LAG(amount, 1) OVER (ORDER BY order_date) AS 上单金额,
       amount - LAG(amount, 1) OVER (ORDER BY order_date) AS 差值
FROM orders;

-- 累计求和
SELECT order_id, amount,
       SUM(amount) OVER (ORDER BY order_date
                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS 累计
FROM orders;

-- 3 笔移动平均
SELECT order_date, amount,
       AVG(amount) OVER (ORDER BY order_date
                         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS 移动平均
FROM orders;

-- 组内占比
SELECT name, dept_id, salary,
       ROUND(salary / SUM(salary) OVER (PARTITION BY dept_id) * 100, 2) AS 部门占比
FROM employees;
窗口范围(Frame)对比表(★难理解,重点看)
窗口定义 含义
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 组开头 ~ 当前行(带 ORDER BY 时的默认)
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 整个分组(不带 ORDER BY 时的默认)
ROWS BETWEEN n PRECEDING AND CURRENT ROW 前 n 行 ~ 当前行(滑动平均)
ROWS BETWEEN n PRECEDING AND m FOLLOWING 前 n 行 ~ 后 m 行
RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW 按值范围(近 7 天),不是按行数
关键词 含义
UNBOUNDED PRECEDING 分组第一行
UNBOUNDED FOLLOWING 分组最后一行
n PRECEDING 当前行往前 n 行
n FOLLOWING 当前行往后 n 行
CURRENT ROW 当前行

ROWS vs RANGE 区别:

ROWS RANGE
单位 物理行 逻辑值
并列值处理 每行单独算 同值行视为一组一起算
示例 ROWS 1 PRECEDING = 恰好前 1 行 RANGE 1 PRECEDING = 值 ≤ 当前值 -1 的所有行
命名窗口(复用定义)
sql 复制代码
SELECT name, salary,
  ROW_NUMBER() OVER w AS rn,
  RANK()       OVER w AS rk,
  SUM(salary)  OVER w AS cum
FROM employees
WINDOW w AS (PARTITION BY dept_id ORDER BY salary DESC);

⚠️ 窗口函数在 SELECT 阶段执行,晚于 WHERE / GROUP BY / HAVING 。

所以 WHERE ROW_NUMBER() OVER(...) = 1 是非法的,必须子查询或 CTE 包一层。

8.3 CTE(WITH 子句)⭐⭐⭐⭐

sql 复制代码
WITH dept_stat AS (
    SELECT dept_id, AVG(salary) AS avg_sal
    FROM employees GROUP BY dept_id
)
SELECT e.name, e.salary, d.avg_sal
FROM employees e JOIN dept_stat d ON e.dept_id = d.dept_id
WHERE e.salary > d.avg_sal;
CTE vs 子查询 / 临时表
对比 CTE 嵌套子查询 临时表
可读性 高(自顶向下) 低(层层嵌套) 中
可复用 ✅ 定义一次多处引用 ❌ 需重复写 ✅
递归 ✅ WITH RECURSIVE ❌ ❌
是否落盘 否(多数引擎内联优化) 否 ✅ 落盘
作用域 单条语句 单条语句 整个会话

递归 CTE(树形结构、层级、日期序列生成):

sql 复制代码
WITH RECURSIVE org AS (
    SELECT emp_id, name, manager_id, 1 AS lvl
    FROM employees WHERE manager_id IS NULL          -- 锚点:根节点
    UNION ALL
    SELECT e.emp_id, e.name, e.manager_id, o.lvl + 1
    FROM employees e JOIN org o ON e.manager_id = o.emp_id   -- 递归:下级
)
SELECT * FROM org ORDER BY lvl;

-- 生成连续日期(报表补零用)
WITH RECURSIVE dates AS (
    SELECT DATE('2024-01-01') AS d
    UNION ALL
    SELECT d + INTERVAL 1 DAY FROM dates WHERE d < '2024-01-31'
)
SELECT * FROM dates;

8.4 视图与临时表

对象 语法 特点
视图 CREATE VIEW v AS SELECT ... 虚拟表,不存数据,查询时实时计算
物化视图 CREATE MATERIALIZED VIEW(MySQL 用表 + 定时任务模拟) 存结果,需刷新
临时表 CREATE TEMPORARY TABLE 会话级,断开自动消失,仅当前连接可见
派生表 FROM (SELECT ...) t 语句级
sql 复制代码
CREATE OR REPLACE VIEW v_high_salary AS
SELECT name, dept_id, salary FROM employees WHERE salary > 10000;

SELECT * FROM v_high_salary;   -- 像普通表一样查
DROP VIEW v_high_salary;
视图优点 视图缺点
简化复杂查询、隐藏表结构 不存数据,复杂视图查询慢
权限控制(只给视图权限) 嵌套视图易形成性能黑洞
逻辑复用 含聚合/DISTINCT/UNION 的视图不可更新

8.5 预处理语句(防 SQL 注入)⭐⭐⭐

sql 复制代码
PREPARE stmt FROM 'SELECT * FROM employees WHERE dept_id = ?';
SET @dept = 10;
EXECUTE stmt USING @dept;
DEALLOCATE PREPARE stmt;
对比 字符串拼接 参数化(预处理)
SQL 注入风险 ⚠️ 极高 ✅ 无
解析开销 每次硬解析 一次解析多次执行
反例 '... WHERE id = ' + userId 经典注入:' OR '1'='1

永远不要把用户输入拼进 SQL。 应用层应使用 PreparedStatement / ORM 的参数绑定。

8.6 慢查询排查

sql 复制代码
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;      -- 超过 1 秒记录
SHOW PROCESSLIST;                    -- 当前连接 / 正在跑的 SQL
KILL 12345;                          -- 杀掉慢查询
排查步骤 命令
找出慢 SQL 慢查询日志 / SHOW PROCESSLIST
看执行计划 EXPLAIN / EXPLAIN ANALYZE
看索引是否生效 关注 key、type
看索引/统计信息 SHOW INDEX FROM t、ANALYZE TABLE t
看锁等待 SHOW ENGINE INNODB STATUS

九、索引与执行计划 ⭐⭐⭐⭐⭐

9.1 索引数据结构对比

结构 支持情况 特点 适用
B+Tree InnoDB / MyISAM 默认 有序、支持范围查询、树高低 绝大多数场景
Hash Memory / InnoDB 自适应哈希 O(1) 等值,不支持范围/排序 纯等值查询
Full-Text InnoDB 5.6+ 倒排索引 文本搜索
R-Tree MyISAM / InnoDB 空间索引 地理坐标

为什么是 B+Tree?

  • 二叉搜索树:树太高,IO 次数多;
  • B-Tree:非叶子节点也存数据,单页能放的索引项少 → 树更高;
  • B+Tree :非叶子只存键值(一页可放上千个索引项,树高仅 3~4 层),且叶子节点用链表相连,范围查询极快。

9.2 聚簇索引 vs 二级索引(★重点)

对比 聚簇索引(主键) 二级索引(辅助索引)
数量 每表 1 个 可多个
叶子存什么 整行数据 索引列 + 主键值
查到后 直接拿到数据 需回表:拿主键再查聚簇索引
没有主键时 InnoDB 选第一个非空唯一索引;再没有则生成 6 字节隐藏 rowid
复制代码
二级索引 idx(dept_id)              聚簇索引 PRIMARY(emp_id)
┌─────────┬────────┐              ┌────────┬──────────────────────┐
│ dept_id │ emp_id │  --回表-->   │ emp_id │ name salary ... 整行 │
├─────────┼────────┤              ├────────┼──────────────────────┤
│   10    │   1    │              │   1    │ 张三 8000 ...        │
│   10    │   2    │              │   2    │ 李四 12000 ...       │
│   20    │   3    │              │   3    │ 王五 15000 ...       │
└─────────┴────────┘              └────────┴──────────────────────┘
回表与覆盖索引
概念 说明
回表 走二级索引拿到主键后,再去聚簇索引取整行 → 多一次 IO
覆盖索引 查询的列全都在索引里 ,无需回表 → EXPLAIN 的 Extra 显示 Using index
sql 复制代码
-- 假设有索引 idx(dept_id, salary)
SELECT dept_id, salary FROM employees WHERE dept_id = 10;
-- ✅ 覆盖索引,不用回表

SELECT name, salary FROM employees WHERE dept_id = 10;
-- ❌ name 不在索引里 → 必须回表

优化手段:把查询高频的列加进联合索引,做成覆盖索引。

9.3 索引类型

类型 关键字 特点
主键索引 PRIMARY KEY 唯一、非空、聚簇
唯一索引 UNIQUE 值唯一,允许多个 NULL
普通索引 INDEX / KEY 最常用
前缀索引 INDEX(col(n)) 只索引前 n 字符,省空间(牺牲区分度)
联合索引 INDEX(a,b,c) 多列组合,遵守最左前缀
全文索引 FULLTEXT MATCH(col) AGAINST('关键词')

9.4 最左前缀原则(★联合索引核心)

有联合索引 idx(a, b, c),相当于同时拥有 (a)、(a,b)、(a,b,c) 三个索引:

查询条件 是否走索引 用到哪些列
WHERE a=? ✅ a
WHERE a=? AND b=? ✅ a, b
WHERE a=? AND b=? AND c=? ✅ a, b, c(完全命中)
WHERE a=? AND c=? ⚠️ 只用 a(跳过 b,c 用不上)
WHERE b=? AND c=? ❌ 无(跳过 a,索引失效)
WHERE b=? AND a=? ✅ a, b(优化器会自动重排顺序)
WHERE a>? AND b=? ⚠️ 只用 a(范围条件后的列失效)

两条关键结论:

  1. 联合索引要从最左列开始连续匹配;
  2. 遇到范围查询 (>、<、BETWEEN、前缀 LIKE)后,其右边的列不再走索引。

9.5 索引失效场景表(★必背)

场景 反例 正解
对索引列用函数 WHERE YEAR(created_at)=2024 WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
隐式类型转换 WHERE phone = 13800138000(phone 是 VARCHAR) WHERE phone = '13800138000'
前置通配符 WHERE name LIKE '%张' 改用全文索引 / 冗余反转列
OR 连接非索引列 WHERE a=1 OR b=2(b 无索引) 分别建索引 / 改 UNION ALL
!= / <> / NOT IN WHERE status != 1 改写成正向范围
索引列参与运算 WHERE salary + 100 > 5000 WHERE salary > 4900
JOIN 时字符集不一致 utf8mb4 JOIN utf8 统一字符集
区分度太低 WHERE gender='男'(90% 都是) 优化器直接全表,别建
数据分布倾斜 某值占比过高 同上

9.6 EXPLAIN 详解(★重点)

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE dept_id = 10;
EXPLAIN ANALYZE SELECT ...;    -- MySQL 8.0.18+,给出真实耗时
type 访问类型(从好到坏)
type 含义 评价
system 表只有一行 最好
const 主键/唯一索引命中一行 极好
eq_ref JOIN 时主键/唯一索引匹配 极好
ref 普通索引等值匹配 好
range 索引范围扫描 好
index 全索引扫描(扫整棵索引树) 一般
ALL 全表扫描 ⚠️ 必须优化
关键列说明表
列 含义 关注点
id 查询序号 越大越先执行;相同则从上往下
select_type 查询类型 SIMPLE / PRIMARY / SUBQUERY / DERIVED / UNION
table 表名 <derived2> 表示派生表
type 访问类型 见上表
possible_keys 可能用到的索引
key 实际使用的索引 NULL = 没走索引
key_len 使用的索引字节数 可判断联合索引用了几列
ref 与索引比较的列/常量
rows 预估扫描行数 越小越好
filtered 过滤后剩余百分比
Extra 额外信息 见下表
Extra 常见值
Extra 含义 评价
Using index 覆盖索引,无需回表 ✅ 好
Using where 存储引擎返回后还要过滤 正常
Using index condition 索引下推(ICP) ✅ 好
Using filesort 需要额外排序 ⚠️ 考虑加索引消除排序
Using temporary 用了临时表(GROUP BY / DISTINCT) ⚠️ 需优化
Using join buffer JOIN 用了缓冲区(BNL) ⚠️ 加索引
Using union(idx1,idx2) 索引合并 一般

9.7 索引设计原则

原则 说明
建在 WHERE/ORDER BY/GROUP BY/JOIN 的列上 只查不筛的列不必建
区分度高的列优先 COUNT(DISTINCT col)/COUNT(*) 越接近 1 越好
联合索引顺序:等值列在前,范围列在后 WHERE a=1 AND b>2 → idx(a,b)
考虑覆盖索引 把 SELECT 的列纳入索引
前缀索引省空间 长字符串取前 N 字符,保证区分度
索引不是越多越好 每个索引都是一棵 B+Tree,拖慢写入、占空间
单表索引建议 ≤ 5~6 个
优先扩展已有索引,别新建 idx(a) → idx(a,b)
避免冗余索引 有了 (a,b) 就不需要单独的 (a)

9.8 高级优化特性(了解)

特性 说明
ICP 索引下推 5.6+,把 WHERE 过滤下推到存储引擎层,减少回表
MRR 把随机 IO 转成顺序 IO(先排序主键再回表)
索引合并 多个索引的 OR/AND 结果合并(Using union)
自适应哈希 InnoDB 自动为热点页建哈希索引

十、事务与锁 ⭐⭐⭐⭐⭐

10.1 ACID

特性 含义 实现机制
A 原子性 Atomicity 要么全成功,要么全回滚 undo log
C 一致性 Consistency 事务前后完整性约束不变 约束 + A/I/D 共同保证
I 隔离性 Isolation 并发事务互不干扰 锁 + MVCC
D 持久性 Durability 提交后永久保存 redo log
sql 复制代码
BEGIN;                                     -- 或 START TRANSACTION
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;                                    -- 出错则 ROLLBACK

SAVEPOINT sp1;      -- 保存点
ROLLBACK TO sp1;    -- 回滚到保存点(不是全部回滚)

10.2 四种隔离级别(★必背)

级别 脏读 不可重复读 幻读 实现
读未提交 Read Uncommitted ✅ 有 ✅ 有 ✅ 有 无锁(几乎不用)
读已提交 Read Committed ❌ ✅ 有 ✅ 有 每条 SQL 生成新 ReadView
可重复读 Repeatable Read(MySQL 默认) ❌ ❌ ⚠️ 基本解决 事务首次快照读生成 ReadView + 间隙锁
串行化 Serializable ❌ ❌ ❌ 全部加锁,串行执行
sql 复制代码
SELECT @@transaction_isolation;                            -- 查看隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;    -- 设置
三种并发问题
问题 现象 侧重
脏读 读到别人未提交的数据;对方回滚后数据就是假的
不可重复读 同一事务内两次读同一行,值不同 侧重 UPDATE / DELETE
幻读 同一事务内两次范围查询,行数不同 侧重 INSERT(多出"幻影行")

10.3 MVCC 多版本并发控制(★难点)

概念 说明
隐藏列 每行有 DB_TRX_ID(最后修改的事务 ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID
undo log 版本链 每次修改把旧值串成链表,可沿链找回历史版本
ReadView 快照读时生成,记录"当前活跃事务列表",决定哪个版本可见
快照读 普通 SELECT,不加锁,读历史版本
当前读 SELECT ... FOR UPDATE、LOCK IN SHARE MODE、INSERT/UPDATE/DELETE,读最新版本并加锁
隔离级别 ReadView 生成时机 效果
RC 每条 SQL 语句都生成新的 每次看到最新已提交数据 → 会不可重复读
RR 事务中第一次快照读时生成,之后复用 整个事务看到同一版本 → 可重复读
复制代码
版本链示意(undo log)
当前行  --roll_ptr-->  版本2  --roll_ptr-->  版本1  -->  (最初)
trx_id=30              trx_id=20              trx_id=10

10.4 锁(★重点)

按粒度分
锁 粒度 并发 开销 引擎
表锁 整表 低 小 MyISAM、InnoDB(DDL 时)
行锁 单行 高 大 InnoDB
页锁 一页 中 中 BDB
按类型分
锁 全称 兼容性 加锁方式
共享锁 S Shared Lock(读锁) 多个 S 可共存,S 与 X 互斥 SELECT ... LOCK IN SHARE MODE
排他锁 X Exclusive Lock(写锁) 与任何锁互斥 SELECT ... FOR UPDATE / UPDATE / DELETE
意向共享 IS 表级,声明"我要加行级 S 锁" 自动
意向排他 IX 表级,声明"我要加行级 X 锁" 自动
InnoDB 行锁三种算法(★难点)
锁类型 锁定范围 解决的问题
Record Lock 记录锁 锁定单条索引记录 防止该行被修改
Gap Lock 间隙锁 锁定两条记录之间的间隙(不含记录本身) 防止插入 → 解决幻读
Next-Key Lock 临键锁 Record + Gap ,左开右闭 (a, b] InnoDB 默认行锁算法
复制代码
索引值:     10        20        30
间隙:    (-∞,10)  (10,20)  (20,30)  (30,+∞)
Next-Key: (-∞,10]  (10,20]  (20,30]  (30,+∞]

注意 :间隙锁只在 RR 及以上隔离级别生效(RC 下没有)。

锁升级为表锁的情况
场景 原因
不走索引的 UPDATE / DELETE InnoDB 只能锁住扫描到的所有行 ≈ 锁全表
索引失效 同上

⚠️ UPDATE t SET x=1 WHERE name='张三' 若 name 无索引 → 锁全表! 线上最常见事故之一。

10.5 死锁

项目 说明
产生条件 互斥、占有且等待、不可抢占、循环等待
InnoDB 处理 检测死锁后回滚代价较小的事务 (wait-for graph),或等 innodb_lock_wait_timeout(默认 50s)超时
排查 SHOW ENGINE INNODB STATUS 看 LATEST DETECTED DEADLOCK 段
避免手段 ① 固定加锁顺序;② 缩小事务、尽快提交;③ 走索引减少锁范围;④ 避免大事务;⑤ 必要时降级到 RC

10.6 三大日志与两阶段提交

日志 层级 作用
redo log InnoDB 引擎层,物理日志 崩溃恢复,保证 D(持久性)
undo log InnoDB 引擎层,逻辑日志 回滚 + MVCC 版本链,保证 A(原子性)
binlog MySQL Server 层,逻辑日志 主从复制、数据恢复、审计
复制代码
写入流程(WAL:Write-Ahead Logging)
UPDATE  →  修改内存 Buffer Pool
        →  写 redo log(prepare 状态)
        →  写 binlog
        →  redo log 标记 commit      ← 两阶段提交,保证 redo 与 binlog 一致
        →  后台线程异步刷脏页

为什么需要两阶段提交:防止 redo log 写成功但 binlog 写失败,导致主库(redo)与从库(binlog)数据不一致。


十一、数据库设计基础 ⭐⭐⭐

11.1 三范式

范式 要求 反例 → 正解
1NF 列不可再分(原子性) address='北京市朝阳区xx路' 存一整串 → 拆成省/市/区/详细地址
2NF 在 1NF 基础上,非主键列必须完全依赖主键(消除部分依赖) 订单明细 (订单ID,商品ID) → 商品名称,商品名称只依赖商品ID → 拆出商品表
3NF 在 2NF 基础上,非主键列不能传递依赖 员工表有 部门ID → 部门名称 → 部门位置 → 拆出部门表
BCNF 3NF 基础上,主属性也不能部分/传递依赖于码 更严格的 3NF

口诀:1NF 原子、2NF 完全依赖、3NF 直接依赖(不传递)。

11.2 范式 vs 反范式

项目 范式化(拆表) 反范式化(冗余)
数据冗余 少 多
一致性 容易保证 难(要靠程序/触发器同步)
查询 需 JOIN,慢 少 JOIN,快
写入 快、无异常 慢(要更新多处)
适用 OLTP(业务系统) OLAP(报表、数仓)

实践 :核心业务表做到 3NF;报表/查询热点做适度冗余 (如订单表冗余 user_name 快照)。

11.3 主键选型对比

方案 优点 缺点 适用
AUTO_INCREMENT 自增 顺序写入、索引紧凑、性能最好 分库分表需额外处理;易泄露业务量 单体首选
UUID 全局唯一、可提前生成 36 字节太长、随机写入导致页分裂、索引膨胀 一般不用
UUID 存 BINARY(16) 省空间(16 字节) 仍是无序写入 折中
雪花算法 Snowflake 全局唯一、趋势递增、8 字节 需发号器、依赖时钟 分布式首选

11.4 表设计检查清单

# 检查项
1 每张表都有主键,且是 BIGINT / INT
2 字符集统一 utf8mb4,排序规则统一
3 所有字段尽量 NOT NULL + 合理 DEFAULT
4 金额一律 DECIMAL,不用 FLOAT
5 时间统一 DATETIME,不要用字符串存时间
6 表/字段有 COMMENT 注释
7 外键是否使用(大厂多数禁用,改由应用层保证)
8 必备字段:created_at、updated_at、is_deleted
9 长文本(TEXT)是否垂直拆分
10 高频查询条件是否有对应索引

十二、易错点与调优清单 ⭐⭐⭐

12.1 高频踩坑表

坑 错误写法 正确写法
NULL 判断 WHERE x = NULL WHERE x IS NULL
NOT IN 遇 NULL WHERE id NOT IN (SELECT...) 用 NOT EXISTS 或子查询加 IS NOT NULL
全表更新 UPDATE t SET x=1; 必须带 WHERE
除零 a / b a / NULLIF(b, 0)
浮点比较 WHERE price = 0.1 ABS(price-0.1) < 1e-6 或改用 DECIMAL
COUNT 语义 COUNT(col) 当行数 行数用 COUNT(*)
深分页 LIMIT 1000000, 20 游标:WHERE id > 上页最大id LIMIT 20
GROUP BY 漏列 SELECT dept,name,COUNT(*) GROUP BY dept 非聚合列全部进 GROUP BY
LEFT JOIN 退化 LEFT JOIN ... WHERE 右表列=值 条件写在 ON 里
WHERE 用别名 SELECT a+b AS s ... WHERE s>1 重写表达式或包子查询
窗口函数过滤 WHERE ROW_NUMBER() OVER(...)=1 子查询/CTE 包一层
字符串日期 WHERE dt='2024/1/1' 统一 '2024-01-01'
隐式转换 WHERE varchar_col = 123 WHERE varchar_col = '123'
大事务 一次 UPDATE 百万行 分批 + LIMIT
中文乱码 / emoji 存不进 用了 utf8 必须 utf8mb4

12.2 SQL 优化清单

# 优化点
1 不用 SELECT *,只查需要的列(利于覆盖索引)
2 WHERE 条件列建索引,遵守最左前缀
3 避免索引列上使用函数 / 运算 / 隐式转换
4 LIKE 避免前置 %,长文本用全文索引
5 UNION ALL 替代 UNION(能确定无重复时)
6 小表驱动大表(JOIN 时小表放前面)
7 深分页用游标(记录上次最大 ID)
8 大批量操作分批处理,避免长事务、大锁
9 子查询结果大时用 EXISTS 替代 IN
10 避免 !=、OR(改 UNION ALL)
11 ORDER BY 字段建索引,消除 filesort
12 GROUP BY 字段建索引,消除临时表
13 合理冗余,减少 JOIN
14 大表 ALTER 用 pt-osc / gh-ost
15 定期 ANALYZE TABLE 更新统计信息
16 冷热数据分离,归档历史数据
17 读写分离(主写从读)
18 分库分表(单表超千万行 / 2GB 时考虑)
19 连接池参数合理(避免连接数打满)
20 上线前先 EXPLAIN

12.3 深分页优化对比

sql 复制代码
-- ❌ 慢:扫描并丢弃前 100 万行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

-- ✅ 方案1:游标(记住上一页最后一条 id)------ 最快
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;

-- ✅ 方案2:延迟关联(覆盖索引先查 id,再回表)
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;

-- ✅ 方案3:业务上限制只能翻前 100 页
方案 适用 优点 缺点
游标 可连续翻页(信息流) 最快 不能跳页
延迟关联 必须支持跳页 比直接 LIMIT 快很多 仍需扫描索引
限制页数 搜索类 简单 体验受限

12.4 扩展:分库分表(了解)

概念 说明
垂直拆分 按业务 拆库(用户库、订单库);按字段拆表(大字段独立)
水平拆分 按规则 把数据分到多张同构表(如 user_id % 8)
分片键 拆分依据的字段,选最常用于查询条件的字段
中间件 ShardingSphere、MyCat
带来的问题 分布式事务、跨库 JOIN、全局 ID、扩容迁移

十三、SQL 方言对比 ⭐⭐

能力 MySQL PostgreSQL Oracle SQL Server
取前 N 行 LIMIT n LIMIT n ROWNUM <= n / FETCH FIRST n ROWS ONLY TOP n
分页 LIMIT n OFFSET m LIMIT n OFFSET m OFFSET m ROWS FETCH NEXT n ROWS ONLY OFFSET m ROWS FETCH NEXT n ROWS ONLY
字符串拼接 CONCAT(a,b) `a b`
自增列 AUTO_INCREMENT SERIAL / IDENTITY SEQUENCE + 触发器 IDENTITY(1,1)
当前时间 NOW() NOW() SYSDATE GETDATE()
空值兜底 IFNULL(a,b) COALESCE(a,b) NVL(a,b) ISNULL(a,b)
截取字符串 SUBSTRING(s,1,3) SUBSTRING(s FROM 1 FOR 3) SUBSTR(s,1,3) SUBSTRING(s,1,3)
日期格式化 DATE_FORMAT(d,'%Y-%m-%d') TO_CHAR(d,'YYYY-MM-DD') TO_CHAR(d,'YYYY-MM-DD') FORMAT(d,'yyyy-MM-dd')
字符串转日期 STR_TO_DATE(s,f) TO_DATE(s,'YYYY-MM-DD') TO_DATE(s,'YYYY-MM-DD') CONVERT(DATE,s)
日期差 DATEDIFF(d1,d2) d1 - d2(整数天) d1 - d2 DATEDIFF(day,d1,d2)
FULL OUTER JOIN ❌ 不支持(需 UNION 模拟) ✅ ✅ ✅
窗口函数 8.0+ ✅ 很早支持 ✅ ✅
CTE / 递归 CTE 8.0+ ✅ ✅(另有 CONNECT BY) ✅
布尔类型 TINYINT(1) BOOLEAN 无(用 NUMBER(1)) BIT
标识符引用符 反引号 ````` 双引号 " 双引号 " 方括号 []
分组内拼接 GROUP_CONCAT STRING_AGG LISTAGG STRING_AGG
正则 REGEXP ~ / SIMILAR TO REGEXP_LIKE LIKE(有限)

迁移提醒 :重点关注分页、字符串拼接、日期函数、标识符引用符这四类,改不完会直接报错。


十四、面试高频题 ⭐⭐⭐⭐

题目 核心考点 思路
查各部门薪资最高的人 窗口函数 / 相关子查询 ROW_NUMBER() OVER(PARTITION BY dept_id ORDER BY salary DESC) 取 rn=1
第 N 高的薪水(去重) DENSE_RANK / LIMIT+OFFSET 注意并列,用 DENSE_RANK()=N 更稳
查连续 3 天登录的用户 窗口 + 日期差 ROW_NUMBER() 与日期做差,差值相同即连续
删除重复数据只留一条 窗口 + 自连接 按业务键分区,ROW_NUMBER()>1 的删掉
次日留存率 自连接 + DATEDIFF 今日活跃 JOIN 次日活跃,相除
行转列 / 列转行 CASE WHEN / UNION ALL 行→列用聚合 + CASE;列→行用 UNION ALL
找出连续区间的起止 窗口 + 差值分组 id - ROW_NUMBER() 作为分组标识
累计 / 环比 / 同比 窗口函数 SUM() OVER / LAG()
求中位数 窗口 + 计数 正排 + 倒排取中间,或用 PERCENT_RANK
组内占比 / Top N% 窗口函数 x / SUM(x) OVER(PARTITION BY ...)
每个用户最近一笔订单 窗口 + 过滤 ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY date DESC)=1
树形结构(部门层级) 递归 CTE WITH RECURSIVE
大表 COUNT(*) 慢 索引 / 汇总表 / 近似值 二级索引计数、维护汇总表、SHOW TABLE STATUS 估算
这条 SQL 为什么没走索引 EXPLAIN 见 §9.5 失效场景

例题 1:各部门薪资最高的员工(并列都要)

sql 复制代码
WITH ranked AS (
  SELECT name, dept_id, salary,
         DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drk
  FROM employees
)
SELECT name, dept_id, salary FROM ranked WHERE drk = 1;

例题 2:连续登录 ≥ 3 天的用户

sql 复制代码
WITH t AS (
  SELECT DISTINCT user_id, login_date FROM login_log
), g AS (
  SELECT user_id, login_date,
         DATE_SUB(login_date, INTERVAL
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp
  FROM t
)
SELECT user_id, COUNT(*) AS 连续天数
FROM g GROUP BY user_id, grp HAVING COUNT(*) >= 3;
-- 关键技巧:连续日期 - 行号 = 同一个常数,据此分组

例题 3:次日留存率

sql 复制代码
SELECT COUNT(DISTINCT b.user_id) / COUNT(DISTINCT a.user_id) AS 次日留存率
FROM login_log a
LEFT JOIN login_log b
  ON a.user_id = b.user_id
 AND DATEDIFF(b.login_date, a.login_date) = 1
WHERE a.login_date = '2024-01-01';

十五、附录

15.1 学习路径

阶段 目标 建议时长 练习重点
第 1 阶段 ⭐ 会建表、会查单表 2~3 天 数据类型、DDL、SELECT / WHERE / ORDER BY / LIMIT
第 2 阶段 ⭐⭐ 会增删改、会统计 3~5 天 INSERT/UPDATE/DELETE、GROUP BY、JOIN
第 3 阶段 ⭐⭐⭐ 复杂查询 1~2 周 子查询、CASE WHEN、各类函数、CTE
第 4 阶段 ⭐⭐⭐⭐ 数据分析 1~2 周 窗口函数、透视表、留存/漏斗分析
第 5 阶段 ⭐⭐⭐⭐⭐ 性能与原理 3~4 周 索引、EXPLAIN、事务、锁、MVCC、分库分表

练习资源:

练习方法:

  1. 每题先自己写,写完再对题解;
  2. 一题多解(窗口函数 / 子查询 / JOIN 各写一遍);
  3. 写完 EXPLAIN 看有没有走索引;
  4. 把常用片段存成自己的 snippet 库。

15.2 可直接执行的练习脚本

sql 复制代码
-- 建库
CREATE DATABASE IF NOT EXISTS sql_study
  DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE sql_study;

-- 部门表
CREATE TABLE departments (
    dept_id   INT PRIMARY KEY,
    dept_name VARCHAR(50) NOT NULL,
    location  VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 员工表
CREATE TABLE employees (
    emp_id     BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '员工ID',
    name       VARCHAR(50)     NOT NULL                COMMENT '姓名',
    dept_id    INT                                     COMMENT '部门ID',
    salary     DECIMAL(10,2)   NOT NULL DEFAULT 0.00   COMMENT '月薪',
    hire_date  DATE            NOT NULL                COMMENT '入职日期',
    manager_id BIGINT UNSIGNED                         COMMENT '上级ID',
    PRIMARY KEY (emp_id),
    KEY idx_dept (dept_id),
    KEY idx_salary (salary),
    CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';

-- 订单表
CREATE TABLE orders (
    order_id   BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id    BIGINT        NOT NULL,
    amount     DECIMAL(10,2) NOT NULL,
    order_date DATE          NOT NULL,
    status     VARCHAR(20)   NOT NULL DEFAULT '待付款',
    KEY idx_user_date (user_id, order_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入数据
INSERT INTO departments VALUES
  (10,'技术部','北京'), (20,'市场部','上海'), (30,'财务部','北京');

INSERT INTO employees (name, dept_id, salary, hire_date, manager_id) VALUES
  ('张三', 10,  8000.00, '2021-03-01', 3),
  ('李四', 10, 12000.00, '2020-07-15', 3),
  ('王五', 20, 15000.00, '2019-01-20', NULL),
  ('赵六', 30,  9000.00, '2022-11-05', 3),
  ('孙七', NULL, 7000.00, '2023-02-10', NULL);

INSERT INTO orders (user_id, amount, order_date, status) VALUES
  (1, 120.50, '2024-01-05', '已付款'),
  (1, 300.00, '2024-02-11', '已付款'),
  (2,  80.00, '2024-01-20', '已退款'),
  (3, 500.00, '2024-03-01', '已付款');

-- 验证
SELECT * FROM employees;
SELECT * FROM departments;
SELECT * FROM orders;

15.3 术语速查表

缩写 / 术语 全称 / 含义
DDL / DML / DQL / DCL / TCL 语句五大分类,见 §1.1
ACID Atomicity Consistency Isolation Durability,事务四特性
MVCC Multi-Version Concurrency Control,多版本并发控制
WAL Write-Ahead Logging,预写日志
B+Tree 索引默认数据结构
ICP Index Condition Pushdown,索引下推
MRR Multi-Range Read,多范围读
OLTP / OLAP 在线事务处理(业务系统)/ 在线分析处理(报表数仓)
QPS / TPS 每秒查询数 / 每秒事务数
回表 二级索引查到主键后再查聚簇索引
覆盖索引 Covering Index,查询列全在索引中,无需回表
最左前缀 联合索引的匹配规则
深分页 OFFSET 很大的分页查询
页分裂 乱序插入导致 B+Tree 节点分裂,性能下降

📌 本文基于 MySQL 8.0。要点回顾三句话:

  1. 写查询:想清楚「要什么列、从哪张表、关联谁、怎么过滤、怎么分组、怎么排序」;
  2. 建表:类型选最小、金额用 DECIMAL、时间用 DATETIME、字符集 utf8mb4、字段尽量 NOT NULL;
  3. 调优 :先 EXPLAIN,看 type / key / rows / Extra,消灭 ALL、filesort、temporary。
相关推荐
润乾软件2 小时前
TRAE + SQLazy MCP 自动化编写 SQL 实践
sql·sqlazy
ly76892 小时前
PostgreSQL 执行计划深读:从 EXPLAIN 输出到代价模型驱动的 SQL 改写
数据库·sql·postgresql·explain·执行计划·代价模型
川石课堂软件测试3 小时前
涨薪技术|0到1学会性能测试第8课-Analysis分析图
服务器·数据库·python·sql·jmeter·mysql·容器
我命由我123453 小时前
SQL 检测脚本
java·数据库·sql·功能测试·单元测试·压力测试·可用性测试
我的愿望是成为富婆5 小时前
2027校招|大数据管理与应用专业:从会用技术工具到解决业务问题
sql
鸽芷咕5 小时前
把 SQL 编辑器用顺:KSQL Developer 从补全到事务的实战笔记
笔记·sql·编辑器
呆萌很5 小时前
DISTINCT 和 GROUP BY 核心区别
数据库·sql
ProcessOn1 天前
纯干货!MySql实战02:一条SQL更新语句执行流程大探索!
sql·mysql·sql server
geovindu1 天前
sql: JSON and XML Data Handling in SQL using mysql 9.0
数据库·sql·mysql·数据库开发