PostgreSQL中LIMIT子句的执行机制探究
PostgreSQL中LIMIT子句的执行机制解析
场景与查询计划
我有一张包含1000行数据的表,执行查询语句select * from hugedata limit 1后,查询计划显示先执行Seq Scan(全表扫描),再执行Limit算子,具体查询计划如下:
[ { "Plan": { "Node Type": "Limit", "Parallel Aware": false, "Async Capable": false, "Startup Cost": 0.00, "Total Cost": 0.02, "Plan Rows": 1, "Plan Width": 41, "Actual Startup Time": 0.007, "Actual Total Time": 0.007, "Actual Rows": 1, "Actual Loops": 1, "Output": ["pk", "description", "flags"], "Shared Hit Blocks": 1, "Shared Read Blocks": 0, "Shared Dirtied Blocks": 0, "Shared Written Blocks": 0, "Local Hit Blocks": 0, "Local Read Blocks": 0, "Local Dirtied Blocks": 0, "Local Written Blocks": 0, "Temp Read Blocks": 0, "Temp Written Blocks": 0, "WAL Records": 0, "WAL FPI": 0, "WAL Bytes": 0, "Plans": [ { "Node Type": "Seq Scan", "Parent Relationship": "Outer", "Parallel Aware": false, "Async Capable": false, "Relation Name": "hugedata", "Schema": "public", "Alias": "hugedata", "Startup Cost": 0.00, "Total Cost": 15406.01, "Plan Rows": 1000001, "Plan Width": 41, "Actual Startup Time": 0.006, "Actual Total Time": 0.006, "Actual Rows": 1, "Actual Loops": 1, "Output": ["pk", "description", "flags"], "Shared Hit Blocks": 1, "Shared Read Blocks": 0, "Shared Dirtied Blocks": 0, "Shared Written Blocks": 0, "Local Hit Blocks": 0, "Local Read Blocks": 0, "Local Dirtied Blocks": 0, "Local Written Blocks": 0, "Temp Read Blocks": 0, "Temp Written Blocks": 0, "WAL Records": 0, "WAL FPI": 0, "WAL Bytes": 0 } ] }, "Settings": {}, "Planning": { "Shared Hit Blocks": 0, "Shared Read Blocks": 0, "Shared Dirtied Blocks": 0, "Shared Written Blocks": 0, "Local Hit Blocks": 0, "Local Read Blocks": 0, "Local Dirtied Blocks": 0, "Local Written Blocks": 0, "Temp Read Blocks": 0, "Temp Written Blocks": 0 }, "Planning Time": 0.035, "Triggers": [], "Execution Time": 0.069 } ]
问题
PostgreSQL中的LIMIT子句具体是如何执行的?
- 方式1:数据库引擎先查询出全表1000条记录,再将这些记录传递给limit算子,算子仅保留第一条记录,丢弃其余999条;
- 方式2:数据库引擎取出第一条记录后提交给limit算子,算子接受该记录;接着尝试获取第二条记录时被limit算子拒绝,引擎随即停止查询。两种方式的区别在于:方式2仅会获取N+1(N为LIMIT指定的数量)条记录,而方式1会获取全表所有数据。
我曾误以为LIMIT算子是按第一种方式工作的,若真是如此,为何该实现会如此低效?还是我忽略了某些细节?
PostgreSQL官方文档提到生成查询计划时会考虑LIMIT,但未明确说明是否在获取N+1条记录后停止执行,或是“考虑LIMIT”另有其他含义。
解答
实际执行逻辑:方式2才是正确的
从你提供的查询计划就能直接验证这一点:
- 子节点
Seq Scan的Actual Rows为1,而非全表的1000行,说明全表扫描仅读取1条记录就停止了; Seq Scan的Actual Total Time仅0.006毫秒,远低于扫描全表所需的时间;Shared Hit Blocks为1,说明只读取了1个数据块,而非全表所有数据块。
PostgreSQL的执行器采用**按需拉取(pull-based)**模型:上层算子(此处为Limit)向下层算子(Seq Scan)请求数据,下层返回一条记录后,Limit算子检查是否已收集到足够记录(此处为1条),若满足条件则停止向下层请求,下层算子也随之终止执行。
“查询计划考虑LIMIT”的含义
官方文档提到的“生成查询计划时考虑LIMIT”,指优化器会根据LIMIT数值选择更高效的执行计划,而非执行阶段的行为:
- 比如使用
LIMIT 1时,优化器不会选择需要排序的计划(如ORDER BY对应列无索引的情况),因为排序需全表扫描后再排序,成本远高于直接取第一条; - 优化器计算不同计划的成本时,会考虑LIMIT带来的提前终止特性,例如Seq Scan的总成本原本为15406.01,但Limit算子的总成本仅0.02,就是因为优化器预估到扫描会提前终止。
为何会误以为是方式1?
可能混淆了查询计划的结构与执行流程:查询计划显示Limit在Seq Scan之上,并不代表Seq Scan需先执行完所有数据再传给Limit,而是表示数据从Seq Scan流向Limit,但执行是按需的,一旦Limit满足条件,整个执行就会停止。
内容的提问来源于stack exchange,提问作者Lau
相关产品推荐
相关产品推荐

