请开启 JavaScript
说说SQL调优的那些事 – 老迟笔记

说说SQL调优的那些事

一、概述

SQL调优是通过优化SQL语句、索引、表结构等方式,降低数据库的CPU、内存、磁盘I/O消耗,提升查询和写入效率的过程。调优的核心思路是“减少数据扫描量”“利用索引加速查找”“避免低效操作”,需结合业务场景、数据量和执行计划综合分析。

无论面对哪种场景,调优都遵循 诊断->定位->优化->验证 的顺序。

 

二、SQL调优的核心步骤(通用流程)

1、查看执行计划

通过 EXPLAIN(MySQL)/ EXPLAIN ANALYZE(PostgreSQL)分析SQL的执行逻辑(是否走索引、扫描行数、连接方式等)。

2、定位瓶颈

判断是“全表扫描导致I/O过高”“索引失效”“连接方式低效”还是“子查询过多”等问题。

3、实施优化

针对性调整SQL、添加索引、修改表结构等。

4、验证效果

通过 EXPLAIN 对比优化前后的扫描行数、索引使用情况,或通过 SHOW PROFILE 查看执行耗时。

注:关于MySQL EXPLAIN,你需要知道的点

 

三、高频调优场景与案例

以下结合实际业务场景,分析常见问题及优化方案,覆盖“查询优化”“索引优化”“写入优化”三大核心方向。

1、查询慢——全表扫描导致的低效(最常见)
(1)业务场景

某电商平台的“订单查询”功能,用户输入“用户ID=10086”查询其所有订单,SQL如下:

-- 原始SQL
SELECT order_id, order_time, total_amount 
FROM orders 
WHERE user_id = 10086;
(2)问题诊断

用 EXPLAIN 查看执行计划,发现 type 列为 ALL(全表扫描),rows 列为1000000(扫描100万行数据)——因 user_id 未建索引,数据库需遍历整张表匹配条件。

(3)优化方案

为查询条件 user_id 添加单列索引

-- 添加索引
CREATE INDEX idx_orders_userid ON orders(user_id);
(4)优化效果

EXPLAIN 显示 type 变为 ref(索引查找),rows 降至10(仅扫描该用户的10条订单),查询耗时从500ms降至5ms。

(5)延伸思考

若查询需要返回 order_id, user_id, order_time(均为索引覆盖的字段),可创建覆盖索引进一步优化(避免“回表查询”):

-- 覆盖索引:包含查询所需的所有字段
CREATE INDEX idx_orders_userid_cover ON orders(user_id) INCLUDE (order_id, order_time);

 

2、索引失效——看似用了索引,实则没生效
(1)业务场景

同上“订单表”,查询“2024年1月1日后,用户ID=10086的未支付订单”,SQL如下:

-- 原始SQL(假设已建 idx_orders_userid 索引)
SELECT order_id, total_amount 
FROM orders 
WHERE user_id = 10086 
  AND order_time > '2024-01-01' 
  AND status = 0; -- 0=未支付
(2)问题诊断

EXPLAIN 显示 type 仍为 ref,但 rows 仍有100(扫描了该用户的100条订单,而非符合时间和状态的10条)——因索引仅包含 user_id,后续条件需“回表”后过滤。

若尝试对 order_time 单独建索引,仍会因“索引选择性低”(多个用户共享同一时间)导致失效。

(3)优化方案

创建联合索引(遵循“最左前缀原则”)

联合索引的字段顺序需按“过滤性从高到低”排列(过滤性=区分度,如 user_id 区分度远高于 order_time):

-- 联合索引:user_id(高区分度)→ order_time(中区分度)→ status(低区分度)
CREATE INDEX idx_orders_userid_time_status ON orders(user_id, order_time, status);
(4)优化原理

联合索引会先按 user_id 筛选,再在结果中按 order_time 筛选,最后按 status 筛选,全程通过索引完成,无需回表。

EXPLAIN 显示 type 变为 range(范围索引查找),rows 降至10,耗时进一步缩短至2ms。

(5)避坑点

索引失效的常见情况:

对索引字段做函数操作:WHERE DATE(order_time) = '2024-01-01'(应改为 order_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59')。

使用 不等于(!=、<>) 或 IS NOT NULL:索引无法高效定位范围,易触发全表扫描。

联合索引不满足“最左前缀”:如用 idx_orders_userid_time_status 时,WHERE order_time > '2024-01-01' 会跳过 user_id,导致索引失效。

 

3、子查询低效——多层嵌套导致的性能损耗
(1)业务场景

查询“购买过‘iPhone 15’的用户姓名和手机号”,SQL如下:

-- 原始SQL(子查询嵌套)
SELECT username, phone 
FROM users 
WHERE user_id IN (
    SELECT user_id 
    FROM orders 
    WHERE order_id IN (
        SELECT order_id 
        FROM order_items 
        WHERE product_name = 'iPhone 15'
    )
);
(2)问题诊断

多层子查询会导致数据库多次“嵌套执行”(先查 order_items,再查 orders,最后查 users),且可能产生临时表,耗时300ms。

(3)优化方案1

替换为“JOIN连接”**

JOIN是数据库优化器更易处理的方式,可减少临时表创建:

-- 优化后:JOIN连接
SELECT DISTINCT u.username, u.phone 
FROM users u
JOIN orders o ON u.user_id = o.user_id
JOIN order_items oi ON o.order_id = oi.order_id
WHERE oi.product_name = 'iPhone 15';
(4)优化方案2

若子查询结果唯一,用“EXISTS”替代“IN”

当子查询返回大量数据时,IN 会将结果存入临时表,而 EXISTS 是“半连接”(找到匹配即停止),效率更高

-- 优化后:EXISTS半连接
SELECT username, phone 
FROM users u
WHERE EXISTS (
    SELECT 1 
    FROM orders o
    JOIN order_items oi ON o.order_id = oi.order_id
    WHERE o.user_id = u.user_id 
      AND oi.product_name = 'iPhone 15'
);
(5)优化效果

两种方案耗时均降至30ms,比原始子查询提升10倍。

 

4、分页查询越往后越慢——LIMIT offset过大
(1)业务场景

电商商品列表分页,查询“第1000页,每页20条商品”,SQL如下:

-- 原始SQL
SELECT product_id, product_name, price 
FROM products 
ORDER BY create_time DESC 
LIMIT 20000, 20; -- 跳过前20000条,取20条
(2)问题诊断

LIMIT 20000,20 会先扫描前20020条数据,再丢弃前20000条——随着 offset 增大,扫描行数呈线性增长,第1000页耗时达200ms。

(3)优化方案1:用“索引定位”替代“offset跳过”

利用有序索引(如 create_time),先找到第20001条数据的 create_time,再通过条件筛选:

-- 步骤1:找到第20001条数据的create_time
SELECT create_time 
FROM products 
ORDER BY create_time DESC 
LIMIT 20000, 1; -- 假设结果为 '2024-05-01 10:00:00'

-- 步骤2:基于create_time筛选后续数据
SELECT product_id, product_name, price 
FROM products 
WHERE create_time < '2024-05-01 10:00:00'
ORDER BY create_time DESC 
LIMIT 20;
(4)优化方案2:用“主键分页”(若主键有序)

product_id 是自增主键(与 create_time 同步递增),可直接通过主键定位:

-- 假设第1000页的起始product_id为20001
SELECT product_id, product_name, price 
FROM products 
WHERE product_id > 20000
ORDER BY product_id DESC 
LIMIT 20;
(5)优化效果:两种方案均避免了大量扫描,耗时降至10ms以内。

 

5、写入慢——批量插入 vs 单条插入
(1)业务场景:电商后台导入1000条商品数据,原始代码用“循环单条插入”
-- 原始SQL(循环执行1000次)
INSERT INTO products (product_name, price, create_time) 
VALUES ('商品A', 99.9, NOW());
INSERT INTO products (product_name, price, create_time) 
VALUES ('商品B', 199.9, NOW());
-- ... 重复998次
(2)问题诊断

单条插入每次都需建立数据库连接、解析SQL、刷盘,1000条插入耗时5000ms(5秒)。

(3)优化方案1:使用“批量INSERT”

将多条记录合并为一个INSERT语句,减少连接和解析开销:

-- 优化后:批量插入
INSERT INTO products (product_name, price, create_time) 
VALUES 
('商品A', 99.9, NOW()),
('商品B', 199.9, NOW()),
-- ... 其余998条记录
('商品Z', 299.9, NOW());
(4)优化方案2:关闭“自动提交”(事务批量提交)

若无法合并SQL,可通过事务减少刷盘次数(MySQL默认“自动提交”,每条INSERT都刷盘)

-- 优化后:事务批量提交
START TRANSACTION; -- 开启事务
INSERT INTO products (...) VALUES (...); -- 第1条
INSERT INTO products (...) VALUES (...); -- 第2条
-- ... 其余998条
COMMIT; -- 一次性提交
(5)优化效果:批量插入耗时降至200ms,事务提交降至300ms,效率提升10倍以上。

 

6、表结构设计不合理——冗余字段 vs 过度拆分
(1)业务场景

某系统的“用户表”和“用户详情表”拆分过细,查询用户信息时需频繁JOIN

-- 原始查询:JOIN两张表
SELECT u.user_id, u.username, ud.address, ud.phone 
FROM users u
LEFT JOIN user_details ud ON u.user_id = ud.user_id
WHERE u.user_id = 10086;
(2)问题诊断

虽符合“三范式”,但高频查询需JOIN,增加CPU和I/O消耗;若 user_details 数据量大,JOIN效率更低。

(3)优化方案:适度“反范式”设计,增加冗余字段

将高频查询的 addressphone 冗余到 users 表中,避免JOIN

-- 1. 修改users表,增加冗余字段
ALTER TABLE users ADD COLUMN address VARCHAR(255), ADD COLUMN phone VARCHAR(20);

-- 2. 优化后查询:无需JOIN
SELECT user_id, username, address, phone 
FROM users 
WHERE user_id = 10086;
(4)平衡原则

反范式会增加“数据一致性”维护成本(如更新地址时需同步两张表),需满足:

冗余字段是高频查询字段

字段更新频率(如地址、手机号变更少)。

 

四、进阶调优:数据库层面的优化

除了SQL和索引,数据库配置和架构也会影响性能。

1、调整MySQL配置

innodb_buffer_pool_size:设置为物理内存的50%-70%(缓存索引和数据,减少磁盘I/O)

innodb_flush_log_at_trx_commit:非金融场景可设为2(每秒刷盘,平衡性能和安全性)

join_buffer_size/sort_buffer_size:优化JOIN和排序的内存分配。

2、分库分表

当单表数据量超过1000万行时,即使索引优化到位,查询仍会变慢,需通过“分库分表”拆分数据:

  • 水平分表:按用户ID哈希(如 user_id % 4 分为4张表),分散单表数据量。

  • 垂直分表:将大表按“冷热字段”拆分(如订单表拆为“订单基本表”和“订单详情表”)。

3、读写分离

通过主库(写)+ 从库(读)架构,将查询流量分流到从库,减轻主库压力(需解决“主从延迟”问题)。

 

五、调优总结:核心原则

1、优先优化索引

索引是“最性价比”的调优手段,避免过早依赖分库分表。

2、基于执行计划调优

不盲目优化,用 EXPLAIN 定位瓶颈,避免“想当然”。

3、平衡性能与业务

调优需结合业务场景(如金融场景优先保证一致性,电商场景优先保证查询速度)。

4、避免过度优化

简单SQL无需复杂索引,过度索引会降低写入性能。

通过以上场景的实战优化,可显著提升数据库的响应速度,支撑高并发业务场景。