1. 为什么SQL查询会变慢?
在数据库应用中,90%的性能问题都源于不当的查询设计。当数据量达到百万级别时,一个未经优化的查询可能需要数秒甚至数分钟才能完成,而经过合理优化的相同查询可能只需要几十毫秒。这种性能差异的核心在于数据库引擎如何处理数据检索请求。
数据库执行查询时,最耗时的操作是磁盘I/O。在没有索引的情况下,数据库必须执行全表扫描(Full Table Scan),即逐行检查表中的每一条记录。假设一个表有100万行数据,即使只需要查找其中10条记录,数据库也不得不读取全部100万行。这就是为什么随着数据量增长,查询性能会呈指数级下降。
注意:全表扫描的时间复杂度是O(n),而使用索引的理想情况下可以降到O(log n),这就是为什么合理使用索引能带来数量级的性能提升。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 索引的工作原理与类型选择
2.1 B-Tree索引:关系型数据库的基石
B-Tree(平衡树)是大多数关系型数据库默认的索引结构,包括MySQL的InnoDB、PostgreSQL等。它的核心优势在于保持数据有序的同时,提供稳定的查询性能。B-Tree索引特别适合范围查询(BETWEEN、>、<等操作符)和精确匹配(=操作符)。
创建B-Tree索引的基本语法:
sql复制CREATE INDEX idx_user_name ON users(name);
2.2 哈希索引:极致快速的等值查询
哈希索引通过对索引键应用哈希函数来定位数据,理论上可以实现O(1)的时间复杂度。但它的局限性也很明显:
- 仅支持精确匹配,不支持范围查询
- 不支持部分索引匹配(如LIKE 'abc%')
- 哈希冲突会影响性能
MySQL的Memory引擎默认使用哈希索引,InnoDB也支持自适应哈希索引(Adaptive Hash Index)。
2.3 复合索引:多列查询的优化利器
当查询条件涉及多个列时,复合索引(Composite Index)往往比单列索引更有效。例如对于查询:
sql复制SELECT * FROM orders WHERE user_id = 100 AND status = 'completed';
创建复合索引的正确方式:
sql复制CREATE INDEX idx_user_status ON orders(user_id, status);
关键技巧:复合索引的列顺序至关重要。应该将选择性高(唯一值多)的列放在前面,遵循"最左前缀原则"。
3. 实战:索引优化让查询快10倍
3.1 案例背景分析
我们有一个电商平台的订单表,包含500万条记录,结构如下:
sql复制CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
product_id BIGINT,
amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP
);
未优化前的查询(平均执行时间1.2秒):
sql复制SELECT * FROM orders
WHERE user_id = 12345 AND status = 'shipped'
ORDER BY created_at DESC;
3.2 优化方案实施
首先分析现有索引:
sql复制SHOW INDEX FROM orders;
发现只有主键id的索引。我们添加复合索引:
sql复制CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
3.3 优化效果验证
执行相同的查询,现在只需要0.11秒,性能提升超过10倍。使用EXPLAIN查看执行计划:
sql复制EXPLAIN SELECT * FROM orders
WHERE user_id = 12345 AND status = 'shipped'
ORDER BY created_at DESC;
优化前后的执行计划对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 扫描行数 | 5,000,000 | 15 |
| 执行时间 | 1200ms | 110ms |
| 使用索引 | 无 | idx_user_status_created |
| 排序操作 | Using filesort | 无 |
4. 高级索引策略与避坑指南
4.1 索引选择性:为什么不是所有列都该建索引
索引选择性是指索引列中不同值的数量与表中记录总数的比值。高选择性的列(如用户ID)适合建索引,而低选择性的列(如性别)建索引效果有限。
计算选择性的SQL:
sql复制SELECT
COUNT(DISTINCT status)/COUNT(*) AS selectivity
FROM orders;
4.2 覆盖索引:避免回表操作
当索引包含查询所需的所有字段时,数据库可以直接从索引获取数据而无需回表,这称为覆盖索引。例如:
sql复制-- 需要回表
SELECT * FROM orders WHERE user_id = 100;
-- 覆盖索引
SELECT user_id, status FROM orders WHERE user_id = 100;
创建覆盖索引:
sql复制CREATE INDEX idx_covering ON orders(user_id, status, created_at);
4.3 索引失效的常见陷阱
-
隐式类型转换:当查询条件与索引列类型不一致时
sql复制-- user_id是BIGINT,但用字符串查询 SELECT * FROM orders WHERE user_id = '100'; -
使用函数或运算:对索引列使用函数会使索引失效
sql复制SELECT * FROM orders WHERE DATE(created_at) = '2023-01-01'; -
前导通配符:LIKE '%abc'无法使用索引
sql复制SELECT * FROM products WHERE name LIKE '%手机%'; -
OR条件不当使用:OR条件可能导致索引失效
sql复制SELECT * FROM orders WHERE user_id = 100 OR amount > 1000;
5. 特殊场景的索引优化
5.1 全文索引:文本搜索的加速器
对于文本内容的搜索,常规索引效率低下。全文索引(FULLTEXT)采用倒排索引结构,特别适合LIKE '%关键词%'这类查询。
创建全文索引:
sql复制ALTER TABLE products ADD FULLTEXT INDEX ft_idx_name_desc(name, description);
使用全文搜索:
sql复制SELECT * FROM products
WHERE MATCH(name, description) AGAINST('智能手机' IN NATURAL LANGUAGE MODE);
5.2 部分索引:减少索引存储开销
当只需要索引表中部分数据时,可以创建条件索引。例如只索引未完成的订单:
sql复制CREATE INDEX idx_pending_orders ON orders(status)
WHERE status IN ('pending', 'processing');
5.3 索引合并与索引跳跃扫描
现代数据库引擎支持高级索引技术:
- 索引合并:对多个单列索引的条件使用UNION或INTERSECT
- 索引跳跃扫描:即使不满足最左前缀,也能利用复合索引
MySQL 8.0+支持索引跳跃扫描,可以通过优化器开关控制:
sql复制SET optimizer_switch = 'skip_scan=on';
6. 监控与维护索引健康
6.1 识别未使用的索引
长期不用的索引会拖慢写操作,应该定期清理:
sql复制-- MySQL
SELECT * FROM sys.schema_unused_indexes;
-- PostgreSQL
SELECT * FROM pg_stat_all_indexes
WHERE idx_scan = 0 AND idx_tup_read = 0;
6.2 索引统计信息更新
数据库依赖统计信息决定是否使用索引,过时的统计信息会导致优化器做出错误决策。
更新统计信息:
sql复制-- MySQL
ANALYZE TABLE orders;
-- SQL Server
UPDATE STATISTICS orders;
6.3 索引碎片整理
随着数据增删改,索引会产生碎片,影响性能。
重建索引:
sql复制-- MySQL
ALTER TABLE orders ENGINE=InnoDB;
-- SQL Server
ALTER INDEX idx_user_status ON orders REBUILD;
在实际生产环境中,我通常会设置定期维护任务,在业务低峰期执行这些优化操作。对于特别大的表,采用在线DDL工具(如pt-online-schema-change)可以避免锁表影响业务。
