Skip to content

Commit 4456d00

Browse files
Do not sort batches for Batch sorted merge over unordered chunks if query sort matches compressed sort
1 parent 68820ea commit 4456d00

8 files changed

Lines changed: 657 additions & 116 deletions

File tree

tsl/src/nodes/columnar_scan/columnar_scan.c

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -978,27 +978,45 @@ cost_batch_sorted_merge(PlannerInfo *root, const CompressionInfo *compression_in
978978
{
979979
Path sort_path; /* dummy for result of cost_sort */
980980

981-
/*
982-
* Don't disable the compressed batch sorted merge plan with the enable_sort
983-
* GUC. We have a separate GUC for it, and this way you can try to force the
984-
* batch sorted merge plan by disabling sort.
985-
*/
986-
const bool old_enable_sort = enable_sort;
987-
enable_sort = true;
988-
cost_sort(&sort_path,
989-
root,
990-
dcpath->required_compressed_pathkeys,
981+
/* We are utilizing compressed sort for batch sorted merge: do not need extra sort */
982+
if (dcpath->required_compressed_pathkeys &&
983+
pathkeys_contained_in(dcpath->required_compressed_pathkeys, compressed_path->pathkeys))
984+
{
985+
sort_path.rows = compressed_path->rows;
986+
sort_path.startup_cost = compressed_path->startup_cost;
987+
sort_path.total_cost = compressed_path->total_cost;
991988
#if PG18_GE
992-
compressed_path->disabled_nodes,
989+
/* PG18 changes the way we handle disabled nodes so we
990+
* need to take those into account as well.
991+
*
992+
* https://github.com/postgres/postgres/commit/e2225346
993+
*/
994+
sort_path.disabled_nodes = compressed_path->disabled_nodes;
993995
#endif
994-
compressed_path->total_cost,
995-
compressed_path->rows,
996-
compressed_path->pathtarget->width,
997-
0.0,
998-
work_mem,
999-
-1);
1000-
enable_sort = old_enable_sort;
1001-
996+
}
997+
else
998+
{
999+
/*
1000+
* Don't disable the compressed batch sorted merge plan with the enable_sort
1001+
* GUC. We have a separate GUC for it, and this way you can try to force the
1002+
* batch sorted merge plan by disabling sort.
1003+
*/
1004+
const bool old_enable_sort = enable_sort;
1005+
enable_sort = true;
1006+
cost_sort(&sort_path,
1007+
root,
1008+
dcpath->required_compressed_pathkeys,
1009+
#if PG18_GE
1010+
compressed_path->disabled_nodes,
1011+
#endif
1012+
compressed_path->total_cost,
1013+
compressed_path->rows,
1014+
compressed_path->pathtarget->width,
1015+
0.0,
1016+
work_mem,
1017+
-1);
1018+
enable_sort = old_enable_sort;
1019+
}
10021020
/*
10031021
* In compressed batch sorted merge, for each distinct segmentby value we
10041022
* have to keep the corresponding latest batch open. Estimate the number of
@@ -1432,6 +1450,18 @@ build_on_single_compressed_path(PlannerInfo *root, const Chunk *chunk, RelOptInf
14321450
return NIL;
14331451
}
14341452

1453+
if (ts_guc_debug_require_batch_sorted_merge == DRO_Require ||
1454+
ts_guc_debug_require_batch_sorted_merge == DRO_Force)
1455+
{
1456+
if (!(sort_info->use_batch_sorted_merge && ts_guc_enable_decompression_sorted_merge))
1457+
{
1458+
ereport(ERROR,
1459+
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
1460+
errmsg("debug: batch sorted merge is required but not possible at planning "
1461+
"time")));
1462+
}
1463+
}
1464+
14351465
/*
14361466
* Filter out all paths that try to JOIN the compressed chunk on the
14371467
* hypertable or the uncompressed chunk
@@ -1477,8 +1507,6 @@ build_on_single_compressed_path(PlannerInfo *root, const Chunk *chunk, RelOptInf
14771507
*/
14781508
if (sort_info->use_batch_sorted_merge && ts_guc_enable_decompression_sorted_merge)
14791509
{
1480-
Assert(!sort_info->use_compressed_sort);
1481-
14821510
ColumnarScanPath *path_copy =
14831511
copy_columnar_scan_path((ColumnarScanPath *) chunk_path_no_sort);
14841512

@@ -1492,6 +1520,12 @@ build_on_single_compressed_path(PlannerInfo *root, const Chunk *chunk, RelOptInf
14921520
* query here.
14931521
*/
14941522
path_copy->custom_path.path.pathkeys = sort_info->decompressed_sort_pathkeys;
1523+
1524+
/* Batch sorted merge over unordered chunk can utilize compressed sort, copy the relevant
1525+
* fields */
1526+
path_copy->needs_sequence_num = sort_info->needs_sequence_num;
1527+
path_copy->required_compressed_pathkeys = sort_info->required_compressed_pathkeys;
1528+
14951529
cost_batch_sorted_merge(root, compression_info, path_copy, compressed_path);
14961530

14971531
if (ts_guc_debug_require_batch_sorted_merge == DRO_Force)
@@ -1502,22 +1536,13 @@ build_on_single_compressed_path(PlannerInfo *root, const Chunk *chunk, RelOptInf
15021536

15031537
decompressed_paths = lappend(decompressed_paths, path_copy);
15041538
}
1505-
else if (ts_guc_debug_require_batch_sorted_merge == DRO_Require ||
1506-
ts_guc_debug_require_batch_sorted_merge == DRO_Force)
1507-
{
1508-
ereport(ERROR,
1509-
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
1510-
errmsg("debug: batch sorted merge is required but not possible at planning "
1511-
"time")));
1512-
}
1513-
15141539
/*
15151540
* If we can push down the sort below the ColumnarScan node, we set the
15161541
* pathkeys of the decompress node to the decompressed_sort_pathkeys. We
15171542
* will determine whether to put an actual sort between the decompression
15181543
* node and the scan during plan creation.
15191544
*/
1520-
if (sort_info->use_compressed_sort)
1545+
else if (sort_info->use_compressed_sort)
15211546
{
15221547
ColumnarScanPath *columnar_scan_with_compressed_sort = NULL;
15231548
Path dummy_sort_path; /* dummy for result of cost_sort */
@@ -3229,6 +3254,18 @@ build_sortinfo(PlannerInfo *root, const Chunk *chunk, RelOptInfo *chunk_rel,
32293254
compression_info,
32303255
/* for_batch_sorted_merge = */ true,
32313256
&sort_info.reverse);
3257+
3258+
/* Pathkeys are matching leading orderby metadata column:
3259+
* can use already sorted compressed data for batch sorted merge.
3260+
*/
3261+
if (sort_info.use_batch_sorted_merge && ts_guc_enable_decompression_sorted_merge &&
3262+
!sort_info.reverse && list_length(pathkeys) == 1 &&
3263+
/* Needs to be sorted on metadata, not sequence number */
3264+
!compression_info->has_seq_num)
3265+
{
3266+
sort_info.needs_sequence_num = true;
3267+
sort_info.use_compressed_sort = true;
3268+
}
32323269
}
32333270
return sort_info;
32343271
}

tsl/src/nodes/columnar_scan/exec.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ columnar_scan_state_create(CustomScan *cscan)
9898
Assert(list_length(chunk_state->decompression_map) ==
9999
list_length(chunk_state->is_segmentby_column));
100100

101+
chunk_state->done_fetching_batches = false;
101102
return (Node *) chunk_state;
102103
}
103104

@@ -201,6 +202,7 @@ columnar_scan_begin(CustomScanState *node, EState *estate, int eflags)
201202
Plan *compressed_scan = linitial(cscan->custom_plans);
202203
Assert(list_length(cscan->custom_plans) == 1);
203204

205+
chunk_state->done_fetching_batches = false;
204206
ts_stats_compression_acc_init(&dcontext->observ_acc);
205207

206208
PlanState *ps = &node->ss.ps;
@@ -451,12 +453,13 @@ columnar_scan_exec_impl(ColumnarScanState *chunk_state, const BatchQueueFunction
451453

452454
bqfuncs->pop(bq, dcontext);
453455

454-
while (bqfuncs->needs_next_batch(bq))
456+
while (!chunk_state->done_fetching_batches && bqfuncs->needs_next_batch(bq))
455457
{
456458
TupleTableSlot *subslot = ExecProcNode(linitial(chunk_state->csstate.custom_ps));
457459
if (TupIsNull(subslot))
458460
{
459461
/* Won't have more compressed tuples. */
462+
chunk_state->done_fetching_batches = true;
460463
break;
461464
}
462465

@@ -491,6 +494,7 @@ static void
491494
columnar_scan_rescan(CustomScanState *node)
492495
{
493496
ColumnarScanState *chunk_state = (ColumnarScanState *) node;
497+
chunk_state->done_fetching_batches = false;
494498
BatchQueue *bq = chunk_state->batch_queue;
495499

496500
bq->funcs->reset(bq);

tsl/src/nodes/columnar_scan/exec.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ typedef struct ColumnarScanState
4242
* evaluate to constant false, hence the flag.
4343
*/
4444
List *vectorized_quals_original;
45+
46+
/* We may be done with compressed batches but not done with the heap,
47+
* let ColumnarScanState know to stop fetching batches in this case. */
48+
bool done_fetching_batches;
4549
} ColumnarScanState;
4650

4751
extern Node *columnar_scan_state_create(CustomScan *cscan);

tsl/src/nodes/columnar_scan/planner.c

Lines changed: 79 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,82 +1298,95 @@ columnar_scan_plan_create(PlannerInfo *root, RelOptInfo *rel, CustomPath *path,
12981298

12991299
sort_options = list_make4(sort_col_idx, sort_ops, sort_collations, sort_nulls);
13001300

1301-
/*
1302-
* Build a sort node for the compressed batches. The sort function is
1303-
* derived from the sort function of the pathkeys, except that it refers
1304-
* to the min and max metadata columns of the batches. We have already
1305-
* verified that the pathkeys match the compression order_by, so this
1306-
* mapping is possible.
1307-
*/
1308-
AttrNumber *sortColIdx = palloc(sizeof(AttrNumber) * numsortkeys);
1309-
Oid *sortOperators = palloc(sizeof(Oid) * numsortkeys);
1310-
Oid *collations = palloc(sizeof(Oid) * numsortkeys);
1311-
bool *nullsFirst = palloc(sizeof(bool) * numsortkeys);
1312-
for (int i = 0; i < numsortkeys; i++)
1301+
/* We can utilize compressed sort for batch sorted merge over unordered chunks: do not need
1302+
* to add extra Sort node */
1303+
if (dcpath->required_compressed_pathkeys &&
1304+
pathkeys_contained_in(dcpath->required_compressed_pathkeys, compressed_path->pathkeys))
1305+
{
1306+
decompress_plan->custom_plans = custom_plans;
1307+
}
1308+
else
13131309
{
1314-
Oid sortop = list_nth_oid(sort_ops, i);
1315-
1316-
/* Find the operator in pg_amop --- failure shouldn't happen */
1317-
Oid opfamily, opcintype;
1318-
CompareType strategy;
1319-
if (!get_ordering_op_properties(list_nth_oid(sort_ops, i),
1320-
&opfamily,
1321-
&opcintype,
1322-
&strategy))
1323-
{
1324-
elog(ERROR, "operator %u is not a valid ordering operator", sortOperators[i]);
1325-
}
1326-
13271310
/*
1328-
* This way to determine the matching metadata column works, because
1329-
* we have already verified that the pathkeys match the compression
1330-
* orderby.
1311+
* Build a sort node for the compressed batches. The sort function is
1312+
* derived from the sort function of the pathkeys, except that it refers
1313+
* to the min and max metadata columns of the batches. We have already
1314+
* verified that the pathkeys match the compression order_by, so this
1315+
* mapping is possible.
13311316
*/
1332-
Assert(strategy == BTLessStrategyNumber || strategy == BTGreaterStrategyNumber);
1333-
char *lower_name;
1334-
char *upper_name;
1335-
orderby_sparse_metadata_names(dcpath->info->settings, i + 1, &lower_name, &upper_name);
1336-
char *meta_col_name = strategy == BTLessStrategyNumber ? lower_name : upper_name;
1317+
AttrNumber *sortColIdx = palloc(sizeof(AttrNumber) * numsortkeys);
1318+
Oid *sortOperators = palloc(sizeof(Oid) * numsortkeys);
1319+
Oid *collations = palloc(sizeof(Oid) * numsortkeys);
1320+
bool *nullsFirst = palloc(sizeof(bool) * numsortkeys);
1321+
for (int i = 0; i < numsortkeys; i++)
1322+
{
1323+
Oid sortop = list_nth_oid(sort_ops, i);
1324+
1325+
/* Find the operator in pg_amop --- failure shouldn't happen */
1326+
Oid opfamily, opcintype;
1327+
CompareType strategy;
1328+
if (!get_ordering_op_properties(list_nth_oid(sort_ops, i),
1329+
&opfamily,
1330+
&opcintype,
1331+
&strategy))
1332+
{
1333+
elog(ERROR, "operator %u is not a valid ordering operator", sortOperators[i]);
1334+
}
13371335

1338-
AttrNumber attr_position =
1339-
get_attnum(dcpath->info->compressed_rte->relid, meta_col_name);
1336+
/*
1337+
* This way to determine the matching metadata column works, because
1338+
* we have already verified that the pathkeys match the compression
1339+
* orderby.
1340+
*/
1341+
Assert(strategy == BTLessStrategyNumber || strategy == BTGreaterStrategyNumber);
1342+
char *lower_name;
1343+
char *upper_name;
1344+
orderby_sparse_metadata_names(dcpath->info->settings,
1345+
i + 1,
1346+
&lower_name,
1347+
&upper_name);
1348+
char *meta_col_name = strategy == BTLessStrategyNumber ? lower_name : upper_name;
1349+
1350+
AttrNumber attr_position =
1351+
get_attnum(dcpath->info->compressed_rte->relid, meta_col_name);
1352+
1353+
if (attr_position == InvalidAttrNumber)
1354+
{
1355+
elog(ERROR, "couldn't find metadata column \"%s\"", meta_col_name);
1356+
}
13401357

1341-
if (attr_position == InvalidAttrNumber)
1342-
{
1343-
elog(ERROR, "couldn't find metadata column \"%s\"", meta_col_name);
1344-
}
1358+
/*
1359+
* If the compressed target list is not based on the layout of
1360+
* the uncompressed chunk (see comment for physical_tlist above),
1361+
* adjust the position of the attribute.
1362+
*/
1363+
if (target_list_compressed_is_physical)
1364+
{
1365+
sortColIdx[i] = attr_position;
1366+
}
1367+
else
1368+
{
1369+
sortColIdx[i] =
1370+
find_attr_pos_in_tlist(compressed_scan->plan.targetlist, attr_position);
1371+
}
13451372

1346-
/*
1347-
* If the compressed target list is not based on the layout of
1348-
* the uncompressed chunk (see comment for physical_tlist above),
1349-
* adjust the position of the attribute.
1350-
*/
1351-
if (target_list_compressed_is_physical)
1352-
{
1353-
sortColIdx[i] = attr_position;
1373+
sortOperators[i] = sortop;
1374+
collations[i] = list_nth_oid(sort_collations, i);
1375+
nullsFirst[i] = list_nth_oid(sort_nulls, i);
13541376
}
1355-
else
1356-
{
1357-
sortColIdx[i] =
1358-
find_attr_pos_in_tlist(compressed_scan->plan.targetlist, attr_position);
1359-
}
1360-
1361-
sortOperators[i] = sortop;
1362-
collations[i] = list_nth_oid(sort_collations, i);
1363-
nullsFirst[i] = list_nth_oid(sort_nulls, i);
1364-
}
13651377

1366-
/* Now build the compressed batches sort node */
1367-
Sort *sort = ts_make_sort((Plan *) compressed_scan,
1368-
numsortkeys,
1369-
sortColIdx,
1370-
sortOperators,
1371-
collations,
1372-
nullsFirst);
1378+
/* Now build the compressed batches sort node */
1379+
Sort *sort = ts_make_sort((Plan *) compressed_scan,
1380+
numsortkeys,
1381+
sortColIdx,
1382+
sortOperators,
1383+
collations,
1384+
nullsFirst);
13731385

1374-
ts_label_sort_with_costsize(root, sort, /* limit_tuples = */ -1.0);
1386+
ts_label_sort_with_costsize(root, sort, /* limit_tuples = */ -1.0);
13751387

1376-
decompress_plan->custom_plans = list_make1(sort);
1388+
decompress_plan->custom_plans = list_make1(sort);
1389+
}
13771390
}
13781391
else
13791392
{

tsl/test/expected/compress_unordered_sort.out

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,8 @@ SET timescaledb.debug_require_batch_sorted_merge = 'force';
369369
GroupAggregate
370370
Group Key: _hyper_1_1_chunk."time"
371371
-> Custom Scan (ColumnarScan) on _hyper_1_1_chunk
372-
-> Sort
373-
Sort Key: _hyper_1_1_chunk_compressed._ts_meta_v2_first_time DESC
374-
-> Index Scan using _hyper_1_1_chunk_compressed_device_sensor__ts_meta_v2_first_idx on _hyper_1_1_chunk_compressed
375-
Index Cond: ((device = 'd1'::text) AND (sensor = 'A'::text))
372+
-> Index Scan using _hyper_1_1_chunk_compressed_device_sensor__ts_meta_v2_first_idx on _hyper_1_1_chunk_compressed
373+
Index Cond: ((device = 'd1'::text) AND (sensor = 'A'::text))
376374

377375
select time, avg(value) from metrics where device = 'd1' and sensor='A' group by time order by time DESC;
378376
time | avg

0 commit comments

Comments
 (0)