web-dev-qa-db-ja.com

配列text []でフィルタリングし、タイムスタンプでソートします

説明

Linux上のPostgreSQL 9.6、_tags_tmp_テーブルのサイズ〜30 GB(1000万行)、tagsは_text[]_であり、値は6つだけです。

_tags_tmp(id int, tags text[], maker_date timestamp, value text)
_
_id  tags        maker_date      value
1   {a,b,c}     2016-11-09      This is test 
2   {a}         2016-11-08      This is test 
3   {b,c}       2016-11-07      This is test 
4   {c}         2016-11-06      This is test 
5   {d}         2016-11-05      This is test 
_

tagsのフィルターと_order by_の_maker_date desc_を使用してデータを取得する必要があります。両方の_tags & maker_date desc_列にインデックスを作成できますか?

そうでない場合、他のアイデアを提案できますか?

クエリ例

_select id, tags, maker_date, value
from tags_tmp
where  tags && array['a','b']
order by maker_date desc
limit 5 offset 0
_

SQLコード:

_create index idx1 on tags_tmp using gin (tags);
create index idx2 on tags_tmp using btree(maker_date desc);

explain (analyse on, costs on, verbose)
select id, tags, maker_date, value
from tags_tmp
where tags && array['funny','inspiration']
order by maker_date desc
limit 5 offset 0 ;
_

結果を説明してください:

_Limit  (cost=233469.63..233469.65 rows=5 width=116) (actual time=801.482..801.483 rows=5 loops=1)
  Output: id, tags, maker_date, value
  ->  Sort  (cost=233469.63..234714.22 rows=497833 width=116) (actual time=801.481..801.481 rows=5 loops=1)
        Output: id, tags, maker_date, value
        Sort Key: tags_tmp.maker_date DESC
        Sort Method: top-N heapsort  Memory: 25kB
        ->  Bitmap Heap Scan on public.tags_tmp  (cost=6486.58..225200.81 rows=497833 width=116) (actual time=212.982..696.650 rows=366392 loops=1)
              Output: id, tags, maker_date, value
              Recheck Cond: (tags_tmp.tags && '{funny,inspiration}'::text[])
              Heap Blocks: exact=120034
              ->  Bitmap Index Scan on idx1  (cost=0.00..6362.12 rows=497882 width=0) (actual time=171.742..171.742 rows=722612 loops=1)
                    Index Cond: (tags_tmp.tags && '{funny,inspiration}'::text[])
Planning time: 0.185 ms
Execution time: 802.128 ms
_

詳しくは

もちろん、1つのタグのみに部分インデックスを使用してテストしましたが、より高速です。しかし、私はtagをたくさん持っています。例:create index idx_tmp on tags_tmp using btree (maker_date desc) where (tags && array['tag1') or tags && array['tag2'] or ... or tags && array['tag6']。そして、私は_tags && array['tag1']_と'tag1' = any(tags)の間でテストしましたが、パフォーマンスは同じです。

  1. _text[]_には6つの値= _a, b, c, d, e, f_しかありません。例:_tags={a,b,c}, tags={a}, tags={a,c}, tags={a,b,c,d,e,f}, tags={b,f}_など。しかし、_g->z, A-Z_などの値を持つことはできません。

  2. create table tags_tmp(id int primary key not null, tags text[] not null, maker_date timestamp not null, value text)

  3. distinct配列値に関して、tagsを含むaは、テーブルの20%行を取得しますwhere 'a' = any(tags)、b = 20%where 'b' = any(tags)、c = 20%where 'c' = any(tags)、d = 20%where 'd' = any(tags)、e = 10%where 'e' = any(tags)、f = 10%where 'f' = any(tags)

  4. また、_(tags, maker_date)_は一意ではありません。

  5. このテーブルは読み取り専用ではありません。

  6. _sort on timestamp_ですが、私の例では日付を表示しています。

現在の状況:_tags = 'a' or tags = 'b' or tags = 'c'_など

(1)_GIN index_または_text[] to int[]_の変換、および_text[] to int_の変換などを使用すると、マルチタグでビットマップインデックスが使用されます。最後に、テスト後、古いソリューションを使用することを決め、ORを多くのUNION句に変更しました。それぞれのUNIONはデータ数を制限します。もちろん、タグ値ごとに_partial index_を作成し、上記の(1)と組み合わせることもできます。 OFFSETに関しては、代わりにWHERE句で1つ以上の条件を使用します。

_EXPLAIN (ANALYSE ON, costs ON, VERBOSE)
SELECT rs.*
FROM (
        (SELECT tags,
                id,
                maker_date
         FROM tags_tmp
         WHERE 'a' = any(tags)
           AND maker_date <= '2016-03-28 05:43:57.779528'::TIMESTAMP
         ORDER BY maker_date DESC LIMIT 5)
      UNION
        (SELECT tags,
                id,
                maker_date
         FROM tags_tmp
         WHERE 'b' = any(tags)
           AND maker_date <= '2016-03-28 05:43:57.779528'::TIMESTAMP
         ORDER BY maker_date DESC LIMIT 5)
      UNION
        (SELECT tags,
                id,
                maker_date
         FROM tags_tmp
         WHERE 'c' = any(tags)
           AND maker_date <= '2016-03-28 05:43:57.779528'::TIMESTAMP
         ORDER BY maker_date DESC LIMIT 5)) rs
ORDER BY rs.maker_date DESC LIMIT 5 ;
_
7
Luan Huynh

一般的な考慮事項

インデックスの最適化は常に complete 画像に依存します。テーブルサイズ、行サイズ、カーディナリティ、値の頻度、一般的なクエリの選択性、Postgresバージョン、一般的なアクセスパターンなど。

あなたのケースは2つの理由で特に困難です:

  1. WHEREと_ORDER BY_で使用される異なる列。
  2. 配列のフィルターはGINまたはGistインデックスで最も効率的ですが、どちらのインデックスタイプもソートされた出力を生成しません。 マニュアル:

    現在PostgreSQLでサポートされているインデックスタイプのうち、Bツリーのみがソートされた出力を生成できます。他のインデックスタイプは、一致する行を実装に依存しない特定の順序で返します。

can _(tags, maker_date)_またはそれ以上の列に複数列のGINインデックスを作成できます(インデックス列の順序はGINインデックスには関係ありません)。ただし、追加のモジュール_btree_gin_をインストールする必要があります。手順:

そして、それはあなたの問題の_ORDER BY_コンポーネントの助けにはなりません。

もう1つの明確化:_OFFSET m LIMIT n_は、通常、_LIMIT m+n_と同じくらい高価です。

追加仕様の解決策

あなたは明確にしました:6つの異なるタグのみ可能です。それは重要です。

テーブルは大きく、テーブルの定義には改善の余地があります。 サイズが重要大きなテーブルの場合。あなたの数値(30 GB、1000万行)も大きな平均を示唆しています。行サイズ〜3 KB。表示またはテーブルの膨張よりも多くの列があり、_VACUUM FULL_実行(または同様の)が必要であるか、value列が大きくてTOASTされているため、メインの関係から改善がさらに効果的になるこれでサイズの半分以下にカットされます:

_CREATE TABLE tags_tmp (
  id         int PRIMARY KEY -- assuming PK
, tags       int NOT NULL    -- also assuming NOT NULL
, value      text
, maker_date timestamp NOT NULL  -- NOT NULL!
);
_

列の順序は、アラインメントのパディングのため、適切です。詳細:

さらに重要なのは、これは_tags int_です。どうして?

配列には、24バイト(行と同様)のかなりのオーバーヘッドと実際のアイテムがあります。

したがって、あなたがデモンストレーションを行うような1〜6の項目(「面白い」、「インスピレーション」、...)を含む_text[]_は、〜56バイトの平均を占めます。また、6つの異なる値は6ビットの情報のみで表すことができます(配列のソート順は無関係であると想定)。さらに圧縮することもできますが、便利なintegerタイプ(占有4バイト)を選択しました。これにより、最大31個のタグにスペースを確保できます。これにより、テーブルスキーマを変更せずに、後で追加できるようになります。詳細な根拠:

タグはビットマップのビットにマップされ、_'a'_が最下位ビット(右側)になります。

_tag:       a | b | c | d |  e |  f
position:  0 | 1 | 2 | 3 |  4 |  5
int value: 1 | 2 | 4 | 8 | 16 | 32
_

したがって、タグ配列_'{a,d,f}'_は_41_にマップされます。 'a'-'f'の代わりに任意の文字列を使用できますが、問題ありません。

ロジックをカプセル化するには、2つの補助関数を簡単に拡張できます。

タグ->整数:

_CREATE OR REPLACE FUNCTION f_tags2int(text[])
  RETURNS int AS
$func$
SELECT bit_or(CASE x
            WHEN 'a' THEN  1
            WHEN 'b' THEN  2
            WHEN 'c' THEN  4
            WHEN 'd' THEN  8
            WHEN 'e' THEN 16
            WHEN 'f' THEN 32
            -- more?
           END)
FROM    unnest ($1) x
$func$  LANGUAGE SQL IMMUTABLE;
_

整数->タグ:

_CREATE OR REPLACE FUNCTION f_int2tags(int)
  RETURNS text[] AS
$func$
SELECT array_remove(ARRAY [CASE WHEN $1 &  1 > 0 THEN 'a' END
                         , CASE WHEN $1 &  2 > 0 THEN 'b' END
                         , CASE WHEN $1 &  4 > 0 THEN 'c' END
                         , CASE WHEN $1 &  8 > 0 THEN 'd' END
                         , CASE WHEN $1 & 16 > 0 THEN 'e' END
                         , CASE WHEN $1 & 32 > 0 THEN 'f' END], NULL)
                         -- more? 
$func$  LANGUAGE SQL IMMUTABLE;
_

ここでの基本:

便宜上、viewを追加して、以前のようにタグをテキスト配列として表示できます。

_CREATE VIEW tags_tmp_pretty AS
SELECT id, tags
     , f_int2tags(tags) AS tags_pretty
     , maker_date, value
FROM   tags_tmp;
_

これで、基本的なqueryは次のようになります。

_SELECT id, tags, maker_date, value
FROM   tags_tmp
WHERE  tags & f_tags2int('{a,b}') > 0  -- any of the tags matched
ORDER  by maker_date DESC
LIMIT  5;
_

バイナリAND演算子_&_ を使用します。列を操作するための より多くの演算子 があります。 バイナリ文字列演算子get_bit()およびset_bit()も便利です。

上記のクエリは、サイズが小さく安価なオペレーターだけでも、すでに高速ですが、まだ革新的なものはありません。作成するには fast インデックスが必要ですが、上記ではまだインデックスを使用できません。

すべてのタグに対して1つの部分インデックス

_CREATE INDEX foo_tag_a ON tags_tmp(maker_date DESC) WHERE tags & 1 > 0;
CREATE INDEX foo_tag_b ON tags_tmp(maker_date DESC) WHERE tags & 2 > 0;
...
CREATE INDEX foo_tag_f ON tags_tmp(maker_date DESC) WHERE tags & 32 > 0;
_

このクエリは上記と同等ですが、インデックスを利用できます。

_SELECT *
FROM   tags_tmp_pretty
WHERE (tags & f_tags2int('{a}') > 0   -- same as tags & 1
    OR tags & f_tags2int('{e}') > 0)  -- same as tags & 32
ORDER  BY maker_date DESC
LIMIT  10;
_

Postgresは、いくつかのビットマップインデックススキャンをBitmapOrステップで非常に効率的に組み合わせることができます。これは SQL Fiddle で示されています。

別のインデックス条件を追加して、インデックスを _maker_date_>一定のタイムスタンプ(およびクエリで逐語的条件を繰り返す)に制限して、サイズを(大幅に)削減できます。関連する例:

より洗練された:

その他の関連する回答:

または、6つのboolean列だけ...

単純に6つのブール列がさらに良い選択かもしれません。どちらのソリューションにもいくつかの長所と短所があります...

_CREATE TABLE tags_tmp (
  id         int PRIMARY KEY -- assuming PK
, tag_a      bool 
, tag_b      bool 
  ...
, tag_f      bool 
, value      text
, maker_date timestamp NOT NULL  -- NOT NULL!
);
_

完全なユースケースに応じて、フラグ_NOT NULL_を定義できます。

考慮してください:

部分インデックスは単純に:

_CREATE INDEX foo_tag_a ON tags_tmp(maker_date DESC) WHERE tag_a;
CREATE INDEX foo_tag_b ON tags_tmp(maker_date DESC) WHERE tag_b;
_

等。

あなたの特別なケースの代替

さらに考えると、すべてのほとんどのタグはそれほど一般的ではないであり、複数のタグをOR=と組み合わせるとさらに選択性が低下するため、btree _maker_date DESC_のインデックス。Postgresはインデックスをトラバースし、タグの条件を満たす行をフィルタリングできます。これは、Postgresがより有用な列を持っているため、配列またはエンコードされた整数の代わりに個別のブール列と組み合わせて機能します。個別の列の統計。

_CREATE INDEX tags_tmp_date ON tags_tmp(maker_date DESC);
_

その後:

_SELECT *
FROM   tags_tmp_pretty
WHERE  tag_a
   OR  tag_b
ORDER  BY maker_date DESC
LIMIT  10;
_

ページング

ページングを機能させるには、結果セットにあいまいでないソート順が必要です。私はこの答えで気にしませんでした、それはすでに長すぎます。通常、_ORDER BY_に列を追加します。それでページングを効率的に機能させる方法:

5

テストケースに関するさまざまな問題:

  1. idは_int8_になりました。元の質問でそれをintと宣言しました大きな違いではありませんが、なぜ最初に混乱するのですか?これは、行サイズと配置パディングに関係します。質問では、実際の正確で完全なテーブル定義を必ず宣言してください。

  2. テストデータのデータ分布は非現実的です。タグの組み合わせは6つしかなく、すべての行にタグ_'1'_があります。ライブテーブルには63の可能な組み合わせがすべてあり、質問で追加したようにタグが分散されていると思います。

  3. テストテーブルには、古いおよび新しいタグ列が含まれているため、ストレージサイズへの影響がなくなりました。行のサイズがさらに大きくなりました。行サイズは124-164バイトですが、私のテストでは68バイトのみです(パディングとアイテムポインターを含む)。サイズが2倍を超えると違いが生じます。

  4. size = 4163 MBと書き込みます。 サイズは?

  5. テストデータ用のorder by random()があります。あなたの生産的なテーブルは本当にランダムですか?通常、データはタイムスタンプで大まかにソートされます。 実際の状況はどうですか?

  6. 選択されるプランを確認するには、実際にクエリを実行する前に、EXPLAINでテストしてクエリプランのみを確認してください。大きなテーブルで多くの時間を節約できます。ただし、常にEXPLAIN (ANALYZE, BUFFERS)の出力をここに提供してください。 (質問とは対照的に)answerで、_cost=_の見積もりがありません。そのため、問題を推測するのが難しくなります。

ただし、これらの問題のnoneは、_enable_seqscan = off_ ; Postgres 9.5worksを搭載した私のラップトップでの簡単なテスト。同じことがpg 9.6にも当てはまります。

_CREATE TABLE tags_tmp(
   id         bigserial PRIMARY KEY, 
   maker_date timestamp NOT NULL,
   tags       int NOT NULL,
   value      text
);

INSERT INTO tags_tmp (tags, maker_date, value)
SELECT EXTRACT('minute' FROM ts)::int    -- int between 1 and 60 (no 61,62,63), pretty good.
     , ts + random() * interval '5 min'  -- some limited randomness
     , 'This is test on ' || EXTRACT('minute' FROM ts)
FROM   generate_series(timestamp '2016-01-01 00:00'
                     , timestamp '2016-01-13 00:00', '10 second') ts;
-- 103681 rows affected, 836 msec execution time.

-- create adapted function f_tags2int
-- create adapted function f_int2tags

CREATE INDEX tags_tmp_1 ON tags_tmp(maker_date DESC) WHERE tags &  1 > 0;
CREATE INDEX tags_tmp_2 ON tags_tmp(maker_date DESC) WHERE tags &  2 > 0;
CREATE INDEX tags_tmp_3 ON tags_tmp(maker_date DESC) WHERE tags &  4 > 0;
CREATE INDEX tags_tmp_4 ON tags_tmp(maker_date DESC) WHERE tags &  8 > 0;
CREATE INDEX tags_tmp_5 ON tags_tmp(maker_date DESC) WHERE tags & 16 > 0;
CREATE INDEX tags_tmp_6 ON tags_tmp(maker_date DESC) WHERE tags & 32 > 0;

SELECT id, tags, maker_date, value
FROM   tags_tmp
WHERE (tags & f_tags2int(array['5']) > 0 OR
       tags & f_tags2int(array['6']) > 0)
ORDER  BY maker_date DESC
LIMIT  5;
_
_QUERY PLAN
Limit  (cost=3811.93..3811.94 rows=5 width=38) (actual time=46.586..46.586 rows=5 loops=1)
  Buffers: shared hit=1132
  ->  Sort  (cost=3811.93..3955.93 rows=57601 width=38) (actual time=46.584..46.585 rows=5 loops=1)
        Sort Key: maker_date DESC
        Sort Method: top-N heapsort  Memory: 25kB
        Buffers: shared hit=1132
        ->  Bitmap Heap Scan on tags_tmp  (cost=607.78..2855.20 rows=57601 width=38) (actual time=13.699..27.674 rows=76032 loops=1)
              Recheck Cond: (((tags & 16) > 0) OR ((tags & 32) > 0))
              Heap Blocks: exact=864
              Buffers: shared hit=1132
              ->  BitmapOr  (cost=607.78..607.78 rows=69121 width=0) (actual time=13.549..13.549 rows=0 loops=1)
                    Buffers: shared hit=268
                    ->  Bitmap Index Scan on tags_tmp_5 cost=0.00..289.49 rows=34560 width=0) (actual time=8.745..8.745 rows=48384 loops=1)
                          Buffers: shared hit=134
                    ->  Bitmap Index Scan on tags_tmp_6 (cost=0.00..289.49 rows=34560 width=0) (actual time=4.800..4.800 rows=48384 loops=1)
                          Buffers: shared hit=134
Planning time: 3.976 ms
Execution time: 46.653 ms
_

すでに SQL Fiddle で説明したように。

すべてのインデックスを適切に作成しましたか?

1