那天下午三点,运维群里突然炸开了锅——用户反馈系统卡顿严重。我打开监控一看,一个原本应该毫秒级响应的订单查询接口,竟然平均耗时达到了4.7秒。通过EXPLAIN分析执行计划,发现这个简单的SELECT查询正在对百万级的订单表进行全表扫描。这就像要在图书馆找一本特定书籍,却选择从第一排书架开始逐本翻阅,效率可想而知。
全表扫描(Full Table Scan)是数据库查询中最耗资源的操作之一。当表没有合适的索引时,数据库引擎别无选择,只能逐行读取整张表的所有数据页。我见过一个真实案例:某电商平台的商品表从最初的3万条增长到300万条后,一个原本200ms的查询逐渐恶化到8秒,罪魁祸首就是全表扫描。
关键指标:全表扫描的耗时与表大小呈线性关系。根据MySQL基准测试,无索引的100万行表简单查询需要约1.2秒,而1000万行则需要12秒左右。
现代数据库都采用基于成本的优化器(Cost-Based Optimizer,CBO),它就像个精明的老会计。当收到SQL查询时,会计算各种执行路径的"成本",包括:
以PostgreSQL为例,其成本计算公式为:
code复制总成本 = (seq_page_cost × 读取的页面数)
+ (cpu_tuple_cost × 扫描的行数)
+ (cpu_operator_cost × 操作次数)
优化器的决策质量高度依赖统计信息的准确性。这些统计信息包括:
我曾遇到一个统计信息过期的案例:某表经过大量DELETE操作后,优化器仍按照旧的行数估算,错误选择了全表扫描而非索引扫描。通过ANALYZE table_name更新统计信息后,查询速度立即提升了20倍。
主流数据库的默认索引类型都是B-Tree(PostgreSQL还支持BRIN、GIN等)。其核心特点包括:
一个典型的B-Tree索引查找过程:
联合索引就像多级分类的图书编目系统。假设有索引(A,B,C):
WHERE A=1 AND B=2 AND C=3WHERE A=1 AND B>2WHERE B=2实际案例:用户行为日志表常用查询是WHERE user_id=? AND create_time BETWEEN ? AND ?,最佳索引应该是(user_id, create_time)而非反过来。
区分度(Selectivity)的计算公式:
code复制区分度 = 不同值的数量 / 总行数
经验阈值:
我曾优化过一个查询:原本在"状态"字段(只有5个枚举值)上建了独立索引,查询仍需3秒。改为(状态, create_time)联合索引后,降至200ms。
根据查询模式设计索引:
WHERE id=123 → 单列索引WHERE time>'2023-01-01' → 该列放在联合索引右侧ORDER BY create_time DESC → 确保索引包含排序字段每个索引的写入开销:
建议监控pg_stat_user_indexes中的idx_scan与idx_tup_read,定期清理使用率低的索引。
典型反模式:
sql复制SELECT * FROM table ORDER BY id LIMIT 10 OFFSET 1000000
优化方案1:游标分页
sql复制-- 第一页
SELECT * FROM table ORDER BY id LIMIT 10
-- 后续页(记住最后一条记录的id)
SELECT * FROM table WHERE id > last_id ORDER BY id LIMIT 10
优化方案2:覆盖索引
sql复制-- 先通过索引获取主键
SELECT id FROM table ORDER BY create_time LIMIT 10 OFFSET 100
-- 再精确获取数据
SELECT * FROM table WHERE id IN (...)
当WHERE条件包含多个索引列时,数据库可能选择:
示例:
sql复制-- PostgreSQL可能使用位图扫描
EXPLAIN SELECT * FROM orders
WHERE user_id = 100 AND status = 'paid';
处理变形查询的利器:
sql复制-- 查询邮箱域名
CREATE INDEX idx_email_domain ON users((substring(email FROM '@(.+)$')));
-- 不区分大小写查询
CREATE INDEX idx_name_lower ON users(lower(name));
常见问题:
sql复制-- user_id是字符串类型,但传入数字
SELECT * FROM users WHERE user_id = 123;
-- 等价于
SELECT * FROM users WHERE CAST(user_id AS INT) = 123;
-- 导致索引失效
解决方案:保持应用层与数据库类型一致,或显式使用正确类型。
PostgreSQL的EXPLAIN输出关键指标:
Seq Scan:全表扫描(警惕)Index Scan:索引扫描后回表Index Only Scan:仅用索引(最优)Bitmap Heap Scan:位图扫描Sort:内存排序(可能需work_mem调整)配置示例(postgresql.conf):
code复制log_min_duration_statement = 1000 # 记录超过1秒的查询
log_statement = 'none'
分析工具推荐:
关键性能计数器:
pg_stat_database:数据库级统计pg_stat_user_tables:表级读写统计pg_statio_user_tables:表I/O统计某促销活动期间,订单查询API响应时间从平均200ms飙升到5秒,导致前端超时。
通过pg_stat_statements定位慢查询:
sql复制SELECT * FROM orders
WHERE user_id = $1 AND status = 'paid'
ORDER BY create_time DESC
LIMIT 20;
EXPLAIN分析显示:
idx_user_id单列索引创建联合索引:
sql复制CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time DESC);
查询重写为:
sql复制SELECT * FROM orders
WHERE user_id = $1 AND status = 'paid'
ORDER BY create_time DESC
LIMIT 20;
优化效果:查询时间从5秒降至50ms,CPU使用率下降40%。
经过上百次优化实践,我总结出这些铁律:
最后分享一个检查清单,每次设计索引前都应该自问: