联系我们
18591797788
hubin@rlctech.com
北京市海淀区中关村南大街乙12号院天作国际B座1708室
18681942657
lvyuan@rlctech.com
上海市浦东新区商城路660号乐凯大厦26c-1
18049488781
xieyi@rlctech.com
广州市越秀区东风东路华宫大厦808号1608房
029-81109312
service@rlctech.com
西安市高新区天谷七路996号西安国家数字出版基地C座501
ORDER BY LIMIT 快不仅仅是因为最终只返回少量结果。真正决定 ORDER BY LIMIT 性能的,往往不是最后返回了几行,而是优化器和执行器能不能借着这个 LIMIT,把大量本来要做的无效工作提前砍掉。
几类无效工作包括:
全表/大范围扫描 -> SORT -> LIMIT N
如果既没有可用有序路径,又无法有效做 Top-N,最终就会退化为普通 SORT。这通常意味着:
全表/大范围扫描 -> TOP-N SORT -> 输出前 N 行
如果下层不能直接提供完整顺序,但查询带 LIMIT,优化器会尽量把 LIMIT 并入排序,形成 TOP-N SORT。Top-N 不是“全部排完再取前 N”,不需要长期保留所有候选行输出阶段可以在达到 topn_cnt_ 后提前停止。
有序索引扫描 -> LIMIT N
如果访问路径天然有序,比如索引顺序与 ORDER BY 一致,那么优化器会直接复用下层顺序,不再分配 SORT。
例1(无prefix_pos 不能消序)
explain EXTENDED_NOADDR SELECT * from t1_order where c2=10 ORDER by c4 limit 10;
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
-----------------------------------------------------------------
|0 |TOP-N SORT | |1 |5 |
|1 |└─TABLE RANGE SCAN|t1_order(idx_c2_c3)|1 |5 |
=================================================================
Outputs & filters:
-------------------------------------
0 - output([t1_order.c1], [t1_order.c2], [t1_order.c3], [t1_order.c4]), filter(nil), rowset=16
sort_keys([t1_order.c4, ASC]), topn(10)
1 - output([t1_order.c2], [t1_order.c1], [t1_order.c3], [t1_order.c4]), filter(nil), rowset=16
access([t1_order.__pk_increment], [t1_order.c2], [t1_order.c1], [t1_order.c3], [t1_order.c4]), partitions(p0)
is_index_back=true, is_global_index=false,
range_key([t1_order.c2], [t1_order.c3], [t1_order.__pk_increment]), range(10,MIN,MIN ; 10,MAX,MAX),
range_cond([t1_order.c2 = 10])
例2 (有prefix_pos(1) 部分消序)
explain EXTENDED_NOADDR SELECT * from t1_order where c2=10 ORDER by c3,c4 limit 10;
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
-----------------------------------------------------------------
|0 |TOP-N SORT | |1 |5 |
|1 |└─TABLE RANGE SCAN|t1_order(idx_c2_c3)|1 |5 |
=================================================================
Outputs & filters:
-------------------------------------
0 - output([t1_order.c1], [t1_order.c2], [t1_order.c3], [t1_order.c4]), filter(nil), rowset=16
sort_keys([t1_order.c3, ASC], [t1_order.c4, ASC]), topn(10), prefix_pos(1)
1 - output([t1_order.c2], [t1_order.c1], [t1_order.c3], [t1_order.c4]), filter(nil), rowset=16
access([t1_order.__pk_increment], [t1_order.c2], [t1_order.c1], [t1_order.c3], [t1_order.c4]), partitions(p0)
例3 (无SORT算子:完全消序)
explain EXTENDED_NOADDR SELECT * from t1_order where c2=10 ORDER by C3 limit 10;
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
---------------------------------------------------------------
|0 |TABLE RANGE SCAN|t1_order(idx_c2_c3)|1 |5 |
===============================================================
Outputs & filters:
-------------------------------------
0 - output([t1_order.c1], [t1_order.c2], [t1_order.c3], [t1_order.c4]), filter(nil), rowset=16
access([t1_order.__pk_increment], [t1_order.c2], [t1_order.c1], [t1_order.c3], [t1_order.c4]), partitions(p0)
limit(10), offset(nil), is_index_back=true, is_global_index=false,
SELECT user_id, score
FROM player_score
ORDER BY score DESC
LIMIT 100;
SELECT id, title, publish_time
FROM article
WHERE tenant_id = 1001
ORDER BY publish_time DESC
LIMIT 50;
SELECT a.*, b.name
FROM big_order a
JOIN user_info b ON a.user_id = b.id
WHERE a.status = 1
ORDER BY a.create_time DESC
LIMIT 20;
先对驱动表做 ORDER BY LIMIT,再 JOIN ,NL 流式处理
在实际调优中,一个很常见的误区是:只要看见 TOP-N SORT,就觉得这个 SQL 已经优化得不错了。实际上未必。
如果 LIMIT 10,但底层为了找到这 10 行仍然扫描了几十万行,那么虽然计划看上去“已经做了 Top-N”,但收益仍然可能很有限。
因为这时真正省掉的只是全量排序的那部分代价,而不是扫描本身的代价。
比如:
WHERE status = 0
ORDER BY create_time
LIMIT 10
如果只有 create_time 索引,没有 (status, create_time) 联合索引,那么即使可以按 create_time 顺序扫描,也可能需要扫很多行,才能凑出 10 条满足 status = 0 的数据。
这种场景下,顺序是有了,但早停能力并不强,收益自然会打折。
单分区场景下,索引保序更容易直接转化成全局有序;但多分区场景就复杂得多。很多时候每个分区内部可以各自有序,最终仍需要上层做归并。
所以有些时候你虽然没有看到传统意义上的全量排序,但仍然可能存在 merge sort 或 local merge sort 一类的额外工作。
像 LIMIT 10 OFFSET 100000 这类 SQL,即使最后仍然只返回 10 行,也不代表代价低。因为前面的 100000 行并不会凭空消失,执行器通常还是得处理它们的顺序问题。这也是为什么很多业务的深分页最终都要转向基于游标或 seek 的分页方式。