Skip to content

Commit 0e49a7a

Browse files
committed
Use firstlast sparse index for orderby metadata
New compressed chunks now carry both minmax and firstlast metadata for every orderby column, and the compressed chunk index is built on the firstlast columns. Predicate pushdown picks firstlast when the leading orderby is NOT NULL, falling back to minmax otherwise.
1 parent a290f97 commit 0e49a7a

129 files changed

Lines changed: 13151 additions & 12492 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ tags
3131
/compile_commands.json
3232
/.DS_Store
3333
/.clangd
34+
/.claude
3435
/.cache
3536

3637
/CMakeSettings.json

.unreleased/pr_9784

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implements: #9784 Use firstlast sparse index for orderby metadata on new compressed chunks

src/ts_catalog/compression_settings.c

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -964,27 +964,41 @@ ts_add_orderby_sparse_index(CompressionSettings *settings)
964964
}
965965
}
966966

967-
/* add orderby sparse settings */
967+
/*
968+
* For each orderby column, record both the minmax and firstlast sparse
969+
* indexes that the compressed chunk carries by default. The physical
970+
* columns are always created for orderby columns, so the index
971+
* configuration should reflect that.
972+
*
973+
* If the column already appears anywhere in the user-supplied index
974+
* configuration, leave it alone so user intent wins.
975+
*/
968976
ArrayIterator it = array_create_iterator(settings->fd.orderby, 0, NULL);
969977
while (array_iterate(it, &datum, &isnull))
970978
{
971-
/*
972-
* check if sparse index for column already exists
973-
* Validation is done by ts_compression_settings_update
974-
*/
979+
char *col_name = TextDatumGetCString(datum);
980+
975981
if (settings->fd.index &&
976982
ts_jsonb_has_key_value_str_field(settings->fd.index,
977983
ts_sparse_index_common_keys[SparseIndexKeyCol],
978-
TextDatumGetCString(datum)))
984+
col_name))
979985
{
980986
continue;
981987
}
982988

983-
MinmaxIndexColumnConfig config;
984-
config.base.type = _SparseIndexTypeEnumMinmax;
985-
config.col = TextDatumGetCString(datum);
986-
config.base.source = _SparseIndexSourceEnumOrderby;
987-
ts_convert_sparse_index_config_to_jsonb(parse_state, (SparseIndexConfigBase *) &config);
989+
MinmaxIndexColumnConfig minmax_config;
990+
minmax_config.base.type = _SparseIndexTypeEnumMinmax;
991+
minmax_config.base.source = _SparseIndexSourceEnumOrderby;
992+
minmax_config.col = col_name;
993+
ts_convert_sparse_index_config_to_jsonb(parse_state,
994+
(SparseIndexConfigBase *) &minmax_config);
995+
996+
FirstLastIndexColumnConfig firstlast_config;
997+
firstlast_config.base.type = _SparseIndexTypeEnumFirstLast;
998+
firstlast_config.base.source = _SparseIndexSourceEnumOrderby;
999+
firstlast_config.col = col_name;
1000+
ts_convert_sparse_index_config_to_jsonb(parse_state,
1001+
(SparseIndexConfigBase *) &firstlast_config);
9881002
}
9891003

9901004
return JsonbValueToJsonb(pushJsonbValue(&parse_state, WJB_END_ARRAY, NULL));

tsl/src/chunk_split.c

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,22 +1210,27 @@ chunk_split_chunk(PG_FUNCTION_ARGS)
12101210
NameStr(splitdim_name));
12111211

12121212
/*
1213-
* Get the attribute numbers for the primary dimension's min and max
1214-
* values in the compressed relation. We'll use these to get the time
1215-
* range of compressed segments in order to route segments to the
1216-
* right result chunk.
1213+
* Get the attribute numbers for the primary dimension's lower- and
1214+
* upper-bound metadata columns in the compressed relation. We'll use
1215+
* these to get the time range of compressed segments in order to
1216+
* route segments to the right result chunk.
12171217
*/
1218-
const char *min_attname = column_segment_min_name(orderby_pos);
1219-
const char *max_attname = column_segment_max_name(orderby_pos);
1218+
AttrNumber lower_attno;
1219+
AttrNumber upper_attno;
1220+
orderby_sparse_metadata_attnos(compress_settings,
1221+
compress_settings->fd.compress_relid,
1222+
orderby_pos,
1223+
&lower_attno,
1224+
&upper_attno);
12201225

12211226
CompressedSplitPoint csp = {
12221227
.base = {
12231228
.point = split_at,
12241229
.dim = hyperspace_get_open_dimension(ht->space, 0),
12251230
.route_next_tuple = route_next_compressed_tuple,
12261231
},
1227-
.attnum_min = get_attnum(compress_settings->fd.compress_relid, min_attname),
1228-
.attnum_max = get_attnum(compress_settings->fd.compress_relid, max_attname),
1232+
.attnum_min = lower_attno,
1233+
.attnum_max = upper_attno,
12291234
.attnum_count = get_attnum(compress_settings->fd.compress_relid, COMPRESSION_COLUMN_METADATA_COUNT_NAME),
12301235
.noncompressed_tupdesc = CreateTupleDescCopy(RelationGetDescr(srcrel)),
12311236
};

tsl/src/compression/compression.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -942,9 +942,6 @@ build_column_map(const CompressionSettings *settings, const TupleDesc in_desc,
942942
segment_max_attr_offset));
943943
}
944944

945-
Ensure(!is_orderby || has_minmax_metadata,
946-
"orderby columns must have minmax metadata");
947-
948945
const AttrNumber bloom_attr_number =
949946
compressed_column_metadata_attno(settings,
950947
settings->fd.relid,
@@ -976,9 +973,11 @@ build_column_map(const CompressionSettings *settings, const TupleDesc in_desc,
976973
attr->attnum,
977974
settings->fd.compress_relid,
978975
"last");
976+
bool has_firstlast_metadata = false;
979977
if (AttributeNumberIsValid(first_attr_number) &&
980978
AttributeNumberIsValid(last_attr_number))
981979
{
980+
has_firstlast_metadata = true;
982981
const int16 first_attr_offset = AttrNumberGetAttrOffset(first_attr_number);
983982
const int16 last_attr_offset = AttrNumberGetAttrOffset(last_attr_number);
984983
metadata_builders =
@@ -989,6 +988,9 @@ build_column_map(const CompressionSettings *settings, const TupleDesc in_desc,
989988
last_attr_offset));
990989
}
991990

991+
Ensure(!is_orderby || has_minmax_metadata || has_firstlast_metadata,
992+
"orderby columns must have sparse index metadata");
993+
992994
*column = (PerColumn){
993995
.compressor = compressor_for_type(attr->atttypid),
994996
.segmentby_column_index = -1,

tsl/src/compression/compression_storage.c

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -331,10 +331,14 @@ create_compressed_chunk_indexes(Chunk *chunk, CompressionSettings *settings)
331331
initStringInfo(&orderby_buf);
332332
for (int i = 1; i <= ts_array_length(settings->fd.orderby); i++)
333333
{
334+
char *lower_name;
335+
char *upper_name;
336+
orderby_sparse_metadata_names(settings, i, &lower_name, &upper_name);
337+
334338
resetStringInfo(&orderby_buf);
335-
/* Add min metadata column */
336-
IndexElem *orderby_min_elem = makeNode(IndexElem);
337-
orderby_min_elem->name = column_segment_min_name(i);
339+
/* Lower-boundary metadata column. */
340+
IndexElem *orderby_lower_elem = makeNode(IndexElem);
341+
orderby_lower_elem->name = lower_name;
338342
if (ts_array_get_element_bool(settings->fd.orderby_desc, i))
339343
{
340344
appendStringInfoString(&orderby_buf, " DESC");
@@ -345,11 +349,11 @@ create_compressed_chunk_indexes(Chunk *chunk, CompressionSettings *settings)
345349
appendStringInfoString(&orderby_buf, " ASC");
346350
ordering = SORTBY_ASC;
347351
}
348-
orderby_min_elem->ordering = ordering;
352+
orderby_lower_elem->ordering = ordering;
349353

350354
if (ts_array_get_element_bool(settings->fd.orderby_nullsfirst, i))
351355
{
352-
if (orderby_min_elem->ordering != SORTBY_DESC)
356+
if (orderby_lower_elem->ordering != SORTBY_DESC)
353357
{
354358
appendStringInfoString(&orderby_buf, " NULLS FIRST");
355359
nulls_ordering = SORTBY_NULLS_FIRST;
@@ -361,7 +365,7 @@ create_compressed_chunk_indexes(Chunk *chunk, CompressionSettings *settings)
361365
}
362366
else
363367
{
364-
if (orderby_min_elem->ordering != SORTBY_DESC)
368+
if (orderby_lower_elem->ordering != SORTBY_DESC)
365369
{
366370
nulls_ordering = SORTBY_NULLS_DEFAULT;
367371
}
@@ -371,21 +375,21 @@ create_compressed_chunk_indexes(Chunk *chunk, CompressionSettings *settings)
371375
nulls_ordering = SORTBY_NULLS_LAST;
372376
}
373377
}
374-
orderby_min_elem->nulls_ordering = nulls_ordering;
375-
appendStringInfoString(&buf, orderby_min_elem->name);
378+
orderby_lower_elem->nulls_ordering = nulls_ordering;
379+
appendStringInfoString(&buf, orderby_lower_elem->name);
376380
appendStringInfoString(&buf, orderby_buf.data);
377381
appendStringInfoString(&buf, ", ");
378-
indexcols = lappend(indexcols, orderby_min_elem);
379-
380-
/* Add max metadata column */
381-
IndexElem *orderby_max_elem = makeNode(IndexElem);
382-
orderby_max_elem->name = column_segment_max_name(i);
383-
orderby_max_elem->ordering = orderby_min_elem->ordering;
384-
orderby_max_elem->nulls_ordering = orderby_min_elem->nulls_ordering;
385-
appendStringInfoString(&buf, orderby_max_elem->name);
382+
indexcols = lappend(indexcols, orderby_lower_elem);
383+
384+
/* Upper-boundary metadata column. */
385+
IndexElem *orderby_upper_elem = makeNode(IndexElem);
386+
orderby_upper_elem->name = upper_name;
387+
orderby_upper_elem->ordering = orderby_lower_elem->ordering;
388+
orderby_upper_elem->nulls_ordering = orderby_lower_elem->nulls_ordering;
389+
appendStringInfoString(&buf, orderby_upper_elem->name);
386390
appendStringInfoString(&buf, orderby_buf.data);
387391
appendStringInfoString(&buf, ", ");
388-
indexcols = lappend(indexcols, orderby_max_elem);
392+
indexcols = lappend(indexcols, orderby_upper_elem);
389393
}
390394

391395
stmt.indexParams = indexcols;

tsl/src/compression/create.c

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,79 @@ column_segment_max_name(int16 column_index)
156156
return compression_column_segment_metadata_name("max", column_index);
157157
}
158158

159+
OrderbySparseKind
160+
orderby_sparse_kind(const CompressionSettings *settings, int orderby_pos)
161+
{
162+
Assert(settings != NULL);
163+
Assert(orderby_pos >= 1 && orderby_pos <= ts_array_length(settings->fd.orderby));
164+
165+
if (settings->fd.index == NULL)
166+
{
167+
return ORDERBY_SPARSE_MINMAX;
168+
}
169+
170+
const char *col_name = ts_array_get_element_text(settings->fd.orderby, orderby_pos);
171+
172+
SparseIndexSettings *parsed = ts_convert_to_sparse_index_settings(settings->fd.index);
173+
List *per_col = ts_get_per_column_compression_settings(parsed);
174+
PerColumnCompressionSettings *col_settings =
175+
ts_get_per_column_compression_settings_by_column_name(per_col, col_name);
176+
177+
OrderbySparseKind kind = ORDERBY_SPARSE_MINMAX;
178+
if (col_settings != NULL && col_settings->firstlast_obj_id >= 0)
179+
{
180+
kind = ORDERBY_SPARSE_FIRSTLAST;
181+
}
182+
183+
ts_free_sparse_index_settings(parsed);
184+
return kind;
185+
}
186+
187+
void
188+
orderby_sparse_metadata_names(const CompressionSettings *settings, int orderby_pos,
189+
char **lower_name, char **upper_name)
190+
{
191+
Assert(lower_name != NULL && upper_name != NULL);
192+
193+
OrderbySparseKind kind = orderby_sparse_kind(settings, orderby_pos);
194+
195+
if (kind == ORDERBY_SPARSE_MINMAX)
196+
{
197+
*lower_name = column_segment_min_name(orderby_pos);
198+
*upper_name = column_segment_max_name(orderby_pos);
199+
return;
200+
}
201+
202+
const char *col_name = ts_array_get_element_text(settings->fd.orderby, orderby_pos);
203+
const char *col_names[1] = { col_name };
204+
bool desc = ts_array_get_element_bool(settings->fd.orderby_desc, orderby_pos);
205+
206+
if (!desc)
207+
{
208+
*lower_name = compressed_column_metadata_name_v2("first", col_names, 1);
209+
*upper_name = compressed_column_metadata_name_v2("last", col_names, 1);
210+
}
211+
else
212+
{
213+
/* DESC sort puts the largest value at the first row of each batch. */
214+
*lower_name = compressed_column_metadata_name_v2("last", col_names, 1);
215+
*upper_name = compressed_column_metadata_name_v2("first", col_names, 1);
216+
}
217+
}
218+
219+
void
220+
orderby_sparse_metadata_attnos(const CompressionSettings *settings, Oid compressed_relid,
221+
int orderby_pos, AttrNumber *lower_attno, AttrNumber *upper_attno)
222+
{
223+
Assert(lower_attno != NULL && upper_attno != NULL);
224+
225+
char *lower_name;
226+
char *upper_name;
227+
orderby_sparse_metadata_names(settings, orderby_pos, &lower_name, &upper_name);
228+
*lower_attno = get_attnum(compressed_relid, lower_name);
229+
*upper_attno = get_attnum(compressed_relid, upper_name);
230+
}
231+
159232
/*
160233
* Get metadata name for a given column name and metadata type, format version 2.
161234
* We can't reference the attribute numbers, because they can change after
@@ -582,7 +655,11 @@ build_columndefs(CompressionSettings *settings, Oid src_reloid)
582655
errdetail("Could not identify a less-than operator for the type.")));
583656
}
584657

585-
/* segment_meta min and max columns */
658+
/*
659+
* Every orderby column gets minmax metadata so range predicates
660+
* on any orderby position can be pushed down as a filter on the
661+
* compressed scan.
662+
*/
586663
ColumnDef *def = makeColumnDef(column_segment_min_name(index),
587664
attr->atttypid,
588665
attr->atttypmod,
@@ -595,6 +672,29 @@ build_columndefs(CompressionSettings *settings, Oid src_reloid)
595672
attr->attcollation);
596673
def->storage = TYPSTORAGE_PLAIN;
597674
compressed_column_defs = lappend(compressed_column_defs, def);
675+
676+
/*
677+
* When firstlast is configured for the orderby column, also add
678+
* first/last metadata. These columns are a part of the compressed chunk
679+
* btree and let predicates on the leading orderby become index
680+
* conditions when the column is NOT NULL.
681+
*/
682+
bool add_firstlast = per_column_setting != NULL && composite_attr_lists != NULL &&
683+
per_column_setting->firstlast_obj_id != -1;
684+
if (add_firstlast)
685+
{
686+
ColumnDef *first_def =
687+
create_sparse_index_column_def(composite_attr_lists[per_column_setting
688+
->firstlast_obj_id],
689+
"first");
690+
compressed_column_defs = lappend(compressed_column_defs, first_def);
691+
692+
ColumnDef *last_def =
693+
create_sparse_index_column_def(composite_attr_lists[per_column_setting
694+
->firstlast_obj_id],
695+
"last");
696+
compressed_column_defs = lappend(compressed_column_defs, last_def);
697+
}
598698
}
599699
else if (per_column_setting != NULL && composite_attr_lists != NULL)
600700
{
@@ -604,12 +704,11 @@ build_columndefs(CompressionSettings *settings, Oid src_reloid)
604704
bool is_firstlast = per_column_setting->firstlast_obj_id != -1;
605705

606706
/*
607-
* We allow only one sparse index per column. Columns used in the ORDER BY
608-
* clause implicitly have a minmax index and adding a bloom filter on them is not
609-
* allowed.
707+
* We allow only one sparse index per column. Adding a bloom filter
708+
* on a column that already has a sparse index is not allowed.
610709
*
611-
* The parser is expected to enforce this constraint earlier, but we check again
612-
* here as a safeguard.
710+
* The parser is expected to enforce this constraint earlier, but we
711+
* check again here as a safeguard.
613712
*/
614713
Ensure((!is_bloom || !is_minmax),
615714
"Should not create bloom filter for minmax column \"%s\"",

tsl/src/compression/create.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@ int compressed_column_metadata_attno(const CompressionSettings *settings, Oid ch
3737
AttrNumber chunk_attno, Oid compressed_reloid,
3838
char const *metadata_type);
3939

40+
/*
41+
* Per-orderby-column sparse-index metadata dispatch.
42+
*
43+
* Each orderby has a pair of metadata columns that bound the orderby values
44+
* in each batch. Two shapes are supported:
45+
*
46+
* minmax - statistical extremes; direction-blind (lower=min, upper=max).
47+
* firstlast - first/last row values in sort order. Under ASC, lower=first
48+
* and upper=last; under DESC, the roles flip.
49+
*
50+
* Callers should ask for "lower"/"upper" boundaries instead of min/max
51+
* directly so the same code works across both shapes. orderby_pos is 1-based
52+
* to match settings->fd.orderby and the column_segment_*_name helpers.
53+
*/
54+
typedef enum OrderbySparseKind
55+
{
56+
ORDERBY_SPARSE_MINMAX,
57+
ORDERBY_SPARSE_FIRSTLAST,
58+
} OrderbySparseKind;
59+
60+
OrderbySparseKind orderby_sparse_kind(const CompressionSettings *settings, int orderby_pos);
61+
void orderby_sparse_metadata_names(const CompressionSettings *settings, int orderby_pos,
62+
char **lower_name, char **upper_name);
63+
void orderby_sparse_metadata_attnos(const CompressionSettings *settings, Oid compressed_relid,
64+
int orderby_pos, AttrNumber *lower_attno,
65+
AttrNumber *upper_attno);
66+
4067
void tsl_columnstore_setup(Hypertable *ht, WithClauseResult *with_clause_options);
4168

4269
void compression_settings_set_defaults(Hypertable *ht, CompressionSettings *settings,

0 commit comments

Comments
 (0)