跳转至

411_SQL查询优化与执行计划


一句话说明

SQL查询优化就是让数据库少扫描数据、多用索引,把原本跑10分钟的查询压缩到1秒钟完成。


核心知识点

执行计划关键概念

术语含义关注点
Seq Scan全表扫描大表出现要警惕
Index Scan索引扫描选择性高时用
Index Only Scan只扫索引最优情况
Hash Join哈希连接大表join首选
Nested Loop嵌套循环小表join大表
cost估算代价越小越好
rows估算行数与actual偏差大时说明统计信息过期

常见性能杀手

  1. 在WHERE中用函数 → 无法走索引
  2. SELECT * → 传输多余数据
  3. 缺索引的JOIN → 全表扫描
  4. N+1查询 → 循环中查数据库
  5. 没有LIMIT的查询 → 返回全量数据

实战SQL示例

-- ========== PostgreSQL环境 ==========

-- ========== 1. 查看执行计划(EXPLAIN ANALYZE)==========
-- 先用EXPLAIN看估算计划,再用EXPLAIN ANALYZE看实际执行
EXPLAIN ANALYZE
SELECT g.gene_id, g.gene_name, e.expression
FROM genes g
JOIN gene_expression e ON g.gene_id = e.gene_id
WHERE e.condition = 'treatment'
  AND e.expression > 100;

-- 输出解读示例:
-- Seq Scan on gene_expression  (cost=0.00..4521.00 rows=1500 width=32)
--   Filter: (expression > 100 AND condition = 'treatment')
-- -> 说明在全表扫描,应该加索引!

-- Hash Join  (cost=85.50..892.30 rows=450 width=48)
--   Hash Cond: (e.gene_id = g.gene_id)
-- -> Hash Join说明两表都较大,用哈希连接

-- ========== 2. 创建合适的索引 ==========
-- 单列索引:经常用于WHERE过滤的列
CREATE INDEX idx_expression_condition 
ON gene_expression (condition);  -- 经常按condition过滤

-- 复合索引:多列一起用于过滤时,顺序很重要(选择性高的放前面)
CREATE INDEX idx_expr_condition_value 
ON gene_expression (condition, expression);  -- WHERE condition='x' AND expression>y
-- 注意:复合索引 (condition, expression) 可用于 WHERE condition='x'
-- 但不能用于单独 WHERE expression>y(最左前缀原则!)

-- 覆盖索引(Index Only Scan):查询列都在索引中,不用回表
CREATE INDEX idx_covering 
ON gene_expression (condition, expression) 
INCLUDE (gene_id, sample_id);  -- INCLUDE列不参与排序,但加入索引存储
-- SELECT gene_id, sample_id WHERE condition='x' AND expression>y
-- 可以只扫索引,不访问原表

-- 部分索引:只索引满足条件的行(节省空间)
CREATE INDEX idx_high_expression 
ON gene_expression (gene_id) 
WHERE expression > 1000;  -- 只索引高表达的行

-- ========== 3. 常见反模式与修复 ==========
-- ❌ 反模式1:WHERE中对列使用函数(导致无法走索引)
SELECT * FROM samples WHERE UPPER(sample_name) = 'S001';
-- ✅ 修复:在插入时就统一大小写,或建函数索引
CREATE INDEX idx_upper_name ON samples (UPPER(sample_name));

-- ❌ 反模式2:WHERE列隐式类型转换
SELECT * FROM gene_expression WHERE gene_id = 12345;  -- gene_id是VARCHAR但传了INT
-- ✅ 修复:确保类型一致
SELECT * FROM gene_expression WHERE gene_id = '12345';  -- 加引号

-- ❌ 反模式3:OR条件(可能导致索引失效)
SELECT * FROM gene_expression WHERE condition = 'T1' OR condition = 'T2';
-- ✅ 修复:用IN替代
SELECT * FROM gene_expression WHERE condition IN ('T1', 'T2');

-- ❌ 反模式4:不必要的SELECT *
SELECT * FROM gene_expression WHERE condition = 'treatment';
-- ✅ 修复:只选需要的列
SELECT gene_id, expression FROM gene_expression WHERE condition = 'treatment';

-- ❌ 反模式5:对大表不加LIMIT
SELECT * FROM gene_expression;  -- 可能有百万行
-- ✅ 修复:加LIMIT
SELECT * FROM gene_expression LIMIT 1000 OFFSET 0;

-- ========== 4. JOIN优化 ==========
-- ❌ 低效:在大表上做笛卡尔积后过滤
SELECT * FROM gene_expression e, samples s
WHERE e.sample_id = s.sample_id;  -- 老式写法,容易遗漏条件变成笛卡尔积

-- ✅ 高效:明确写JOIN,思路清晰
SELECT e.gene_id, e.expression, s.patient_id
FROM gene_expression e
INNER JOIN samples s ON e.sample_id = s.sample_id  -- 明确连接条件
WHERE e.condition = 'treatment';

-- 确保JOIN列有索引(非常重要!)
CREATE INDEX idx_expression_sample ON gene_expression (sample_id);
CREATE INDEX idx_samples_id ON samples (sample_id);

-- ========== 5. 更新统计信息(解决估算偏差问题)==========
-- 当执行计划中 rows估算与actual rows差距很大时,需要更新统计信息
ANALYZE gene_expression;  -- 更新单表统计信息
ANALYZE;                  -- 更新所有表统计信息(耗时)

-- ========== 6. 实用:找出慢查询 ==========
-- PostgreSQL:查询慢查询日志(需开启pg_stat_statements扩展)
SELECT 
    query,
    calls,                                          -- 执行次数
    ROUND(total_exec_time / calls, 2) AS avg_ms,   -- 平均执行时间(毫秒)
    rows / calls AS avg_rows                        -- 平均返回行数
FROM pg_stat_statements
ORDER BY avg_ms DESC  -- 按平均执行时间排序
LIMIT 10;             -- 取最慢的10个查询

面试常问点

  1. Q: 什么是最左前缀原则? A: 复合索引(A,B,C)只能从左开始使用:WHERE A=1WHERE A=1 AND B=2可走索引;WHERE B=2不能走索引。

  2. Q: LIKE '%abc%'能走索引吗? A: 不能(前缀是通配符)。LIKE 'abc%'(只有后缀通配符)可以走索引。全文搜索用全文索引(GIN)。

  3. Q: 什么时候全表扫描比索引快? A: 当查询返回行数超过总行数的20-30%时,全表顺序扫描比随机IO的索引扫描更快。数据库优化器会自动判断。

  4. Q: EXPLAIN和EXPLAIN ANALYZE有什么区别? A: EXPLAIN只显示估算计划(不执行SQL);EXPLAIN ANALYZE实际执行SQL并显示实际耗时和行数(会修改数据!SELECT可以放心用,UPDATE要小心)。


速查表

-- 查看执行计划
EXPLAIN SELECT ...;
EXPLAIN ANALYZE SELECT ...;  -- 实际执行并显示真实统计

-- 创建索引
CREATE INDEX idx_name ON table (col);
CREATE INDEX idx_name ON table (col1, col2);  -- 复合索引
DROP INDEX idx_name;

-- 查看索引使用情况(PostgreSQL)
SELECT indexname, idx_scan, idx_tup_read FROM pg_stat_user_indexes;

-- 强制使用/禁用索引(PostgreSQL调试用)
SET enable_seqscan = off;   -- 禁用全表扫描(强制用索引)
SET enable_seqscan = on;    -- 恢复默认