|
| 1 | +-- Test hash index scans of a bucket pair whose split is incomplete |
| 2 | +-- |
| 3 | +-- The behavior tested here is not specific to incomplete splits: it |
| 4 | +-- applies to any scan that runs while the bucket pair's split-in-progress |
| 5 | +-- flags are set, which is the case for the duration of every split. |
| 6 | +-- Interrupting a split is merely the most convenient way to hold the |
| 7 | +-- bucket pair in that state: it makes the tests below deterministic, |
| 8 | +-- without any need for a second session. The hash-split isolation test |
| 9 | +-- provides equivalent coverage for scans that run during a live split. |
| 10 | +CREATE EXTENSION injection_points; |
| 11 | +SELECT injection_points_set_local(); |
| 12 | + injection_points_set_local |
| 13 | +---------------------------- |
| 14 | + |
| 15 | +(1 row) |
| 16 | + |
| 17 | +CREATE TABLE hash_split_test (v int4) WITH (autovacuum_enabled = false); |
| 18 | +CREATE INDEX hash_split_index ON hash_split_test USING hash (v); |
| 19 | +-- Determine the layout of the (never-split) index: one metapage, nbuckets |
| 20 | +-- bucket pages, and one bitmap page. The index's first bucket split will |
| 21 | +-- split bucket 0, creating bucket nbuckets. |
| 22 | +CREATE TEMP TABLE hash_split_geom AS |
| 23 | + SELECT nbuckets, |
| 24 | + (current_setting('block_size')::bigint / 16) * nbuckets AS fillrows |
| 25 | + FROM (SELECT pg_relation_size('hash_split_index') / |
| 26 | + current_setting('block_size')::bigint - 2 AS nbuckets) g; |
| 27 | +-- sanity: initial bucket counts are always powers of two |
| 28 | +SELECT nbuckets > 0 AND (nbuckets & (nbuckets - 1)) = 0 AS nbuckets_ok |
| 29 | + FROM hash_split_geom; |
| 30 | + nbuckets_ok |
| 31 | +------------- |
| 32 | + t |
| 33 | +(1 row) |
| 34 | + |
| 35 | +-- Choose a value that maps to bucket 0 before the split and to the new |
| 36 | +-- bucket afterwards |
| 37 | +CREATE TEMP TABLE hash_split_key AS |
| 38 | + SELECT min(v)::int4 AS k |
| 39 | + FROM generate_series(1, 10000) v, hash_split_geom |
| 40 | + WHERE (hashint4(v::int4) & (2 * nbuckets - 1)) = nbuckets; |
| 41 | +INSERT INTO hash_split_test |
| 42 | + SELECT k FROM hash_split_key, generate_series(1, 10); |
| 43 | +-- Error out during the first bucket split, leaving it incomplete. |
| 44 | +-- fillrows is guaranteed to cross the split threshold of ffactor * nbuckets |
| 45 | +-- tuples: a hash index entry occupies at least 16 bytes (line pointer |
| 46 | +-- included), and the default fillfactor targets 75% page fullness, so |
| 47 | +-- ffactor can never exceed block_size / 16 tuples per bucket. |
| 48 | +SELECT injection_points_attach('hash-split-before-relocation', 'error'); |
| 49 | + injection_points_attach |
| 50 | +------------------------- |
| 51 | + |
| 52 | +(1 row) |
| 53 | + |
| 54 | +INSERT INTO hash_split_test |
| 55 | + SELECT 1000000 + g FROM hash_split_geom, generate_series(1, fillrows) g; |
| 56 | +ERROR: error triggered for injection point hash-split-before-relocation |
| 57 | +SELECT injection_points_detach('hash-split-before-relocation'); |
| 58 | + injection_points_detach |
| 59 | +------------------------- |
| 60 | + |
| 61 | +(1 row) |
| 62 | + |
| 63 | +-- This insertion goes to the new bucket, which is still flagged as being |
| 64 | +-- populated by the incomplete split |
| 65 | +INSERT INTO hash_split_test SELECT k FROM hash_split_key; |
| 66 | +-- Scans of that bucket must visit both buckets of the incomplete split: |
| 67 | +-- the fresh row from the new bucket, plus the 10 older rows that remain |
| 68 | +-- in bucket 0 |
| 69 | +SET enable_seqscan = off; |
| 70 | +SET enable_bitmapscan = off; |
| 71 | +EXPLAIN (COSTS OFF) |
| 72 | + SELECT count(*) FROM hash_split_test |
| 73 | + WHERE v = (SELECT k FROM hash_split_key); |
| 74 | + QUERY PLAN |
| 75 | +------------------------------------------------------------ |
| 76 | + Aggregate |
| 77 | + InitPlan expr_1 |
| 78 | + -> Seq Scan on hash_split_key |
| 79 | + Disabled: true |
| 80 | + -> Index Scan using hash_split_index on hash_split_test |
| 81 | + Index Cond: (v = (InitPlan expr_1).col1) |
| 82 | +(6 rows) |
| 83 | + |
| 84 | +SELECT count(*) FROM hash_split_test |
| 85 | + WHERE v = (SELECT k FROM hash_split_key); |
| 86 | + count |
| 87 | +------- |
| 88 | + 11 |
| 89 | +(1 row) |
| 90 | + |
| 91 | +-- Backward scans must visit both buckets too, in the opposite order |
| 92 | +BEGIN; |
| 93 | +DECLARE c SCROLL CURSOR FOR |
| 94 | + SELECT v = (SELECT k FROM hash_split_key) FROM hash_split_test |
| 95 | + WHERE v = (SELECT k FROM hash_split_key); |
| 96 | +MOVE FORWARD ALL IN c; |
| 97 | +FETCH BACKWARD ALL FROM c; |
| 98 | + ?column? |
| 99 | +---------- |
| 100 | + t |
| 101 | + t |
| 102 | + t |
| 103 | + t |
| 104 | + t |
| 105 | + t |
| 106 | + t |
| 107 | + t |
| 108 | + t |
| 109 | + t |
| 110 | + t |
| 111 | +(11 rows) |
| 112 | + |
| 113 | +COMMIT; |
| 114 | +-- Same count via a bitmap scan, which shares the underlying scan code |
| 115 | +SET enable_indexscan = off; |
| 116 | +SET enable_bitmapscan = on; |
| 117 | +SELECT count(*) FROM hash_split_test |
| 118 | + WHERE v = (SELECT k FROM hash_split_key); |
| 119 | + count |
| 120 | +------- |
| 121 | + 11 |
| 122 | +(1 row) |
| 123 | + |
| 124 | +-- And the ground truth, via the heap |
| 125 | +SET enable_bitmapscan = off; |
| 126 | +RESET enable_seqscan; |
| 127 | +SELECT count(*) FROM hash_split_test |
| 128 | + WHERE v = (SELECT k FROM hash_split_key); |
| 129 | + count |
| 130 | +------- |
| 131 | + 11 |
| 132 | +(1 row) |
| 133 | + |
| 134 | +RESET enable_indexscan; |
| 135 | +RESET enable_bitmapscan; |
| 136 | +DROP TABLE hash_split_test; |
| 137 | +DROP EXTENSION injection_points; |
0 commit comments