一般に、複数列のBツリーインデックスをサポートするSQLデータベースは、インデックスの最初の列である場合にのみ、インデックス内の列のサブセットによるルックアップもサポートしています。たとえば、列(a, b, c, d)
にインデックスがあり、実行したい場合:
SELECT * FROM my_table
WHERE b = 7 AND a = 'foo';
次に、これはインデックスを使用して高速になります。ペア(a, b)
はインデックスの先頭にあり、データベースはツリーをナビゲートして('foo', 7, ... )
で始まるインデックスレコードを探します。ただし、実行すると
SELECT * FROM my_table
WHERE b = 7 AND c = 'bar';
一致するレコードは列a
にある値に応じてインデックス全体に分散されるため、インデックスは使用されません*。
*(下記の Evanの答え で説明されているように、インデックスの完全または部分的なスキャンを行うことを除いて-ただし、完全なインデックススキャンは、全表スキャン、および部分索引スキャンも潜在的に実行されますが、これはあまり役に立ちません。)
n列と潜在的に膨大な数の行を持つテーブルがあります。また、ユーザーがこれらの列の任意の組み合わせの正確な値でフィルタリングし、結果の表を表示できるようにするフロントエンドGUIも用意しています。このフロントエンドによって生成されたanyクエリが全テーブルスキャンの結果を取得することは許容されません。可能なすべてのフィルターは、インデックスによってサポートされる必要があります。
n列の場合、列のすべての可能な組み合わせがいくつかのインデックスでカバーされるようにするために作成する必要があるBツリーインデックスの最小数はいくつですか?
テーブルに4つの列があるとします:a
、b
、c
、およびd
。次に、ユーザーがフィルターする可能性のある列の15の異なる組み合わせがあります。
a b c d
a b c
a b d
a c d
b c d
a b
a c
a d
b c
b d
c d
a
b
c
d
ただし、次の6つのBツリーインデックスのみを使用して、これら4つの列の任意のサブセットからのルックアップをサポートできます。
(a, b, c, d)
(b, c, d)
(c, d, a)
(d, b, a)
(a, c)
(a, d)
これを説明するために、以下はそのサブセットによるルックアップを行うために使用できるインデックスと一緒に15の可能なサブセットです。
a b c d (a, b, c, d)
a b c (a, b, c, d)
a b d (d, b, a)
a c d (c, d, a)
b c d (b, c, d)
a b (a, b, c, d)
a c (a, c)
a d (a, d)
b c (b, c, d)
b d (d, b, a)
c d (c, d, a)
a (a, b, c, d)
b (b, c, d)
c (c, d, a)
d (d, b, a)
ただし、このアイデアを列数の多いテーブルに一般化する方法や、必要なインデックスの数がnでどのようにスケーリングされるかはわかりません。したがって、私の質問:このアプローチをn列に拡張するために必要なインデックスの数と、それらのインデックスをどのように判断することができますか?
(私は主に、Bツリーインデックスを使用してこれらのルックアップをサポートする方法の特定の理論的な問題に関心がありますが、たとえば、この正確な問題を束よりもうまく解決するインデックスタイプがある場合は、他のソリューションを受け入れますBツリーインデックスは。
N列のサブセットによるルックアップをサポートするために必要なBツリーインデックスの数
少なくともOracle(12.x)とPostgresの場合、答えは次のとおりです。
つまり、列ごとに1つのインデックスです。どちらのDBMSも、1つのクエリで複数のインデックスを組み合わせることができます。古いバージョンのOracleでは、すべての列で単一のビットマップインデックスを使用する必要がありました。 Oracle 12.1はビットマップの「スキャン」をその場で実行できます(11.xでそれがすでに可能であったかどうかはわかりません。テストする11.xのインストールがありません)。
テスト設定:
create table idx_test (a int, b int, c int, d int, e int);
create index idx_a on idx_test(a);
create index idx_b on idx_test(b);
create index idx_c on idx_test(c);
create index idx_d on idx_test(d);
ここで、テーブルに100万行と各列のランダムな値を入力します。私はPostgresでこれを使用しました:
insert into idx_test (a,b,c,d,e)
select (random() * 10000 + 1)::int,
(random() * 100000 + 1)::int,
(random() * 1000000 + 1)::int,
(random() * 100000 + 1)::int,
(random() * 10000 + 1)::int
from generate_series(1,1000000);
次に、行をOracleサーバーにコピーしました。
次のクエリ
select *
from idx_test
where b = 42 or a = 24 or c = 100;
は、Postgresでの次の実行プランを示しています。
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on idx_test (cost=6.25..166.36 rows=111 width=20) (actual time=0.032..0.244 rows=113 loops=1)
Recheck Cond: ((b = 42) OR (a = 24) OR (c = 100))
Heap Blocks: exact=113
-> BitmapOr (cost=6.25..6.25 rows=111 width=0) (actual time=0.020..0.020 rows=0 loops=1)
-> Bitmap Index Scan on idx_test_b_idx (cost=0.00..1.96 rows=11 width=0) (actual time=0.009..0.009 rows=11 loops=1)
Index Cond: (b = 42)
-> Bitmap Index Scan on idx_test_a_idx (cost=0.00..2.27 rows=98 width=0) (actual time=0.007..0.007 rows=100 loops=1)
Index Cond: (a = 24)
-> Bitmap Index Scan on idx_test_c_idx (cost=0.00..1.93 rows=2 width=0) (actual time=0.003..0.003 rows=2 loops=1)
Index Cond: (c = 100)
Planning time: 0.077 ms
Execution time: 0.270 ms
各列に対応するインデックスが使用されたことがわかります。 Oracle 12.1の実行計画はほぼ同じです。
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------
Plan hash value: 2332290563
--------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)|
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 43 | 2795 | 740 (0)|
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| IDX_TEST | 43 | 2795 | 740 (0)|
| 2 | BITMAP CONVERSION TO ROWIDS | | | | |
| 3 | BITMAP OR | | | | |
| 4 | BITMAP CONVERSION FROM ROWIDS | | | | |
|* 5 | INDEX RANGE SCAN | IDX_C | | | 3 (0)|
| 6 | BITMAP AND | | | | |
| 7 | BITMAP CONVERSION FROM ROWIDS | | | | |
|* 8 | INDEX RANGE SCAN | IDX_A | | | 3 (0)|
| 9 | BITMAP CONVERSION FROM ROWIDS | | | | |
|* 10 | INDEX RANGE SCAN | IDX_B | | | 3 (0)|
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("C"=100)
8 - access("A"=24)
10 - access("B"=42)
列が2つしかないクエリは、2つのインデックスのみを使用します。
select *
from idx_test
where a = 1001 and b = 45877;
Postgres:
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on idx_test (cost=4.48..5.99 rows=1 width=20) (actual time=0.118..0.118 rows=0 loops=1)
Recheck Cond: ((b = 45877) AND (a = 1001))
-> BitmapAnd (cost=4.48..4.48 rows=1 width=0) (actual time=0.117..0.117 rows=0 loops=1)
-> Bitmap Index Scan on idx_test_b_idx (cost=0.00..1.96 rows=11 width=0) (actual time=0.075..0.075 rows=11 loops=1)
Index Cond: (b = 45877)
-> Bitmap Index Scan on idx_test_a_idx (cost=0.00..2.27 rows=98 width=0) (actual time=0.038..0.038 rows=93 loops=1)
Index Cond: (a = 1001)
Planning time: 0.150 ms
Execution time: 0.157 ms
そしてOracle:
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------
Plan hash value: 3618970768
--------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)|
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 78 | 5070 | 22 (0)|
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| IDX_TEST | 78 | 5070 | 22 (0)|
| 2 | BITMAP CONVERSION TO ROWIDS | | | | |
| 3 | BITMAP AND | | | | |
| 4 | BITMAP CONVERSION FROM ROWIDS | | | | |
|* 5 | INDEX RANGE SCAN | IDX_A | 43 | | 3 (0)|
| 6 | BITMAP CONVERSION FROM ROWIDS | | | | |
|* 7 | INDEX RANGE SCAN | IDX_B | 43 | | 3 (0)|
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("A"=1001)
7 - access("B"=45877)
インデックスはOR
条件にも使用されます。
select *
from idx_test
where a = 1001 or b = 45877;
Postgres:
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on idx_test (cost=4.29..161.31 rows=109 width=20) (actual time=0.053..0.240 rows=104 loops=1)
Recheck Cond: ((a = 1001) OR (b = 45877))
Heap Blocks: exact=104
-> BitmapOr (cost=4.29..4.29 rows=109 width=0) (actual time=0.038..0.038 rows=0 loops=1)
-> Bitmap Index Scan on idx_test_a_idx (cost=0.00..2.27 rows=98 width=0) (actual time=0.030..0.030 rows=93 loops=1)
Index Cond: (a = 1001)
-> Bitmap Index Scan on idx_test_b_idx (cost=0.00..1.96 rows=11 width=0) (actual time=0.007..0.007 rows=11 loops=1)
Index Cond: (b = 45877)
Planning time: 0.111 ms
Execution time: 0.273 ms
Oracle:
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------
Plan hash value: 2853963857
--------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)|
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 62 | 4030 | 879 (0)|
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| IDX_TEST | 62 | 4030 | 879 (0)|
| 2 | BITMAP CONVERSION TO ROWIDS | | | | |
| 3 | BITMAP OR | | | | |
| 4 | BITMAP CONVERSION FROM ROWIDS | | | | |
|* 5 | INDEX RANGE SCAN | IDX_A | | | 3 (0)|
| 6 | BITMAP CONVERSION FROM ROWIDS | | | | |
|* 7 | INDEX RANGE SCAN | IDX_B | | | 3 (0)|
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("A"=1001)
7 - access("B"=45877)
SQL Server 2016を使用した簡単なテストでは、SQL Server 2016が基本的に同じことを行うことが示されています。
StmtText | StmtId | NodeId | Parent | PhysicalOp | LogicalOp | Argument
-------------------------------------------------------------------------------------------------------------------------------------+--------+--------+--------+--------------+------------+------------------------------------------------------------------------------------------------------
select * from idx_test where b = 42 and a = 24 | 1 | 1 | 0 | | | 1
|--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000])) | 1 | 2 | 1 | Nested Loops | Inner Join | OUTER REFERENCES:([Bmk1000])
|--Hash Match(Inner Join, HASH:([Bmk1000])=([Bmk1000]), RESIDUAL:([Bmk1000] = [Bmk1000])) | 1 | 4 | 2 | Hash Match | Inner Join | HASH:([Bmk1000])=([Bmk1000]), RESIDUAL:([Bmk1000] = [Bmk1000])
| |--Index Seek(OBJECT:([TestDB].[dbo].[idx_test].[idx_b]), SEEK:([TestDB].[dbo].[idx_test].[b]=(42)) ORDERED FORWARD) | 1 | 5 | 4 | Index Seek | Index Seek | OBJECT:([TestDB].[dbo].[idx_test].[idx_b]), SEEK:([TestDB].[dbo].[idx_test].[b]=(42)) ORDERED FORWARD
| |--Index Seek(OBJECT:([TestDB].[dbo].[idx_test].[idx_a]), SEEK:([TestDB].[dbo].[idx_test].[a]=(24)) ORDERED FORWARD) | 1 | 6 | 4 | Index Seek | Index Seek | OBJECT:([TestDB].[dbo].[idx_test].[idx_a]), SEEK:([TestDB].[dbo].[idx_test].[a]=(24)) ORDERED FORWARD
|--RID Lookup(OBJECT:([TestDB].[dbo].[idx_test]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD) | 1 | 16 | 2 | RID Lookup | RID Lookup | OBJECT:([TestDB].[dbo].[idx_test]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD
これは最も効率的なインデックスアクセスですか?おそらくそうではありませんが、それはおそらく、インデックスのオーバーヘッドとクエリの柔軟性との間の最良の妥協点です。
理論を少し脇に置くと、より広いインデックスで消失するリターンが見られる可能性が高いため、たとえば、2列の組み合わせごとにインデックスのみを提供する方がよい場合があります。
とにかくクラスター化されたインデックスを参照してプルアウトする必要がある場合は特に、a=1 AND b=2
でインデックスを検索するよりも(a,b)
でインデックスを検索する場合に比べてc=3
のインデックスの検索よりも(a,b,c)
の検索のほうが大幅に効率が悪い(またはまったく効率が悪い)かどうかをチェックします。フィルタリング句にないデータ?クラスター化インデックスで数ページの読み取りを保存する可能性がありますが、インデックスが小さいほど各インデックスページがより多くの行を保持するため、インデックスでページ読み取りを保存する可能性があります。また、ディスク上のスペースを節約することで、より効率的に使用できます。バッファーキャッシュ(DBがそれほど小さくないと想定している場合RAMしたがって、これは重要です)少数の通常のクエリでのみ使用されるより広いインデックスのページはありません。
その場合の答えはn * (n-1) /2
であるか、列の1つがクラスタリングキー(n * (n-1) / 2) - 1
であるかどうかです。
本当に幅の広いテーブルの場合は、3つまたはすべての列の組み合わせに対して、1つのインデックスにまで達する可能性があります。
もちろん、列の選択性にも違いが生じ、ユーザーが特定の組み合わせをクエリする可能性が高くなります。さらに別の考慮事項は、ユーザーが等値以外のフィルターを指定できるようにする場合です。複合インデックスが大なり検索または小なり検索に直面した場合の効率は、インデックスの順序によって異なります。 ASC
&DESC
も考慮に入れますか?
特定のデータベースの場合、一部はスキップスキャン検索をサポートしているため、最初の列がフィルタリング句で見つからないインデックスを利用できます(Oracleは、IIRC Postgresは、SQL Serverはパーティションインデックスを使用する場合があります)。これにより、一部の組み合わせ、特に幅広い組み合わせの有用性のバランスが再び変わる可能性があります。
二。
最初に、すべてのフィールド値をすべての組み合わせで列順に組み合わせます。つまり、(a、b)、(a、c)..(c、d)、(a、b、c)、(a、b、d)..(a、b、c、 d)。この結合された値をハッシュします。ハッシュを最初のインデックスのキーとして使用します。このインデックスは、データテーブルに一意の代理キーを提供します。そのサロゲートを使用して、データ行を見つけます。 2番目のインデックスは、データテーブルの代理キーにあります。
例として、私のデータテーブルに次の行があるとします。
+----+--------+--------+--------+-------+
| id | a | b | c | d |
+----+--------+--------+--------+-------+
| 1 | foo | bar | baz | qux |
| 2 | foo | quux | quuz | corge |
| 3 | wibble | wobble | wubble | flob |
| 4 | blep | blah | boop | blem |
+----+--------+--------+--------+-------+
Table 1: data
特に、行1と2の列「a」の値が同じであることに注意してください。
したがって、行ID 1の場合、次のハッシュを生成します(私の魔法のハッシュ関数を使用)。
+--------------+-------------+
| value | hash |
+--------------+-------------+
| foo | 2085198204 |
| bar | 1242944992 |
| .. | |
| foobar | 1242667234 |
| .. | |
| foobarbaz | -1417643972 |
| .. | |
| barbaz | -1478044229 |
| .. | |
| foobarbazqux | 54278474 |
+--------------+-------------+
そして、行ID 2はこれらのハッシュを生成します
+------------------+-------------+
| value | hash |
+------------------+-------------+
| foo | 2085198204 |
| quux | -406244122 |
| quuz | -821744750 |
| corge | -4773002 |
| fooquux | -237289772 |
| fooquuz | -1705377365 |
| .. | |
| fooquuxquuzcorge | 174363790 |
+------------------+-------------+
行1と2は列 "a"に同じ値を持っているので、その列だけの値がハッシュされたときに同じハッシュ出力を生成します。
これで、ハッシュ/ IDマッピングをテーブルに入力できます。
+-------------+----+
| hash | id |
+-------------+----+
| 2085198204 | 1 |
| 1242944992 | 1 |
| 1242667234 | 1 |
| -1417643972 | 1 |
| -1478044229 | 1 |
| 54278474 | 1 |
| 2085198204 | 2 |
| -406244122 | 2 |
| -821744750 | 2 |
| -4773002 | 2 |
| -237289772 | 2 |
| -1705377365 | 2 |
| 174363790 | 2 |
+-------------+----+
Table 2: hash/id map
..加えて、以前のハッシュ計算の例からスキップした他のもの。
クエリが入力されます。述語は "a = foo"です。値を取得してハッシュし、2085198204を取得します。表2 where hash = 2085198204
を読み取ります。 id 1とid 2の2つの行を取得します。テーブル1 where id = 1 or id = 2
を読み取り、完全な行を取得します。
2番目のクエリが到着します。述語 "c = baz and b = bar"があります。列の順序で値を並べ替え、-1478044229にハッシュされる「barbaz」を取得します。このハッシュ値のテーブル2を読み取ると、IDは1になります。
値が複数の列で発生する可能性がある場合(おそらく「foo」が列aだけでなく列dでも発生する可能性があります)、上記の単純なスキームは誤検知を返す可能性があります。データ行の二次チェックは面倒です。ハッシュに列を含める方が良いと思うので、「a = foo」は「d = foo」よりも異なる値にハッシュします。
ifとonly ifそれらが最初の列の場合、これはインデックスを使用します[...それ以外の場合]インデックスは使用されません。レコードは、列aにある値に応じて、インデックス全体に分散されます。
それは単に真実ではありません。たとえばPostgreSQLでは、これは適用されません。使用した例に従って、(a, b, c, d)
にインデックスがあり、実行したいとします。
SELECT * FROM my_table
WHERE c = 7 AND a = 'foo';
それでもインデックスを使用します。実際には、
SELECT * FROM my_table
WHERE c = 7 AND b = 5;
また、プランナが適切と判断した場合は、インデックスを使用します。証拠が必要ですか?このサンプルデータを使用して、上記のクエリを実行できます。これらはすべてインデックススキャンになります。
CREATE TABLE my_table
AS
SELECT
CASE WHEN x%3=0 THEN 'foo' END AS a,
x::int % 7 AS b,
x::int % 5 AS c,
x AS d
FROM generate_series(1,1e6)
AS gs(x);
CREATE INDEX ON my_table (a,b,c,d);
ANALYZE my_table;
SET enable_seqscan = 0; -- just to be sure it uses them =)
PostgreSQLのマルチカラムインデックスのドキュメント
複数列のBツリーインデックスは、インデックスの列のサブセットを含むクエリ条件で使用できますが、先頭(左端)の列に制約がある場合に最も効率的です。正確なルールは、先行列の等価制約と、等価制約のない最初の列の不等制約を使用して、スキャンされるインデックスの部分を制限することです。これらの列の右側にある列の制約はインデックスでチェックされるため、テーブルへの訪問を適切に保存できますが、スキャンする必要のあるインデックスの部分は削減されません。たとえば、(a、b、c)のインデックスとクエリ条件WHERE a = 5 AND b> = 42 AND c <77の場合、a = 5とb =の最初のエントリからインデックスをスキャンする必要があります42 = a = 5の最後のエントリまで。c> = 77のインデックスエントリはスキップされますが、それでもスキャンする必要があります。 このインデックスは、aおよびbに制約がないクエリにも原則的に使用できますが、インデックス全体をスキャンする必要があります、ほとんどの場合、プランナはインデックスを使用するよりも順次テーブルスキャンを優先します。
私が強調を加えました。これはifであり、ifだけではありません。本質的に、左端の条件を使用すると、インデックスのスキャンにかかるコストの見積もりを大幅に削減できますが、インデックス全体をスキャンする方が、シーケンシャルスキャンでテーブルにアクセスするよりも安くなる可能性があります。
この情報を使用して、リクエストを確認しましょう。質問では、あなたは上のインデックスを求めます
(a, c)
(a, d)
ただし、(a、c、d)のインデックスは(a,c)
を正常にカバーし、(a,d)
のworstケースシナリオは、 d
を検索するための(a,c)
の全範囲。
そのようなことは、この質問全体の根拠に欠陥を与えます。次に、タスクに実際に必要なインデックスは何ですか?
質問の要点として、インデックスを作成するこの形式は非常に問題があります。すべてのインデックスは、挿入と更新を遅くし、行のヒープのみの更新を無効にします-少なくともその最適化を持つPostgreSQLでは。カスタマイズされたインデックスの作成があまり習得したくないスキルである場合は、誰かに手伝ってもらうか、 dexter のようなツールを使用します。
この問題の数学的な解決策が必要な場合、私の提案は Mathmatics.SE です。私はそれが組み合わせ論に該当すると思います。