4. 索引选择策略:基于查询模式的索引选择原则
在 CI/CD 调度平台中,数据库索引的选择直接影响任务调度、状态查询和报表分析的响应速度。索引设计必须与查询模式(等值查询、范围查询、排序、分组)深度绑定,才能实现最优的 I/O 成本和 CPU 开销。本节从四种核心查询模式出发,给出索引选择的量化原则与避坑指南。
核心原则:索引不是越多越好,而是越“匹配查询模式”越好。一个不匹配的索引比没有索引更糟糕(写放大 + 维护成本)。
4.1 等值查询:精确匹配的索引选择
等值查询是最常见的查询模式,例如 WHERE task_id = 'abc-123' 或 WHERE status = 'SUCCESS'。其索引选择原则如下:
- 唯一索引优先:对于主键或业务唯一键(如调度任务 ID、流水号),直接使用唯一索引(UNIQUE INDEX),B+ 树最多扫描 1 条记录,回表次数为 0。
- 普通索引 + 覆盖索引:对于非唯一等值查询(如按状态查询),如果查询列全部在索引中,使用覆盖索引避免回表。
- 复合索引左前缀:如果等值条件涉及多个列(如
WHERE tenant_id = ? AND job_type = ?),将选择性最高的列放在复合索引最左侧。
| 查询示例 | 推荐索引 | 理由 |
|---|---|---|
WHERE id = 1001 |
PRIMARY KEY (id) | 唯一索引,直接命中叶子节点 |
WHERE status = 'RUNNING' |
INDEX (status) + 覆盖列 | 等值过滤,覆盖索引避免回表 |
WHERE tenant_id = 't1' AND job_type = 'spark' |
INDEX (tenant_id, job_type) | 复合索引左前缀,等值匹配 |
避坑:不要在低选择性的列上单独建索引(如
status 只有 3 个值)。此时等值查询可能退化为全表扫描,因为优化器认为索引回表成本高于顺序扫描。
4.2 范围查询:索引选择与边界控制
范围查询在调度平台中非常常见,例如 WHERE create_time BETWEEN '2025-01-01' AND '2025-01-31' 或 WHERE retry_count > 3。范围查询的索引选择与等值查询有显著差异:
- 范围列放在复合索引最后:在复合索引中,范围条件列必须放在等值条件列之后。例如
INDEX (tenant_id, create_time)优于INDEX (create_time, tenant_id),因为前者能先通过等值过滤缩小范围。 - 避免大范围回表:如果范围查询返回大量行(超过表的 20%),优化器可能放弃索引而选择全表扫描。此时应通过分页或分区表控制范围大小。
- 索引下推(ICP):MySQL 5.6+ 支持索引下推,可以在索引层面过滤范围条件后的行,减少回表次数。确保数据库版本开启 ICP。
-- 推荐:等值列在前,范围列在后
CREATE INDEX idx_tenant_time ON task_schedule (tenant_id, create_time);
-- 不推荐:范围列在前,等值列在后
-- CREATE INDEX idx_time_tenant ON task_schedule (create_time, tenant_id);
-- 上述索引无法高效利用等值过滤
4.3 排序查询:避免文件排序的索引策略
排序操作(ORDER BY)是调度平台中性能瓶颈的高发区,尤其是分页排序查询。索引选择原则如下:
- 索引天然有序:B+ 树索引的叶子节点已经按索引列排序。如果
ORDER BY列与索引列顺序一致,数据库可以直接利用索引顺序返回数据,避免filesort。 - 排序方向一致:索引默认 ASC,如果
ORDER BY使用 DESC,需要确认索引是否支持反向扫描(InnoDB 支持,但性能略低于正向)。 - 复合索引排序匹配:如果
ORDER BY涉及多列,复合索引的列顺序必须与排序顺序完全一致,且排序方向相同。
| 查询示例 | 索引设计 | 是否避免 filesort |
|---|---|---|
ORDER BY create_time |
INDEX (create_time) | 是 |
WHERE tenant_id = 't1' ORDER BY create_time |
INDEX (tenant_id, create_time) | 是(等值 + 排序) |
ORDER BY tenant_id, create_time DESC |
INDEX (tenant_id ASC, create_time DESC) | 是(需匹配方向) |
ORDER BY create_time, tenant_id |
INDEX (tenant_id, create_time) | 否(顺序不匹配) |
性能指标:避免 filesort 的排序查询延迟通常在 1ms 以内;而 filesort 在数据量超过 10 万行时,延迟可能飙升到 100ms+。在调度平台的分页查询中,这是最常见的慢查询来源。
4.4 分组查询:索引与临时表的博弈
分组查询(GROUP BY)在调度平台中用于统计任务成功率、资源使用率等。分组查询的索引选择与排序类似,但有其特殊性:
- 索引消除临时表:如果
GROUP BY列与索引列顺序一致,数据库可以直接利用索引分组,避免创建临时表。 - 分组 + 聚合函数:对于
COUNT(*)、SUM()等聚合函数,如果索引覆盖了所有查询列,可以在索引层面完成聚合,无需回表。 - 松散索引扫描(Loose Index Scan):MySQL 8.0+ 支持松散索引扫描,对于
GROUP BY且没有WHERE条件的查询,可以只扫描索引的极小部分。需要索引列与分组列完全匹配。
-- 分组查询示例:按租户统计任务数
SELECT tenant_id, COUNT(*) AS task_count
FROM task_schedule
WHERE create_time > '2025-01-01'
GROUP BY tenant_id;
-- 推荐索引:INDEX (tenant_id, create_time)
-- 索引可以同时满足 WHERE 过滤和 GROUP BY 分组
4.5 综合索引选择矩阵
下表总结了四种查询模式下的索引选择优先级和注意事项:
| 查询模式 | 索引类型 | 复合索引列顺序 | 关键指标 | 常见陷阱 |
|---|---|---|---|---|
| 等值查询 | 唯一索引 / 覆盖索引 | 高选择性列在前 | 回表次数 = 0 | 低选择性列单独建索引 |
| 范围查询 | 复合索引(等值 + 范围) | 等值列在前,范围列在后 | 扫描行数 < 20% | 范围列放在复合索引最左侧 |
| 排序查询 | 索引排序(避免 filesort) | 与 ORDER BY 顺序一致 | Extra 无 Using filesort | 排序方向不一致 |
| 分组查询 | 索引分组(避免临时表) | 与 GROUP BY 顺序一致 | Extra 无 Using temporary | 分组列未包含在索引中 |
4.6 调度平台实战案例:索引选择错误导致调度延迟
某 CI/CD 调度平台在高峰期出现任务调度延迟飙升,经排查发现以下慢查询:
-- 慢查询:按租户和创建时间排序分页
SELECT task_id, job_name, create_time
FROM task_schedule
WHERE tenant_id = 't1'
ORDER BY create_time DESC
LIMIT 20;
-- 原索引:INDEX (create_time DESC)
-- 问题:tenant_id 过滤后,create_time 索引无法直接排序,导致 filesort
优化方案:将索引改为 INDEX (tenant_id, create_time DESC),使索引同时满足等值过滤和排序需求。优化后查询延迟从 120ms 降至 2ms。
经验法则:在调度平台中,80% 的慢查询源于索引与查询模式不匹配。每次新增查询前,先用
EXPLAIN 验证 Extra 字段是否包含 Using filesort、Using temporary 或 Using where(回表过多)。这些是索引需要调整的信号。