功能概述
Ordering Index(有序索引)不是一种新的索引类型,而是指优化器利用普通索引天然有序的特性,按查询需要的顺序读取数据。
当索引顺序与
ORDER BY 或 GROUP BY 的顺序要求兼容时,数据库可以获得以下收益:Skip Sort:避免
filesort,减少排序的 CPU、内存和临时空间开销;流式聚集:相同分组的数据连续到达,完成一组即可输出一组;
LIMIT 提前终止:按目标顺序取得足够结果后停止扫描;
LIMIT-aware Join Order:多表查询中,将有序读取和提前终止的收益纳入连接顺序选择。
该能力适合分页、Top-N、时间线、排行榜,以及按维度分组的聚集查询。
说明:
“使用 Ordering Index”既可能表示当前过滤索引本身已经满足顺序,也可能表示优化器从过滤性更强的索引切换到能够提供顺序的索引。
支持版本
适用于 TDSQL Boundless V21.6.4.0 及以上版本。
索引如何提供顺序
以联合索引
INDEX(a, b, c) 为例,索引先按 a 排序,再在相同 a 内按 b、c 排序。因此,下列查询可以直接利用索引顺序:SELECT *FROM tORDER BY a, b, c;
如果前导列被等值条件固定,也可以从后续列开始利用顺序:
SELECT *FROM tWHERE a = 10ORDER BY b, c;
此时
a 只有一个值,INDEX(a, b, c) 对当前结果集提供的有效顺序等价于 (b, c)。以下查询通常不能利用该索引完成全局排序:
SELECT *FROM tORDER BY b, c;
不同
a 值内部的 (b, c) 分别有序,但合并后不具备全局 (b, c) 顺序。排序方向
索引可以正向或反向扫描。升序索引可以通过反向扫描满足:
SELECT *FROM tWHERE a = 10ORDER BY b DESCLIMIT 20;
传统
EXPLAIN 可能显示 Backward index scan。如果
ORDER BY 包含混合方向,建议使索引定义与排序方向一致:CREATE INDEX idx_score_timeON ranking(category ASC, score DESC, created_at ASC);
对应查询:
SELECT *FROM rankingWHERE category = 1ORDER BY score DESC, created_at ASCLIMIT 20;
方向不匹配、存储引擎不支持反向读取,或混合方向的多范围扫描无法保持顺序时,优化器仍会使用
filesort。范围条件的影响
联合索引前导列上的范围条件可能破坏后续列的全局顺序:
-- 索引为 INDEX(a, b)SELECT *FROM tWHERE a BETWEEN 1 AND 10ORDER BY b;
每个
a 值内部的 b 有序,但多个 a 范围合并后通常不能保证全局 b 有序,因此仍可能需要 filesort。IN 形成多个范围时也需按执行计划确认。利用 Ordering Index 实现 Skip Sort
如果访问路径输出的记录已经满足最终顺序,执行阶段不再创建额外排序步骤,这就是 Skip Sort。
有序路径:
索引范围扫描或索引扫描-> 条件过滤-> 返回结果
普通排序路径:
范围扫描或全表扫描-> 条件过滤-> 临时结果-> filesort-> 返回结果
Skip Sort 不要求索引覆盖所有返回列。非覆盖索引也可以提供顺序,但可能需要逐行回表。优化器会综合比较:
当前过滤访问路径的读取成本;
Ordering Index 的扫描和回表成本;
过滤条件选择率;
排序成本;
LIMIT 大小;多表连接的 fanout。
单表 Top-N 示例
CREATE TABLE events (id BIGINT PRIMARY KEY,user_id BIGINT NOT NULL,created_at DATETIME NOT NULL,payload VARCHAR(200),KEY idx_user_time(user_id, created_at DESC));EXPLAINSELECT id, created_at, payloadFROM eventsWHERE user_id = 100ORDER BY created_at DESCLIMIT 20;
理想计划特征:
key 为 idx_user_time;Extra 中没有 Using filesort;反向读取升序索引时可能显示
Backward index scan。是否使用 Ordering Index 由成本决定。数据规模很小,或其他索引过滤后只需排序少量数据时,选择
filesort 也可能是更优计划。利用 Ordering Index 实现流式聚集
工作原理
对于
GROUP BY,只要输入数据按分组键兼容的顺序排列,相同分组的记录就会连续到达。执行器可以:1. 初始化当前分组的聚集状态;
2. 持续累加当前组记录;
3. 发现分组键变化时输出上一组;
4. 重置状态并开始下一组。
该方式通常只需维护当前分组及聚集函数状态,不需要同时保存全部分组,因而可以降低聚集内存和首批结果延迟。
使用示例
CREATE TABLE metrics (id BIGINT PRIMARY KEY,tenant_id BIGINT NOT NULL,category INT NOT NULL,value DECIMAL(18,2) NOT NULL,KEY idx_tenant_category(tenant_id, category));EXPLAINSELECT category, COUNT(*), SUM(value)FROM metricsWHERE tenant_id = 100GROUP BY category;
tenant_id 被等值条件固定后,索引可以按 category 连续输出数据。理想计划特征:key 为 idx_tenant_category;Extra 中没有 Using temporary;Extra 中没有 Using filesort。prefer_ordering_index
功能说明
prefer_ordering_index 控制优化器是否更积极地搜索能够满足 ORDER BY 或 GROUP BY 顺序的替代索引。开启后,如果当前过滤路径不能提供所需顺序,优化器会评估其他候选索引,主要考虑:
是否完整提供目标顺序;
正向还是反向扫描;
是否为覆盖索引;
Ordering Index 的扫描、回表成本;
当前访问路径的读取成本;
LIMIT 大小;GROUP BY 平均每组记录数;后续连接表的 fanout。
该开关允许优化器选择成本合理的有序路径,并不强制使用某个索引。
配置方式
当前会话开启:
SET SESSION optimizer_switch = 'prefer_ordering_index=on';
单语句开启:
SELECT /*+ SET_VAR(optimizer_switch='prefer_ordering_index=on') */id, created_at, payloadFROM eventsWHERE user_id = 100ORDER BY created_at DESCLIMIT 20;
关闭:
SET SESSION optimizer_switch = 'prefer_ordering_index=off';
不同版本或部署模板的默认值可能不同,应以实例的
@@global.optimizer_switch 和 @@session.optimizer_switch 为准。关闭后仍可能 Skip Sort
prefer_ordering_index=off 不等于禁用所有索引排序优化。以下场景仍可能 Skip Sort:当前因过滤条件选择的索引本身已经提供顺序;
当前访问方式为表扫描或全索引扫描,优化器找到成本合适的有序索引;
使用了
FORCE INDEX FOR ORDER BY 或 FORCE INDEX FOR GROUP BY;其他优化规则已经确定有序访问路径。
该开关主要影响是否主动从已有访问路径切换到另一个 Ordering Index。
适用场景与风险
典型场景:
SELECT ...FROM tWHERE filter_col = ?ORDER BY order_col DESCLIMIT 10;
小
LIMIT、符合条件的记录在 Ordering Index 中分布较均匀,且待排序数据量较大时,收益通常更明显。Ordering index 优先保证顺序,不一定具有最强过滤能力。例如:
SELECT *FROM messagesWHERE receiver_id = 100AND status = 1ORDER BY created_at DESCLIMIT 20;
只使用
INDEX(created_at) 可能扫描大量无效记录。更合适的索引通常是:CREATE INDEX idx_receiver_status_timeON messages(receiver_id, status, created_at DESC);
如果版本启用了 Adaptive Ordering Index,系统可以在实际扫描明显超出估算时重新优化,作为运行时保护。可通过下列状态观察是否触发:
SHOW SESSION STATUS LIKE 'Adaptive_ordering_used';
自适应保护不能替代合理的联合索引和准确的统计信息。
LIMIT-aware Join Order
功能说明
传统连接顺序优化主要比较各表的过滤、连接和读取成本。对于多表
ORDER BY ... LIMIT,如果排序列位于非驱动表,通常需要完成较多连接后再排序。LIMIT-aware Join Order 会评估另一类计划:
1. 让能按
ORDER BY 顺序读取的表作为驱动表;2. 通过 Ordering Index 按目标顺序扫描;
3. 逐行连接后续表;
4. 产生足够结果后提前停止。
这样可能同时减少驱动表扫描量、后续连接次数和最终排序开销。
配置方式
开启连接顺序成本优化:
SET SESSION optimizer_switch = 'limit_aware_join_order=on';
生产使用通常建议与 Ordering Index 偏好一并开启,使单表访问路径选择和多表连接顺序选择协同工作:
SET SESSION optimizer_switch ='prefer_ordering_index=on,limit_aware_join_order=on';
单语句开启时,应在同一个
optimizer_switch 字符串中合并两个子选项:SELECT /*+ SET_VAR(optimizer_switch='prefer_ordering_index=on,limit_aware_join_order=on') */o.id, o.updated_at, d.tagFROM order_detail AS dJOIN orders AS o ON o.org_id = d.org_idWHERE d.tag = 1AND o.region_id = 1ORDER BY o.updated_at DESCLIMIT 5;
多表 Top-N 示例
建议索引:
CREATE INDEX idx_orders_region_timeON orders(region_id, updated_at);
查询:
EXPLAINSELECT o.id, o.updated_at, d.tagFROM order_detail AS dJOIN orders AS o ON o.org_id = d.org_idWHERE d.tag = 1AND o.region_id = 1ORDER BY o.updated_at DESCLIMIT 5;
region_id 被等值固定后,orders 可以通过 idx_orders_region_time 反向扫描 updated_at。优化前可能表现为:
order_detail 为驱动表;连接后出现
Using temporary; Using filesort。优化后可能表现为:
orders 成为第一张非 const 表;使用
idx_orders_region_time;显示
Backward index scan;不再显示
Using filesort。成本估算
优化器根据后续连接的平均 fanout,估算为了产生
LIMIT 条最终结果需要从驱动表读取多少行:> 预计驱动表读取行数 = max(LIMIT / 有效后缀 fanout, 1)
再按预计读取量占驱动表候选行数的比例折算完整计划成本。
生效条件与限制
典型生效条件包括:
多表查询包含有效的
ORDER BY 和 LIMIT;ORDER BY 可由驱动表的可用 ref 或 range 索引满足;排序项是可索引字段,而不是普通索引无法直接满足的表达式;
驱动表有序输出能被后续连接算子保持;
预计只需读取驱动表的一部分数据即可产生足够结果;
调整后的完整计划成本优于其他连接顺序。
以下场景通常不应用该优化:
没有
LIMIT 或没有 ORDER BY;LIMIT 很大,提前停止收益不足;排序字段来自多张表,不能由一个驱动表提供最终顺序;
包含聚集、
HAVING、窗口函数或 DISTINCT;ORDER BY 使用普通索引不能满足的表达式;后续 join buffer、BKA/MRR 等执行方式不能保持驱动表顺序;
所选 range 访问无法按需要方向稳定输出。
STRAIGHT_JOIN 或固定连接顺序的 Hint 会限制优化器调整 join order。希望该能力自动选择驱动表时,不应固定一个与 Ordering Index 方案冲突的连接顺序。索引设计指南
WHERE + ORDER BY + LIMIT
推荐的联合索引顺序通常是:
等值过滤列 -> 排序列 -> 其他用于覆盖的列
例如:
SELECT id, created_at, titleFROM articlesWHERE tenant_id = 10AND status = 1ORDER BY created_at DESCLIMIT 20;
推荐索引:
CREATE INDEX idx_article_tenant_status_timeON articles(tenant_id, status, created_at DESC);
WHERE + GROUP BY
推荐顺序通常是:
等值过滤列 -> 分组列 -> 其他用于过滤或覆盖的列
例如:
SELECT category, COUNT(*), SUM(amount)FROM salesWHERE tenant_id = 10GROUP BY category;
推荐索引:
CREATE INDEX idx_sales_tenant_categoryON sales(tenant_id, category);
覆盖索引
将高频返回列纳入索引可以减少回表,但需要权衡索引空间、写放大、DML 性能和缓存利用率。不建议仅为消除一次小规模排序而创建过宽索引。
分页方式
即使 Skip Sort,大偏移分页仍需跳过大量索引项:
ORDER BY created_at DESCLIMIT 100000, 20;
建议使用游标式翻页:
SELECT id, created_at, payloadFROM eventsWHERE tenant_id = 10AND (created_at, id) < (?, ?)ORDER BY created_at DESC, id DESCLIMIT 20;
配套索引:
CREATE INDEX idx_events_tenant_time_idON events(tenant_id, created_at DESC, id DESC);
将主键等唯一列加入排序,可以在排序列重复时保证分页结果稳定。
如何确认优化是否生效
查看 EXPLAIN
检查项 | 说明 |
table 顺序 | 多表查询中,排序列所在表是否成为第一张非 const 表 |
key | 是否选择能够提供目标顺序的索引 |
type | 可能是 ref、range 或 index,不能只凭访问类型判断 |
Using filesort | 消失通常表示已 Skip Sort |
Using temporary | 在分组查询中消失通常表示避免临时表聚集 |
Backward index scan | 表示通过反向扫描提供顺序 |
Using index | 表示覆盖索引,不等同于使用索引排序 |
Using index for group-by | 表示 Loose Index Scan,不是普通流式聚集的必要标志 |
不要只看
key。即使选择了排序列相关索引,如果 Extra 仍有 Using filesort,说明该访问路径没有完整满足最终顺序。查看 Optimizer Trace
SET SESSION optimizer_trace = 'enabled=on';SET SESSION optimizer_trace_max_mem_size = 1048576;EXPLAINSELECT ...;SELECT JSON_PRETTY(TRACE)FROM information_schema.optimizer_trace;SET SESSION optimizer_trace = 'enabled=off';
Ordering Index 选择重点关注:
ordering_index_selection;context:order_by、group_by 或 table_scan;indexes_considered;direction、used_key_parts、is_covering;index_scan_cost、chosen;selected_index、selected_direction。常见未选原因包括:
index cannot provide required order;index scan cost higher than read_time;no limit and not covering;covering index preferred over non-covering;ref_key covering with better selectivity。LIMIT-aware Join Order 重点关注:
ordering_limit_aware_cost;ordering_key、select_limit;suffix_fanout、fanout_safety_factor;driving_tab_early_stop_ratio;new_cost_for_plan。如果没有
ordering_index_selection,可能是当前路径已经天然有序,无需搜索替代索引;也可能是查询不满足优化条件。能力边界总结
能力 | 主要场景 | 主要收益 | 核心条件 |
Ordering Index Skip Sort | ORDER BY | 避免 filesort | 索引完整提供目标顺序 |
Ordering Index 流式聚集 | GROUP BY | 避免排序/临时聚集,降低内存和首行延迟 | 输入按分组键连续 |
prefer_ordering_index | 单表或既定 join order 下的排序、分组 | 在过滤路径与有序路径之间做成本选择 | 开关开启且有序路径成本更优 |
LIMIT-aware Join Order | 多表 ORDER BY ... LIMIT | 调整驱动表,按序连接并提前停止 | 顺序可保持、LIMIT 收益明显 |
Adaptive Ordering Index | Ordering Index 估算偏差 | 运行时避开扫描量过大的偏好路径 | 版本支持且自适应能力开启 |
Ordering Index 的核心价值不是尽可能以索引代替排序,而是在过滤、读取、回表、连接、排序和提前终止之间选择总体成本更低的执行路径。