Skip to content

Commit d969a2b

Browse files
renovate[bot]antekresic
authored andcommitted
Add compact_chunk function
This new function will compact the chunk by looking for overlapping batches and combining them together in order to produce globally ordered chunks. This change is a first step towards supporting direct compress in production workloads.
1 parent bb985ce commit d969a2b

18 files changed

Lines changed: 3834 additions & 4 deletions

.unreleased/pr_9957

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implements: #9957 Add compact_chunk function

sql/maintenance_utils.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ CREATE OR REPLACE FUNCTION _timescaledb_functions.recompress_chunk_segmentwise(
8989
if_compressed BOOLEAN = true
9090
) RETURNS REGCLASS AS '@MODULE_PATHNAME@', 'ts_recompress_chunk_segmentwise' LANGUAGE C STRICT VOLATILE;
9191

92+
CREATE OR REPLACE FUNCTION _timescaledb_functions.compact_chunk(
93+
uncompressed_chunk REGCLASS
94+
) RETURNS REGCLASS AS '@MODULE_PATHNAME@', 'ts_compact_chunk' LANGUAGE C STRICT VOLATILE;
95+
9296
-- find the index on the compressed chunk that can be used to recompress efficiently
9397
-- this index must contain all the segmentby columns and the meta_sequence_number column last
9498
CREATE OR REPLACE FUNCTION _timescaledb_functions.get_compressed_chunk_index_for_recompression(

sql/updates/reverse-dev.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
DROP FUNCTION IF EXISTS _timescaledb_functions.decompress_batch(record);
22
DROP FUNCTION IF EXISTS _timescaledb_functions.estimate_uncompressed_size(regclass, double precision);
3+
DROP FUNCTION IF EXISTS _timescaledb_functions.compact_chunk(REGCLASS);

src/cross_module_fn.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ CROSSMODULE_WRAPPER(chunk_freeze_chunk);
100100
CROSSMODULE_WRAPPER(chunk_unfreeze_chunk);
101101

102102
CROSSMODULE_WRAPPER(recompress_chunk_segmentwise);
103+
CROSSMODULE_WRAPPER(compact_chunk);
103104
CROSSMODULE_WRAPPER(get_compressed_chunk_index_for_recompression);
104105
CROSSMODULE_WRAPPER(merge_chunks);
105106
CROSSMODULE_WRAPPER(split_chunk);
@@ -397,6 +398,7 @@ TSDLLEXPORT CrossModuleFunctions ts_cm_functions_default = {
397398
.chunk_freeze_chunk = error_no_default_fn_pg_community,
398399
.chunk_unfreeze_chunk = error_no_default_fn_pg_community,
399400
.recompress_chunk_segmentwise = error_no_default_fn_pg_community,
401+
.compact_chunk = error_no_default_fn_pg_community,
400402
.get_compressed_chunk_index_for_recompression = error_no_default_fn_pg_community,
401403

402404
.preprocess_query_tsl = preprocess_query_tsl_default_fn_community,

src/cross_module_fn.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ typedef struct CrossModuleFunctions
174174
PGFunction chunk_freeze_chunk;
175175
PGFunction chunk_unfreeze_chunk;
176176
PGFunction recompress_chunk_segmentwise;
177+
PGFunction compact_chunk;
177178
PGFunction get_compressed_chunk_index_for_recompression;
178179

179180
void (*preprocess_query_tsl)(Query *parse, int *cursor_opts);
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# compact_chunk
2+
3+
Merges overlapping compressed batches within a chunk. Only touches batches
4+
that need fixing — correctly ordered batches are left as-is.
5+
6+
Overlap detection reads the firstlast sparse metadata, which stores the exact
7+
orderby values of each batch's first and last rows. Two adjacent batches overlap
8+
when the current batch's first row sorts before the previous batch's last row.
9+
Because the metadata holds the real boundary rows for every orderby column, no
10+
decompression is needed, even for multi-column orderby.
11+
12+
## How It Works
13+
14+
```
15+
Phase 1: FIND Phase 2: RECOMPRESS Phase 3: VERIFY
16+
┌──────────────┐ ┌──────────────────┐ ┌────────────────┐
17+
│ Index scan │──────▶│ Decompress+merge │─────▶│ Re-scan with │
18+
│ Stop at │ │ overlapping │ │ fresh snapshot │
19+
│ first issue │ │ batches, continue│ │ Clear UNORDERED│
20+
└──────────────┘ │ scanning for more│ │ if clean │
21+
└──────────────────┘ └────────────────┘
22+
```
23+
24+
## Handling specific compression and batch configurations
25+
26+
### 1. No overlaps (no-op)
27+
28+
```
29+
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
30+
│ 1..100 │ │101..200│ │201..300│ ───▶ │ 1..100 │ │101..200│ │201..300│
31+
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘
32+
(unchanged, UNORDERED cleared)
33+
```
34+
35+
### 2. Overlapping batches
36+
37+
```
38+
┌──────────┐ ┌───────┐┌───────┐┌───────┐┌──┐┌────────┐
39+
│ 1..100 │ │ 1..50 ││51..100││101 ││ ││201..300│
40+
└──────────┘ │ ││ ││ ..150 ││… │└────────┘
41+
┌──────────┐ ───▶ └───────┘└───────┘└───────┘└──┘
42+
│ 50..150 │ ◄────── merged + re-sorted ──► ◄ kept ►
43+
└──────────┘
44+
┌────────┐
45+
│201..300│
46+
└────────┘
47+
```
48+
49+
### 3. Segmentby — independent per segment
50+
51+
```
52+
d1: ┌──────┐ ┌───────┐ d1: ┌──────────────┐
53+
│1..100│ │50..200│ ──▶ │ 1..200 merged│
54+
└──────┘ └───────┘ └──────────────┘
55+
d2: ┌──────┐ ┌───────┐ d2: ┌──────────────┐
56+
│1..100│ │50..200│ ──▶ │ 1..200 merged│
57+
└──────┘ └───────┘ └──────────────┘
58+
```
59+
60+
### 4. DESC orderby
61+
62+
```
63+
orderby='time DESC' max◄────────────────────►min
64+
65+
┌──────────┐ ┌──────────────────┐
66+
│ 200..100 │ │ 200..........100 │
67+
└──────────┘ ──▶ │ merged │
68+
┌──────────┐ └──────────────────┘
69+
│ 150..50 │
70+
└──────────┘
71+
```
72+
73+
### 5. Multi-column orderby — boundary tie resolution
74+
75+
When col1 first/last tie, compare the secondary columns straight from the
76+
first/last metadata — no decompression.
77+
78+
```
79+
orderby='device,time' Both batches tie on device (first=d2, last=d2)
80+
81+
Batch 1 last row: (d2, 08:20) ◄─ from last metadata
82+
Batch 2 first row: (d2, 08:21) ◄─ from first metadata
83+
08:20 < 08:21 → no overlap ✓
84+
85+
Batch 1 last row: (d2, 08:20)
86+
Batch 2 first row: (d2, 08:11)
87+
08:20 > 08:11 → OVERLAP → merge
88+
```
89+
90+
### 6. Mixed-null batch overlaps a neighbor
91+
92+
A batch with both NULL and non-NULL values in the first orderby column has a
93+
NULL boundary row. With NULLS LAST its last row is NULL, which sorts after a
94+
following non-null batch — so the two batches overlap. The merge re-sorts the
95+
rows and the NULLs settle at the end (NULLS LAST). (A mixed-null batch with no
96+
neighbor to overlap is already ordered and is left as-is.)
97+
98+
```
99+
orderby='value NULLS LAST' last row of batch 1 is NULL, sorts after batch 2
100+
101+
┌─────────────────────┐ ┌──────────┐ ┌────────────────────────────────┐
102+
│ 1001..1800, NULL×200│ │1801..2800│ ──▶ │ 1001..2800 re-sorted, NULL×200 │
103+
└─────────────────────┘ └──────────┘ └────────────────────────────────┘
104+
first=1001, last=NULL ──▶ overlap merged, NULLs at end (NULLS LAST)
105+
```
106+
107+
### 7. Overlap merge preserving NULLs
108+
109+
When overlapping batches with nullable first orderby are merged, the re-sort
110+
keeps the NULL rows in their correct ordered position.
111+
112+
```
113+
orderby='value NULLS LAST'
114+
115+
┌──────────────────┐ ┌──────────────────────────┐
116+
│ 1..400, NULL×100 │ ──▶ │ 1..699 re-sorted, NULL×100│
117+
└──────────────────┘ └──────────────────────────┘
118+
┌──────────┐ overlap NULLs kept at end (NULLS LAST)
119+
│ 200..699 │ on 200..400
120+
└──────────┘
121+
```
122+
123+
### 8. Secondary column NULLs at boundary tie
124+
125+
The boundary comparison uses the column's sort order, which already places NULLs
126+
per its NULLS FIRST/LAST setting, so a NULL boundary value compares like any
127+
other value.
128+
129+
```
130+
orderby='time, value NULLS LAST'
131+
132+
Batch 1 last row: (08:20, NULL) CORRECT: NULL with NULLS LAST
133+
Batch 2 first row: (08:20, 1001) means NULL > 1001 → OVERLAP → merge
134+
135+
136+
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐
137+
│ ..., (08:20,NULL)│ │(08:20,1001), ... │ ──▶ │ merged + correctly sorted│
138+
└──────────────────┘ └──────────────────┘ └──────────────────────────┘
139+
```

tsl/src/compression/compression.c

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3235,10 +3235,10 @@ tsl_compressed_data_info(PG_FUNCTION_ARGS)
32353235
return HeapTupleGetDatum(tuple);
32363236
}
32373237

3238-
extern Datum
3239-
tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS)
3238+
bool
3239+
compressed_data_has_nulls(Datum compressed_data)
32403240
{
3241-
const CompressedDataHeader *header = get_compressed_data_header(PG_GETARG_DATUM(0));
3241+
const CompressedDataHeader *header = get_compressed_data_header(compressed_data);
32423242
bool has_nulls = false;
32433243

32443244
switch (header->compression_algorithm)
@@ -3269,7 +3269,13 @@ tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS)
32693269
break;
32703270
}
32713271

3272-
return BoolGetDatum(has_nulls);
3272+
return has_nulls;
3273+
}
3274+
3275+
extern Datum
3276+
tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS)
3277+
{
3278+
return BoolGetDatum(compressed_data_has_nulls(PG_GETARG_DATUM(0)));
32733279
}
32743280

32753281
extern CompressionStorage

tsl/src/compression/compression.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ extern Datum tsl_compressed_data_in(PG_FUNCTION_ARGS);
349349
extern Datum tsl_compressed_data_out(PG_FUNCTION_ARGS);
350350
extern Datum tsl_compressed_data_info(PG_FUNCTION_ARGS);
351351
extern Datum tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS);
352+
extern bool compressed_data_has_nulls(Datum compressed_data);
352353
extern Datum tsl_compressed_data_column_size(PG_FUNCTION_ARGS);
353354
extern Datum tsl_compressed_data_to_array(PG_FUNCTION_ARGS);
354355
extern Datum tsl_decompress_batch(PG_FUNCTION_ARGS);

0 commit comments

Comments
 (0)