web-dev-qa-db-ja.com

SELECT(regexのJOIN)が+1行で30倍遅くなるのはなぜですか(クエリプランは同じままです)?記憶の欠如?

これが私のクエリです、それはかかります2.7秒

_SELECT t2.id, t1.id FROM sample t1
INNER JOIN (select * from regex_set limit 33) t2 ON t1.data ~* t2.regex;
_

関連するEXPLAIN (ANALYSE,BUFFERS)出力:

_Nested Loop (cost=0.00..1108.30 rows=356 width=8) (actual time=56.130..2740.906 rows=33 loops=1)
 Join Filter: (t1.data ~* regex_set.regex)
 Rows Removed by Join Filter: 65967
 Buffers: local hit=18
 -> Seq Scan on sample t1 (cost=0.00..38.59 rows=2159 width=36) (actual time=0.014..1.534 rows=2000 loops=1)
    Buffers: local hit=17
 -> Materialize (cost=0.00..1.08 rows=33 width=36) (actual time=0.000..0.004 rows=33 loops=2000)
    Buffers: local hit=1
    -> Limit (cost=0.00..0.59 rows=33 width=36) (actual time=0.005..0.014 rows=33 loops=1)
       Buffers: local hit=1
       -> Seq Scan on regex_set (cost=0.00..22.70 rows=1270 width=36) (actual time=0.004..0.008 rows=33 loops=1)
          Buffers: local hit=1
Planning time: 0.129 ms
Execution time: 2740.952 ms
_

制限を32に設定した同じクエリは、同じプランで.25秒しかかかりません(行33には特別なことはありません:テスト済み_limit 32 offset 1_、以下と同じ結果が得られました):

_Nested Loop (cost=0.00..1075.88 rows=345 width=8) (actual time=5.871..255.315 rows=32 loops=1)
 Join Filter: (t1.data ~* regex_set.regex)
 Rows Removed by Join Filter: 63968
 Buffers: local hit=18
 -> Seq Scan on sample t1 (cost=0.00..38.59 rows=2159 width=36) (actual time=0.008..0.498 rows=2000 loops=1)
    Buffers: local hit=17
 -> Materialize (cost=0.00..1.05 rows=32 width=36) (actual time=0.000..0.002 rows=32 loops=2000)
    Buffers: local hit=1
    -> Limit (cost=0.00..0.57 rows=32 width=36) (actual time=0.003..0.013 rows=32 loops=1)
       Buffers: local hit=1
       -> Seq Scan on regex_set (cost=0.00..22.70 rows=1270 width=36) (actual time=0.003..0.006 rows=32 loops=1)
          Buffers: local hit=1
Planning time: 0.109 ms
Execution time: 255.374 ms  
_

これらのテストに使用した初期化スクリプトは次のとおりです。

_DO $$
  DECLARE s_length INT=2000;
  DECLARE r_length INT=50;
BEGIN
    DROP TABLE IF EXISTS sample;
    CREATE TEMP TABLE sample AS
      SELECT generate_series(1, s_length) id, md5(random() :: text) AS data;

    DROP TABLE IF EXISTS regex_set;
    CREATE TEMP TABLE regex_set AS
      SELECT g.id, s.data regex
      FROM generate_series(1, r_length) g (id),
           sample s
      WHERE s.id = round(g.id * s_length / r_length);
END $$;
_

私の実際のデータでは、まったく同じ_33 limit_で遅延が発生しました(時差は1から30でした)。

Dockerコンテナ(2 cpu/2GB RAM)でPostgres 10を実行しており、デフォルトのpostgres.confを次のように(任意に)編集しています(あまり効果はありませんでした)。

_shared_buffers = 512MB
temp_buffers = 32MB
work_mem = 16MB (also set to 64MB, had no effect)
_

同等のクエリは、デフォルトのMySQL5.8コンテナではるかに高速です。

何が起こっているのかを説明できるPostgresマスターはいますか?

2
klarezz

完全な例をありがとう。それと「perf」と「gdb」からの少しの助けを借りて、私は問題をこれまでたどりました:

src/backend/utils/adt/regexp.c:#define MAX_CACHED_RES   32

33個の正規表現を作業メモリーに保持して予測可能なサイクルでそれらにアクセスしようとすると、それぞれが次に必要になる正規表現をキャッシュからプッシュするため、すべての正規表現は必要になるたびに再コンパイルされます。この再コンパイルは遅いです。

その値に32が選択された理由はわかりませんが、明らかにキャッシュのサイズを無制限にすることはできません。コンパイルするのではなく、ユーザーが設定できるのはいいことかもしれません。

この知識があれば、クエリを微調整して結合実行の順序を変更し、次の正規表現に進む前に1つの正規表現がテストされて完了するようにするのは簡単です。

SELECT t2.id, t1.id FROM sample t1
right JOIN (select * from regex_set limit 33) t2 ON t1.data ~* t2.regex

ただし、t1の行に一致しない正規表現がt2にある場合、このクエリはそれらの行に対してnull拡張結果を返し始めます。これは、既存のクエリからの動作の変更です。また、単純なWHERE句を追加してこれらの余分な行を除外しようとすると、プランナーはトリックを確認し、遅い結合順序の使用に戻ります。不透明な同等のものを使用して、それらを除外することができます。

SELECT t2.id, t1.id FROM sample t1
right JOIN (select * from regex_set limit 33) t2 ON t1.data ~* t2.regex 
where coalesce(t1.id,-5) != -5

(実際のt1.idが常に正であると仮定)

しかし、おそらくこの問題の最善の解決策は、正規表現クエリをサポートする特殊なインデックスを作成することです。

create extension pg_trgm ;
create index on sample using gin (data gin_trgm_ops);

そのインデックスを使用する可能性がある場合は、本質的に結合順序を高速化する必要があり、インデックスを実際に使用するとクエリも高速化されます。

1
jjanes