jsonb 數組上的 Postgres btree 部分索引 - > 數組表達式似乎在 9.5.x 版本中因較大的表而損壞
我正在研究 jsonb 屬性的索引,我發現 Postgres 9.5.x 有一些可疑之處,但在更高版本中沒有。以下是我所做的觸發了奇怪的查詢錯誤的操作。可能是我做錯了什麼,但是在較新的 Postgres 版本中看到這項工作讓我認為這是 9.5.x 中的一個錯誤(我已經嘗試了 9.5.21 版本)。
我一直看到這一點,表大小約為 100 萬行或更高。
jsonb 列中的 json 包含代表不同 json 類型單值和數組的屬性。我有字元串、布爾值、數字整數、數字浮點數和日期格式的字元串。我看到的錯誤是
<
整數數組的數組運算符(我沒有全部嘗試過)。從錯誤看來,column -> 'attribute'
表達式的一部分似乎無法檢索到 jsonb 值的正確部分,並說 int 數組獲取附近的字元串數組等。這實際上在執行中發生變化,因為數據是隨機的。列中 json 的結構對於column
properties
的每個值都是固定的(確定性的)type
。所以每一行type = 8
總是有一個整數數組properties -> 'r'
。type = 7
具有數組 atproperties -> 'q'
,type = 9
具有數組 atproperties -> 's'
等。換句話說type
,就 json in 的結構(或“模式”)而言,它是一種邏輯類型,properties
並且所有具有相同值的行都type
具有同質 json 結構,就節點名稱和值類型而言(值本身是隨機的)。同樣,現在數組的長度始終為 3。這是一個錯誤嗎?還是我做錯了什麼?
CREATE TABLE test1 ( id SERIAL PRIMARY KEY, type INTEGER NOT NULL, properties jsonb ); -- generates test data wherein the json structure of "properties" column varies by "type" column INSERT INTO test1 (type, properties) SELECT s.type AS type, json_build_object(CHR(s.type + 100), md5(random() :: TEXT), CHR(s.type + 101), (random() * 100)::INTEGER, CHR(s.type + 102), (random() * 10)::DOUBLE PRECISION, CHR(s.type + 103), random()::INTEGER::BOOLEAN , CHR(s.type + 104), to_char(to_timestamp((random() * 1500000000)::DOUBLE PRECISION), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), CHR(s.type + 105), ARRAY[md5(random() :: TEXT), md5(random() :: TEXT), md5(random() :: TEXT)], CHR(s.type + 106), ARRAY[(random() * 100)::INTEGER, (random() * 100)::INTEGER, (random() * 100)::INTEGER], CHR(s.type + 107), ARRAY[(random() * 10)::DOUBLE PRECISION, (random() * 10)::DOUBLE PRECISION, (random() * 10)::DOUBLE PRECISION], CHR(s.type + 108), ARRAY[random()::INTEGER::BOOLEAN, random()::INTEGER::BOOLEAN, random()::INTEGER::BOOLEAN], CHR(s.type + 109), ARRAY[ to_char(to_timestamp((random() * 1500000000)::DOUBLE PRECISION), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), to_char(to_timestamp((random() * 1500000000)::DOUBLE PRECISION), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), to_char(to_timestamp((random() * 1500000000)::DOUBLE PRECISION), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') ] ) AS properties FROM (SELECT (random() * 10) :: INT AS type FROM generate_series(1, 1000000)) s; CREATE OR REPLACE FUNCTION jsonb_array_int_array(JSONB) RETURNS INTEGER[] AS $$ DECLARE result INTEGER[]; BEGIN IF $1 ISNULL THEN result := NULL; ELSEIF jsonb_array_length($1) = 0 THEN result := ARRAY [] :: INTEGER[]; ELSE SELECT array_agg(x::INTEGER) FROM jsonb_array_elements_text($1) t(x) INTO result; END IF; RETURN result; END; $$ LANGUAGE plpgsql IMMUTABLE; -- properties -> 'r' field of type 8 is always an array of integers CREATE INDEX test1_properties_r_int_array_index ON test1 USING btree (jsonb_array_int_array(properties -> 'r')) WHERE type = 8; -- this works SELECT count(*) FROM test1 WHERE type = 8 AND jsonb_array_int_array(properties -> 'r') < ARRAY[50]; -- this fails SELECT count(*) FROM test1 WHERE type = 8 AND jsonb_array_int_array(properties -> 'r') < ARRAY[100]; -- but DROP INDEX test1_properties_r_int_array_index; -- now it works SELECT count(*) FROM test1 WHERE type = 8 AND jsonb_array_int_array(properties -> 'r') < ARRAY[100]; -- also CREATE INDEX test1_properties_r_int_array_index ON test1 USING gin (jsonb_array_int_array(properties -> 'r')) WHERE type = 8; -- works here too SELECT count(*) FROM test1 WHERE type = 8 AND jsonb_array_int_array(properties -> 'r') < ARRAY[100];
感謝您的幫助。
編輯:
這是有關它如何失敗的一些說明。我剛剛重新執行了上面的,查詢失敗如下
sql> SELECT count(*) FROM test1 WHERE type = 8 AND jsonb_array_int_array(properties -> 'r') < ARRAY[100] [2020-03-04 00:46:20] [22P02] ERROR: invalid input syntax for integer: "1.73782130237668753" [2020-03-04 00:46:20] Where: SQL statement "SELECT array_agg(x::INTEGER) FROM jsonb_array_elements_text($1) t(x)" [2020-03-04 00:46:20] PL/pgSQL function jsonb_array_int_array(jsonb) line 12 at SQL statement
我從錯誤消息中掃描了隨機值
SELECT id AS txt FROM test1 WHERE position('1.73782130237668753' IN properties::text) > 0;
並發現導致錯誤的行實際上
type
等於 7 而不是 8,如查詢的 where 子句。因此,似乎在返回的行中不滿足索引條件。這是失敗查詢的計劃
Aggregate (cost=69293.65..69293.66 rows=1 width=0) -> Bitmap Heap Scan on test1 (cost=1228.78..69208.38 rows=34111 width=0) Recheck Cond: ((jsonb_array_int_array((properties -> 'r'::text)) < '{100}'::integer[]) AND (type = 8)) -> Bitmap Index Scan on test1_properties_r_int_array_index (cost=0.00..1220.25 rows=34111 width=0) Index Cond: (jsonb_array_int_array((properties -> 'r'::text)) < '{100}'::integer[])
編輯2:
在 Laurenz Albe 的回復之後,我進行了以下測試。我定義了一個新功能
CREATE OR REPLACE FUNCTION jsonb_array_int_array2(json_value JSONB, actual_type INTEGER, expected_type INTEGER) RETURNS INTEGER[] AS $$ DECLARE result INTEGER[]; BEGIN IF actual_type <> expected_type THEN RAISE EXCEPTION 'unexpected type % instead of %', actual_type, expected_type; END IF; IF $1 ISNULL OR actual_type <> expected_type THEN result := NULL; ELSEIF jsonb_array_length(json_value) = 0 THEN result := ARRAY [] :: INTEGER[]; ELSE SELECT array_agg(x::INTEGER) FROM jsonb_array_elements_text(json_value) t(x) INTO result; END IF; RETURN result; END; $$ LANGUAGE plpgsql IMMUTABLE;
我重新定義了索引並重組了查詢如下
CREATE INDEX test1_properties_r_int_array_index ON test1 USING btree (jsonb_array_int_array2(properties -> 'r', type, 8)) WHERE type = 8; SELECT count(*) FROM test1 WHERE type = 8 AND jsonb_array_int_array2(properties -> 'r', type, 8) < ARRAY[100];
現在我得到了
[2020-03-04 09:47:34] [P0001] ERROR: unexpected type 7 instead of 8
這表明對所有行執行了一個步驟,而不僅僅是 where
type = 8
。這可能是計劃中的嗎Recheck Cond: ((jsonb_array_int_array((properties -> 'r'::text)) < '{50}'::integer[]) AND (type = 8))
如果這是評估的順序,是否可以將其反轉並
type = 8
在之前檢查jsonb_array_int_array((properties -> 'r'::text)
?同樣從性能來看(一旦我刪除了異常檢查並重新執行),似乎整個表都被掃描了。
這是預期的嗎?
編輯3:
我意識到這現在已成為一個不同的問題,而 Laurenz Albe 出色而詳細的回答解決了“為什麼它不起作用”的原始問題。現在的問題是如何最好地開始我所追求的原始計劃。我想我將不得不將其提煉成一個單獨的問題。
謝謝!
順便說一句,正如 Laurenz 預測的那樣,我能夠使用更多數據在 Postgres 10.x 上重現該問題。
編輯4:
作為記錄,這並不特定於數組。在這種情況下,任何值的轉換最終都會因大表而失敗。因此,當這也不安全時,
properties ->> 'm'
它始終是整數type = 8
CREATE INDEX test1_properties_m_int_index ON test1 (((properties ->> 'm')::INTEGER)) WHERE type = 8;
和查詢
SELECT count(*) FROM test1 WHERE type = 8 AND (properties ->> 'm')::INTEGER < 50;
失敗了
[2020-03-05 09:35:24] [22P02] ERROR: invalid input syntax for integer: "["a1c815126aa058706476b21f37f60038", "450513bd0f25abf8bd39b1b4645a1427", "e51acc579414985eaa59d9bdc3dc8187"]"
這裡的教訓是,如果 json 模式未固定在跨表的列中,則無論進行什麼轉換,它都必須在不加選擇地掃描表的各個部分期間預測任何 jsonb 輸入。
這是一個有趣的問題,所以我會盡力給出一個好的答案。
簡而言之,問題在於您的函式定義,它對它必須處理的 JSON 對象的種類做出了毫無根據的假設。
錯誤解釋:
執行範例時出現的錯誤不是確定性的;這取決於您範例中的隨機數。我明白了,例如:
ERROR: cannot get array length of a non-array CONTEXT: PL/pgSQL function jsonb_array_int_array(jsonb) line 6 at IF
但原因是一樣的。
觀察您的執行計劃使用Bitmap Index Scan。也就是說,PostgreSQL 在記憶體中建構了一個點陣圖,指示表中的哪些行滿足索引條件。第二步,點陣圖堆掃描,然後訪問實際的表行。
你可以想像這樣的點陣圖會消耗記憶體。現在點陣圖的記憶體量受配置參數的限制
work_mem
。如果work_mem
太小而無法包含每個表行包含一位的點陣圖,PostgreSQL 將部分降級為每 8 KB 塊僅包含一位的“有損點陣圖”,指示該塊是否包含匹配的行。您可以在輸出中看到這一點EXPLAIN (ANALYZE)
,但在您的情況下看不到,因為查詢失敗。如果您有一個有損點陣圖,則必須重新檢查點陣圖指示的塊中的所有行以過濾掉誤報,因此您的函式將呼叫根本不在索引中的參數。
該錯誤是由
ELSEIF jsonb_array_length($1) = 0
或者
SELECT array_agg(x::INTEGER) FROM jsonb_array_elements_text($1) t(x) INTO result;
這兩行都假設參數是一個
jsonb
數組,第二行嘗試將數組元素轉換為integer
. 您得到的實際錯誤取決於恰好由jsonb_array_int_array
.此類問題與某個 PostgreSQL 版本無關,您在 9.5 上看到它是一個巧合。也許在處理
work_mem
限制時發生了一些變化,或者當點陣圖變得有損時,也許隨機數恰好不同。我的理論的證明:
增加
work_mem
,您會看到錯誤神奇地消失了,因為生成的點陣圖不再有損。解決方案:
更改您的函式,使其不會因 JSON 值而不是整數數組而失敗。