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)
の間でテストしましたが、パフォーマンスは同じです。
_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
_などの値を持つことはできません。
create table tags_tmp(id int primary key not null, tags text[] not null, maker_date timestamp not null, value text)
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)
。
また、_(tags, maker_date)
_は一意ではありません。
このテーブルは読み取り専用ではありません。
_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 ;
_
インデックスの最適化は常に complete 画像に依存します。テーブルサイズ、行サイズ、カーディナリティ、値の頻度、一般的なクエリの選択性、Postgresバージョン、一般的なアクセスパターンなど。
あなたのケースは2つの理由で特に困難です:
WHERE
と_ORDER BY
_で使用される異なる列。配列のフィルターは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
_>一定のタイムスタンプ(およびクエリで逐語的条件を繰り返す)に制限して、サイズを(大幅に)削減できます。関連する例:
より洗練された:
その他の関連する回答:
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
_に列を追加します。それでページングを効率的に機能させる方法:
テストケースに関するさまざまな問題:
id
は_int8
_になりました。元の質問でそれをint
と宣言しました大きな違いではありませんが、なぜ最初に混乱するのですか?これは、行サイズと配置パディングに関係します。質問では、実際の正確で完全なテーブル定義を必ず宣言してください。
テストデータのデータ分布は非現実的です。タグの組み合わせは6つしかなく、すべての行にタグ_'1'
_があります。ライブテーブルには63の可能な組み合わせがすべてあり、質問で追加したようにタグが分散されていると思います。
テストテーブルには、古いおよび新しいタグ列が含まれているため、ストレージサイズへの影響がなくなりました。行のサイズがさらに大きくなりました。行サイズは124-164バイトですが、私のテストでは68バイトのみです(パディングとアイテムポインターを含む)。サイズが2倍を超えると違いが生じます。
size = 4163 MBと書き込みます。 サイズは?
テストデータ用のorder by random()
があります。あなたの生産的なテーブルは本当にランダムですか?通常、データはタイムスタンプで大まかにソートされます。 実際の状況はどうですか?
選択されるプランを確認するには、実際にクエリを実行する前に、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 で説明したように。
すべてのインデックスを適切に作成しましたか?