Skip to content

Commit 597d0d0

Browse files
Do not sort batches for Batch sorted merge over unordered chunks if query sort matches compressed sort
1 parent c3dc5b2 commit 597d0d0

10 files changed

Lines changed: 557 additions & 129 deletions

tsl/src/nodes/columnar_scan/batch_queue.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ typedef struct BatchQueueFunctions
2424
void (*push_batch)(struct BatchQueue *, DecompressContext *, TupleTableSlot *);
2525
void (*reset)(struct BatchQueue *);
2626
TupleTableSlot *(*top_tuple)(struct BatchQueue *);
27+
void (*is_done)(struct BatchQueue *);
2728
} BatchQueueFunctions;
2829

2930
typedef struct BatchQueue

tsl/src/nodes/columnar_scan/batch_queue_fifo.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,20 @@ batch_queue_fifo_top_tuple(BatchQueue *bq)
5757
return compressed_batch_current_tuple(batch_array_get_at(&bq->batch_array, 0));
5858
}
5959

60+
static inline void
61+
batch_queue_fifo_is_done(BatchQueue *bq)
62+
{
63+
/* no-op */
64+
}
65+
6066
static const struct BatchQueueFunctions BatchQueueFunctionsFifo = {
6167
.free = batch_queue_fifo_free,
6268
.needs_next_batch = batch_queue_fifo_needs_next_batch,
6369
.pop = batch_queue_fifo_pop,
6470
.push_batch = batch_queue_fifo_push_batch,
6571
.reset = batch_queue_fifo_reset,
6672
.top_tuple = batch_queue_fifo_top_tuple,
73+
.is_done = batch_queue_fifo_is_done,
6774
};
6875

6976
extern BatchQueue *batch_queue_fifo_create(int num_compressed_cols,

tsl/src/nodes/columnar_scan/batch_queue_heap.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ typedef struct BatchQueueHeap
4646
*/
4747
TupleTableSlot *last_batch_first_tuple_slot;
4848
HeapEntryColumn *last_batch_first_tuple_entry;
49+
50+
/* When we have no more compressed batches but may still have tuples on the heap,
51+
do not check if we need next batch when there are none. */
52+
bool done_fetching_compressed_batches;
4953
} BatchQueueHeap;
5054

5155
/*
@@ -199,6 +203,12 @@ batch_queue_heap_needs_next_batch(BatchQueue *_queue)
199203
{
200204
BatchQueueHeap *queue = (BatchQueueHeap *) _queue;
201205

206+
/* We consumed all of the compressed batches, time to empty the heap */
207+
if (queue->done_fetching_compressed_batches)
208+
{
209+
return false;
210+
}
211+
202212
if (binaryheap_empty(queue->merge_heap))
203213
{
204214
return true;
@@ -319,6 +329,7 @@ batch_queue_heap_reset(BatchQueue *bq)
319329
{
320330
BatchQueueHeap *bqh = (BatchQueueHeap *) bq;
321331
binaryheap_reset(bqh->merge_heap);
332+
bqh->done_fetching_compressed_batches = false;
322333
}
323334

324335
/*
@@ -343,13 +354,21 @@ batch_queue_heap_free(BatchQueue *_queue)
343354
pfree(queue);
344355
}
345356

357+
static void
358+
batch_queue_heap_is_done(BatchQueue *bq)
359+
{
360+
BatchQueueHeap *bqh = (BatchQueueHeap *) bq;
361+
bqh->done_fetching_compressed_batches = true;
362+
}
363+
346364
const struct BatchQueueFunctions BatchQueueFunctionsHeap = {
347365
.free = batch_queue_heap_free,
348366
.needs_next_batch = batch_queue_heap_needs_next_batch,
349367
.pop = batch_queue_heap_pop,
350368
.push_batch = batch_queue_heap_push_batch,
351369
.reset = batch_queue_heap_reset,
352370
.top_tuple = batch_queue_heap_top_tuple,
371+
.is_done = batch_queue_heap_is_done,
353372
};
354373

355374
static SortSupport
@@ -412,5 +431,7 @@ batch_queue_heap_create(int num_compressed_cols, const List *sortinfo,
412431
queue->last_batch_first_tuple_entry = palloc(sizeof(HeapEntryColumn) * queue->nkeys);
413432
queue->queue.funcs = funcs;
414433

434+
queue->done_fetching_compressed_batches = false;
435+
415436
return &queue->queue;
416437
}

tsl/src/nodes/columnar_scan/columnar_scan.c

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -978,27 +978,44 @@ 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+
{
984+
sort_path.rows = compressed_path->rows;
985+
sort_path.startup_cost = compressed_path->startup_cost;
986+
sort_path.total_cost = compressed_path->total_cost;
991987
#if PG18_GE
992-
compressed_path->disabled_nodes,
988+
/* PG18 changes the way we handle disabled nodes so we
989+
* need to take those into account as well.
990+
*
991+
* https://github.com/postgres/postgres/commit/e2225346
992+
*/
993+
sort_path.disabled_nodes = compressed_path->disabled_nodes;
993994
#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-
995+
}
996+
else
997+
{
998+
/*
999+
* Don't disable the compressed batch sorted merge plan with the enable_sort
1000+
* GUC. We have a separate GUC for it, and this way you can try to force the
1001+
* batch sorted merge plan by disabling sort.
1002+
*/
1003+
const bool old_enable_sort = enable_sort;
1004+
enable_sort = true;
1005+
cost_sort(&sort_path,
1006+
root,
1007+
dcpath->required_compressed_pathkeys,
1008+
#if PG18_GE
1009+
compressed_path->disabled_nodes,
1010+
#endif
1011+
compressed_path->total_cost,
1012+
compressed_path->rows,
1013+
compressed_path->pathtarget->width,
1014+
0.0,
1015+
work_mem,
1016+
-1);
1017+
enable_sort = old_enable_sort;
1018+
}
10021019
/*
10031020
* In compressed batch sorted merge, for each distinct segmentby value we
10041021
* have to keep the corresponding latest batch open. Estimate the number of
@@ -1477,8 +1494,6 @@ build_on_single_compressed_path(PlannerInfo *root, const Chunk *chunk, RelOptInf
14771494
*/
14781495
if (sort_info->use_batch_sorted_merge && ts_guc_enable_decompression_sorted_merge)
14791496
{
1480-
Assert(!sort_info->use_compressed_sort);
1481-
14821497
ColumnarScanPath *path_copy =
14831498
copy_columnar_scan_path((ColumnarScanPath *) chunk_path_no_sort);
14841499

@@ -1492,6 +1507,12 @@ build_on_single_compressed_path(PlannerInfo *root, const Chunk *chunk, RelOptInf
14921507
* query here.
14931508
*/
14941509
path_copy->custom_path.path.pathkeys = sort_info->decompressed_sort_pathkeys;
1510+
1511+
/* Batch sorted merge over unordered chunk can utilize compressed sort, copy the relevant
1512+
* fields */
1513+
path_copy->needs_sequence_num = sort_info->needs_sequence_num;
1514+
path_copy->required_compressed_pathkeys = sort_info->required_compressed_pathkeys;
1515+
14951516
cost_batch_sorted_merge(root, compression_info, path_copy, compressed_path);
14961517

14971518
if (ts_guc_debug_require_batch_sorted_merge == DRO_Force)
@@ -1517,7 +1538,7 @@ build_on_single_compressed_path(PlannerInfo *root, const Chunk *chunk, RelOptInf
15171538
* will determine whether to put an actual sort between the decompression
15181539
* node and the scan during plan creation.
15191540
*/
1520-
if (sort_info->use_compressed_sort)
1541+
if (sort_info->use_compressed_sort && !sort_info->use_batch_sorted_merge)
15211542
{
15221543
ColumnarScanPath *columnar_scan_with_compressed_sort = NULL;
15231544
Path dummy_sort_path; /* dummy for result of cost_sort */
@@ -3229,6 +3250,15 @@ build_sortinfo(PlannerInfo *root, const Chunk *chunk, RelOptInfo *chunk_rel,
32293250
compression_info,
32303251
/* for_batch_sorted_merge = */ true,
32313252
&sort_info.reverse);
3253+
3254+
/* Pathkeys are matching leading orderby metadata column:
3255+
* can use already sorted compressed data for batch sorted merge.
3256+
*/
3257+
if (sort_info.use_batch_sorted_merge && !sort_info.reverse)
3258+
{
3259+
sort_info.needs_sequence_num = true;
3260+
sort_info.use_compressed_sort = true;
3261+
}
32323262
}
32333263
return sort_info;
32343264
}

tsl/src/nodes/columnar_scan/exec.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ columnar_scan_exec_impl(ColumnarScanState *chunk_state, const BatchQueueFunction
457457
if (TupIsNull(subslot))
458458
{
459459
/* Won't have more compressed tuples. */
460+
bqfuncs->is_done(bq);
460461
break;
461462
}
462463

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)