Contents
pg_sift 4.2.0: the aggregate scan answers more of what an application runs (jsonb presence, containment and key tests, HAVING with sub-selects, FETCH FIRST .. WITH TIES, array and range tests) and is chosen more often, VACUUM gives the free blocks of a sift_col index back online, and ten changes are worth reading before the update; extension SQL 4.2 (update chain 4.0 -> 4.0.3 -> 4.1 -> 4.2): install the files, run the preflight scripts/upgrade-check.sh, restart the server (a standby too), then run ALTER EXTENSION pg_sift UPDATE in each database.
Highlights
The figures are those CHANGELOG.md states for this release, with their cases and tests, as the normal plan (PostgreSQL’s plan without the scan) then the scan, or as before then after.
- Faster:
jsonbtests on the index (NOT,NULL, containment and key forms).NOT (doc @> '{"b": "x"}'),(doc @> ..) IS FALSE,NOT (doc ? 'k'),(a AND b) IS FALSEandFILTER (WHERE NOT (a OR b))of containments, the same in aCASE, anddoc IS [NOT] NULLof ajsonbcolumn that may be NULL are answered by the index, where they ran on the normal plan. The index keeps a boolean column for eachjsonbcolumn with paths that may be NULL (written byCREATE INDEXandREINDEX; an index built before it has none and keeps the normal plan for those forms untilREINDEX). 150,000 documents, 1% to 50% of them without a declared member:NOTof a containment 85 M to 121 M cycles down to 7 M to 10 M, the count ofdoc IS NULL35 M to 1.9 M; it costs 2.0% of the index of 1,000,000 rows after a merge and 3.2% of its build (insert +0.4%, promotion +4.6%, merge +5.0%). A containment is also answered asIS NOT TRUE, in aCASEpivot, in aFILTERand over anAND/ORwith other conditions (M cycles of one backend:WHERE (doc @> ..) IS NOT TRUE69.9 to 3.5, aCASEpivot by group 138.3 to 8.9,FILTER (WHERE (doc @> ..) IS NOT TRUE)133.1 to 8.0).doc ? 'k',doc ?| array[..]anddoc ?& array[..]are answered in aFILTER, anORor aCASEtoo, and the value of a key read beside its test (sum((doc ->> 'k')::float8) FILTER (WHERE doc ? 'k')) as well:attrs ? 'a' AND dim_a_id < 500over 300,000 documents of 400 bytes takes 15.4 M instructions and 1.1 ms where the normal plan takes 250 M instructions and 14,252 blocks. A segment written before 4.2 does not say whether a document that is a string was a missing key, so a table of old segments stays on the normal plan fordoc ? 'k'untilVACUUMflags them (about 140 ms for 600,000 documents of 400 bytes). A containment with a boolean, a JSON null or a string that reads as a boolean or a number as a member ({"flag": true},{"k": null}) is answered too, and a containment or?test that names a path the index does not declare keeps the normal plan under the new reasonjsonb_path_not_declared, which names the path. - Faster: the planner chooses the aggregate scan more often. For a grouped aggregate of one table (no lookup join, no search, no top-N) whose normal plan is a sequential scan and a hash of the groups, the scan’s own work per row is now counted in the units of the plan it is compared with (PostgreSQL’s estimate of that plan runs at about 210 cost units a millisecond, the scan’s price at 833), so a scan that was up to 4 times faster is no longer refused as
costwhen its rows are not few:max(ends - starts)by tenant over 150,000 rows 18.6 to 7.1, over 18,750 rows 2.7 to 1.4,COALESCEmeasures over 20,000 events 3.4 to 2.1,NOT lower_inc(r)0.25 to 0.04 (milliseconds of the statement); a statement the scan does not win stays on the normal plan. A grouped aggregate with lookup joins (facts LEFT JOIN links ON links.parent_id = facts.dim_id GROUP BY ..) takes the scan more often when the normal plan reads more than 2,000 cost units from the scanned table: on 250,000 rows of asift_tablein a tuned profile (seq_page_cost100, 8 workers, 4 GBwork_mem) 193.6 to 107.9 ms and 361,019 to 851 buffers, and no statement of the 132 plan shapes in 8 settings gets slower. The scan prices an expression that PostgreSQL evaluates (max(ends - starts)) at 21 ns for each tuple read and 69 ns for each distinct combination of its columns, so its estimate is about 3 times higher on 150,000 rows of 10 distinct pairs (291 to 907 units for 3.1 ms). The price of the normal plan that probes the keys of a lookup is multiplied by the whole skew of the key column, where it was multiplied by its square root: a statement of 12 hot keys of a column of skew 7 was refused withcostand took 282 million cycles, 42 ms, with the normal plan against 8.2 million, 3.4 ms, with the scan. ANOT EXISTS(orEXISTS, or a scalar sub-select) over a table with a condition that its index does not take is planned by what the child costs for every row the scan reads, where a probe priced at a hundredth of what it takes was chosen and ran several times longer than the normal plan. The heap fetch of a top-N is priced at a page of the b-tree plan with a LIMIT, so a list of rows of one entity (ORDER BY .. LIMIT noverWHERE entity = k) keeps the b-tree plan on (entity, key) where that plan is faster: the scan took 8 of 18 such lists measured on 600,000 rows and 11 of 12 on 200,000, at 1.7 to 2.4 times the instructions of the b-tree plan, and all 30 are the b-tree’s now (1.09 million instructions and 0.09 ms against 1.88 million and 0.16 to 0.25 ms of the scan); the lists the b-tree cannot give in order keep the scan, which takes 4 to 80 times fewer instructions. The direct probe of a correlatedLIMIT 1child is priced at 55 operators where it was 800, so the probes win up to a fifth of the rows of the table as keys: of 60 choices between them 21 were wrong by instructions (1,470 million lost) and none are now. - Faster:
HAVING column IN (SELECT ..)andEXISTS (SELECT ..), sub-selects of the groups, joins to small tables and lookups. In a plainGROUP BYsuch aHAVINGcondition is always answered as a filter of the rows, as a semi join the scan runs, where PostgreSQL keeps it above the aggregate and the sub-select ran for each group (GROUP BY kind_id HAVING kind_id IN (SELECT id FROM kinds WHERE enabled): 300,000 rows, 1,000 keys, 100 of them in the list, 40.1 to 0.58 ms and 342 to 5.7 M instructions); the rows form was the cheaper in all 40 cells measured, at 0.09 to 0.78 of the cycles. A sub-select in the select list or theHAVINGthat reads the group keys or the aggregates is evaluated by the scan once for each group (300,000 rows and 1,000 groups: 36.8 to 1.85 ms and 350 to 23.4 M instructions).NOT IN,NOT EXISTS, an expression or a cast of the key, a correlatedIN, grouping sets and aHAVINGwithoutGROUP BYkeep theirHAVING. A table left-joined to a join of two small tables (FROM kinds k CROSS JOIN regions r LEFT JOIN events e ON ..: every kind and region with its count, zero for the pairs of no event) runs in the scan forcount,sumof integers and numerics,minandmax, andcount(e)andcount(e.*)of the whole row of the scanned table are answered ascount(*). The table of a lookup join that has its own bounds (dim_id IN (SELECT id FROM dims WHERE id <= 2000)) is read by an index scan with the bounds as its keys: a lookup table of 200,000 rows read forid <= 2000loads in 0.24 ms for 2,000 rows read (was 10 ms for the whole table), and a grouped statement over 300,000 rows that keeps those ids runs in 16.8 M cycles (was 39.1 M). The direct probe of a correlated child (LEFT JOIN LATERAL (.. LIMIT 1)) over a uuid, a date, a timestamp or a timestamptz key compares it in the probe itself: 40,000 probes into a lookup table of 300,000 rows take 585 M instructions for a uuid key where they took 756 M, 247 M for a date key where they took 347 M and 261 M for a timestamptz key where they took 360 M; over a key of several columns 452 M where 549 M. - Faster: top-N, ties and ranks. A correlated
min()ormax()sub-select of a table ((SELECT max(ts) FROM events WHERE entity = e.id)in a select list or a condition) is the first row of its entity in the order of the column, where it was a pass over the rows of the entity for each row of the query around it, or the table grouped and joined: it is planned a second time asORDER BY ts DESC LIMIT 1over the non-NULL values, and the planner keeps it where it runs in the scan and costs less; the rows are those of the normal plan, NULLs, empty entities and NULL keys included.FETCH FIRST n ROWS WITH TIESby integer, date or timestamp columns of the index is a top-N of the scan, which gives thenbest rows and every row that ties with then-th:ORDER BY dim_a_id DESC FETCH FIRST 10 ROWS WITH TIESover 300,000 rows takes 10.3 M instructions and 0.94 ms where the normal plan takes 387 M and 44.2 ms.rank() OVER (PARTITION BY k ORDER BY o) <= Nanddense_rank() ..(N up to 1,000, N = 1 included) keep in the scan the rows of rank <= N of each group, andSELECT .. FROM (SELECT .., row_number() OVER (PARTITION BY k ORDER BY o) rn FROM t) s WHERE rn <= N(N > 1, and up to 1,000 rows of each group) keeps only the first N rows of each group as the rows pass, where the plan sorted every row; the rows are those of the window. The rows of a top-N per group whose first sort key is a plain column are dropped on that key alone once they come after the last kept one of their group. - Faster: array tests and
CREATE INDEXon array columns.NOT (tags && ARRAY[..]),NOT (tags @> ARRAY[..])andNOT (tags <@ ARRAY[..])over an array column the index holds run on the index (21 of 30 statements of the matrix of negations ran on the index before, all 30 now), with the same rows as the normal plan over NULL and empty arrays, NULL elements, duplicates and multidimensional arrays.CREATE INDEXon a table with an array column (text,varchar,int2,int4orint8) whose arrays repeat keeps the elements beside the whole arrays, where the build kept the arrays alone and@>,&&,= ANYandunnestcost 150 to 250 instructions a row: on 300,000 rows of two-element integer arrays (441 different), cycles of the statement with the arrays alone, with the elements, and with the normal plan:tags && ARRAY[3, 7]24.1 M (6.8 ms) to 5.3 M (2.5 ms), normal plan 19.4 M (9.8 ms);tags @> ARRAY[5]23.3 M (7.4 ms) to 3.7 M (2.0 ms), normal plan 11.6 M (6.0 ms);7 = ANY (tags)23.8 M (6.7 ms) to 4.0 M (2.2 ms), normal plan 67.3 M (18.6 ms);unnest(tags)grouped 12.6 M (3.7 ms) to 1.5 M (0.8 ms), normal plan 128.5 M (49.9 ms). The index grows by 5.0% (5.57 to 5.85 MB, the build 15% longer). An index built earlier keeps its layout:ALTER INDEX i SET (paths = '+tags[] int4')andREINDEX INDEX iadd the elements (0.31 s for 300,000 rows). A test of the elements of an array column that the index holds as whole arrays only (paths = 'auto:') is now priced for each row it reads, 170 instructions a row: a containment of one element over 300,000 rows of repeating pairs took 24.2 M cycles on the scan against 13.6 M on the GIN bitmap of the normal plan and now keeps the normal plan (sift.explain_why:cost). - Faster: tests of ranges, wide values, wrapped columns and
INlists.lower(r)andupper(r)of a range column as an aggregate argument, a group key or a condition are read from the two bounds the index keeps for the containment operators, where the image of each distinct range was decoded (75,000 spans, M cycles:min(lower(r)), max(upper(r))3.7 (was 31.3), a month oflower(r)5.0 (was 97.6), aWHEREonlower(r)1.2 (was 26.6)).isempty(r),lower_inf(r),upper_inf(r),lower_inc(r)andupper_inc(r)and theirNOTare tests of the index too, and a test of a range, a network or a wide value (numericof more than 18 digits,money,interval) runs there as a branch of anOR, in aFILTER, in aCASEand asIS NOT TRUE(150,000 rows, M cycles before and after:count(*) FILTER (WHERE isempty(r))66.4 and 2.8,FILTER (WHERE n > ..)of anumeric(30,4)97.6 and 3.8). A condition that wraps a column incoalesce(col, c),nullif(col, c),col IS [NOT] DISTINCT FROM kor an integer widening cast (int4_col::bigint = k) is run as tests of the column, where it was an expression evaluated for every row. AnINlist tested against the key summaries of the units is cut to the keys of each unit’s range (dim_a_id IN (75 keys)over 300,000 rows: 3.4 M instructions and 0.33 ms where it took 5.2 M and 0.50 ms). - Faster: percentiles,
mode(), variance and correlation,CASEaggregates and grouping. The values of an ordered set that outgrowwork_memgo to sorted runs in a temporary file, where every value went through a tuplesort: the median and the 95th percentile of 300,000amount::float8in 40 groups take 188 M instructions and 32 ms where they took 1,205 M and 86 ms,mode()229 M and 33 ms where it took 1,231 M and 106 ms. The values of apercentile_contorpercentile_discthat are nearly all distinct go to an array of pairs (200,000 distinct float8 values: 49 M instructions and 5.1 ms where it took 805 M and 76.5 ms),mode() WITHIN GROUP (ORDER BY x)of a type with a hash operator counts each group in a hash (300,000 rows: 163 M instructions and 13.6 ms where it took 903 M and 79.6 ms), andpercentile_cont(array[0.25, 0.5, 0.75])of a constant array of fractions is one aggregate for each fraction on the same values.variance,var_pop,var_samp,stddev,stddev_popandstddev_sampof a float8, int4 or int2 column take 60 M instructions and 3.8 ms over 300,000 rows where they took 187 M and 11.2 ms, andcorr,covar_pop,covar_sampand theregr_*functions 137 M and 11.1 ms where they took 347 M and 20.4 ms, in the state of PostgreSQL’s own transition function.sum,avg,min,maxandcountoverCASE WHEN p THEN xrun as the aggregate of x with a filter on p (sift.agg_case_filter, on), also with an ELSE that is a value (avg(CASE WHEN status = 'paid' THEN qty ELSE 0 END): a group where the condition never holds is 0, not NULL, and the zeros count in an average, as in theCASE).array_agg(DISTINCT x ORDER BY x)andstring_agg(DISTINCT x, ',' ORDER BY x)sort the distinct values of a group once. A grouped statement numbers the groups of an integer key from the value by pages of ids while the values lie in a dense range (GROUP BY id / 2over 300,000 rows 433 M instructions and 37 ms where it took 475 M and 53 ms); a grouped statement whose keys are sparse takes one hash table that holds 16 bytes for each key, a little more memory than the two tables did, so a scan with a smallwork_memmay spill a stage earlier. - Smaller files and fewer reads of the heap: free blocks given back online by
VACUUM, and maintenance that reads pay for. AVACUUMof asift_colindex whoseAccessExclusiveLockstays refused for the whole wait (readers that overlap all the time, a writer) gives the free blocks back online, as it does with a standby that streams and without one: it seals and promotes a write buffer above the end the file can have, moves the segments above that end down and cuts the free end under anExclusiveLock(readers are never kept out, a writer waits for the cut only: longest statement 18 ms in a run of two overlapping readers and a writer); a file of 42.3 MB for 10.8 MB live was 10.9 MB after a fewVACUUMs with them running, 28.2 MB with the code before, and theVACUUMsays what it did once:index "t_idx": a lock is held on it (waited 5000 ms): 2628 blocks moved, 0 given back online. AVACUUMof an index that is due for a rewrite no longer leaves the free blocks to the automatic rewrite when it waits for its WAL budget (sift.col_auto_merge_wal_pct) or for snapshots, or when only the write buffer is due: a file of 27.0 MB for 11.4 MB live is 11.6 MB after oneVACUUM, and under two overlapping readers the file stayed at 2.0 times the live bytes for 56 s of writes and is 1.19 times on average after. The cut of asift_colindex or asift_tableasks again every 50 ms for up to 5 seconds without queueing, like the truncation of a heap, where it asked once and gave up on a table that is read all the time (a shorterlock_timeoutends the wait). The background worker runs each step in a transaction of its own, so the blocks a step freed are reusable by the next: after 40 rounds of churn with the cut off the file is 11,015 blocks for 7,408 live where it was 14,608 for 7,822. AVACUUMthat cannot cut says why, once, with the free blocks that stay in it (a lock another session holds, a standby that streams, or a snapshot that may still read the blocks):INFOforVACUUM (VERBOSE),LOGfor an autovacuum that logs, for exampleindex "u_idx": not truncating, a lock is held on it (waited 5000 ms): 1809 free blocks stay. The write buffer of asift_colindex is promoted once the backends that read it have paid for its promotion (18,600 instructions and 36 bytes of WAL a row, once): the first statement of a new backend over 200,000 rows with 25,000 of them in the buffer takes 3.79 million instructions and 2.3 ms for one entity where it took 96.4 million and 10.3 ms, 4.47 million for 75 entities where it took 98.2 million, and 19.2 million for the grouped sum where it took 120.3 million. A scan keeps what the tombstone log says of each heap block it asks about twice (a churned 600,000-row index with 59,768 entries in the log and noVACUUMsince: the grouped sum of the table 142.2 M instructions on a backend’s second statement where it took 207.7 M), and the merge of the small overlay segments of akey_cutindex counts the rows of the units a request scans and also serves an index that is not cut into partitions (one entity of 200,000 rows after 16 promotions: 0.93 million instructions and 110 buffers with 17 segments, 0.48 million and 31 buffers after the merge of the overlays). - Faster: BM25 (
sift) search over the write buffer. The write buffer of a BM25 index is opened in buffer format 6, whose full pages have a bloom of term hashes of 8,192 bits (2 probes, 4% false positives at 900 terms) where format 0 has 256, so a search for a term that none of the buffered documents has no longer reads every page of the buffer: 220,000 documents and a buffer of 2,000, a term in no document 1.96 to 1.23 M cycles, a buffer of 5,000 2.60 to 1.24 M cycles; a term of 5% of the documents is read as before. The documents that searches walk in the open buffer are added up, and once they cost half of its promotion the worker seals and promotes it (a 5,000-document buffer: a count of a term of 5% of the documents 3.3 M cycles with the buffer, 1.2 M after; promoted after 110 searches that walk all of it; a buffer of fewer than 2,000 documents is left forwrite_buffer_limit,VACUUMand the seal). A bitmap heap scan over asiftindex with a stemmer or any analyzer other than the default (body @@> 'qaaaf' AND category_id = 3with a btree oncategory_id) checks the match again only on the rows of a lossy page: 200,000 documents, the count of one word in one of 50 tenants 28.1 to 6.8 M cycles and 60.7 to 11.8 M instructions, and the correlated sub-select that counts it for each of 50 tenants 123 to 69 M cycles. The first statement of a backend that has not loaded the library (noshared_preload_libraries) gets the same recheck. A BM25 index written by 4.2 (once a buffer of format 6 is open) is not readable by 4.1 or older; see item 2 below. - Changed and added (the rest, compactly).
sift.workerslists thesift_tabletables the caller can read next to the indexes (kindissift_table;failuresis 0 and the columns that note one are NULL, as isdue) andsift.worker_statetakes asift_table; the update replaces the view with the same columns, so a view of yours that reads it still works.sift.help(slug)answers the slug of a refusal ofsift.explain_whyand of the noticeSiftAgg not usedas well (kindrefusal).sift.merge,sift.promoteandsift.verifytake asift_colindex too and runsift.col_merge,sift.col_promoteandsift.col_verifyon it.CREATE INDEXandALTER INDEX .. SETof asiftindex with the reloptionmutable_limitsayNOTICE: reloption "mutable_limit" is the older name of "write_buffer_limit"; nothing else changes.sift.agg_lookup_max_rows(deprecated since 3.2;sift.agg_lookup_max_kbreplaces it) warns once per session, at the first aggregate plan that its limit decides, withWARNING: sift.agg_lookup_max_rows is deprecated and will be removed in 5.0, and keeps limiting the rows as before.DROP INDEX CONCURRENTLYof asift_colindex that tracks its changes is refused before it changes anything, withALTER INDEX i SET (track_changes = 'off')first, orDROP INDEXwithoutCONCURRENTLY, in the hint. A correlated lookup (LEFT JOIN LATERAL .. LIMIT 1into a large mapping table) no longer asks the OS for the whole table and its index once its probes have read a sixteenth of it: the direct probe asks for the heap blocks of the entries it has found, together, and only while the statement is reading heap pages from the OS. The settings and reloptions removed or renamed are items 7 to 10 below. Added:sift.refusal_actions()returns what to change for each slug ofsift.refusal_slugs(), so thatsift.explain_why(q) w JOIN sift.refusal_actions() a USING (slug)lists it; options of onesift_tableas functions, because a table access method has no reloptions before PostgreSQL 20 (sift.table_set_option(table, name, value),sift.table_get_option,sift.table_reset_option,sift.table_options; the rows live insift.table_option_values, whichpg_dumpwrites with the database, and follow the table through a rename,TRUNCATE,VACUUM FULL, a restore,pg_upgradeand a standby; a grouped read of 20,000 rows costs 299,000 instructions with and without options, and a backend pays 328,000 instructions once, when it first opens the table of options);sift.explain_whyaccepts theEXECUTEof a preparedSELECT, the notice ofsift.explain_refusalscarries aHINTfor each of the 38 refusal slugs, and theLookup:line ofEXPLAIN ANALYZEsays what the direct probes of a correlated child did and how well the lookup tables are vacuumed (99 % of the lookup tables' pages all-visible, and aLookup Hint: VACUUM the lookup tables ..where a lookup table of 64 pages or more has under half of its pages all-visible).pg_squeezeand a killedpg_repackare tested and documented as they behave: the log of asift_colindex withtrack_changesis not complete after a squeeze, the reads check the heap and answer as it does, and the nextVACUUMre-baselines it.docs/TUNING.mdis new: the ten things worth touching, the diagnostic options and when to pick which of two pairs of options that look alike (point_tid_columnsandpoint_columns,order_array_columnsandorder_columns). - Fixed. On PostgreSQL 19,
corr,covar_pop,covar_sampand theregr_*aggregates of float8 columns failed in the aggregate scan withfloat8_regr_accum: expected 8-element float8 array, because the state of the transition function has 8 elements from 19; the scan runs PostgreSQL’s function for them on 19 (17 and 18 keep the kernel), andvar_*andstddev_*are unchanged.pg_repack --only-indexesof asift_colindex withtrack_changesfailed withDROP INDEX CONCURRENTLY must be first action in transactionand left itsindex_<oid>copy behind (4.0.0 to 4.1.3); it works now, and the log is not complete after the swap until the nextVACUUMre-baselines it.sift.index_advice,sift.index_choices,sift.key_cut_decision,sift.col_verify,sift.col_clustering,sift.col_tracking,sift.col_stamps,sift.col_rollup_refresh,sift.col_refresh,sift.col_unit_aggandsift.col_unit_histlock the table before the index, asDROP TABLEandALTER TABLEdo: a function that held the index and then asked for the table waited for aDROP TABLEthat waited for the index, and PostgreSQL aborted one of them withdeadlock detected; no result was wrong. Thesift_coloptionscalendar_columnsandrollup(rollup_leaves) take the words their docs promised:calendar_columns = 'auto'is the default like the empty value, androllup = 'auto'or'default'is the byte-budget rule.scripts/upgrade-check.sh --to 4.1(or any release before 4.2) listed a BM25 index whose write buffer is format 6 (index format 1.6) and asift_colindex with the presence leaf of ajsonbdocument as ok, though 4.1 and older refuse both; they are blockers now. The log of atrack_changesindex whose triggers do not vouch for it is made complete again by itself (item 5 below).sift.table_maintain()asks for the lock of the cut once and never waits for a reader (it waited up to the 5 seconds of theVACUUMretry, or itsstatement_timeout, where its contract is to take only what is free now;VACUUMkeeps the retry). A worker that looked the table of an index up before the transaction that created the index committed locked the index without the table, the reverse of the order ofDROP TABLE; a server built with assertions stopped the worker, and it locks the table first now.GROUP BY k % 3 HAVING k % 3 IN (SELECT ..)on PostgreSQL 18 and 19 moved the condition to the rows because the grouped expression looked like a column; it keeps itsHAVINGas before 18, and avarcharkey cast totextdoes the same on 17. On PostgreSQL 18 aWHERE id IN (SELECT id FROM t ..)over the table itself by its unique key saidSiftAgg not used: outer_join, a label for an outer join the query does not have; it names the merge of the tables (the query keeps the normal plan as before). TheVACUUMfixes of the cut of the free end are in the space highlight above.
Who is affected and what to do
- Update (the steps, in this order): 4.2 is
default_version = '4.2'and the update chain is 4.0 -> 4.0.3 -> 4.1 -> 4.2, whichALTER EXTENSION pg_sift UPDATEruns in turn. (1) Install the files. (2) Run the preflightscripts/upgrade-check.sh(--allfor every database; it changes nothing;--to 4.1also lists what 4.1 could not read, for a standby or a node that stays on 4.1): it lists as blockers a stored setting of items 7, 8 and 9 below and, with--to 4.1, the indexes of items 1 and 2. (3) Restart the server (a standby too), because a backend that already loaded the old library keeps it until its connection ends. (4) RunALTER EXTENSION pg_sift UPDATEin each database. The update runspg_sift--4.1--4.2.sql, the first SQL of 4.2 (the options of asift_tableandsift.refusal_actions()); only items 7, 8 and 9 below ask for a step (remove a stored setting before the update, step 2), and the new library reads the indexes of 4.1 and older as they are. - Going back: docs/UPGRADING.md, “An older release”. Only items 1 and 2 leave files that an older library refuses, and both go back the same way: install the older release and
REINDEXthe index with it; there is no switch for either. - The ten changes worth reading (each says what changes, what an older library does with it, and the switch; docs/UPGRADING.md, “What 4.2 changes”, has the measurements and the queries):
- 1. The index keeps a presence bit for the
jsonbcolumns that may be NULL.CREATE INDEXandREINDEXof asift_colindex write one boolean leaf for eachjsonbcolumn that haspathsand noNOT NULLconstraint on the column of the table (an index expression counts as nullable). No option, no NOTICE, no column format minor changes; an index built before has no leaf, keeps the normal plan for those forms and is not wrong, andREINDEXwrites the leaf. An older library (3.2, 4.0 and 4.1, tried with 3.2.3, 4.0.5 and 4.1.4) refuses an index that has the leaf: a read fails withsift_col index "x" does not match its column layout(hintREINDEX the index.), a write withindex "x" was built with other column definitions, andsift.col_verifyreportsThe metapage does not describe this column layout.The way back isREINDEX INDEX xwith the older release; there is no switch.scripts/upgrade-check.sh --to 4.1lists the indexes with the leaf as blockers. - 2. The write buffer of a BM25 (
sift) index is format 6. The buffer that is open at the update keeps format 0 and is read as it is; the next one is format 6, and the first of them raises the metapage of the index to minor 1.6, which nothing lowers (an index that has taken no insert since the update is still format 0;sift_colindexes andsift_tabletables have no such buffer). An older library (4.1 and older) refuses a BM25 index of 1.6 on a read, asift.verifyand an insert:has on-disk format 1.6where that build reads up to 1.5, with the hint to install the release that wrote the index on every node that reads it, orREINDEXit; a standby or a node that stays on the older release is such a node. To go back, install the older release andREINDEXthe index there; there is no switch.scripts/upgrade-check.sh --to 4.1lists the index as a blocker. - 3. The aggregate scan is chosen more often for a grouped aggregate of one table with no lookup join, search or top-N whose normal plan is a sequential scan and a hash. It changes plans, not files: an older library plans a statement as it did,
REINDEXis not needed, and no switch separates this choice;SET sift.enable_agg = off(a session, a role or a database) plans the statement as PostgreSQL does without pg_sift, andsift.explain_why(query)says why a statement did or did not take the scan. - 4. One rule for
HAVING column IN (SELECT ..)andEXISTS (SELECT ..)of a plainGROUP BY, and sub-selects of the groups. The condition is a filter of the rows, and a sub-select in the select list or theHAVINGthat reads the group keys or the aggregates is evaluated by the scan; the rows are the same as with the scan off (46 statements). Both are changes of plans, not of files: an older library andREINDEXare not involved, andsift.enable_agg = offis the only switch. - 5. A
sift_colindex withtrack_changesrepairs its own log in more cases. The log of an index whose triggers do not vouch for it (afterpg_upgrade,pg_restore --disable-triggers,DISABLE TRIGGER ALLthenENABLE TRIGGER ALL,CREATE INDEX CONCURRENTLY,REINDEX CONCURRENTLY,DETACH PARTITION) was checked against the heap on every read untilREINDEXorALTER INDEX; now the nextVACUUM, the first read that checks the heap or the maintenance worker that a write asks for re-baselines it, with aShareUpdateExclusiveLockonly. Triggers enabled for the origin only (whatENABLE TRIGGER ALLleaves) are enabled for all sessions by the worker, which asks for the lock without waiting; triggers you disabled stay disabled and the reads keep checking the heap. No format change and nothing to run; answers are right before and after. An older library (tried with an earlier 4.1 release) reads and writes an index that 4.2 repaired without an error andsift.col_verifyis clean, but it does not take the log for complete, so its reads check the heap as before the repair.REINDEXis not needed; there is no switch for the repair, andALTER INDEX x SET (track_changes = 'off')drops the log and its triggers for an index that should not have them. - 6.
sift.refusal_actions()is a new function.sift.refusal_slugs()keeps its three columns, andpg_sift--4.1--4.2.sqldrops or replaces no function that a view, a default or a function of yours may read, soALTER EXTENSION pg_sift UPDATEdoes not fail for a database that has a view oversift.refusal_slugs(). No files, noREINDEX, no switch. - 7.
sift.eval_methodsno longer takeslatest_hint,snapshot_masks,unit_rewriteandskip_trigger_lookup(nor the-nameform);penalty_maintenanceis the only name left, andskip_trigger_lookupis now always on. With the library loaded, aSET,ALTER ROLE .. SET,ALTER DATABASE .. SETorALTER SYSTEM SETthat names one fails withinvalid value for parameter "sift.eval_methods"andUnknown evaluation method, so a script that runs one breaks; a value already stored (postgresql.conf,postgresql.auto.conf, a role or database setting) does not stop the server or a login, but every session that loads the library and the log say the same text as a WARNING, and the setting is ignored. Remove the names before updating (docs/UPGRADING.mdhas the queries;scripts/upgrade-check.shlists each as a blocker with the command, for any release of the library). Segments an earlier release wrote withunit_rewritestay readable. NoREINDEX; the names are gone, leave the setting empty. - 8. The setting
sift.enable_colis removed. It never changed anything (sift.enable_col_scanandsift.enable_aggare the switches). With the library loaded, aSET,ALTER ROLE .. SET,ALTER DATABASE .. SETorALTER SYSTEM SETthat names it fails withinvalid configuration parameter name "sift.enable_col"("sift" is a reserved prefix.); a value already stored does not stop the server or a login, but every session that loads the library and the log sayinvalid configuration parameter name "sift.enable_col", removing it, andSHOW sift.enable_colisunrecognized configuration parameter. Remove it frompostgresql.confand from the settings of roles and databases before updating (docs/UPGRADING.mdhas the queries; the preflight lists each as a blocker). No files, noREINDEX; to compare a plan with the normal one,SET sift.enable_agg = offandSET sift.enable_col_scan = off. - 9. The setting
sift.table_auto_defrag_dead_pctis removed (no effect since 4.0). It is refused and dropped exactly assift.enable_colof item 8, with the same queries, the same advice and the same blocker in the preflight. No files, noREINDEX;sift.table_auto_defrag = offturns the background pass of asift_tableoff. - 10. The
sift_colreloptionsrollupandrollup_dimsarerollup_leavesandrollup_columns. The old names stay accepted through 4.x:CREATE INDEXandALTER INDEX .. SETwith one sayNOTICE: reloption "rollup" is the older name of "rollup_leaves"("rollup_dims"and"rollup_columns"for the other) and read it as the new one, the new name wins when both are given,ALTER INDEX .. SETandRESETof the old name work, and an index that carries the old name keeps it inpg_class.reloptions(pg_dumpwrites it as it is,pg_upgradeandALTER EXTENSION UPDATEneed nothing). The messages,sift.index_choices(it reportsrollup_columns) and the errors name the new spelling, so a script or a view of yours that matches the text ofpg_class.reloptionsforrollup_dimsshould matchrollup_columnstoo; the limit ids ofsift.stats()keep their names (limit.rollup_dims). The files hold no option names, so noREINDEX; there is no switch, and the old names go away in 5.0.
Compatibility
- PostgreSQL 17, 18 and 19; the packages below cover all three (other builds from source with
make USE_PGXS=1). Extension SQL 4.2 (pg_sift--4.1--4.2.sql, reached from 4.0 through 4.0.3 and 4.1); no column format minor changes; a BM25 index takes metapage minor 1.6 once a buffer of format 6 is open (item 2).
Packages
x86-64 and arm64 packages for PostgreSQL 17, 18 and 19 are attached below (SHA256SUMS covers every asset); other versions build from source with make USE_PGXS=1.
- Debian 12 and 13, Ubuntu 22.04 and 24.04 (PGDG
postgresql-N, N = 17, 18 or 19):sudo apt install ./postgresql-N-pg-sift_4.2.0-1_amd64.deb(_arm64.debon arm64) - RHEL, Rocky, Alma 9 and 10 (PGDG
postgresqlN-server):sudo dnf install ./pg_sift_N-4.2.0-1.el9.x86_64.rpm(.aarch64.rpmon arm64) - Amazon Linux 2023 (the distribution’s
postgresqlN-server, N = 17 or 18):sudo dnf install ./pg_sift_N-4.2.0-1.amzn2023.x86_64.rpm(.aarch64.rpmon arm64) - Arch Linux: AUR
pg_sift.
Every change: CHANGELOG.md.
SQLite extension
pg_sift for SQLite 4.2.0 (sift, a loadable extension; a separate component from the PostgreSQL extension above). Rebuilt with the PostgreSQL 4.2.0 release: no change to the SQL interface or the shared format.
- No change: the changes of this release are in the PostgreSQL column index, which is not in this extension. No speed-up or slow-down is claimed.
- Needs SQLite 3.38 or newer; Linux x86-64, glibc 2.34 or newer. Built on Debian 12, loaded into the SQLite of Debian 12, Ubuntu 24.04 and AlmaLinux 10 before release.
- Storage format 2: tables of 3.4.0 to 3.7.0 open as they are, nothing to rebuild.
Download sift-sqlite-4.2.0-linux-x86_64.tar.gz (below, covered by SHA256SUMS with the packages), unpack it and .load ./sift from the sqlite3
shell (sha256sum --ignore-missing -c SHA256SUMS). Documentation: sqlite/README.md.
Every change: sqlite/CHANGELOG.md.
MariaDB extension
pg_sift for MariaDB 4.2.0 (sift, a UDF library and two SQL files; a separate component from the extensions above). Rebuilt with the PostgreSQL 4.2.0 release: no change to the SQL files.
- No change: the changes of this release are in the PostgreSQL column index, which is not in this extension. No speed-up or slow-down is claimed.
- One library per MariaDB series, because it calls the server’s own symbols and refuses to load into another series: 10.11, 11.4, 11.8, 12.1 and 13.0; Linux x86-64. Each was built in the stock
mariadb:<series>image against that server’s headers and loaded into a server of the same image before release.BUILD-INFOin the tarball lists the server version and the glibc each library needs. - Index format 2: an index of 3.4.0 to 3.7.0 opens as it is, nothing to rebuild.
Download sift-mariadb-4.2.0-linux-x86_64.tar.gz (below, covered by SHA256SUMS with the packages), unpack it, copy lib/<series>/libsift_udf.so of your server’s series
(SELECT VERSION()) into its plugin_dir, then SOURCE sql/functions.sql once per server and sql/sift.sql once per database (sha256sum --ignore-missing -c SHA256SUMS).
Documentation: mariadb/README.md.
Every change: mariadb/CHANGELOG.md.