导航菜单

索引设计与优化

🔴 困难

题目描述

给定以下查询,应该如何设计索引?

-- 查询 1
SELECT * FROM orders WHERE user_id = 1 AND status = 1;

-- 查询 2
SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at DESC;

-- 查询 3
SELECT * FROM orders WHERE status = 1 AND created_at >= '2024-01-01';

-- 查询 4
SELECT user_id, COUNT(*) FROM orders WHERE status = 1 GROUP BY user_id;

-- 查询 5
SELECT * FROM orders WHERE user_id IN (1, 2, 3) AND status IN (1, 2);

表结构

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    product_id BIGINT,
    status INT,
    amount DECIMAL(10,2),
    created_at DATETIME,
    updated_at DATETIME
) ENGINE=InnoDB;

提示

  • 考虑查询频率
  • 考虑区分度
  • 考虑最左前缀
  • 避免冗余索引

解法

参考答案 (3 个标签)
索引设计 联合索引 查询优化

分析查询模式

查询 1:等值查询

SELECT * FROM orders WHERE user_id = 1 AND status = 1;

-- 索引方案 1:联合索引
CREATE INDEX idx_user_status ON orders(user_id, status);

-- 分析:
-- 1. user_id 区分度高(假设 100万用户)
-- 2. status 区分度低(只有 5 个状态)
-- 3. 联合索引 (user_id, status) 效率高

-- 索引方案 2:两个独立索引
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_status ON orders(status);

-- 分析:
-- 1. MySQL 可能只使用一个索引(user_id)
-- 2. status 需要通过 Using where 过滤
-- 3. 不如联合索引高效

最优方案

CREATE INDEX idx_user_status ON orders(user_id, status);

查询 2:排序查询

SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at DESC;

-- 索引方案
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);

-- 分析:
-- 1. user_id 用于过滤
-- 2. created_at 用于排序(避免 filesort)
-- 3. Extra: Using where, Using index (如果是覆盖索引)

最优方案

CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);

查询 3:范围查询

SELECT * FROM orders WHERE status = 1 AND created_at >= '2024-01-01';

-- 索引方案
CREATE INDEX idx_status_created ON orders(status, created_at);

-- 分析:
-- 1. status 用于过滤
-- 2. created_at 用于范围查询
-- 3. 注意:created_at >= 之后的部分不能使用索引

最优方案

CREATE INDEX idx_status_created ON orders(status, created_at);

查询 4:聚合查询

SELECT user_id, COUNT(*) FROM orders WHERE status = 1 GROUP BY user_id;

-- 索引方案
CREATE INDEX idx_status_user ON orders(status, user_id);

-- 分析:
-- 1. status 用于过滤
-- 2. user_id 用于 GROUP BY(避免临时表)
-- 3. Extra: Using index (如果是覆盖索引)

最优方案

-- 如果只需要 user_id
CREATE INDEX idx_status_user ON orders(status, user_id);

-- 如果需要其他字段,考虑覆盖索引
CREATE INDEX idx_status_user_amount ON orders(status, user_id, amount);

查询 5:IN 查询

SELECT * FROM orders WHERE user_id IN (1, 2, 3) AND status IN (1, 2);

-- 索引方案
CREATE INDEX idx_user_status ON orders(user_id, status);

-- 分析:
-- 1. user_id IN (1, 2, 3) 可以使用索引(多次等值查询)
-- 2. status IN (1, 2) 可以使用索引
-- 3. 联合索引 (user_id, status) 有效

最优方案

CREATE INDEX idx_user_status ON orders(user_id, status);

综合索引设计

-- 方案 1:多个独立索引(不够优化)
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_status ON orders(status);
CREATE INDEX idx_created_at ON orders(created_at);

-- 方案 2:联合索引(推荐)
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);

-- 分析:
-- 查询 1: ✓ 使用 (user_id, status)
-- 查询 2: ✓ 使用 (user_id, created_at) - 部分使用
-- 查询 3: ✗ 不使用(user_id 在最左)
-- 查询 4: ✓ 使用 (user_id)
-- 查询 5: ✓ 使用 (user_id, status)

-- 方案 3:多个联合索引(最优)
CREATE INDEX idx_user_status ON orders(user_id, status);
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);
CREATE INDEX idx_status_created ON orders(status, created_at);

-- 分析:
-- 查询 1: ✓ 使用 idx_user_status
-- 查询 2: ✓ 使用 idx_user_created
-- 查询 3: ✓ 使用 idx_status_created
-- 查询 4: ✓ 使用 idx_user_status
-- 查询 5: ✓ 使用 idx_user_status

最终推荐方案

-- 核心索引
CREATE INDEX idx_user_status ON orders(user_id, status);
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);
CREATE INDEX idx_status_created ON orders(status, created_at);

-- 覆盖索引(如果查询频繁)
CREATE INDEX idx_user_status_created_amount ON 
    orders(user_id, status, created_at DESC, amount);

-- 删除冗余索引
-- DROP INDEX idx_user_id;  -- 被 idx_user_status 覆盖
-- DROP INDEX idx_status;   -- 被 idx_status_created 覆盖

索引设计原则

1. 区分度优先

-- ❌ 区分度低
CREATE INDEX idx_gender ON users(gender);  -- 只有 M/F

-- ✓ 区分度高
CREATE INDEX idx_user_id ON orders(user_id);  -- 假设 100万用户

-- 计算区分度
SELECT 
    COUNT(DISTINCT user_id) / COUNT(*) AS user_id_selectivity,
    COUNT(DISTINCT status) / COUNT(*) AS status_selectivity
FROM orders;
-- user_id_selectivity: 0.9 (高)
-- status_selectivity: 0.0001 (低)

2. 最左前缀原则

-- ✓ 联合索引 (a, b, c)
CREATE INDEX idx_abc ON table(a, b, c);

-- 可以使用索引:
-- WHERE a = 1
-- WHERE a = 1 AND b = 2
-- WHERE a = 1 AND b = 2 AND c = 3
-- WHERE a = 1 AND b = 2 AND c > 3

-- 不能使用索引(或部分使用):
-- WHERE b = 2
-- WHERE c = 3
-- WHERE b = 2 AND c = 3
-- WHERE a = 1 AND c = 3 (只使用 a)

3. 覆盖索引

-- ✓ 覆盖索引(不回表)
CREATE INDEX idx_user_status_amount ON orders(user_id, status, amount);

SELECT user_id, status, amount FROM orders WHERE user_id = 1;
-- Extra: Using index

-- ✗ 需要回表
SELECT * FROM orders WHERE user_id = 1;
-- 需要回表获取其他字段

4. 避免冗余索引

-- ❌ 冗余
CREATE INDEX idx_a ON table(a);
CREATE INDEX idx_a_b ON table(a, b);

-- ✓ 只保留联合索引
CREATE INDEX idx_a_b ON table(a, b);
-- 可以满足 WHERE a = 1 的查询

5. 索引列顺序

-- 区分度高的放左边
CREATE INDEX idx_user_status ON orders(user_id, status);
-- ✓ user_id 区分度高
-- ✓ status 区分度低

-- 考虑排序需求
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);
-- ✓ 避免 filesort

索引维护

查看索引使用情况

-- 查看索引基数
SELECT 
    table_name,
    index_name,
    cardinality,
    seq_in_index,
    column_name
FROM information_schema.statistics
WHERE table_schema = 'your_database'
  AND table_name = 'orders';

-- 查看未使用的索引
SELECT 
    object_schema,
    object_name,
    index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
  AND count_star = 0
ORDER BY object_schema, object_name;

删除冗余索引

-- 分析冗余索引
-- 1. 查看所有索引
SHOW INDEX FROM orders;

-- 2. 识别冗余
-- idx_user (user_id)
-- idx_user_status (user_id, status)
-- idx_user_status 是超集,可以删除 idx_user

-- 3. 删除前备份
-- ALTER TABLE orders DROP INDEX idx_user;

索引重建

-- 重建索引(清理碎片)
ALTER TABLE orders ENGINE=InnoDB;
-- 或
OPTIMIZE TABLE orders;

-- 分析表
ANALYZE TABLE orders;

搜索