即使查询使用了索引,仍然可能很慢,主要原因可以归纳为 6 大类:

| 类别 | 具体原因 | 影响 |
|---|---|---|
| 索引设计问题 | 索引选择性差、索引列顺序不当、索引冗余 | 扫描行数过多 |
| SQL 写法问题 | SELECT *、LIKE '%xxx'、函数操作、类型转换 | 索引失效或回表过多 |
| 数据分布问题 | 数据量过大、数据倾斜严重、热点数据集中 | 即使走索引也慢 |
| 表结构问题 | 字段过大、行过长、未分区 | I/O 开销大 |
| 数据库配置问题 | 缓冲池过小、连接数不足、参数配置不当 | 整体性能下降 |
| 硬件资源瓶颈 | 磁盘 I/O、内存不足、CPU 瓶颈 | 物理层面慢 |
一句话总结:索引只是加速查询的必要条件而非充分条件,需要结合索引设计、SQL 优化、表结构设计、数据库配置和硬件资源进行系统性优化。
索引选择性对查询性能的影响。选择性越高,索引的效果越好;选择性越低,索引的效果越差。
user_id 字段,100 万用户有 100 万个不同的 user_id,选择性为 1.0,查询时可以快速定位到唯一行。gender,只有 "男" 和 "女" 两个值,100 万行数据的选择性仅为 0.000002,查询 "男" 性用户时需要扫描约 50 万行,数据库优化器可能直接选择全表扫描。判断标准:一般建议索引选择性大于 0.1,可以通过以下 SQL 计算:
-- 计算字段的选择性SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivityFROM table_name;-- 示例:gender 字段的选择性SELECT COUNT(DISTINCT gender) / COUNT(*) AS selectivityFROM users;-- 结果:0.000002(非常低,不适合单独建索引)
-- 创建联合索引CREATE INDEX idx_name_age_gender ON users(name, age, gender);-- ✅ 走索引:符合最左前缀SELECT * FROM users WHERE name = '张三';SELECT * FROM users WHERE name = '张三' AND age = 25;SELECT * FROM users WHERE name = '张三' AND age = 25 AND gender = '男';-- ❌ 不走索引:违反最左前缀SELECT * FROM users WHERE age = 25; -- 缺少 nameSELECT * FROM users WHERE gender = '男'; -- 缺少 name 和 ageSELECT * FROM users WHERE age = 25 AND gender = '男'; -- 缺少 name-- ⚠️ 部分走索引:只有 name 走索引,后面的列无法利用索引排序SELECT * FROM users WHERE name = '张三' AND gender = '男'; -- age 跳过了
核心原则:联合索引要遵循 "最左前缀原则",索引列的顺序非常重要。将区分度高、经常用于查询条件的列放在左边。
-- ❌ 冗余索引示例CREATE INDEX idx_name ON users(name); -- 单列索引CREATE INDEX idx_name_age ON users(name, age); -- 联合索引-- idx_name 是冗余的,因为 idx_name_age 可以覆盖 name 单列查询-- ✅ 优化后:删除冗余索引DROP INDEX idx_name ON users;-- 只保留联合索引 idx_name_age
SELECT * 查询的回表过程。如果查询只需要部分字段,但使用了 SELECT *,就会导致不必要的回表操作,严重影响性能。
name 索引)中找到满足条件的主键 ID,然后根据主键 ID 到聚簇索引中查找完整的行数据。优化方案:使用覆盖索引,只查询索引列,避免回表。
-- ❌ 慢查询:需要回表SELECT * FROM users WHERE name = '张三';-- ✅ 优化:使用覆盖索引,无需回表-- 假设有索引 idx_name_age(name, age)SELECT name, age FROM users WHERE name = '张三';
-- ✅ 走索引:前缀匹配SELECT * FROM users WHERE name LIKE '张%';-- ❌ 不走索引:后缀匹配或包含匹配SELECT * FROM users WHERE name LIKE '%张'; -- 后缀匹配SELECT * FROM users WHERE name LIKE '%张%'; -- 包含匹配-- ✅ 优化方案 1:使用覆盖索引-- 即使 LIKE '%张%' 不走索引,如果只查询索引列,可能走索引扫描SELECT name FROM users WHERE name LIKE '%张%';-- ✅ 优化方案 2:使用全文索引ALTER TABLE users ADD FULLTEXT INDEX ft_name(name);SELECT * FROM users WHERE MATCH(name) AGAINST('张');-- ✅ 优化方案 3:使用搜索引擎(如 Elasticsearch)-- ❌ 不走索引:对索引列使用函数SELECT * FROM users WHERE DATE(create_time) = '2024-01-01';SELECT * FROM users WHERE YEAR(create_time) = 2024;SELECT * FROM users WHERE SUBSTRING(name, 1, 1) = '张';-- ✅ 走索引:使用范围查询或等值查询SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';-- ❌ 不走索引:索引列参与计算SELECT * FROM users WHERE age + 1 = 26;-- ✅ 走索引:调整计算方式SELECT * FROM users WHERE age = 25;
-- 假设 user_id 是 VARCHAR 类型CREATE INDEX idx_user_id ON users(user_id);-- ❌ 不走索引:字符串与数字比较,发生隐式类型转换SELECT * FROM users WHERE user_id = 123;-- 等价于:SELECT * FROM users WHERE CAST(user_id AS SIGNED) = 123;-- ✅ 走索引:类型一致SELECT * FROM users WHERE user_id = '123';
核心原则:索引列的类型必须与查询条件的类型完全一致,否则会发生隐式类型转换,导致索引失效。
-- 查询表的总行数和大小SELECT table_name, table_rows, ROUND(data_length / 1024 / 1024, 2) AS data_size_mb, ROUND(index_length / 1024 / 1024, 2) AS index_size_mbFROM information_schema.tablesWHERE table_schema = 'your_database';
优化方案:
-- 查看数据分布SELECT gender, COUNT(*) as countFROM usersGROUP BY gender;-- 假设结果:-- gender | count-- -------|---------- 男 | 999000-- 女 | 1000-- 其他 | 0-- 查询 "男" 性用户时,即使有索引,也可能走全表扫描-- 因为优化器认为扫描 999000 行和全表扫描差不多
优化方案:
FORCE INDEX 强制走索引(谨慎使用)-- 强制使用索引SELECT * FROM users FORCE INDEX(idx_gender) WHERE gender = '男';
-- ❌ 慢查询:大字段导致行过长CREATE TABLE articles ( id INT PRIMARY KEY, title VARCHAR(255), content TEXT, -- 大字段 author VARCHAR(100), create_time DATETIME);-- ✅ 优化:将大字段拆分到单独的表CREATE TABLE articles ( id INT PRIMARY KEY, title VARCHAR(255), author VARCHAR(100), create_time DATETIME);CREATE TABLE article_contents ( article_id INT PRIMARY KEY, content LONGTEXT, FOREIGN KEY (article_id) REFERENCES articles(id));
-- ❌ 不合理:使用 VARCHAR 存储 IP 地址CREATE TABLE logs ( id INT PRIMARY KEY, ip VARCHAR(15));-- ✅ 优化:使用 INT UNSIGNED 存储 IP 地址CREATE TABLE logs ( id INT PRIMARY KEY, ip INT UNSIGNED);-- 插入时转换INSERT INTO logs (ip) VALUES (INET_ATON('192.168.1.1'));-- 查询时转换SELECT INET_NTOA(ip) FROM logs;-- 查看当前 InnoDB 缓冲池大小SHOW VARIABLES LIKE 'innodb_buffer_pool_size';-- 建议设置为服务器内存的 70%-80%-- 例如:16GB 内存的服务器,设置为 12GBSET GLOBAL innodb_buffer_pool_size = 12884901888; -- 12GB
-- 查看当前配置SHOW VARIABLES LIKE 'innodb_io_capacity'; -- 磁盘 I/O 能力SHOW VARIABLES LIKE 'innodb_read_io_threads'; -- 读线程数SHOW VARIABLES LIKE 'innodb_write_io_threads'; -- 写线程数SHOW VARIABLES LIKE 'max_connections'; -- 最大连接数-- 根据服务器配置调整-- SSD 硬盘可以设置更高的 innodb_io_capacity
-- 查看执行计划EXPLAIN SELECT * FROM users WHERE name = '张三';-- 关键字段解读-- id: 查询标识符-- select_type: 查询类型(SIMPLE, PRIMARY, SUBQUERY 等)-- table: 访问的表-- type: 访问类型(从好到差:system > const > eq_ref > ref > range > index > ALL)-- possible_keys: 可能使用的索引-- key: 实际使用的索引-- key_len: 使用的索引长度-- rows: 预估扫描的行数-- Extra: 额外信息(Using index, Using where, Using filesort 等)
重点关注:
type 字段:如果是 ALL,说明是全表扫描;如果是 index,说明是索引扫描rows 字段:预估扫描的行数,越大越慢Extra 字段:Using filesort(文件排序)、Using temporary(临时表)都是性能杀手