SELECT employees.name, departments.department_nameFROM employeesINNER JOIN departments ON employees.dept_id = departments.id;
LEFT JOIN / LEFT OUTER JOIN(左外连接)
功能:返回左表所有记录,即使右表没有匹配
特点:
左表所有行都会出现在结果中
右表无匹配时显示NULL值
常用于"包含所有A,以及匹配的B"场景
SELECT customers.customer_name, orders.order_dateFROM customersLEFT JOIN orders ON customers.id = orders.customer_id;
RIGHT JOIN / RIGHT OUTER JOIN(右外连接)
功能:返回右表所有记录,即使左表没有匹配
特点:
右表所有行都会出现在结果中
左表无匹配时显示NULL值
使用频率通常低于LEFT JOIN
SELECT departments.department_name, employees.nameFROM employeesRIGHT JOIN departments ON employees.dept_id = departments.id;
FULL OUTER JOIN(全外连接)
功能:返回两表所有记录,无匹配时对应侧为NULL MySQL实现方式:通过UNION模拟
SELECT * FROM table1 LEFT JOIN table2 ON table1.id = table2.idUNIONSELECT * FROM table1 RIGHT JOIN table2 ON table1.id = table2.idWHERE table1.id IS NULL;
-- 开始事务START TRANSACTION;-- 从Alice的账户中扣除100元UPDATE accounts SET balance = balance - 100 WHERE id = 1;-- 向Bob的账户中增加100元UPDATE accounts SET balance = balance + 100 WHERE id = 2;-- 提交事务COMMIT;
事务的四大特性
事务4大特性
原子性、一致性、隔离性、持久性
原⼦性
事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么全不执行
说明:
如果在执行UPDATE accounts SET balance = balance - 100 WHERE id = 1;时成功,但在执行UPDATE accounts SET balance = balance + 100 WHERE id = 2;时失败(例如,Bob的账户不存在),那么整个事务会回滚。
-- 事务ASTART TRANSACTION;UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 未提交-- 事务B(READ UNCOMMITTED)START TRANSACTION;SELECT balance FROM accounts WHERE id = 1; -- 可能读到事务A未提交的修改
读已提交
READ COMMITTED
实现原理
使用行级锁(Record Lock)保证写操作隔离
基于MVCC(多版本并发控制)实现读操作
每次SELECT都会生成新的ReadView(数据快照)
关键机制
锁机制:
写操作获取行级排他锁(X锁)
读操作不加锁(快照读)
MVCC实现
每行记录包含隐藏字段:DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针)
Undo Log存储历史版本,形成版本链
每次查询生成新的ReadView,判断数据可见性
特点
避免了脏读
可能出现不可重复读和幻读
Oracle等数据库的默认级别
-- 事务ASTART TRANSACTION;SELECT balance FROM accounts WHERE id = 1; -- 第一次读取,值为1000-- 事务BSTART TRANSACTION;UPDATE accounts SET balance = 900 WHERE id = 1;COMMIT;-- 事务A再次读取SELECT balance FROM accounts WHERE id = 1; -- 读到新值900(不可重复读)
可重复读
REPEATABLE READ
实现原理:
行锁 + 间隙锁(Gap Lock)组合成Next-Key Lock
MVCC机制下,事务首次读取时生成ReadView并保持到事务结束
InnoDB在此级别通过间隙锁避免了幻读
关键机制
锁机制:
Next-Key Lock = 行锁 + 间隙锁
锁定范围包括记录本身和索引记录前的间隙
防止其他事务在范围内插入新记录(解决幻读)
MVCC增强:
事务首次SELECT时生成ReadView
后所有读操作使用同一ReadView
保证事务内看到一致的数据快照
-- 事务ASTART TRANSACTION;SELECT * FROM accounts WHERE balance > 1000; -- 返回2条记录-- 事务BSTART TRANSACTION;INSERT INTO accounts(id, balance) VALUES(3, 1500); -- 被间隙锁阻塞COMMIT;-- 事务A再次查询SELECT * FROM accounts WHERE balance > 1000; -- 仍返回2条记录(无幻读)
可序列化
SERIALIZABLE
实现原理
所有普通SELECT语句自动转为SELECT ... FOR SHARE(加共享锁)
使用严格的锁机制实现完全串行化执行
读操作获取共享锁(S锁),写操作获取排他锁(X锁)
关键机制
锁转换:
自动为SELECT加共享锁
其他事务可以加共享锁但不能加排他锁
读写操作完全互斥
锁范围:
如果使用索引,锁定符合条件的行
无索引时退化为表锁
特点
最高的隔离级别
性能最差,并发度最低
完全避免所有并发问题
适用于严格要求一致性的金融交易等场景
-- 事务A(SERIALIZABLE隔离级别)START TRANSACTION;SELECT balance FROM accounts WHERE id = 1;-- 实际执行:SELECT balance FROM accounts WHERE id = 1 FOR SHARE;-- 对id=1的行加S锁(共享锁)-- 事务B(任何隔离级别)START TRANSACTION;UPDATE accounts SET balance = 800 WHERE id = 1;-- 尝试对id=1的行加X锁(排他锁),但S锁与X锁互斥,因此被阻塞-- 直到事务A提交或超时(innodb_lock_wait_timeout)
避免过度使用SERIALIZABLE: 除非必须防止所有并发异常,否则优先考虑 REPEATABLE READ + 显式锁(如 SELECT ... FOR UPDATE)。
优化策略:
缩小事务范围,减少锁持有时间
为查询条件添加索引,避免锁升级为表锁
监控 innodb_lock_wait_timeout(默认50秒)
替代方案:对于需要强一致性的读操作,可使用:
START TRANSACTION;SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 显式加X锁COMMIT;
索引
数据结构
存储方式
聚簇索引
非聚簇索引
字段特性
字段数量
联合索引
设计原则
高选择等值 → 范围 → 排序 → 覆盖
等值过滤频繁且选择性高的列放在最左。
范围过滤放在等值列之后,防止范围后面的列失效。
排序 / GROUP BY / DISTINCT 顺序要与索引一致,避免额外排序。
覆盖查询时,把 SELECT 里的列全部放到索引尾部(冗余列)形成覆盖索引。
控制宽度:索引列总长度 ≤ 3072 bytes(InnoDB 限制),过长用前缀索引或哈希。
生效原则
CREATE TABLE t ( a INT, b INT, c INT, d INT, INDEX idx_a_b_c (a, b, c));
查询条件
是否用到索引
用到列
原因
WHERE a=1
✅
a
最左前缀
WHERE a=1 AND b=2
✅
a,b
连续
WHERE a=1 AND c=3
✅(部分)
a
b 缺失,c 无法使用
WHERE b=2
❌
–
跳过 a
WHERE a=1 AND b>2 AND c=3
✅(部分)
a,b
b 范围后 c 失效
WHERE a=1 AND b IN (2,3) AND c=4
✅(部分)
a,b
IN 也是范围
ORDER BY a,b,c
✅
a,b,c
顺序完全匹配
ORDER BY b,a
❌
–
顺序不匹配
SELECT a,b,c FROM t WHERE a=1
✅(覆盖)
a,b,c
无需回表
查询场景:
-- 完全匹配规则 ---- ✅ 完全匹配(使用全部索引)SELECT * FROM table WHERE a=1 AND b=2 AND c=3;-- ✅ 顺序不影响(优化器自动调整)SELECT * FROM table WHERE b=2 AND a=1 AND c=3;-- 最左前缀规则 ---- ✅ 使用a列索引SELECT * FROM table WHERE a=1;-- ✅ 使用a,b列索引SELECT * FROM table WHERE a=1 AND b=2;-- ❌ 缺少最左列a(索引失效)SELECT * FROM table WHERE b=2;-- ❌ 跳过中间列b(只能用到a)SELECT * FROM table WHERE a=1 AND c=3;-- 范围查询后的列失效 ---- ✅ 使用a,b列索引(c失效)SELECT * FROM table WHERE a=1 AND b>2 AND c=3;-- ✅ 使用a,b,c索引(LIKE右匹配不算范围)SELECT * FROM table WHERE a=1 AND b LIKE 'abc%' AND c=3;-- ❌ 范围查询放前面(只能用到a)SELECT * FROM table WHERE a>1 AND b=2 AND c=3;
排序场景:
-- ORDER BY优化 ---- ✅ 使用完整索引排序SELECT * FROM table ORDER BY a, b, c;-- ✅ 使用前缀索引排序SELECT * FROM table ORDER BY a, b;-- ❌ 非最左前缀排序(filesort)SELECT * FROM table ORDER BY b, c;-- 混合排序方向 ---- ✅ 方向一致可使用索引SELECT * FROM table ORDER BY a ASC, b ASC, c ASC;SELECT * FROM table ORDER BY a DESC, b DESC, c DESC;-- ❌ 方向不一致导致filesortSELECT * FROM table ORDER BY a ASC, b DESC;
覆盖索引优化
-- ✅ 使用覆盖索引(无需回表)SELECT a, b FROM table WHERE a=1 AND b=2;-- ❌ 需要回表(查c列但条件不含c)SELECT a, b, c FROM table WHERE a=1 AND b=2;
失效
-- 函数/运算导致失效-- ❌ 索引失效SELECT * FROM table WHERE YEAR(a)=2023;SELECT * FROM table WHERE b+1=10;-- 类型不匹配-- ❌ 假设a是字符串类型SELECT * FROM table WHERE a=123;-- 不等于/IS NOT NULL-- ❌ 索引可能失效SELECT * FROM table WHERE a!=1;SELECT * FROM table WHERE b IS NOT NULL;
查询优化器如何选索引
成本模型:IO + CPU + 内存
候选索引:所有能用到左前缀的联合索引都会进入代价计算。
回表成本:覆盖索引 IO 成本更低,可能反超单列索引。
统计信息:ANALYZE TABLE 更新 innodb_index_stats,否则可能选错。
索引合并(index merge):多列分别走不同索引再合并,通常比联合索引差,可用 USE INDEX 强制。
EXPLAIN SELECT * FROM t WHERE a=1 AND b>2 ORDER BY c;-- 优化器可能选 idx_a_b_c 利用 a,b 过滤,但排序需要 filesort-- 把索引改成 (a,b,c) 或 (a,c,b) 才能避免 filesort,取决于过滤 vs 排序谁更重要
高频误区
误区
解释
“把 WHERE 里所有列都建联合索引”
顺序错、范围列放中间导致后面列失效。
“联合索引会自动解决 OR 条件”
OR 会退化为 index merge 或全表,需改写 UNION。
“ORDER BY 字段只要出现在索引就行”
必须紧跟在等值列后、顺序一致且全是 ASC/DESC 同一方向。
“LIKE '%abc' 会走索引”
前缀模糊无法使用 B+Tree 有序性,只有 LIKE 'abc%' 可走到前缀。
“低基数列放最左”
导致过滤效果差、扫描行多,浪费 IO。
索引优化
失效场景
场景示例(MySQL 8.0)
失效原因
规避方案
WHERE DATE(create_time) = '2024-07-14'
函数/表达式
改为范围:create_time >= '2024-07-14' AND < '2024-07-15'
WHERE col = 123 (col 是 varchar)
隐式转换
显式加 CAST 或改客户端字符集
WHERE a=1 OR b=2 (b 无索引)
OR 跨越非索引列
改为 UNION ALL 或给 b 建索引
LIKE '%abc'
左模糊
使用倒序存储 + 右模糊 LIKE REVERSE('%cba')
NOT IN (subquery)
优化器放弃索引
改写为 LEFT JOIN ... IS NULL
(col1,col2,col3) 但查询 WHERE col2=1
违背最左前缀
调整列顺序或建 (col2,col1)
SELECT ... WHERE col-1 = 9
列参与运算
改为 col = 10
IS NULL 在列定义 NOT NULL
优化器短路
无需处理,逻辑已优化
优化策略
策略
原理
工具/指标
备注
有序主键
顺序写入减少页分裂
innodb_autoinc_lock_mode=2 + 雪花 ID
顺序性 > 安全性
覆盖索引
避免回表,减少 IO
EXPLAIN Extra: Using index
回表次数 = 读取行数
NOT NULL
减少 NULL 位图 & 统计信息复杂度
INFORMATION_SCHEMA.STATISTICS
每 NULL 占 1 byte
前缀索引
降低 B+Tree 高度
SELECT LEFT(col, n) GROUP BY ... 找最小 n
n 需平衡选择性与长度
索引数量
维护成本 ∝ 索引数
Handler_read_key, Handler_read_next
一般 OLTP ≤ 6 个/表
日志
Binlog有哪些格式
Statement模式
记录每一条会修改数据的SQL语句
优点:
日志记录量小,节省磁盘空间和网络IO。
性能较高
缺点:
可能导致主从数据不一致,尤其是涉及非确定性函数(如UUID()、NOW())或存储过程。
需要记录上下文信息以确保SQL语句在从服务器上执行结果一致
结果:
Binlog记录了实际执行的SQL语句(INSERT、UPDATE、DELETE)。
每条记录包含时间戳、线程ID、执行时间等元数据。
# at 4#250708 10:00:00 server id 1 end_log_pos 123 CRC32 0x12345678 Query thread_id=1 exec_time=0 error_code=0SET TIMESTAMP=1688790000/*!*/;INSERT INTO test VALUES (1, 'Kimi')/*!*/;# at 123#250708 10:00:01 server id 1 end_log_pos 234 CRC32 0x87654321 Query thread_id=1 exec_time=0 error_code=0SET TIMESTAMP=1688790001/*!*/;UPDATE test SET name = 'Moonshot' WHERE id = 1/*!*/;# at 234#250708 10:00:02 server id 1 end_log_pos 345 CRC32 0x11223344 Query thread_id=1 exec_time=0 error_code=0SET TIMESTAMP=1688790002/*!*/;DELETE FROM test WHERE id = 1/*!*/;
ROW
记录每一行数据的变化,而不是SQL语句
优点:
记录详细,可以精确反映数据的变化。
避免了STATEMENT格式中可能出现的数据一致性问题
缺点:
日志量大,尤其是批量操作时。
性能可能受影响
结果: 在ROW格式下,Binlog会记录每一行数据的变化,而不是SQL语句。记录的内容大致如下:
# at 4#250708 10:00:00 server id 1 end_log_pos 123 CRC32 0x12345678 Table_map: `test`.`test` mapped to number 1#250708 10:00:00 server id 1 end_log_pos 234 CRC32 0x87654321 Write_rows: table id 1 flags: STMT_END_F### INSERT INTO `test`.`test`### SET### @1=1### @2='Kimi'# at 234#250708 10:00:01 server id 1 end_log_pos 345 CRC32 0x11223344 Update_rows: table id 1 flags: STMT_END_F### UPDATE `test`.`test`### WHERE### @1=1### @2='Kimi'### SET### @1=1### @2='Moonshot'# at 345#250708 10:00:02 server id 1 end_log_pos 456 CRC32 0x44556677 Delete_rows: table id 1 flags: STMT_END_F### DELETE FROM `test`.`test`### WHERE### @1=1### @2='Moonshot'
# at 4#250708 10:00:00 server id 1 end_log_pos 123 CRC32 0x12345678 Query thread_id=1 exec_time=0 error_code=0SET TIMESTAMP=1688790000/*!*/;INSERT INTO test VALUES (1, 'Kimi')/*!*/;# at 123#250708 10:00:01 server id 1 end_log_pos 234 CRC32 0x87654321 Query thread_id=1 exec_time=0 error_code=0SET TIMESTAMP=1688790001/*!*/;UPDATE test SET name = 'Moonshot' WHERE id = 1/*!*/;# at 234#250708 10:00:02 server id 1 end_log_pos 345 CRC32 0x11223344 Query thread_id=1 exec_time=0 error_code=0SET TIMESTAMP=1688790002/*!*/;DELETE FROM test WHERE id = 1/*!*/;
BEGIN;-- 自动在表上加IX锁,在行id=1上加X锁SELECT * FROM users WHERE id = 1 FOR UPDATE;-- 另一个事务尝试加表锁时会检查意向锁LOCK TABLE users WRITE; -- 会阻塞,因为检测到IX锁存在
记录锁 (Record Lock)
描述:锁定索引中的单条记录 使用场景:精确锁定特定行
库存扣减(防止超卖)
-- 事务1:下单扣库存BEGIN;-- 锁定商品库存记录SELECT stock FROM products WHERE product_id = 1001 FOR UPDATE;-- 检查库存IF stock > 0 THEN UPDATE products SET stock = stock - 1 WHERE product_id = 1001;END IF;COMMIT;
间隙锁 (Gap Lock)
描述:锁定索引记录之间的间隙,防止其他事务在间隙中插入数据。
特点:
只在REPEATABLE READ隔离级别下有效
可以防止幻读问题
只锁定范围,不锁定记录本身
使用场景:
防止幻读(在REPEATABLE READ隔离级别下)
锁定一个范围但不包括记录本身
-- 假设现有id为1,5,10的记录BEGIN;SELECT * FROM products WHERE id BETWEEN 3 AND 7 FOR UPDATE;-- 这会锁定(1,5)和(5,10)这两个间隙-- 其他事务不能插入id=4或id=6的记录
-- 1. 使用STRAIGHT_JOIN强制顺序SELECT * FROM small_table STRAIGHT_JOIN large_table ON...-- 2. 调整FROM子句顺序(优化器会重排)SELECT * FROM table1, table2 WHERE...-- 3. 使用优化器提示SELECT /*+ JOIN_ORDER(t1, t2) */ * FROM t1 JOIN t2...
查询重写技巧
子查询: 改写为JOIN,减少临时表创建
多表JOIN: 分解为多个简单查询,降低复杂度
OR条件: 改用UNION ALL,避免全表扫描
派生表: 提前过滤或物化,减少处理数据量
-- 低效: 使用子查询SELECT * FROM orders WHERE customer_id IN(SELECT id FROM customers WHERE status='active');-- 高效: 改写为JOINSELECT o.* FROM orders o JOIN customers cON o.customer_id=c.id WHERE c.status='active';
-- 分批删除SET @batch_size = 10000;SET @offset = 0;WHILE (1) DO DELETE FROM your_table LIMIT @batch_size OFFSET @offset; SET @offset = @offset + @batch_size; -- 暂停一段时间,避免对服务器造成过大压力 DO SLEEP(1);END WHILE;
分解关联查询
将复杂的关联查询分解为多个简单的查询,可以提高缓存效率。
示例: 假设有一个复杂的关联查询:
SELECT a.*, b.*FROM table_a aJOIN table_b b ON a.id = b.a_idWHERE a.status = 'active';
优化:
-- 先查询 table_a 中符合条件的记录SELECT id, nameFROM table_aWHERE status = 'active';-- 然后根据查询结果逐条查询 table_bSELECT *FROM table_bWHERE a_id IN (SELECT id FROM table_a WHERE status = 'active');
减少冗余记录的查询
避免在查询中返回不必要的列和行,减少数据传输量。
-- 原查询SELECT * FROM your_table WHERE status = 'active';-- 优化后的查询SELECT id, name, statusFROM your_tableWHERE status = 'active';
在应用层做关联
在应用层进行数据关联,可以更容易地对数据库进行拆分,提高系统的可扩展性。
示例: 假设需要从两个表中获取数据并进行关联。
-- 查询表 ASELECT id, nameFROM table_aWHERE status = 'active';-- 查询表 BSELECT id, a_id, valueFROM table_bWHERE a_id IN (SELECT id FROM table_a WHERE status = 'active');
执行单个查询减少锁的竞争
尽量减少单次查询的资源消耗,避免长时间锁定资源。
-- 原查询SELECT * FROM your_table FOR UPDATE;-- 优化后的查询SELECT id, nameFROM your_tableWHERE status = 'active' FOR UPDATE;
优化LIMIT分页
ID游标法
-- 第一页(常规查询)SELECT * FROM orders ORDER BY id LIMIT 20;-- 后续分页(记住上次最大ID)SELECT * FROM ordersWHERE id > 上一页最后一条记录的IDORDER BY idLIMIT 20;
复合键游标法
-- 按时间+ID排序的分页SELECT * FROM postsWHERE (created_at, id) > ('2023-01-01 12:00:00', 1000)ORDER BY created_at, idLIMIT 20;
优化子查询
用关联查询替代子查询
子查询在某些情况下效率较低,尤其是当子查询返回大量数据时。可以尝试用关联查询替代子查询。
-- 原子查询SELECT a.*FROM table_a aWHERE a.id IN (SELECT b.a_id FROM table_b b WHERE b.status = 'active');-- 优化后的关联查询SELECT a.*FROM table_a aJOIN table_b b ON a.id = b.a_idWHERE b.status = 'active';
优化 GROUP BY 和 DISTINCT
通过索引来优化,这是最有效的优化方法之一。
-- 原查询SELECT user_id, COUNT(*)FROM ordersGROUP BY user_id;-- 索引优化CREATE INDEX idx_user_id ON orders(user_id);-- 查询优化SELECT user_id, COUNT(*)FROM ordersGROUP BY user_id;
标识列分组原理
为什么标识列分组更高效
索引利用率:主键/唯一键必然有索引,分组时可直接利用
比较效率:整型主键比较速度比字符串快3-5倍
内存消耗:更小的分组键减少临时表内存使用
基础优化:使用主键替代其他列分组
-- 原查询(低效)SELECT department_name, COUNT(*)FROM employeesGROUP BY department_name;-- 优化后(高效)SELECT d.department_name, COUNT(*)FROM employees eJOIN departments d ON e.department_id = d.department_idGROUP BY e.department_id; -- 使用关联键分组
多表关联时的分组优化
-- 原查询(使用非索引列分组)SELECT c.country_name, COUNT(*)FROM orders oJOIN customers c ON o.customer_id = c.customer_idGROUP BY c.country_name;-- 优化方案1:使用客户ID分组SELECT c.country_name, COUNT(*)FROM orders oJOIN customers c ON o.customer_id = c.customer_idGROUP BY o.customer_id; -- 订单表已有customer_id索引-- 优化方案2:使用覆盖索引ALTER TABLE customers ADD INDEX idx_country (country_name, customer_id);SELECT c.country_name, COUNT(*)FROM customers cWHERE customer_id IN (SELECT customer_id FROM orders)GROUP BY c.country_name; -- 现在country_name在索引中
避免不必要的 ORDER BY
如果不需要 ORDER BY,在 GROUP BY 时加上 ORDER BY NULL,MySQL 不会再进行文件排序。
-- 原查询SELECT user_id, COUNT(*)FROM ordersGROUP BY user_idORDER BY user_id;-- 优化后的查询SELECT user_id, COUNT(*)FROM ordersGROUP BY user_idORDER BY NULL;
避免WITH ROLLUP
WITH ROLLUP 会增加额外的聚合计算,可以将其逻辑挪到应用程序中处理,减少数据库的负担。
-- 原查询SELECT user_id, COUNT(*)FROM ordersGROUP BY user_id WITH ROLLUP;-- 优化后的查询SELECT user_id, COUNT(*)FROM ordersGROUP BY user_id;-- 在应用程序中处理超级聚合
WHERE子句
检查清单
是否避免全表扫描?
索引使用是否最优?
WHERE条件是否可索引?
是否有不必要的数据类型转换?
是否避免使用导致索引失效的操作?
索引使用优化
索引失效的常见场景
问题类型
错误示例
优化方案
NULL判断
WHERE col IS NULL
设默认值,改用WHERE col=0
不等操作
WHERE col != 10
改写为WHERE col > 10 OR col < 10
OR条件
WHERE col=1 OR col=2
改用UNION ALL合并查询结果
前导模糊
WHERE col LIKE '%abc'
使用后缀匹配WHERE col LIKE 'abc%'
隐式转换
WHERE str_col=123
保持类型一致WHERE str_col='123'
函数操作
WHERE YEAR(date)=2023
改用范围WHERE date BETWEEN '2023-01-01' AND '2023-12-31'
复合索引最佳实践
-- 创建最左匹配索引ALTER TABLE users ADD INDEX idx_name_age (last_name, age);-- 有效查询SELECT * FROM usersWHERE last_name='Smith' AND age>30; -- 使用索引-- 无效查询SELECT * FROM users WHERE age>30; -- 不使用索引
条件表达式优化
范围查询优化
-- 低效写法SELECT * FROM ordersWHERE total_amount/100 > 50;-- 高效写法SELECT * FROM ordersWHERE total_amount > 50*100;
IN与BETWEEN选择
-- 离散值用INSELECT * FROM productsWHERE category_id IN (1, 3, 5);-- 连续范围用BETWEENSELECT * FROM ordersWHERE order_date BETWEEN '2023-01-01' AND '2023-01-31';
OR条件优化方案
-- 低效ORSELECT * FROM logsWHERE type='error' OR priority=1;-- 优化方案1:UNION ALLSELECT * FROM logs WHERE type='error'UNION ALLSELECT * FROM logs WHERE priority=1;-- 优化方案2:改写为INSELECT * FROM logsWHERE type='error' OR (type='warning' AND priority=1);
参数化查询优化
预处理语句优势
-- 低效(可能不使用索引)PREPARE stmt FROM 'SELECT * FROM users WHERE age > ?';SET @age = 30;EXECUTE stmt USING @age;-- 高效(强制索引)PREPARE stmt FROM 'SELECT * FROM users FORCE INDEX(idx_age) WHERE age > ?';
存储过程优化
DELIMITER //CREATE PROCEDURE GetUsers(IN minAge INT)BEGIN -- 使用提示强制索引 SELECT * FROM users USE INDEX(idx_age) WHERE age > minAge;END //DELIMITER ;
核心原则
索引优先:WHERE 和 ORDER BY 的列必须建索引
左侧纯净:WHERE 条件左侧不要有计算/函数
避免全扫:杜绝导致索引失效的写法
死锁
死锁四要素
互斥条件:资源一次只能被一个事务占有
请求与保持:事务持有资源的同时请求新资源
不剥夺条件:已分配资源不能被强制收回
循环等待:事务间形成环形等待链
当两个或多个事务互相持有并请求对方持有的锁时,就会发生死锁。
-- 事务1BEGIN;UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 获取id=1的X锁-- 同时事务2BEGIN;UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 获取id=2的X锁-- 事务1尝试获取id=2的锁UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待事务2释放锁-- 事务2尝试获取id=1的锁UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 等待事务1释放锁-- 此时MySQL检测到死锁,会回滚其中一个事务
预防死锁
索引优化策略
WHERE无索引:为条件列添加索引; 行锁→索引锁,减少锁范围
索引区分度低:使用复合索引或函数索引; 提高过滤效率
全表扫描:强制索引(FORCE INDEX); 避免意外表锁
-- 低效(可能锁全表)UPDATE orders SET status = 'shipped' WHERE customer_id = 100;-- 优化后ALTER TABLE orders ADD INDEX idx_customer (customer_id);UPDATE orders USE INDEX(idx_customer) SET status = 'shipped' WHERE customer_id = 100;
-- 粗粒度锁(不推荐)SELECT * FROM products WHERE category = 'electronics' FOR UPDATE;-- 细粒度锁(推荐)SELECT * FROM products WHERE sku = 'ABC123' FOR UPDATE;
乐观锁替代方案
UPDATE inventorySET stock = stock - 1, version = version + 1WHERE item_id = 100 AND version = 5;
死锁场景
循环等待锁
特征:事务A等待事务B持有的锁,同时事务B等待事务A持有的锁
解决方案:
统一资源访问顺序
使用SELECT ... FOR UPDATE一次性锁定所有需要的资源
间隙锁冲突
特征:涉及范围查询和INSERT操作
-- 将事务隔离级别降为READ COMMITTEDSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 或精确化查询条件避免间隙锁UPDATE orders SET status = 'shipped' WHERE id IN (1, 2, 3);
# my.cnf配置[mysqld]innodb_print_all_deadlocks = ON # 记录所有死锁到错误日志innodb_lock_wait_timeout = 30 # 锁等待超时(秒)
死锁日志分析
-- 查看最近死锁SHOW ENGINE INNODB STATUS\G/* 重点观察LATEST DETECTED DEADLOCKTRANSACTION TRX_IDWAITING FOR THIS LOCK*/
WAITING FOR THIS LOCK: 显示被阻塞事务想要的锁
HOLDS THE LOCK: 显示阻塞事务已持有的锁
ROLLING BACK: 显示被选为牺牲品的事务
解读日志
重点关注输出中的LATEST DETECTED DEADLOCK部分
------------------------LATEST DETECTED DEADLOCK------------------------2023-08-20 14:25:36 0x7f8e3c2e6700*** (1) TRANSACTION:TRANSACTION 42156, ACTIVE 12 sec starting index readmysql tables in use 1, locked 1LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)MySQL thread id 32, OS thread handle 139887, query id 1234 192.168.1.1 root updatingUPDATE accounts SET balance = balance - 100 WHERE id = 1*** (1) HOLDS THE LOCK(S):RECORD LOCKS space id 45 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42156 lock_mode X locks rec but not gapRecord lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0*** (1) WAITING FOR THIS LOCK:RECORD LOCKS space id 45 page no 4 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42156 lock_mode X locks rec but not gap waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0*** (2) TRANSACTION:TRANSACTION 42157, ACTIVE 10 sec starting index readmysql tables in use 1, locked 13 lock struct(s), heap size 1136, 2 row lock(s)MySQL thread id 33, OS thread handle 139888, query id 1235 192.168.1.1 root updatingUPDATE accounts SET balance = balance + 100 WHERE id = 2*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 45 page no 4 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42157 lock_mode X locks rec but not gapRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0*** (2) WAITING FOR THIS LOCK:RECORD LOCKS space id 45 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42157 lock_mode X locks rec but not gap waitingRecord lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0*** WE ROLL BACK TRANSACTION (2)
CREATE TABLE Logs( id INT PRIMARY KEY AUTO_INCREMENT, num INT NOT NULL);
数据 :
id
num
1
1
2
1
3
1
4
2
5
1
6
2
7
2
8
2
三表自连接(最朴素)
SELECT DISTINCT l1.num AS ConsecutiveNumsFROM Logs l1JOIN Logs l2 ON l1.id = l2.id + 1 AND l1.num = l2.numJOIN Logs l3 ON l1.id = l3.id + 2 AND l1.num = l3.num;
原理:把 Logs 视为三个副本,错位 1、2 行进行等值连接。
缺点:
只能硬编码 3 次,扩展到 k 次需再 JOIN k-1 次,SQL 爆炸;
大表性能差(三次全表扫描 + 连接)。
索引利用:id 是主键,可用 range 或 index 扫描,但仍需回表 3 次。
窗口函数(最优雅)
WITH cte AS ( SELECT num, LEAD(num,1) OVER (ORDER BY id) AS n1, LEAD(num,2) OVER (ORDER BY id) AS n2 FROM Logs)SELECT DISTINCT num AS ConsecutiveNumsFROM cteWHERE num = n1 AND num = n2;
原理:利用 LEAD 把当前行与后两行投影到同一行,然后过滤。
扩展 k 次:把 2 改为 k-1,并用 COALESCE 处理尾部 NULL。
复杂度:一次排序 O(n log n),内存临时表 O(n)。
版本要求:MySQL 8.0+(支持窗口函数)。
用户变量
SELECT DISTINCT num AS ConsecutiveNumsFROM ( SELECT num, @cnt := IF(@prev = num, @cnt + 1, 1) AS cnt, @prev := num FROM Logs, (SELECT @cnt := 0, @prev := NULL) init ORDER BY id) AS tWHERE cnt >= 3;
WITH RECURSIVE r AS ( -- 锚点:从第 1 行开始 SELECT id, num, 1 AS cnt FROM Logs WHERE id = 1 UNION ALL -- 递归:下一行 SELECT l.id, l.num, CASE WHEN l.num = r.num THEN cnt + 1 ELSE 1 END FROM Logs l JOIN r ON l.id = r.id + 1)SELECT DISTINCT num AS ConsecutiveNumsFROM rWHERE cnt >= 3;