Skip to content

Commit 0f98646

Browse files
committed
Apply selectivity correction to firstlast pairs
The compressed chunk size estimator already corrects Postgres's column-independence assumption for orderby minmax pushdowns: per-batch min and max are tightly correlated, so the estimator collapses the predicate onto a single column to get accurate selectivity. After the orderby default flipped to firstlast, predicates on the leading NOT NULL orderby reference first/last metadata instead, and that correction never fired for them. Extend the same correction to firstlast pairs, direction-aware so ASC and DESC both collapse onto a single metadata column.
1 parent 546aa9b commit 0f98646

36 files changed

Lines changed: 5944 additions & 4872 deletions

tsl/src/compression/compression_scankey.c

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,11 @@ build_heap_scankeys(Oid hypertable_relid, Relation in_rel, Relation out_rel,
224224
continue;
225225
}
226226

227+
/* Always use minmax metadata here. */
227228
int16 index = ts_array_position(settings->fd.orderby, attname);
228-
char *lower_name;
229-
char *upper_name;
230-
orderby_sparse_metadata_names(settings, index, &lower_name, &upper_name);
231229

232230
if (create_segment_filter_scankey(in_rel,
233-
lower_name,
231+
column_segment_min_name(index),
234232
BTLessEqualStrategyNumber,
235233
InvalidOid,
236234
InvalidOid,
@@ -246,7 +244,7 @@ build_heap_scankeys(Oid hypertable_relid, Relation in_rel, Relation out_rel,
246244
}
247245

248246
if (create_segment_filter_scankey(in_rel,
249-
upper_name,
247+
column_segment_max_name(index),
250248
BTGreaterEqualStrategyNumber,
251249
InvalidOid,
252250
InvalidOid,

tsl/src/compression/create.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ build_columndefs(CompressionSettings *settings, Oid src_reloid)
659659
/*
660660
* Every orderby column gets minmax metadata so range predicates
661661
* on any orderby position can be pushed down as a filter on the
662-
* compressed scan, regardless of NULL handling or direction.
662+
* compressed scan.
663663
*/
664664
ColumnDef *def = makeColumnDef(column_segment_min_name(index),
665665
attr->atttypid,
@@ -676,7 +676,7 @@ build_columndefs(CompressionSettings *settings, Oid src_reloid)
676676

677677
/*
678678
* When firstlast is configured for the orderby column, also add
679-
* first/last metadata. These columns lead the compressed-chunk
679+
* first/last metadata. These columns are a part of the compressed chunk
680680
* btree and let predicates on the leading orderby become index
681681
* conditions when the column is NOT NULL.
682682
*/

tsl/src/nodes/columnar_scan/columnar_scan.c

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -399,22 +399,25 @@ copy_columnar_scan_path(ColumnarScanPath *src)
399399
}
400400

401401
/*
402-
* Maps the attno of the min metadata column in the compressed chunk to the
403-
* attno of the corresponding max metadata column. Zero if none or not applicable.
402+
* Maps the attno of a "lower" orderby metadata column on the compressed
403+
* chunk to the attno of the corresponding "upper" column, and vice versa.
404+
* Covers both minmax (lower=min, upper=max) and firstlast pairs.
405+
* Zero entries mean none or not applicable.
404406
*/
405407
typedef struct SelectivityEstimationContext
406408
{
407-
AttrNumber *min_to_max;
408-
AttrNumber *max_to_min;
409+
AttrNumber *lower_to_upper;
410+
AttrNumber *upper_to_lower;
409411

410412
List *vars;
411413
} SelectivityEstimationContext;
412414

413415
/*
414-
* Collect the Vars referencing the "min" metadata columns into the context->vars.
416+
* Collect the Vars referencing the orderby "lower" metadata columns
417+
* (min for minmax pairs, first/last for firstlast pairs) into context->vars.
415418
*/
416419
static bool
417-
min_metadata_vars_collector(Node *orig_node, SelectivityEstimationContext *context)
420+
lower_metadata_vars_collector(Node *orig_node, SelectivityEstimationContext *context)
418421
{
419422
if (orig_node == NULL)
420423
{
@@ -430,7 +433,7 @@ min_metadata_vars_collector(Node *orig_node, SelectivityEstimationContext *conte
430433
/*
431434
* Recurse.
432435
*/
433-
return expression_tree_walker(orig_node, min_metadata_vars_collector, context);
436+
return expression_tree_walker(orig_node, lower_metadata_vars_collector, context);
434437
}
435438

436439
Var *orig_var = castNode(Var, orig_node);
@@ -442,7 +445,7 @@ min_metadata_vars_collector(Node *orig_node, SelectivityEstimationContext *conte
442445
return false;
443446
}
444447

445-
AttrNumber replaced_attno = context->min_to_max[orig_var->varattno];
448+
AttrNumber replaced_attno = context->lower_to_upper[orig_var->varattno];
446449
if (replaced_attno == InvalidAttrNumber)
447450
{
448451
/*
@@ -483,14 +486,14 @@ set_compressed_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel,
483486
* selectivity estimator must see the entire clause list to detect the range
484487
* conditions.
485488
*
486-
* First, build the correspondence of min metadata attno -> max metadata
487-
* attno for all minmax metadata.
489+
* First, build the correspondence of lower metadata attno -> upper
490+
* metadata attno for all orderby metadata pairs (minmax and firstlast).
488491
*/
489492
const int storage_elements = 2 * (compression_info->compressed_rel->max_attr + 1);
490493
AttrNumber *storage = palloc0(storage_elements * sizeof(*storage));
491494
SelectivityEstimationContext context = {
492-
.min_to_max = &storage[0],
493-
.max_to_min = &storage[compression_info->compressed_rel->max_attr],
495+
.lower_to_upper = &storage[0],
496+
.upper_to_lower = &storage[compression_info->compressed_rel->max_attr],
494497
};
495498

496499
for (int uncompressed_attno = 1; uncompressed_attno <= compression_info->chunk_rel->max_attr;
@@ -534,42 +537,74 @@ set_compressed_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel,
534537
compression_info->compressed_rte->relid,
535538
"max");
536539

537-
if (min_attno == InvalidAttrNumber || max_attno == InvalidAttrNumber)
540+
if (min_attno != InvalidAttrNumber && max_attno != InvalidAttrNumber)
538541
{
539-
continue;
542+
Assert(&context.lower_to_upper[min_attno] < &storage[storage_elements]);
543+
Assert(&context.upper_to_lower[max_attno] < &storage[storage_elements]);
544+
545+
context.lower_to_upper[min_attno] = max_attno;
546+
context.upper_to_lower[max_attno] = min_attno;
540547
}
541548

542-
Assert(&context.min_to_max[min_attno] < &storage[storage_elements]);
543-
Assert(&context.max_to_min[max_attno] < &storage[storage_elements]);
549+
/*
550+
* Same correlation hint for the firstlast pair when present. Under ASC
551+
* the lower bound is first and the upper is last; under DESC, the
552+
* roles flip. Mapping is direction-aware so that the Var swap below
553+
* collapses the predicate onto a single column regardless of shape.
554+
*/
555+
AttrNumber first_attno =
556+
compressed_column_metadata_attno(compression_info->settings,
557+
compression_info->chunk_rte->relid,
558+
uncompressed_attno,
559+
compression_info->compressed_rte->relid,
560+
"first");
561+
AttrNumber last_attno =
562+
compressed_column_metadata_attno(compression_info->settings,
563+
compression_info->chunk_rte->relid,
564+
uncompressed_attno,
565+
compression_info->compressed_rte->relid,
566+
"last");
567+
568+
if (first_attno != InvalidAttrNumber && last_attno != InvalidAttrNumber)
569+
{
570+
bool desc =
571+
ts_array_get_element_bool(compression_info->settings->fd.orderby_desc, orderby_pos);
572+
AttrNumber lower_attno = desc ? last_attno : first_attno;
573+
AttrNumber upper_attno = desc ? first_attno : last_attno;
574+
575+
Assert(&context.lower_to_upper[lower_attno] < &storage[storage_elements]);
576+
Assert(&context.upper_to_lower[upper_attno] < &storage[storage_elements]);
544577

545-
context.min_to_max[min_attno] = max_attno;
546-
context.max_to_min[max_attno] = min_attno;
578+
context.lower_to_upper[lower_attno] = upper_attno;
579+
context.upper_to_lower[upper_attno] = lower_attno;
580+
}
547581
}
548582

549583
/*
550-
* Then, replace all conditions on min metadata column with conditions on
551-
* max metadata column.
584+
* Then, collect all Var references to lower-bound metadata columns in the
585+
* restrict clauses so we can rewrite them to their upper-bound counterpart.
552586
*/
553587
ListCell *lc;
554588
foreach (lc, rel->baserestrictinfo)
555589
{
556590
RestrictInfo *orig_restrictinfo = castNode(RestrictInfo, lfirst(lc));
557591
Node *orig_clause = (Node *) orig_restrictinfo->clause;
558-
expression_tree_walker(orig_clause, min_metadata_vars_collector, &context);
592+
expression_tree_walker(orig_clause, lower_metadata_vars_collector, &context);
559593
}
560594

561595
/*
562-
* Temporarily replace "min" with "max" in-place to save on memory allocations.
596+
* Temporarily replace lower-bound Vars with their upper-bound counterpart
597+
* in-place to save on memory allocations.
563598
*/
564599
foreach (lc, context.vars)
565600
{
566601
Var *var = castNode(Var, lfirst(lc));
567602

568603
Assert(var->varattno != InvalidAttrNumber);
569-
Assert(context.min_to_max[var->varattno] != InvalidAttrNumber);
570-
Assert(context.max_to_min[context.min_to_max[var->varattno]] == var->varattno);
604+
Assert(context.lower_to_upper[var->varattno] != InvalidAttrNumber);
605+
Assert(context.upper_to_lower[context.lower_to_upper[var->varattno]] == var->varattno);
571606

572-
var->varattno = context.min_to_max[var->varattno];
607+
var->varattno = context.lower_to_upper[var->varattno];
573608
}
574609

575610
/*
@@ -583,7 +618,7 @@ set_compressed_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel,
583618
foreach (lc, context.vars)
584619
{
585620
Var *var = castNode(Var, lfirst(lc));
586-
var->varattno = context.max_to_min[var->varattno];
621+
var->varattno = context.upper_to_lower[var->varattno];
587622
}
588623

589624
pfree(storage);

tsl/src/nodes/columnar_scan/qual_pushdown.c

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,14 @@ make_segment_meta_opexpr(QualPushdownContext *context, Oid opno, AttrNumber meta
183183
* Returns InvalidAttrNumber via the out params when the expression is not a
184184
* sound pushdown target.
185185
*
186-
* For the leading orderby column under a firstlast-shaped chunk we prefer
187-
* the first/last metadata when the column is NOT NULL: those columns lead
188-
* the compressed-chunk btree, so the pushed-down predicate can become an
189-
* index condition. Every other case (nullable leading orderby, secondary
190-
* orderbys, non-orderby columns with an explicit minmax sparse index, and
191-
* any orderby on a legacy minmax-shaped chunk) falls back to minmax, which
192-
* is always available for orderby columns and direction-blind.
186+
* For the leading orderby column under a firstlast-shaped compressed chunk
187+
* index, we prefer the first/last metadata when the column is NOT NULL:
188+
* those columns are part of the compressed chunk btree index, so the
189+
* pushed-down predicate can become an index condition. Every other case
190+
* (nullable leading orderby, secondary orderbys, non-orderby columns
191+
* with an explicit minmax sparse index, and any orderby on a legacy
192+
* minmax-shaped compressed chunk indexl) falls back to minmax, which
193+
* is always available for orderby columns.
193194
*/
194195
static void
195196
expr_fetch_orderby_range_metadata(QualPushdownContext *context, Expr *expr, AttrNumber *lower_attno,

0 commit comments

Comments
 (0)