web-dev-qa-db-ja.com

Postgresql:オブジェクト列のjsonb配列にインデックスを付ける方法

オブジェクトの配列を持つjsonb列を持つテーブルがあります。

すべての行でこのように見えます。

[{grade: 'A', subject: 'MATH'}, {grade: 'B', subject: 'PHY'}...]

この投稿のおかげでクエリを実行しています https://stackoverflow.com/a/30592076/2405689

問題は、grade IN(A、B、C)を持つすべての学生の数のクエリを完了するのに少なくとも2.4秒かかることです。

私が試したインデックスは何もしなかったので、これをインデックス化するのに助けが欲しいのですが。

DROP INDEX idx_subjects_subject;
DROP INDEX idx_subjects_grade;
CREATE INDEX idx_subjects_subject ON results USING GIN((subjects-> 'subject'));
CREATE INDEX idx_subjects_grade ON results USING GIN((subjects-> 'grade'));

また(個別に)行いました:

DROP INDEX idx_subjects_standard;
CREATE INDEX idx_subjects_standard ON results USING GIN(subjects);

私はそれらをこのようにクエリしています。

SELECT COUNT(*)
FROM results
WHERE EXISTS 
    (
        SELECT 1 
        FROM jsonb_array_elements(subjects) AS j(data) 
        WHERE (data #>> '{subject}') LIKE '%MATH%' 
        AND 
        (data #>> '{grade}') IN ('A', 'B', 'C')
    )
AND
    "examYear" = '2010'
AND
    "examType" = 'CSEE'
;

私もこのようにそれをクエリしてみました:

SELECT COUNT(*)
FROM results
WHERE EXISTS 
    (
    SELECT 1 
    FROM jsonb_array_elements(subjects) AS j(data) 
    WHERE data @> '{"subject": "B/MATH", "grade": "A"}'
    OR data @> '{"subject": "B/MATH", "grade": "B"}'
    OR data @> '{"subject": "B/MATH", "grade": "C"}'

    )
AND
    "examYear" = '2010'
AND
    "examType" = 'CSEE'
;

しかし、これは逆の効果をもたらしました(3秒のクエリ)。

これが私の説明分析ブロックです。

"Aggregate  (cost=1888617.19..1888617.20 rows=1 width=8) (actual time=2517.116..2517.117 rows=1 loops=1)"
"  ->  Bitmap Heap Scan on results  (cost=470767.18..1888021.96 rows=238090 width=0) (actual time=680.456..2514.633 rows=24002 loops=1)"
"        Recheck Cond: ("examYear" = '2010'::text)"
"        Rows Removed by Index Recheck: 557945"
"        Filter: (("examType" = 'CSEE'::text) AND (SubPlan 1))"
"        Rows Removed by Filter: 500054"
"        Heap Blocks: exact=40156 lossy=53452"
"        ->  Bitmap Index Scan on idx_results_subjects  (cost=0.00..470707.66 rows=528405 width=0) (actual time=672.375..672.375 rows=524056 loops=1)"
"              Index Cond: ("examYear" = '2010'::text)"
"        SubPlan 1"
"          ->  Function Scan on jsonb_array_elements j  (cost=0.00..2.13 rows=1 width=0) (actual time=0.003..0.003 rows=0 loops=458487)"
"                Filter: (((data #>> '{subject}'::text[]) ~~ '%MATH%'::text) AND ((data #>> '{grade}'::text[]) = ANY ('{A,B,C}'::text[])))"
"                Rows Removed by Filter: 8"
"Planning time: 0.126 ms"
"Execution time: 2517.145 ms"

私はpostgresqlにいます:9.6.1

3
ArchNoob

次のコードでは、jsonb列でGINインデックスを使用できます。

SELECT COUNT(*) FROM results
WHERE subjects @> '[{"subject": "B/MATH", "grade": "A"}]'

あなたの例との違いは、私は配列をアンパックしないことです。jsonb列を直接クエリするだけです。 GINインデックスは、jsonb列の@>演算子に使用できますが、任意の関数には使用できません。最後に@>演算子を使用していますが、実際のjsonb列ではなく、抽出したjsonを使用しています。

先頭にワイルドカードを使用したLIKE条件のインデックス付けは、はるかに困難です。これには trigram index が必要になりますが、それが可能である場合でも、jsonb列の配列内のデータに使用する方法はわかりません。 LIKEをまったく使用する必要がないように、この情報をより正式な方法で格納することを強く検討する必要があります。

'MATH%'のように最初にワイルドカードがないLIKEは、特定の条件下でbtreeインデックスを使用できます( https://www.postgresql.org/docs/9.5/static/indexes-を参照)。 types.html 詳細については、このロケールに注意する必要があります)。

これが配列ではなく、jsonb列内のプレーンオブジェクトである場合は、関数インデックスを使用できます。

CREATE INDEX ON results((subject->>'grade'));

これはjsonb内の配列でも可能かもしれませんが、今のところこれを行うための合理的な方法は考えられません。

現在のスキーマでは、すべてを1桁も作成することが困難になり、インデックスを適切に活用することが非常に困難になります。オプションがあれば、このデータを件名と成績の列を持つテーブルに保存することを検討してください。これにより、この問題がはるかに簡単になります。

8
Mad Scientist