Skip to content

Commit ff03edc

Browse files
committed
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 is change is a first step towards supporting direct compress in production workloads.
1 parent d05eb06 commit ff03edc

18 files changed

Lines changed: 3803 additions & 4 deletions

.unreleased/pr_9383

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

sql/maintenance_utils.sql

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

87+
CREATE OR REPLACE FUNCTION _timescaledb_functions.compact_chunk(
88+
uncompressed_chunk REGCLASS
89+
) RETURNS REGCLASS AS '@MODULE_PATHNAME@', 'ts_compact_chunk' LANGUAGE C STRICT VOLATILE;
90+
8791
-- find the index on the compressed chunk that can be used to recompress efficiently
8892
-- this index must contain all the segmentby columns and the meta_sequence_number column last
8993
CREATE OR REPLACE FUNCTION _timescaledb_functions.get_compressed_chunk_index_for_recompression(

sql/updates/reverse-dev.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,6 @@ DROP FUNCTION IF EXISTS @extschema@.create_hypertable(relation REGCLASS, time_co
188188
-- Restore the chunk_target_size check constraint dropped in the forward path.
189189
ALTER TABLE _timescaledb_catalog.hypertable
190190
ADD CONSTRAINT hypertable_chunk_target_size_check CHECK (chunk_target_size >= 0);
191+
192+
-- drop new function
193+
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
@@ -98,6 +98,7 @@ CROSSMODULE_WRAPPER(chunk_freeze_chunk);
9898
CROSSMODULE_WRAPPER(chunk_unfreeze_chunk);
9999

100100
CROSSMODULE_WRAPPER(recompress_chunk_segmentwise);
101+
CROSSMODULE_WRAPPER(compact_chunk);
101102
CROSSMODULE_WRAPPER(get_compressed_chunk_index_for_recompression);
102103
CROSSMODULE_WRAPPER(merge_chunks);
103104
CROSSMODULE_WRAPPER(split_chunk);
@@ -385,6 +386,7 @@ TSDLLEXPORT CrossModuleFunctions ts_cm_functions_default = {
385386
.chunk_freeze_chunk = error_no_default_fn_pg_community,
386387
.chunk_unfreeze_chunk = error_no_default_fn_pg_community,
387388
.recompress_chunk_segmentwise = error_no_default_fn_pg_community,
389+
.compact_chunk = error_no_default_fn_pg_community,
388390
.get_compressed_chunk_index_for_recompression = error_no_default_fn_pg_community,
389391

390392
.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
@@ -171,6 +171,7 @@ typedef struct CrossModuleFunctions
171171
PGFunction chunk_freeze_chunk;
172172
PGFunction chunk_unfreeze_chunk;
173173
PGFunction recompress_chunk_segmentwise;
174+
PGFunction compact_chunk;
174175
PGFunction get_compressed_chunk_index_for_recompression;
175176

176177
void (*preprocess_query_tsl)(Query *parse, int *cursor_opts);
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 then pulls the
95+
NULLs into their own batch. (A mixed-null batch with no neighbor to overlap is
96+
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 │
103+
└─────────────────────┘ └──────────┘ └─────────────────────┘ │ ×200 │
104+
first=1001, last=NULL ──▶ overlap merged └──────┘
105+
non-null end (NULLS LAST)
106+
```
107+
108+
### 7. Overlap merge preserving NULLs
109+
110+
When overlapping batches with nullable first orderby are merged, NULL rows are flushed as pure-null batches.
111+
112+
```
113+
orderby='value NULLS LAST'
114+
115+
┌──────────────────┐ ┌───────────────────┐ ┌──────┐
116+
│ 1..400, NULL×100 │ ──▶ │ 1..699 re-sorted │ │NULL │
117+
└──────────────────┘ └───────────────────┘ │ ×100 │
118+
┌──────────┐ overlap └──────┘
119+
│ 200..699 │ on 200..400 NULLs split out during merge
120+
└──────────┘
121+
```
122+
123+
### 8. Secondary column NULLs at boundary tie
124+
125+
Boundary row comparison respects NULLS FIRST/LAST semantics instead of skipping NULLs — skipping would leave the batches unordered.
126+
127+
```
128+
orderby='time, value NULLS LAST'
129+
130+
Batch 1 last row: (08:20, NULL) CORRECT: NULL with NULLS LAST
131+
Batch 2 first row: (08:20, 1001) means NULL > 1001 → OVERLAP → merge
132+
133+
134+
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐
135+
│ ..., (08:20,NULL)│ │(08:20,1001), ... │ ──▶ │ merged + correctly sorted│
136+
└──────────────────┘ └──────────────────┘ └──────────────────────────┘
137+
```

tsl/src/compression/compression.c

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,10 +3109,10 @@ tsl_compressed_data_info(PG_FUNCTION_ARGS)
31093109
return HeapTupleGetDatum(tuple);
31103110
}
31113111

3112-
extern Datum
3113-
tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS)
3112+
bool
3113+
compressed_data_has_nulls(Datum compressed_data)
31143114
{
3115-
const CompressedDataHeader *header = get_compressed_data_header(PG_GETARG_DATUM(0));
3115+
const CompressedDataHeader *header = get_compressed_data_header(compressed_data);
31163116
bool has_nulls = false;
31173117

31183118
switch (header->compression_algorithm)
@@ -3143,7 +3143,13 @@ tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS)
31433143
break;
31443144
}
31453145

3146-
return BoolGetDatum(has_nulls);
3146+
return has_nulls;
3147+
}
3148+
3149+
extern Datum
3150+
tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS)
3151+
{
3152+
return BoolGetDatum(compressed_data_has_nulls(PG_GETARG_DATUM(0)));
31473153
}
31483154

31493155
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

0 commit comments

Comments
 (0)