-- An index-only scan of a sift_col index on the first column gives that column in order when the plan reads it alone (ORDER BY, either direction): the order array of the column -- merged with the write buffer, the heap saying what a row holds when its page is not all-visible. The rows must be the heap's, in order, NULL rows last (first for DESC), after -- deletes, updates, rows in the write buffer, a transaction's own changes, and for an index whose segments have no order array (the rows are sorted by the scan). CREATE EXTENSION pg_sift; SET sift.test_col_legacy_options = on; SET sift.async_maintenance = off; SET timezone = 'UTC'; CREATE TABLE os (id int, k int4, d date, pad text DEFAULT repeat('x', 200)); ALTER TABLE os ALTER COLUMN pad SET STORAGE PLAIN; INSERT INTO os (id, k, d) SELECT i, CASE WHEN i % 13 = 0 THEN NULL ELSE (i * 7919) % 3001 - 1500 END, CASE WHEN i % 17 = 0 THEN NULL ELSE date '2020-01-01' + (i * 31) % 700 END FROM generate_series(1, 9000) i; CREATE INDEX os_i ON os USING sift_col (k, id) WITH (count_columns = 'off', write_buffer_limit = 256, order_columns = 'k', order_array_columns = 'k', order_layout = 'whole'); NOTICE: sift_col index "os_i": 2 automatic choices, 1 advice DETAIL: advice: track_changes = the table has no one-column primary key; order_array_columns = k; order_layout = k exact HINT: Reasons, costs and how to turn each off: SELECT * FROM sift.index_choices('os_i'); NOTICE: sift_col index "os_i" is column format minor 44, which a library before 4.3 cannot read HINT: SET sift.col_format_minor = 14 before the build to write a format the older library reads. CREATE TABLE ot (id int, k int8, pad text DEFAULT repeat('x', 200)); ALTER TABLE ot ALTER COLUMN pad SET STORAGE PLAIN; INSERT INTO ot (id, k) SELECT i, CASE WHEN i % 11 = 0 THEN NULL ELSE (i * 104729::int8) % 5003 - 2500 END FROM generate_series(1, 6000) i; CREATE INDEX ot_i ON ot USING sift_col (k, id) WITH (count_columns = 'off', write_buffer_limit = 256, order_layout = 'whole'); NOTICE: sift_col index "ot_i": 1 advice (track_changes) DETAIL: advice: track_changes = the table has no one-column primary key HINT: Reasons, costs and how to turn each off: SELECT * FROM sift.index_choices('ot_i'); CREATE TABLE oa (k int4, id int4, b int8); INSERT INTO oa SELECT CASE WHEN i % 97 = 0 THEN NULL ELSE i % 700 END, i, i FROM generate_series(1, 3000) i; CREATE INDEX oa_i ON oa USING sift_col (k sift.col_int4_ops, id sift.col_int4_ops, b sift.col_int8_ops); NOTICE: sift_col index "oa_i": 1 advice (track_changes) DETAIL: advice: track_changes = the table has no one-column primary key HINT: Reasons, costs and how to turn each off: SELECT * FROM sift.index_choices('oa_i'); CREATE TABLE oc (id int, k int4); INSERT INTO oc SELECT i, CASE WHEN i % 29 = 0 THEN NULL ELSE ((i::int8 * 2654435761) % 1000000007)::int END FROM generate_series(1, 36000) i; CREATE INDEX oc_i ON oc USING sift_col (k, id) WITH (count_columns = 'off', order_columns = 'k', order_array_columns = 'k', order_layout = 'compact'); NOTICE: sift_col index "oc_i": 2 automatic choices, 1 advice DETAIL: advice: track_changes = the table has no one-column primary key; order_array_columns = k; order_layout = k compact in 49% of chunks HINT: Reasons, costs and how to turn each off: SELECT * FROM sift.index_choices('oc_i'); NOTICE: sift_col index "oc_i" is column format minor 44, which a library before 4.3 cannot read HINT: SET sift.col_format_minor = 14 before the build to write a format the older library reads. VACUUM (ANALYZE) oc; VACUUM (ANALYZE) oa; VACUUM (ANALYZE) os; VACUUM (ANALYZE) ot; -- sorts is 0 for the ordered scan (queries 1 to 4, 6 to 8, 10 to 17: 12 is an aggregate that orders by the first column of an index scan whose subquery names the others, and the scan must give those columns too; a window, a merge join of -- two ordered scans and a sorted GROUP BY read it too, though SiftAgg may answer a statement that aggregates; the window result is the column k itself, as a window nothing reads is dropped from the subquery that os_run hashes) -- and 1 for query 5 (NULLS FIRST ascending is not the order of the scan), 9 and 18 (they read another column) -- materials is 0 throughout (enable_material is off, which leaves the Materialize that a scan with no mark and restore needs): the inner side of a merge join (query 16, with many equal keys) marks its position and goes back to it, and the scan keeps the rows of one group for that, where PostgreSQL would put a Materialize over it -- the rows of q as the plan gives them, in the order it gives them (the ordered scan, or the heap and a Sort with sift off) CREATE FUNCTION os_run(q text, ios bool, OUT n bigint, OUT md5 text, OUT scans int, OUT sorts int, OUT mats int) LANGUAGE plpgsql AS $$ DECLARE line text; BEGIN PERFORM set_config('sift.enable_agg', 'off', true); PERFORM set_config('max_parallel_workers_per_gather', '0', true); PERFORM set_config('sift.enable_col_scan', ios::text, true); PERFORM set_config('enable_seqscan', (NOT ios)::text, true); PERFORM set_config('enable_sort', (NOT ios)::text, true); PERFORM set_config('enable_bitmapscan', 'false', true); PERFORM set_config('enable_hashjoin', 'false', true); PERFORM set_config('enable_nestloop', 'false', true); PERFORM set_config('enable_hashagg', 'false', true); PERFORM set_config('enable_material', 'false', true); scans := 0; sorts := 0; mats := 0; FOR line IN EXECUTE 'EXPLAIN (COSTS OFF) ' || q LOOP IF line ~ 'Index Only Scan (Backward )?using o._i' THEN scans := scans + 1; END IF; IF line ~ '^ *(-> )?Sort$' THEN sorts := sorts + 1; END IF; IF line ~ '^ *(-> )?Materialize$' THEN mats := mats + 1; END IF; END LOOP; EXECUTE 'SELECT count(*), md5(string_agg(coalesce(k::text, ''null''), ''|'')) FROM (' || q || ') w' INTO n, md5; END $$; CREATE FUNCTION os_check(q text, OUT rows bigint, OUT agree bool, OUT ordered_scans int, OUT sorts int, OUT materials int) LANGUAGE plpgsql AS $$ DECLARE a record; h record; BEGIN SELECT * INTO a FROM os_run(q, true); SELECT * INTO h FROM os_run(q, false); rows := h.n; agree := a.n = h.n AND a.md5 IS NOT DISTINCT FROM h.md5; ordered_scans := a.scans; sorts := a.sorts; materials := a.mats; IF NOT agree THEN RAISE EXCEPTION 'the ordered scan gives other rows than the heap: % (% against % rows)', q, a.n, h.n; END IF; END $$; CREATE TABLE os_q (n int, q text); INSERT INTO os_q VALUES (1, 'SELECT k FROM os ORDER BY k'), (2, 'SELECT k FROM os ORDER BY k DESC'), (3, 'SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k'), (4, 'SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC'), (5, 'SELECT k FROM os ORDER BY k NULLS FIRST'), (6, 'SELECT k FROM ot ORDER BY k'), (7, 'SELECT k FROM ot ORDER BY k DESC'), (8, 'SELECT k FROM ot WHERE k < 0 ORDER BY k DESC'), (9, 'SELECT k, d FROM os ORDER BY k'), (10, 'SELECT k FROM oc ORDER BY k'), (11, 'SELECT k FROM oc ORDER BY k DESC'), (12, 'SELECT array_agg(k ORDER BY k)::text AS k FROM (SELECT k, id, b FROM oa WHERE k IS NULL OR k <> ALL (ARRAY(SELECT g * 13 FROM generate_series(1, 500) g))) s'), (13, 'SELECT max(k) OVER (ORDER BY k) AS k FROM os'), (14, 'SELECT max(k) OVER (ORDER BY k DESC) AS k FROM ot'), (15, 'SELECT sum(k) OVER (PARTITION BY k) AS k FROM oc'), (16, 'SELECT a.k FROM os a JOIN os b ON a.k = b.k'), (17, 'SELECT k, count(*) FROM os GROUP BY k'), (18, 'SELECT max(k) OVER (ORDER BY k) + coalesce(d - d, 0) AS k FROM os'); SELECT n, q, (os_check(q)).* FROM os_q ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ----+--------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 9000 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 9000 | t | 1 | 0 | 0 3 | SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k | 2769 | t | 1 | 0 | 0 4 | SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC | 8308 | t | 1 | 0 | 0 5 | SELECT k FROM os ORDER BY k NULLS FIRST | 9000 | t | 1 | 1 | 0 6 | SELECT k FROM ot ORDER BY k | 6000 | t | 1 | 0 | 0 7 | SELECT k FROM ot ORDER BY k DESC | 6000 | t | 1 | 0 | 0 8 | SELECT k FROM ot WHERE k < 0 ORDER BY k DESC | 2715 | t | 1 | 0 | 0 9 | SELECT k, d FROM os ORDER BY k | 9000 | t | 0 | 1 | 0 10 | SELECT k FROM oc ORDER BY k | 36000 | t | 1 | 0 | 0 11 | SELECT k FROM oc ORDER BY k DESC | 36000 | t | 1 | 0 | 0 12 | SELECT array_agg(k ORDER BY k)::text AS k FROM (SELECT k, id, b FROM oa WHERE k IS NULL OR k <> ALL (ARRAY(SELECT g * 13 FROM generate_series(1, 500) g))) s | 1 | t | 1 | 0 | 0 13 | SELECT max(k) OVER (ORDER BY k) AS k FROM os | 9000 | t | 1 | 0 | 0 14 | SELECT max(k) OVER (ORDER BY k DESC) AS k FROM ot | 6000 | t | 1 | 0 | 0 15 | SELECT sum(k) OVER (PARTITION BY k) AS k FROM oc | 36000 | t | 1 | 0 | 0 16 | SELECT a.k FROM os a JOIN os b ON a.k = b.k | 23534 | t | 2 | 0 | 0 17 | SELECT k, count(*) FROM os GROUP BY k | 3002 | t | 1 | 0 | 0 18 | SELECT max(k) OVER (ORDER BY k) + coalesce(d - d, 0) AS k FROM os | 9000 | t | 0 | 1 | 0 (18 rows) -- an order with compact chunks has the bucket of each key, the heap the key: the scan sorts the rows of the units instead, and reads no heap block BEGIN; CREATE TEMP TABLE oc_fetched ON COMMIT DROP AS SELECT pg_stat_get_xact_blocks_fetched('oc'::regclass) AS n; SELECT os_run('SELECT k FROM oc ORDER BY k', true) IS NOT NULL AS ran; ran ----- t (1 row) SELECT pg_stat_get_xact_blocks_fetched('oc'::regclass) - n < 100 AS heap_not_read FROM oc_fetched; heap_not_read --------------- t (1 row) ROLLBACK; -- rows deleted and updated after the build, and rows in the write buffer DELETE FROM os WHERE id % 10 = 3; UPDATE os SET k = k + 5000 WHERE id % 10 = 4; UPDATE os SET k = NULL WHERE id % 10 = 5; INSERT INTO os (id, k) SELECT 9000 + i, (i * 37) % 4000 - 2000 FROM generate_series(1, 300) i; DELETE FROM ot WHERE id % 9 = 2; DELETE FROM oc WHERE id % 10 = 3; INSERT INTO oc SELECT 36000 + i, (i * 31) % 5000 FROM generate_series(1, 200) i; UPDATE ot SET k = -k WHERE id % 9 = 4; SELECT n, q, (os_check(q)).* FROM os_q ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ----+--------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 8400 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 8400 | t | 1 | 0 | 0 3 | SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k | 2017 | t | 1 | 0 | 0 4 | SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC | 6947 | t | 1 | 0 | 0 5 | SELECT k FROM os ORDER BY k NULLS FIRST | 8400 | t | 1 | 1 | 0 6 | SELECT k FROM ot ORDER BY k | 5333 | t | 1 | 0 | 0 7 | SELECT k FROM ot ORDER BY k DESC | 5333 | t | 1 | 0 | 0 8 | SELECT k FROM ot WHERE k < 0 ORDER BY k DESC | 2405 | t | 1 | 0 | 0 9 | SELECT k, d FROM os ORDER BY k | 8400 | t | 0 | 1 | 0 10 | SELECT k FROM oc ORDER BY k | 32600 | t | 1 | 0 | 0 11 | SELECT k FROM oc ORDER BY k DESC | 32600 | t | 1 | 0 | 0 12 | SELECT array_agg(k ORDER BY k)::text AS k FROM (SELECT k, id, b FROM oa WHERE k IS NULL OR k <> ALL (ARRAY(SELECT g * 13 FROM generate_series(1, 500) g))) s | 1 | t | 1 | 0 | 0 13 | SELECT max(k) OVER (ORDER BY k) AS k FROM os | 8400 | t | 1 | 0 | 0 14 | SELECT max(k) OVER (ORDER BY k DESC) AS k FROM ot | 5333 | t | 1 | 0 | 0 15 | SELECT sum(k) OVER (PARTITION BY k) AS k FROM oc | 32600 | t | 1 | 0 | 0 16 | SELECT a.k FROM os a JOIN os b ON a.k = b.k | 16475 | t | 2 | 0 | 0 17 | SELECT k, count(*) FROM os GROUP BY k | 3581 | t | 1 | 0 | 0 18 | SELECT max(k) OVER (ORDER BY k) + coalesce(d - d, 0) AS k FROM os | 8400 | t | 0 | 1 | 0 (18 rows) -- a transaction's own changes (the pages are not all-visible, the snapshot sees them), rolled back after BEGIN; DELETE FROM os WHERE id % 7 = 1; UPDATE os SET k = 100000 - id WHERE id % 7 = 2; INSERT INTO os (id, k) SELECT 20000 + i, i FROM generate_series(1, 50) i; SELECT n, q, (os_check(q)).* FROM os_q WHERE n <= 5 ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ---+----------------------------------------------------------+------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 7249 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 7249 | t | 1 | 0 | 0 3 | SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k | 1489 | t | 1 | 0 | 0 4 | SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC | 6211 | t | 1 | 0 | 0 5 | SELECT k FROM os ORDER BY k NULLS FIRST | 7249 | t | 1 | 1 | 0 (5 rows) ROLLBACK; SELECT n, q, (os_check(q)).* FROM os_q WHERE n <= 5 ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ---+----------------------------------------------------------+------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 8400 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 8400 | t | 1 | 0 | 0 3 | SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k | 2017 | t | 1 | 0 | 0 4 | SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC | 6947 | t | 1 | 0 | 0 5 | SELECT k FROM os ORDER BY k NULLS FIRST | 8400 | t | 1 | 1 | 0 (5 rows) -- after a vacuum (pages all-visible, the dead entries gone from the heap) and a rebuild of the write buffer VACUUM os; VACUUM ot; SELECT n, q, (os_check(q)).* FROM os_q ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ----+--------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 8400 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 8400 | t | 1 | 0 | 0 3 | SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k | 2017 | t | 1 | 0 | 0 4 | SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC | 6947 | t | 1 | 0 | 0 5 | SELECT k FROM os ORDER BY k NULLS FIRST | 8400 | t | 1 | 1 | 0 6 | SELECT k FROM ot ORDER BY k | 5333 | t | 1 | 0 | 0 7 | SELECT k FROM ot ORDER BY k DESC | 5333 | t | 1 | 0 | 0 8 | SELECT k FROM ot WHERE k < 0 ORDER BY k DESC | 2405 | t | 1 | 0 | 0 9 | SELECT k, d FROM os ORDER BY k | 8400 | t | 0 | 1 | 0 10 | SELECT k FROM oc ORDER BY k | 32600 | t | 1 | 0 | 0 11 | SELECT k FROM oc ORDER BY k DESC | 32600 | t | 1 | 0 | 0 12 | SELECT array_agg(k ORDER BY k)::text AS k FROM (SELECT k, id, b FROM oa WHERE k IS NULL OR k <> ALL (ARRAY(SELECT g * 13 FROM generate_series(1, 500) g))) s | 1 | t | 1 | 0 | 0 13 | SELECT max(k) OVER (ORDER BY k) AS k FROM os | 8400 | t | 1 | 0 | 0 14 | SELECT max(k) OVER (ORDER BY k DESC) AS k FROM ot | 5333 | t | 1 | 0 | 0 15 | SELECT sum(k) OVER (PARTITION BY k) AS k FROM oc | 32600 | t | 1 | 0 | 0 16 | SELECT a.k FROM os a JOIN os b ON a.k = b.k | 16475 | t | 2 | 0 | 0 17 | SELECT k, count(*) FROM os GROUP BY k | 3581 | t | 1 | 0 | 0 18 | SELECT max(k) OVER (ORDER BY k) + coalesce(d - d, 0) AS k FROM os | 8400 | t | 0 | 1 | 0 (18 rows) -- a few rows deleted and vacuumed (the units clear them in place), their addresses taken by new rows with other keys, or the same DELETE FROM os WHERE id % 500 = 0; DELETE FROM ot WHERE id % 500 = 0; VACUUM os; VACUUM ot; -- every page is all-visible and the units hold the dead rows' entries: none of them is exact SELECT n, q, (os_check(q)).* FROM os_q WHERE n IN (1, 2, 6, 7, 13, 14) ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ----+---------------------------------------------------+------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 8382 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 8382 | t | 1 | 0 | 0 6 | SELECT k FROM ot ORDER BY k | 5322 | t | 1 | 0 | 0 7 | SELECT k FROM ot ORDER BY k DESC | 5322 | t | 1 | 0 | 0 13 | SELECT max(k) OVER (ORDER BY k) AS k FROM os | 8382 | t | 1 | 0 | 0 14 | SELECT max(k) OVER (ORDER BY k DESC) AS k FROM ot | 5322 | t | 1 | 0 | 0 (6 rows) INSERT INTO os (id, k) SELECT 30000 + i, CASE WHEN i % 2 = 0 THEN (i * 7919) % 3001 - 1500 ELSE 8000 + i END FROM generate_series(1, 1500) i; INSERT INTO ot (id, k) SELECT 30000 + i, CASE WHEN i % 2 = 0 THEN (i * 104729::int8) % 5003 - 2500 ELSE 9000 + i END FROM generate_series(1, 1000) i; SELECT n, q, (os_check(q)).* FROM os_q ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ----+--------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 9882 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 9882 | t | 1 | 0 | 0 3 | SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k | 2261 | t | 1 | 0 | 0 4 | SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC | 8430 | t | 1 | 0 | 0 5 | SELECT k FROM os ORDER BY k NULLS FIRST | 9882 | t | 1 | 1 | 0 6 | SELECT k FROM ot ORDER BY k | 6322 | t | 1 | 0 | 0 7 | SELECT k FROM ot ORDER BY k DESC | 6322 | t | 1 | 0 | 0 8 | SELECT k FROM ot WHERE k < 0 ORDER BY k DESC | 2644 | t | 1 | 0 | 0 9 | SELECT k, d FROM os ORDER BY k | 9882 | t | 0 | 1 | 0 10 | SELECT k FROM oc ORDER BY k | 32600 | t | 1 | 0 | 0 11 | SELECT k FROM oc ORDER BY k DESC | 32600 | t | 1 | 0 | 0 12 | SELECT array_agg(k ORDER BY k)::text AS k FROM (SELECT k, id, b FROM oa WHERE k IS NULL OR k <> ALL (ARRAY(SELECT g * 13 FROM generate_series(1, 500) g))) s | 1 | t | 1 | 0 | 0 13 | SELECT max(k) OVER (ORDER BY k) AS k FROM os | 9882 | t | 1 | 0 | 0 14 | SELECT max(k) OVER (ORDER BY k DESC) AS k FROM ot | 6322 | t | 1 | 0 | 0 15 | SELECT sum(k) OVER (PARTITION BY k) AS k FROM oc | 32600 | t | 1 | 0 | 0 16 | SELECT a.k FROM os a JOIN os b ON a.k = b.k | 21050 | t | 2 | 0 | 0 17 | SELECT k, count(*) FROM os GROUP BY k | 4353 | t | 1 | 0 | 0 18 | SELECT max(k) OVER (ORDER BY k) + coalesce(d - d, 0) AS k FROM os | 9882 | t | 0 | 1 | 0 (18 rows) -- the same keys at the addresses the vacuum freed: the merge keeps one of the two sources' entries of an address and key, and the row must be given once CREATE TEMP TABLE os_gone AS SELECT id, k FROM os WHERE id % 97 = 5 AND id < 9000 ORDER BY id; DELETE FROM os WHERE id IN (SELECT id FROM os_gone); VACUUM os; INSERT INTO os (id, k) SELECT id + 60000, k FROM os_gone ORDER BY id; SELECT n, q, (os_check(q)).* FROM os_q WHERE n <= 5 ORDER BY n; n | q | rows | agree | ordered_scans | sorts | materials ---+----------------------------------------------------------+------+-------+---------------+-------+----------- 1 | SELECT k FROM os ORDER BY k | 9882 | t | 1 | 0 | 0 2 | SELECT k FROM os ORDER BY k DESC | 9882 | t | 1 | 0 | 0 3 | SELECT k FROM os WHERE k BETWEEN -300 AND 700 ORDER BY k | 2261 | t | 1 | 0 | 0 4 | SELECT k FROM os WHERE k IS NOT NULL ORDER BY k DESC | 8430 | t | 1 | 0 | 0 5 | SELECT k FROM os ORDER BY k NULLS FIRST | 9882 | t | 1 | 1 | 0 (5 rows) DROP FUNCTION os_check(text), os_run(text, bool); DROP TABLE os_q, os, ot, oc, oa; DROP EXTENSION pg_sift CASCADE;