web-dev-qa-db-ja.com

JSONBタグをより速くカウントする方法はありますか?

私はPostgres 9.5でこのクエリからさらにパフォーマンスを絞り込もうとしています。 400,000行以上実行しています。

試してみると、CASEステートメントがクエリコストにかなりの量を追加していることに気づきました。既存の列を単純に合計するだけで置き換えると、実行時間が半分になります。これらの合計を計算するより効率的な方法はありますか?

SELECT sum("tag1"), sum("tag2"), sum("total_tags")
FROM (
    SELECT people.data->'recruiter_id' AS recruiter_id,
          (CASE WHEN people.data->'tags' ? 'tag1' THEN 1 END) AS "tag1",
          (CASE WHEN people.data->'tags' ? 'tag2' THEN 1 END) AS "tag2", 
          ((CASE WHEN people.data->'tags' ? 'tag1' THEN 1 ELSE 0 END) +
           (CASE WHEN people.data->'tags' ? 'tag2' THEN 1 ELSE 0 END)) AS total_tags
    FROM people WHERE people.data->'tags' ?| ARRAY['tag1','tag2'] ) AS target
GROUP BY recruiter_id

EXPLAIN ANALYSEの出力:

HashAggregate  (cost=1076.30..1078.22 rows=550 width=202) (actual time=7043.115..7043.208 rows=449 loops=1)
  Group Key: (people.data -> 'recruiter_id'::text)
  ->  Bitmap Heap Scan on people  (cost=12.85..1072.72 rows=550 width=202) (actual time=13.908..2619.878 rows=48492 loops=1)
        Recheck Cond: ((data -> 'tags'::text) ?| '{tag1,tag2}'::text[])
        Heap Blocks: exact=26114
        ->  Bitmap Index Scan on index_people_on_data_tags  (cost=0.00..12.82 rows=550 width=0) (actual time=9.219..9.219 rows=48493 loops=1)
              Index Cond: ((data -> 'tags'::text) ?| '{tag1,tag2}'::text[])
Planning time: 0.139 ms
Execution time: 7043.291 ms

実行中:

Gccでコンパイルされたx86_64-pc-linux-gnu上のPostgreSQL 9.5.5(Ubuntu 4.8.2-19ubuntu1)4.8.2、64ビット

内部クエリと外部クエリは、アプリケーションの別々の部分によって生成されます。再構築せずに最適化することは可能ですか?

3
ChristopherJ

これは全体的に高速でシンプルになるはずです。

SELECT *, tag1 + tag2 AS total_tags
FROM  (
   SELECT (data->>'recruiter_id')::int AS recruiter_id  -- cheaper to group by int
        , count(*) FILTER (WHERE data->'tags' ? 'tag1') AS tag1
        , count(*) FILTER (WHERE data->'tags' ? 'tag2') AS tag2
   FROM   people
   WHERE  data->'tags' ?| ARRAY['tag1','tag2']
   GROUP  BY 1
   ) target;
  • recruiter_idが整数エンティティであると仮定すると、jsonbオブジェクトを含むanよりも整数値でグループ化する方が安価です。 integer値。私はまた、とにかく結果の整数値を取得したいと思います。

  • サブクエリで1回だけカウントしてから、外側のSELECTに合計のカウントを追加します。

  • 条件付きカウントには集約FILTER句を使用します。

  • より短い構文が必要な場合は、これにより同じ結果とパフォーマンスが得られます。

    count(data->'tags' ? 'tag1' OR NULL) AS tag1
    

jsonbのインデックスと不足している統計

通常、indexesは、大きなテーブルでのパフォーマンスを決定する要因です。しかし、クエリは400.000行のうち48.493行、つまり> 12%を取得するため、インデックスはこのクエリをまったく助けません

なぜ悪い決断なのか?クエリプランナーには値がないの内部a json/jsonbオブジェクトを使用し、一般的な選択性推定に基づいて最適なクエリプランを選択する必要があります。 rows = 550が見つかるはずですが、クエリでは実際に〜90 x倍(rows=48493)。ビットマップインデックススキャンを使用するクエリプランは、適切な決定ではありません。順次スキャンの方が高速です(インデックスをまったく使用しません)。

インデックスは、頻度が低いタグ(タグがある場合)に役立ちますが、data->'tags'の式インデックスが最適です。たぶんjsonb_path_opsインデックスと、適応されたクエリとの組み合わせです。もっと:

ただし、この理由やその他の理由により、一般的なタグを使用している場合、プレーンなPostgres配列または完全に正規化されたスキーマは、jsonbオブジェクトのパフォーマンスを大幅に上回ります。

Postgresql-performanceリストでのこの議論は、あなたの問題について正確にです:

5

試す

SELECT count("tag1"), count("tag2"), count("tag1")+count("tag2")
FROM (
    SELECT people.data->'recruiter_id' AS recruiter_id,
          nullif(people.data->'tags' ? 'tag1',false) AS "tag1",
          nullif(people.data->'tags' ? 'tag2',false) AS "tag2"
    FROM people
    ) AS target
GROUP BY recruiter_id
HAVING count("tag1")+count("tag2") > 0

@Dudu Markowitzの答えは正しいと思いますが、クエリを再構成する方法が限られている場合は、少なくとも次のようなことができます。

SELECT sum(tag1) AS sum_tag1, sum(tag2) AS sum_tag2, 
       /* Take out the sum("total_tags") */
       sum(tag1) +            sum(tag2) AS sum_total_tags
FROM (
    SELECT people.data->'recruiter_id' AS recruiter_id,
          (CASE WHEN people.data->'tags' ? 'tag1' THEN 1 END) AS "tag1",
          (CASE WHEN people.data->'tags' ? 'tag2' THEN 1 END) AS "tag2", 
          /* You save one CASE with two "->" */ 
    FROM people 
    WHERE people.data->'tags' ?| ARRAY['tag1','tag2'] 
     ) AS target
GROUP BY recruiter_id
0
joanolo