GCC Code Coverage Report


Directory: src/
File: src/pg/scan_bound.c
Date: 2026-09-30 11:11:31
Exec Total Coverage
Lines: 84 98 85.7%
Functions: 10 10 100.0%
Branches: 58 98 59.2%

Line Branch Exec Source
1 /*
2 * Copyright (c) 2026 Tiger Data, Inc.
3 * Licensed under the PostgreSQL License. See LICENSE for details.
4 *
5 * scan_bound.c - row target for a prism scan
6 *
7 * The index AM API never tells a scan how many rows the query wants: the
8 * executor pulls one tuple at a time until the Limit node above it is
9 * satisfied. A prism scan computes its whole top-k on the first fetch,
10 * so it has to know k up front or it emits a fixed default and a larger
11 * LIMIT silently comes up short.
12 *
13 * Knowing k up front is also what makes the rows it returns correctly
14 * ordered: the whole top-k is elected in one pass over the probed
15 * clusters, so every row is ranked against every candidate. A scan that
16 * instead resumed once its budget ran out could only rank the newcomers
17 * against each other, and might find one nearer than a row it has already
18 * returned -- ordering the caller cannot rely on.
19 *
20 * A scan therefore asks, at rescan, whether it runs under a Limit. The
21 * hook below only records which query is executing; the search runs
22 * inside the scan, where the executor node already points at it and can
23 * be matched by identity rather than by position in the plan tree.
24 *
25 * When the scan carries a filter (a WHERE clause the executor applies
26 * above the index), only a fraction of the emitted top-k survives it, so
27 * k is inflated by the planner's selectivity estimate.
28 */
29
30 #include <postgres.h>
31
32 #include <access/genam.h>
33 #include <executor/executor.h>
34 #include <math.h>
35 #include <nodes/execnodes.h>
36 #include <nodes/nodeFuncs.h>
37 #include <nodes/plannodes.h>
38 #include <optimizer/optimizer.h>
39 #include <utils/rel.h>
40
41 #include "scan.h"
42 #include "scan_bound.h"
43 #include "support_pg.h"
44
45 static ExecutorRun_hook_type prev_ExecutorRun_hook = NULL;
46
47 /*
48 * The query we are executing inside, or NULL when no executor is running
49 * (maintenance code opening its own index scans, say). Saved and restored
50 * around the run, the way the executor maintains ActivePortal: nesting
51 * follows the C stack and names the innermost query, and an error unwinds
52 * this along with the executor.
53 */
54 static QueryDesc *vs_active_query_desc = NULL;
55
56 /*
57 * True if the expression reads an executor parameter that has no value yet.
58 *
59 * A PARAM_EXEC is a parameter whose value another plan node produces during
60 * execution -- an InitPlan's result, or the current row of the outer
61 * relation in a nested loop -- as opposed to a PARAM_EXTERN, which the
62 * client binds before execution begins. Until the node that owes it has
63 * run, its slot carries the subplan to run rather than a value (execPlan is
64 * non-NULL), and evaluating it would execute that subplan from inside a
65 * rescan. Decline in that case.
66 *
67 * Params already produced are fine, which is what asking at rescan buys
68 * over asking at executor start.
69 */
70 static bool
71 289 has_pending_exec_param(Node *node, void *context)
72 {
73
1/2
✓ Branch 0 taken 289 times.
✗ Branch 1 not taken.
289 if (node == NULL)
74 return false;
75
76
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 279 times.
289 if (IsA(node, Param))
77 {
78 10 Param *p = (Param *)node;
79 10 ExprContext *econtext = (ExprContext *)context;
80
81
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 if (p->paramkind != PARAM_EXEC)
82 return false;
83
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 return econtext == NULL || econtext->ecxt_param_exec_vals == NULL ||
84
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
24 p->paramid < 0 ||
85
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 econtext->ecxt_param_exec_vals[p->paramid].execPlan != NULL;
86 }
87 279 return expression_tree_walker(node, has_pending_exec_param, context);
88 }
89
90 /*
91 * Evaluate a LIMIT or OFFSET expression.
92 *
93 * OFFSET counts toward what the scan must produce: the Limit node discards
94 * the skipped rows only after the scan has emitted them, so LIMIT 10 OFFSET
95 * 100 needs 110 rows out of the index, not 10.
96 *
97 * Only values fixed for the rest of this scan qualify: constants,
98 * parameters already bound, and stable expressions over them. False when
99 * the value cannot be known here, or is NULL (which SQL reads as "no
100 * limit").
101 */
102 static bool
103 279 eval_count_expr(
104 Node *expr, ExprState *state, ExprContext *econtext, int64 *value)
105 {
106
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 279 times.
279 if (expr == NULL || state == NULL)
107 return false;
108
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 279 times.
279 if (has_pending_exec_param(expr, econtext))
109 return false;
110
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 279 times.
279 if (contain_volatile_functions(expr))
111 return false;
112
113 279 bool isnull;
114 279 Datum d = ExecEvalExprSwitchContext(state, econtext, &isnull);
115
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 279 times.
279 if (isnull)
116 return false;
117
118 279 *value = DatumGetInt64(d);
119 279 return *value >= 0;
120 }
121
122 /*
123 * Descend from a Limit's input to the IndexScan feeding it, through nodes
124 * that emit exactly one row per input row. Anything that can filter,
125 * aggregate, sort or multiply rows breaks the correspondence between the
126 * LIMIT and the rows the scan must produce, so the descent stops there.
127 */
128 static IndexScanState *
129 281 find_index_scan_state(PlanState *ps)
130 {
131
1/2
✓ Branch 0 taken 281 times.
✗ Branch 1 not taken.
281 while (ps != NULL)
132 {
133
1/2
✓ Branch 0 taken 281 times.
✗ Branch 1 not taken.
281 if (IsA(ps, IndexScanState))
134 281 return (IndexScanState *)ps;
135
136 /* A qual on any node between here and the scan filters rows. */
137 ✗ if (ps->plan->qual != NIL)
138 return NULL;
139
140 ✗ switch (nodeTag(ps))
141 {
142 ✗ case T_ResultState:
143 ✗ ps = outerPlanState(ps);
144 ✗ break;
145
146 ✗ case T_WindowAggState:
147 ✗ if (((WindowAgg *)ps->plan)->runCondition != NIL)
148 return NULL;
149 ✗ ps = outerPlanState(ps);
150 ✗ break;
151
152 ✗ case T_SubqueryScanState:
153 ✗ ps = ((SubqueryScanState *)ps)->subplan;
154 ✗ break;
155
156 default:
157 return NULL;
158 }
159 }
160 return NULL;
161 }
162
163 /*
164 * A LIMIT's count and offset are each clamped to this before being added,
165 * so the sum cannot overflow. Purely an arithmetic guard: what limits a
166 * scan is work_mem, applied where the top-k is allocated.
167 */
168 #define PRISM_LIMIT_SUM_MAX (PG_INT64_MAX / 4)
169
170 /*
171 * Standard deviations of headroom over the rows a filter is expected to
172 * leave.
173 *
174 * A filter that passes a fraction s of the rows needs about 1/s candidates
175 * for s of them to add up to the LIMIT -- but that is a mean, not a
176 * guarantee: with a correct s, the survivors among the top-(k/s) are
177 * distributed Binomial(k/s, s), so the count has mean k and standard
178 * deviation about sqrt(k). Over-fetching by 1 + this/sqrt(k) covers that
179 * noise: roughly 1.95x at k = 10, 1.3x at k = 100, 1.09x at k = 1000. A flat
180 * factor would have to be sized for the smallest k and would then over-fetch
181 * by 1.5x to 2.7x at the larger ones, where the fetches cost most.
182 *
183 * This covers noise, not a wrong estimate. An s off by an order of magnitude
184 * needs an order of magnitude more candidates, which nothing here can
185 * anticipate -- raising work_mem, or a partial index on the filter, is the
186 * answer to that.
187 */
188 #define PRISM_FILTER_NOISE_SIGMAS 3.0
189
190 /*
191 * Smallest estimated selectivity the sizing will divide by.
192 *
193 * One row in ten thousand already asks for a top-k ten thousand times the
194 * LIMIT, which work_mem will cut down anyway. Anything below this is a
195 * planner estimate with no rows behind it -- a default from a missing
196 * statistic, or a conjunction of independent guesses -- and dividing by it
197 * produces a number, not an estimate.
198 */
199 #define PRISM_FILTER_MIN_SELECTIVITY 1e-4
200
201 /*
202 * Size the scan's top-k for the rows the LIMIT will pull.
203 *
204 * A filter on the scan is applied by the executor after the index has emitted
205 * its top-k, so only an estimated fraction s of the emitted rows survive:
206 * multiply the bound by 1/s, with a margin, so that fraction still covers
207 * the LIMIT. The estimate is the plan's own row
208 * count for the scan node against the relation's statistics; without
209 * statistics the plain LIMIT is used, and where the sizing exceeds what
210 * work_mem affords a filtered query comes up short.
211 *
212 * The estimate is only ever a guess -- a filter correlated with vector
213 * proximity (a category living in its own region of the space) can still
214 * starve the result, and nothing here can know which rows survive, because
215 * the pruning gate commits this size before any heap fetch and evaluating a
216 * qual needs one. prism.query_limit is the lever for a caller who knows their
217 * own selectivity.
218 */
219 static uint32_t
220 278 size_top_k(IndexScanState *iss, int64 limit)
221 {
222
3/4
✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 204 times.
✓ Branch 3 taken 74 times.
278 uint32_t k = (uint32_t)Min(Max(limit, 1), (int64)PG_UINT32_MAX);
223 278 Plan *plan = iss->ss.ps.plan;
224 278 Relation heap = iss->ss.ss_currentRelation;
225
226
4/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 272 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
278 if (plan->qual == NIL || heap == NULL || heap->rd_rel->reltuples <= 0.0)
227 return k;
228
229 /*
230 * A row estimate is a planner guess divided by a cached count, so it can
231 * arrive at anything: zero rows from a qual the planner reads as
232 * impossible, more rows than the relation is recorded as holding after a
233 * bulk load, a non-finite value from either being garbage. Only a
234 * fraction strictly inside (0, 1) says anything about filtering; take
235 * the LIMIT unchanged for the rest rather than dividing by them.
236 *
237 * PRISM_FILTER_MIN_SELECTIVITY floors the divisor. Without it a plan_rows
238 * of a millionth of a row asks for a top-k a million times the LIMIT,
239 * and while work_mem would refuse to allocate it, the sizing has no
240 * business proposing it: below this the estimate is noise, not a
241 * measurement.
242 */
243 6 return prism_scan_inflate_for_filter(
244 6 k, plan->plan_rows / heap->rd_rel->reltuples);
245 }
246
247 /*
248 * Inflate a row target for a filter the executor applies above the scan.
249 *
250 * Called by the executor and by the cost model. The executor arrives at the
251 * selectivity by dividing its own plan_rows by the relation's row count;
252 * the planner passes clauselist_selectivity's fraction for the same thing.
253 */
254 uint32_t
255 388 prism_scan_inflate_for_filter(uint32_t k, double selectivity)
256 {
257
2/2
✓ Branch 0 taken 384 times.
✓ Branch 1 taken 4 times.
388 if (k == 0)
258 return 0;
259
260 /*
261 * A row estimate is a planner guess divided by a cached count, so it can
262 * arrive at anything: zero rows from a qual the planner reads as
263 * impossible, more rows than the relation is recorded as holding after a
264 * bulk load, a non-finite value from either being garbage. Only a
265 * fraction strictly inside (0, 1) says anything about filtering; take
266 * the target unchanged for the rest rather than dividing by them.
267 *
268 * PRISM_FILTER_MIN_SELECTIVITY floors the divisor. Without it a plan_rows
269 * of a millionth of a row asks for a top-k a million times the target,
270 * and while work_mem would refuse to allocate it, the sizing has no
271 * business proposing it: below this the estimate is noise, not a
272 * measurement.
273 */
274
3/4
✓ Branch 0 taken 384 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 371 times.
✓ Branch 3 taken 13 times.
384 if (!isfinite(selectivity) || selectivity >= 1.0 || selectivity <= 0.0)
275 return k;
276
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (selectivity < PRISM_FILTER_MIN_SELECTIVITY)
277 ✗ selectivity = PRISM_FILTER_MIN_SELECTIVITY;
278
279 13 double margin = 1.0 + PRISM_FILTER_NOISE_SIGMAS / sqrt((double)k);
280 13 double sized = ceil(margin * (double)k / selectivity);
281
282 /*
283 * Clamped only to keep the cast well defined. The ceiling that matters
284 * is work_mem, applied by prism_scan_resolve_top_k.
285 */
286
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 13 times.
13 return (uint32_t)Min(Max(sized, (double)k), (double)PG_UINT32_MAX);
287 }
288
289 /*
290 * Rows a Limit will pull: its count plus its offset, each clamped so the
291 * sum cannot overflow. False when either cannot be resolved now, when the
292 * count is zero, or under WITH TIES -- which keeps pulling past the count
293 * for rows tying the last one, so the count is a floor on the rows needed
294 * rather than the number of them, and a scan is better left at its default
295 * sizing than cut ties off.
296 */
297 static bool
298 278 limit_total_rows(LimitState *ls, int64 *total)
299 {
300 278 Limit *plan = (Limit *)ls->ps.plan;
301 278 ExprContext *econtext = ls->ps.ps_ExprContext;
302 278 int64 count = 0;
303 278 int64 offset = 0;
304
305 /*
306 * WITH TIES pulls past its count for rows tying the last one, so the
307 * count is a floor rather than the total -- but it is the right thing to
308 * size from. Reporting no bound would have resolve_top_k read the query
309 * as asking for every row, and size FETCH FIRST 5 ROWS WITH TIES for the
310 * whole relation. A tie group straddling the top-k's edge can be cut
311 * short instead, which is the same bound every other query is subject to
312 * and vastly cheaper than ranking the table.
313 */
314
1/2
✓ Branch 1 taken 278 times.
✗ Branch 2 not taken.
278 if (!eval_count_expr(plan->limitCount, ls->limitCount, econtext, &count) ||
315
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 278 times.
278 count <= 0)
316 return false;
317
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 277 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
279 if (plan->limitOffset != NULL &&
318 1 !eval_count_expr(
319 plan->limitOffset, ls->limitOffset, econtext, &offset))
320 return false;
321
322 278 *total = Min(count, PRISM_LIMIT_SUM_MAX) +
323 278 Min(offset, PRISM_LIMIT_SUM_MAX);
324 278 return true;
325 }
326
327 /* The scan looking for its Limit; k stays 0 until one is resolved. */
328 typedef struct LimitSearch
329 {
330 IndexScanDesc scan;
331 uint32_t k;
332 } LimitSearch;
333
334 /*
335 * Look for a Limit whose input is the asking scan. The pairing is by
336 * identity -- nodeIndexscan assigns iss_ScanDesc before calling
337 * index_rescan -- which is what tells two scans of the same index in one
338 * statement apart, where their position in the plan tree cannot.
339 */
340 static bool
341 670 limit_walker(PlanState *ps, void *context)
342 {
343 670 LimitSearch *search = (LimitSearch *)context;
344
345
1/2
✓ Branch 0 taken 670 times.
✗ Branch 1 not taken.
670 if (ps == NULL)
346 return false;
347
348
2/2
✓ Branch 0 taken 281 times.
✓ Branch 1 taken 389 times.
670 if (IsA(ps, LimitState))
349 {
350 281 IndexScanState *iss = find_index_scan_state(outerPlanState(ps));
351 281 int64 total;
352
353
3/4
✓ Branch 0 taken 281 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✓ Branch 3 taken 3 times.
281 if (iss != NULL && iss->iss_ScanDesc == search->scan &&
354
2/4
✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
556 iss->iss_NumOrderByKeys > 0 &&
355 278 limit_total_rows((LimitState *)ps, &total))
356 {
357 278 search->k = size_top_k(iss, total);
358 278 return true; /* this scan's Limit is resolved */
359 }
360 }
361
362 392 return planstate_tree_walker(ps, limit_walker, context);
363 }
364
365 uint32_t
366 282 prism_scan_bound(IndexScanDesc scan)
367 {
368 282 QueryDesc *qd = vs_active_query_desc;
369
370
3/6
✓ Branch 0 taken 282 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 282 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 282 times.
✗ Branch 5 not taken.
282 if (qd == NULL || qd->planstate == NULL || qd->estate == NULL)
371 return 0; /* no executor above us: keep the default sizing */
372
373 282 LimitSearch search = {.scan = scan, .k = 0};
374
375 /*
376 * The main tree, then the plan trees hanging off the EState: CTE
377 * bodies, InitPlans and correlated subplans are not reachable from the
378 * root, and a CTE with its own ORDER BY ... LIMIT is the hybrid-search
379 * shape this exists for.
380 */
381
2/2
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 278 times.
282 if (!limit_walker(qd->planstate, &search))
382 {
383 4 ListCell *lc;
384
385
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 4 times.
8 foreach (lc, qd->estate->es_subplanstates)
386
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 if (limit_walker((PlanState *)lfirst(lc), &search))
387 break;
388 }
389
390 282 return search.k;
391 }
392
393 /*
394 * Record which query is executing, for the duration of its execution.
395 *
396 * An index AM's callbacks are handed a Relation and an IndexScanDesc and
397 * nothing else -- there is no path from a scan back to the executor state
398 * it runs under. Noting the running query here is what lets prism_scan_bound
399 * find the plan tree at rescan and, in it, the Limit above this scan.
400 *
401 * Save, set, restore, exactly as the executor does for ActivePortal.
402 * PG_FINALLY does the restoring so that an error -- which leaves this frame
403 * by longjmp, not by return -- cannot strand a pointer to a QueryDesc whose
404 * memory has gone. Nesting needs no bookkeeping: a query issued from a
405 * function body runs its executor inside its caller's, so the C stack keeps
406 * the saves in order and the variable always names the innermost query.
407 */
408 static void
409 2190 prism_executor_run(QueryDesc *queryDesc, ScanDirection direction, uint64 count)
410 {
411 2190 QueryDesc *save = vs_active_query_desc;
412
413 2190 vs_active_query_desc = queryDesc;
414
2/2
✓ Branch 0 taken 2190 times.
✓ Branch 1 taken 9 times.
2199 PG_TRY();
415 {
416
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2190 times.
2190 if (prev_ExecutorRun_hook)
417 ✗ prev_ExecutorRun_hook(queryDesc, direction, count);
418 else
419 2190 standard_ExecutorRun(queryDesc, direction, count);
420 }
421 2190 PG_FINALLY();
422 {
423 2190 vs_active_query_desc = save;
424 }
425
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2181 times.
2190 PG_END_TRY();
426 2181 }
427
428 void
429 255 prism_scan_bound_init(void)
430 {
431 255 prev_ExecutorRun_hook = ExecutorRun_hook;
432 255 ExecutorRun_hook = prism_executor_run;
433 255 }
434