Contents
What a sift_col index stores besides the columns
A sift_col index keeps every column in units of a few thousand rows. Beside the column data it can keep small extra structures, each of which makes one kind of query cheaper and costs some bytes, build time or both. Most are chosen by CREATE INDEX and REINDEX from the data (the column says auto in pg_class.reloptions, and a NOTICE names the choice); a few are opt-in because the gain does not pay for the growth. This page lists them by what they are for. Every entry has the same lines:
- What: the query it helps, in one sentence.
- Default: what it is when you set nothing, and why.
- Off: how to turn it off (a build or a setting).
- Measured: one absolute figure, with the benchmark case (
bench/cases.tsv), or the regression test it comes from. A figure is one backend, warm, on the rows named. - Message: the NOTICE or HINT of
CREATE INDEXthat names it (the id in docs/MESSAGES.md).
Every setting and option has its full row in docs/OPTIONS.md; the byte layout of each structure, by column format minor, is in docs/FORMATS.md. ALTER INDEX .. SET (option = ..) applies to the segments written afterwards; a REINDEX (or the next merge) rewrites the older ones, and the older ones keep answering meanwhile.
Point lookups
An equality, an IN list or a short range on a column of many distinct values.
point_columns(deprecated in 4.3, removed in 4.4).- What: the directory of the rows of a key, which an equality,
INlist or short range reads to find the units that hold the rows. A build now lists the columns it names in the directory of heap addresses (point_tid_columns), which reads no unit; the segments already written are read as before. - Why: the directory of addresses is within 3% of its size and reads fewer instructions in every shape measured (integer equality 0.119 to 0.020 ms on 200,000 rows with the rows form, 0.013 ms with addresses; a b-tree 0.006 ms), case
point_in_100. - Off:
ALTER INDEX .. RESET (point_columns), and name the columns inpoint_tid_columns.CREATE INDEXandREINDEXsay so once (point-columns-deprecated). - Message:
point-columns-deprecated,point-tid-key-cut.
- What: the directory of the rows of a key, which an equality,
point_tid_columns(opt-in; chosen by the default formula, below).- What: the same lookup, with the directory listing the heap addresses of the rows, so no unit is read.
- Default: off. An equality on a unique id reads 1.67 M instructions with the key filter and 0.66 M with the addresses (2.51x), for +57.6% of the index and +3.6% of the build. Its segments are column format minor 37 or more, which only 3.7 and later read.
- Off:
ALTER INDEX .. RESET (point_tid_columns);sift.col_format_minor <= 14writes no addresses, and the directory has the rows form. - Measured: integer equality 0.013 ms, flat from 26,700 to 1,000,000 rows (a b-tree 0.006 ms); a 100-id
IN45.7 to 0.25 ms on 1,000,000 rows; casepoint_in_100. - Message:
point-directory-auto,point-tid-explicit,point-tid-key-cut.
- Point directory by the default formula (automatic).
- What: when neither option is set, the build writes
point_tid_columnsfor an integer column of nearly unique values that does not follow the order of the table, when the directory gains more than it grows the index and the build. - Default: on, written as
point_tid_columns = 'auto: col': a lookup of 0.17 M instructions plus 5 a row becomes 0.2 M (9.5 times fewer), for 16.5 bytes a row more and a build 21% longer. - Off:
ALTER INDEX .. SET (point_tid_columns = 'off')andREINDEX. - Measured: an equality on a random unique int8 of a 7-column index, 400,000 rows: 1.9 M instructions without the directory, 0.2 M with it (a b-tree 0.2 M); test
col_point_auto. - Message:
point-directory-auto.
- What: when neither option is set, the build writes
- Point directory in the aggregate scan (automatic).
- What: the aggregate scan asks the directory for the rows of a short
INlist, an equality, a short range or the keys of an inner or semi lookup join, and reads only the units that hold them. - Default: on, costed: the planner prices the units the directory leaves, and keeps it off where it costs more than the zones. No format change, no growth.
- Off:
sift.enable_agg = offturns the aggregate scan off;RESET (point_columns)andRESET (point_tid_columns)with aREINDEXdrop the directory. - Measured:
point_keys_aggon 1,000,000 rows, 80.9 to 2.85 ms.
- What: the aggregate scan asks the directory for the rows of a short
Order and top-N
ORDER BY .. LIMIT n, with or without a filter, an offset or a range.
order_columns(deprecated in 4.3, removed in 4.4).- What: each unit kept a sorted run of the column, so
ORDER BY col LIMIT nread the first rows of each unit only. It is accepted and ignored: a build writes no run, and the order arrays (order_array_columns) answer a top-N, a range and anOFFSETover any number of units. The runs already written, and their heads, are read as before. - Why: the runs hold the same bytes as the order array of the column (2.08 against 2.27 bytes a row for a timestamp at 5,000,000 rows) and read 4 to 6 times the instructions beyond a top-N of 32 (
OFFSET4.42 M, a range 16.0 M, anINlist of two groups 47.8 M against 0.4 to 1.4 M with the array); a top-N of 32 or fewer reads 0.38 M with the heads and 0.46 M with the array. - Off:
ALTER INDEX .. RESET (order_columns).CREATE INDEXandREINDEXsay so once (order-columns-deprecated).
- What: each unit kept a sorted run of the column, so
sift.topn_runs(setting, on).- What: lets the top-N scan read the stored runs.
- Default: on; it reads the runs that segments written before 4.3 hold, and costs nothing without them.
- Off:
SET sift.topn_runs = off(session, role, database orpostgresql.conf): whole units are read, the stored runs stay. - Measured:
ORDER BY k LIMIT 100withorder_columns, 1,000,000 rows, 3.5 ms off and 1.4 ms on; casetopn_sorted_runs.
- Heads of the runs (written with the deprecated
order_columns, column format minor 18).- What: the first and last keys of a run (up to 32 at each end, with the heap addresses of their rows) answer
ORDER BY .. LIMIT n(n at most 32, one sort key, no condition, join or aggregate) without reading the unit. - Default: on with
order_columns, which a build ignores since 4.3: 2 to 3 times faster on an unfilteredLIMITup to 32, for 0.6% to 2.4% of the index and write cost inside noise. - Off:
sift.col_format_minor = 14writes runs without heads, orsift.topn_runs = off/sift.enable_topn = off. - Measured:
ORDER BY k LIMIT 100.128 to 0.051 ms on 800,000 rows (a b-tree 0.010 ms).
- What: the first and last keys of a run (up to 32 at each end, with the heap addresses of their rows) answer
- Top-N run probes (automatic).
- What: a top-N over a table with sorted runs (
order_columnsof 4.2 and older) reads the sort key of a unit’s rows only for the rows it keeps, and sizes its candidate buffers by the unit. - Default: on; the same rows and units are read with less work a scan. No format change, no growth.
- Off:
SET sift.topn_runs = off, orSET sift.enable_topn = off. - Measured: 40 scans
ORDER BYa key columnLIMIT 10..49on 1,000,000 orders, 48 to 24.8 ms; casetopn_run_probes.
- What: a top-N over a table with sorted runs (
order_array_columns(defaultauto, column format minor 38).- What: the sorted keys and heap addresses of a whole segment answer a range of a column whose values are scattered over the heap (up to 16,384 rows) without reading a unit, and
ORDER BY col LIMIT n(also with an offset) merges the arrays of the segments and fetches n rows of the heap, whatever the number of units. - Default: on where it pays: 100,000 rows or more, 1,000 or more distinct values, correlation under 0.99, not a column that cuts the units. A narrow range reads 18.4 M instructions without it and 2.4 M with it, for 21% more index and 22% more build.
- Off:
ALTER INDEX .. SET (order_array_columns = 'off')andREINDEX;RESETdecides again at the nextREINDEX. - Measured: a range of 790 of 800,000 random int4 rows, 18.4 M to 2.4 M instructions; test
col_range_order. - Message:
order-array-auto,order-array-listed.
- What: the sorted keys and heap addresses of a whole segment answer a range of a column whose values are scattered over the heap (up to 16,384 rows) without reading a unit, and
order_array_group(defaultauto, column format minor 38).- What: a second order inside each value of a group column answers
WHERE group = x ORDER BY col LIMIT nandmin/max(col) WHERE group = xfrom n entries of the group in each segment, the shape a b-tree on(group, col)serves. An index whose first two columns are the group (NOT NULL) and the column, with whole chunks (order_layout = 'whole'), also givesORDER BY group, col(both ascending or both descending) andWHERE group = x ORDER BY colas an index-only scan that reads the groups one after the other, where a table scan and a Sort were: the scan is priced for the open of each group, so a table of about 4 rows a group or fewer keeps the sort. - Default: on for a date, timestamp or integer array that passes the formula: a group of 400 values costs +13.2% of the index and +5.7% of the build. No column with fewer than 100 values or fewer than 3 rows a value gets one.
- Off:
order_array_group = 'off'andREINDEX; the plain order answers meanwhile. - Measured:
WHERE grp = x ORDER BY ts DESC LIMIT 10, 500,000 rows, 3.34 M to 0.42 M instructions (a b-tree on(grp, ts)0.27 M); testcol_gorder_autogroup. - Message:
order-array-auto.
- What: a second order inside each value of a group column answers
order_layout(defaultauto, column format minor 41).- What: the sorted array of a column keeps each chunk of 512 entries whole (the key of every row) or compact (the heap address and the key to within a bucket; the exact key is read from the row where a scan needs it).
- Default:
auto: a chunk is compact where that is at least a fifth smaller (3.6 bytes a row for a microsecond timestamp against 6.5), so the array is never larger than the whole order; the reads are as fast. - Off:
ALTER INDEX .. SET (order_layout = 'whole')andREINDEX;RESETreturns to auto. - Measured:
OFFSET 100,000of a timestamp, 1,000,000 rows: whole 3.08 ms (37.0 M instructions), compact 3.27 ms (36.3 M), a b-tree 4.10 ms; the index 15.02 MB whole, 12.12 MB compact; caseorder_layout_offset. - Message:
order-layout.
Counts and keys
count(DISTINCT x), count(*), sums and minima by a key, and an equality that most units cannot hold.
key_columns(defaultauto, column format minors 10 and 15).- What: each unit keeps the exact keys, a bloom filter or a bitmap of a column:
count(DISTINCT col)over whole units reads no key chunk, and an equality orINskips the units that lack the key. - Default: on for the foreign keys and
*_idcolumns whose lists fit within 3% of a unit: +1.1% to +1.4% of the index, andcount(DISTINCT)of a foreign key 2.0 to 8.6 times faster. - Off:
SET (key_columns = 'off')thenREINDEX;RESETreturns to auto. - Measured:
count(DISTINCT fk)over whole units 16.1 to 1.11 ms on 1,000,000 rows (123 units); casekey_presence_distinct. - Message:
key-lists,keys-no-summary,keys-no-list.
- What: each unit keeps the exact keys, a bloom filter or a bitmap of a column:
key_table(default on, column format minor 22).- What: a segment keeps one record for each distinct key of a
key_columnsleaf with more than 256 values in a unit, with the rows, sums, minima and maxima:count,sum,min,maxWHERE key = x, anIN, a range orGROUP BYthe key read one record of each segment, not its units. - Default: on; a table is written only when all of them take at most 3% of the segment, so the growth is under 3% by construction.
- Off:
ALTER INDEX .. SET (key_table = off), thenREINDEXor the next merge. - Measured:
count, sum, min, max WHERE key = xover 900,000 rows, 5.20 to 0.107 ms (300 keys); casekey_table_point.
- What: a segment keeps one record for each distinct key of a
count_columns(default on, column format minor 24).- What: the dictionary of a text column also holds the rows of each string, so
count(*)of an equality, a prefix (LIKE 'abc%') or a range in the C collation is read from two places of the dictionary. - Default: on, because it pays: 38 times fewer instructions for 0.17% more index and 0.44% more write cost.
- Off:
ALTER INDEX .. SET (count_columns = 'off'), thenREINDEXor the next merge. - Measured:
count(*) WHERE txt LIKE 'k2a%'(4,012 of 1,000,000 rows) 5.3 to 0.14 ms; testcol_dict_count.
- What: the dictionary of a text column also holds the rows of each string, so
hll_columns(opt-in, column format minor 20).- What: each unit keeps a HyperLogLog sketch of the column, so
sift.approx_count_distinct(col)merges the sketches of the units instead of hashing every row. - Default: off: +23% of the index for four columns of a million rows and a build +70%.
count(DISTINCT)stays exact whatever this is. - Off:
ALTER INDEX .. RESET (hll_columns);sift.col_format_minor <= 14writes none. - Measured: 5.4 ms against 63 ms for the same aggregate by the normal plan, 1,600,000 rows; case
approx_distinct_sketch.
- What: each unit keeps a HyperLogLog sketch of the column, so
Cutting the units by a key
Units that follow a declared key, so a lookup or a range of the key reads the units of its range.
key_cut(defaultauto, column format minor 17).- What: the units are cut by the range of a key column, not by the order of the heap, so an equality, a list or a range of the key reads the few units of its range.
- Default: on where an equality on the key would read at least 8 times the units its rows fill and at least 64 units, and the index is predicted from a sample to grow by at most 3%; a build 41% longer and a promotion 21% longer pay for it.
- Off:
ALTER INDEX .. SET (key_cut = 'off')andREINDEX(orCREATE INDEX .. WITH (key_cut = 'off')). - Measured:
count/sumof one key 5.06 to 0.35 ms on 1,500,000 rows, 419 units; casekeycut_customer_agg. - Message:
key-cut-chosen,key-cut-advised,key-order.
key_cut_rows(deprecated in 4.3, removed in 4.4).- What: the rows a key-cut unit aims at. It is accepted and ignored: the units aim at the rows of an average superblock, which keeps the number of units and the planner’s per-unit costs.
- Why: at 100,000 rows 256 rows is +69.0% of the index and 4,096 is -9.4%, with no query gain; case
keycut_customer_agg. - Off:
RESET (key_cut_rows).CREATE INDEXandREINDEXsay so once (key-cut-rows-deprecated).
- Second key of the cut (automatic).
- What: when the cut is chosen by default and the index has a date or timestamp leaf, the rows of a key are sorted by that leaf too, so a window of dates on the key skips the units outside it.
- Default: on; the index shrinks by 20.9% and the build pays 11% more.
- Off:
ALTER INDEX .. SET (key_cut = 'key')andREINDEX. - Measured: 3,000,000 rows, 6,000 keys: the aggregate over a 90-day window 536.3 to 498.2 M instructions; test
col_key_cut_second.
- Range and network lanes (automatic).
- What: a range column or an
inet/cidrcolumn can cut the units by its first lane, so@>,<@,&&,<<,>>,-|-of a range and<<=,>>=of a network read the few units whose lane meets the constant. - Default: on: 42 times fewer instructions, and the index shrinks by 6.0% for a build 59% longer; a niche class of columns.
- Off:
ALTER INDEX .. SET (key_cut = 'off')andREINDEX. - Measured:
r @> xon 3,000,000 random int4range rows, 27.9 M instructions without the cut and 0.67 M with it; casecontain_key_cut.
- What: a range column or an
- Cut by a date (automatic).
- What: for a date or timestamp column the gain of the cut against its growth and build decides, so a window of dates, an equality on a date and a group by day read the units of their range on a table of any size.
- Default: on: 34 times fewer instructions for a predicted +44.5% of the index and 67% more build (3,000,000 rows of 56 bytes).
- Off:
ALTER INDEX .. SET (key_cut = 'off')andREINDEX. - Measured:
count(*)of a day on 3,000,000 rows scattered over 1,000 days, 25.4 M instructions without the cut and 0.7 M with it; testcol_key_cut_auto.
sift.vis_page_cache(setting, on).- What: the rows of a heap page that a scan reads twice (the units of a key-cut index list rows of the same page) are checked for visibility once.
- Default: on: about 48 bytes for each heap page visited (at most 1,048,576 pages) and nothing written; no gain at 100,000 rows.
- Off:
SET sift.vis_page_cache = off; the page is checked each time. - Measured: a key-cut range query on 100,000 rows, 0.20 ms off and 0.23 ms on (within noise).
Dictionaries and text
Text and other columns stored as a dictionary of distinct values.
dict_lookup(defaultauto, column format minor 14).- What: an equality,
INor prefix test reads a dictionary of 1,024 strings or more in place, from blocks of 64 strings, without parsing it whole. - Default:
auto: the order of the strings is 4 bytes a string (+10% to +70% of a one-column index), so onlypoint_columns,key_columnsandreturn_columnsget it. - Off:
ALTER INDEX .. SET (dict_lookup = off)stores no order;sift.col_format_minor <= 13writes neither the blocks nor the order. - Measured: first query of a new backend, 300,000 rows: uuid equality 12.1 to 0.43 ms, text equality 29.3 to 0.54 ms; case
dict_equality.
- What: an equality,
sift.col_dict_shared_cache_kb(setting, 64 MB).- What: a new backend, a pooled connection or a parallel worker reads the parsed dictionary that another backend parsed, in place, instead of parsing it again.
- Default: 64 MB of shared memory, allocated by the first backend that keeps a dictionary of 128 kB or more; it holds the dictionaries of about 1,000,000 distinct values.
- Off:
SET sift.col_dict_shared_cache_kb = 0(superuser;postgresql.conffor all backends). - Measured: first statement of a new backend,
ip = x/51over 1,000,000 rows, 286 M instructions to 2.5 M once another backend has kept the dictionary; testcol_dict_shared_size.
- Trigram signature of a dictionary (automatic, column format minor 29).
- What: a substring test (
LIKE '%part%') of 3 bytes or more on a text column whose dictionary no longer fits the cache reads only the blocks of 64 strings whose trigram filter holds the part. - Default: on for a dictionary that does not fit the cache: +0.33% of the index on 6 columns, +9.4% for the URL column alone.
- Off:
SET sift.col_format_minor = 14writes leaves without the signature. - Measured:
LIKE '%part%'over 3,000,000 URLs, a part in 42 rows, 1,336 to 944 M instructions; testcol_text_tsig.
- What: a substring test (
- LZ4 for text (automatic).
- What: strings stored inline (over 255 bytes, or more distinct text than the dictionary holds) are wrapped with LZ4 instead of zstd where it pays: a substring, equality or
GROUP BYreads 1.1 to 3.4 times fewer instructions a row. - Default: decided per chunk, from the cost of unwrapping against the growth: sentences take LZ4 (+23.7% of the index on the measured table), titles keep what they have.
- Off:
compression = 'auto; col=zstd'andREINDEX(the NOTICE gives the line);sift.col_format_minor = 14writes no LZ4 dictionary blocks. - Measured: substring of a sentence 2,371 to 683 instructions a row, 3,000,000 rows (a heap scan 4,286); test
col_text_scanwrap. - Message:
text-compression.
- What: strings stored inline (over 255 bytes, or more distinct text than the dictionary holds) are wrapped with LZ4 instead of zstd where it pays: a substring, equality or
sift_table
A sift_table writes the same structures when it promotes and vacuums, not in a statement that could report them, so there is no NOTICE; sift.table_segments(table) gives the bytes and format of its segments.
sift.table_dict_lookup(setting, on).- What: a test for a string reads a dictionary of 1,024 strings or more in place (the same blocks as
dict_lookup). - Default: automatic where the orders of a segment (4 bytes a string) take at most 25% of it: 20.9 times fewer instructions on the first statement of a backend, for 22.9% more segment and 3.0% more build.
- Off:
SET sift.table_dict_lookup = off; new segments return to the compact form, older ones keep the order until a merge orVACUUM FULL. - Measured: first statement of a new backend on 300,000 distinct 33-character strings: prefix test 154.6 M to 7.4 M instructions, equality 137.6 M to 3.4 M; tests
tbl_topn_orderandtbl_fetch_dict.
- What: a test for a string reads a dictionary of 1,024 strings or more in place (the same blocks as
sift.table_key_table(setting, on).- What: the key table of
key_table, for the foreign key and*_idcolumns of asift_table. - Default: on, within 3% of a segment by construction.
- Off:
SET sift.table_key_table = off, thenVACUUM FULLor the next merge. - Measured:
count, sum, min, max WHERE key = xover 900,000 rows, 0.577 to 0.068 ms (300 keys); testtbl_key_table.
- What: the key table of
- Key lists of a table (automatic, column format minor 15).
- What:
count(DISTINCT col)of anint2,int4,int8ordateforeign key or*_idcolumn that is not unique reads the key list or bitmap of each clean, all-visible unit, not its rows. - Default: on: 40 times fewer instructions on
count(DISTINCT fk), growth under 0.6%, write cost 0.0% to 1.6% of the writer’s instructions. - Off:
SET sift.col_format_minor = 14, orSET sift.agg_preagg = off. - Measured:
count(DISTINCT fk)of 500,000 rows, 2.10 to 0.053 ms; testtbl_lists_runs.
- What:
- Run heads of a table (automatic, column format minor 18).
- What:
ORDER BY ts LIMIT n(n at most 32) of a timestamp column the rows follow reads the first and last keys of each clean, all-visible unit, not the unit. - Default: on: 3.7 times fewer instructions at
LIMIT 10 DESC, growth 0.55% to 1.42%. - Off:
SET sift.col_format_minor = 12, orSET sift.topn_runs = off, orSET sift.enable_topn = off. - Measured:
ORDER BY ts DESC LIMIT 10of 500,000 rows, 0.140 to 0.038 ms; testtbl_lists_runs.
- What:
- Batched appends (automatic).
- What: the rows a transaction inserts into the index after its first go to the write buffer a page at a time, one WAL record for a run of rows instead of one for each row.
- Default: on: a standby replays one record a page, not one a row.
- Off: no setting; a one-row transaction, an
ON CONFLICTstatement and a row of more than a page are written at once. - Measured:
COPYof 50,000 rows into a table with a 6-column index: WAL 245 to 186 bytes a row, standby replay 6,205 to 3,390 instructions a row; testcol_wbuf_batch.
Column format
sift.col_format_minor is the newest column format the segments written use. The default writes every structure above; a lower value keeps the segments readable by an older library (6 for a 3.2 library, 4 for 3.1) and writes the structures of the later minors nowhere. The minors, the library that first reads each and the one that refuses it are in docs/FORMATS.md section 4; docs/UPGRADING.md covers the change of a server.