SQL Serverでの数値範囲(間隔)検索の最適化


18

この質問は、IP範囲検索の最適化に似ていますか?ただし、その1つはSQL Server 2000に制限されています。

次のように構造化され、入力されたテーブルに1,000万個の範囲が暫定的に保存されているとします。

CREATE TABLE MyTable
(
Id        INT IDENTITY PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom),
INDEX IX1 (RangeFrom,RangeTo),
INDEX IX2 (RangeTo,RangeFrom)
);

WITH RandomNumbers
     AS (SELECT TOP 10000000 ABS(CRYPT_GEN_RANDOM(4)%100000000) AS Num
         FROM   sys.all_objects o1,
                sys.all_objects o2,
                sys.all_objects o3,
                sys.all_objects o4)
INSERT INTO MyTable
            (RangeFrom,
             RangeTo)
SELECT Num,
       Num + 1 + CRYPT_GEN_RANDOM(1)
FROM   RandomNumbers 

値を含むすべての範囲を知る必要があります50,000,000。私は次のクエリを試します

SELECT *
FROM MyTable
WHERE 50000000 BETWEEN RangeFrom AND RangeTo

SQL Serverは、10,951の論理読み取りがあり、12の一致する行を返すために約500万行が読み取られたことを示しています。

ここに画像の説明を入力してください

このパフォーマンスを改善できますか?テーブルまたは追加のインデックスの再構築は問題ありません。


テーブルの設定を正しく理解している場合、各範囲の「サイズ」に制約はなく、乱数を一様に選択して範囲を形成しています。そして、プローブは全体の範囲1..100Mの中間にあります。その場合-一様なランダム性による明らかなクラスタリングはありません-下限または上限のインデックスが役立つ理由がわかりません。説明できますか?
davidbak

@davidbakこのテーブルの従来のインデックスは、最悪の場合、範囲の半分をスキャンする必要があるため、実際にはあまり役に立ちません。したがって、潜在的な改善を求めます。SQL Server 2000のリンクされた質問には、containsクエリをサポートし、他のデータを追加するように見えるデータ量を減らすのに役立つ空間インデックスが役立つことを願っています。これに対抗するオーバーヘッド。
マーティンスミス

私はそれを試す機能はありません-しかし、2つのインデックス-下限に1つ、上限に1つ-そして内部結合-がクエリオプティマイザーでうまくいくのではないかと思います。
davidbak

回答:


11

ここでは、テーブルの半分をスキャンする非クラスター化インデックスと比較して、Columnstoreが非常に役立ちます。非クラスター化列ストアインデックスはほとんどの利点を提供しますが、クラスター化列ストアインデックスに順序付けされたデータを挿入することはさらに優れています。

DROP TABLE IF EXISTS dbo.MyTableCCI;

CREATE TABLE dbo.MyTableCCI
(
Id        INT PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom),
INDEX CCI CLUSTERED COLUMNSTORE
);

INSERT INTO dbo.MyTableCCI
SELECT TOP (987654321) *
FROM dbo.MyTable
ORDER BY RangeFrom ASC
OPTION (MAXDOP 1);

設計上、RangeFrom列の行グループを削除することで、行グループの半分を削除できます。しかし、データの性質上、RangeTo列の行グループも削除されます。

Table 'MyTableCCI'. Segment reads 1, segment skipped 9.

より多くの変数データを持つ大きなテーブルの場合、両方の列で可能な限り最適な行グループの削除を保証するためにデータをロードするさまざまな方法があります。特にデータの場合、クエリには1ミリ秒かかります。


2000年の制限なしに考慮すべき他のアプローチを間違いなく探しています。そのような音は打たれます。
マーティンスミス

9

ポール・ホワイトは、Itzik Ben Ganによる興味深い記事リンクを含む同様の質問への回答を指摘しました。これは、これを効率的に行うことができる「静的リレーショナルツリー」モデルについて説明しています。

要約すると、このアプローチには、行の間隔値に基づいて計算された(「フォークノード」)値を格納することが含まれます。別の範囲と交差する範囲を検索する場合、一致する行に必要な可能性のあるフォークノード値を事前計算し、これを使用して最大31のシーク操作で結果を見つけることができます(以下は0から最大符号付き32の範囲の整数をサポートしますビットint)

これに基づいて、以下のようにテーブルを再構築しました。

CREATE TABLE dbo.MyTable3
(
  Id        INT IDENTITY PRIMARY KEY,
  RangeFrom INT NOT NULL,
  RangeTo   INT NOT NULL,   
  node  AS RangeTo - RangeTo % POWER(2, FLOOR(LOG((RangeFrom - 1) ^ RangeTo, 2))) PERSISTED NOT NULL,
  CHECK (RangeTo > RangeFrom)
);

CREATE INDEX ix1 ON dbo.MyTable3 (node, RangeFrom) INCLUDE (RangeTo);
CREATE INDEX ix2 ON dbo.MyTable3 (node, RangeTo) INCLUDE (RangeFrom);

SET IDENTITY_INSERT MyTable3 ON

INSERT INTO MyTable3
            (Id,
             RangeFrom,
             RangeTo)
SELECT Id,
       RangeFrom,
       RangeTo
FROM   MyTable

SET IDENTITY_INSERT MyTable3 OFF 

そして、次のクエリを使用しました(記事は交差する間隔を探しているため、ポイントを含む間隔を見つけることはこの縮退したケースです)

DECLARE @value INT = 50000000;

;WITH N AS
(
SELECT 30 AS Level, 
       CASE WHEN @value > POWER(2,30) THEN POWER(2,30) END AS selected_left_node, 
       CASE WHEN @value < POWER(2,30) THEN POWER(2,30) END AS selected_right_node, 
       (SIGN(@value - POWER(2,30)) * POWER(2,29)) + POWER(2,30)  AS node
UNION ALL
SELECT N.Level-1,   
       CASE WHEN @value > node THEN node END AS selected_left_node,  
       CASE WHEN @value < node THEN node END AS selected_right_node,
       (SIGN(@value - node) * POWER(2,N.Level-2)) + node  AS node
FROM N 
WHERE N.Level > 0
)
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
  JOIN N AS L
    ON I.node = L.selected_left_node
    AND I.RangeTo >= @value
    AND L.selected_left_node IS NOT NULL
UNION ALL
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
  JOIN N AS R
    ON I.node = R.selected_right_node
    AND I.RangeFrom <= @value
    AND R.selected_right_node IS NOT NULL
UNION ALL
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
WHERE node = @value;

これは通常1ms、すべてのページがキャッシュ内にあるときにマシンで実行されます(IO統計を使用)。

Table 'MyTable3'. Scan count 24, logical reads 72, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 4, logical reads 374, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

そして計画

ここに画像の説明を入力してください

注:ソースはノードを参加させるために再帰CTEではなく複数ステートメントTVFを使用しますが、回答を自己完結させるために後者を選択しました。実稼働用には、おそらくTVFを使用します。


9

N / CCIアプローチと競合する行モードアプローチを見つけることができましたが、データについて何かを知る必要があります。との違いを含む列がRangeFromありRangeTo、それとともにインデックスを付けたと仮定しますRangeFrom

ALTER TABLE dbo.MyTableWithDiff ADD DiffOfColumns AS RangeTo-RangeFrom;

CREATE INDEX IXDIFF ON dbo.MyTableWithDiff (DiffOfColumns,RangeFrom) INCLUDE (RangeTo);

のすべての個別の値を知っていれば、範囲フィルターを使用してのすべてのDiffOfColumns値に対してシークを実行し、すべての関連データを取得できます。たとえば、= 2がわかっている場合、許可される値は49999998、49999999、および50000000のみです。再帰を使用して、のすべての個別の値を取得できます。私のマシンでは、次のクエリに約6ミリ秒かかります。DiffOfColumnsRangeToDiffOfColumnsRangeFromDiffOfColumns

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        DiffOfColumns
    FROM dbo.MyTableWithDiff AS T
    ORDER BY
        T.DiffOfColumns

    UNION ALL

    -- Recursive
    SELECT R.DiffOfColumns
    FROM
    (
        -- Number the rows
        SELECT 
            T.DiffOfColumns,
            rn = ROW_NUMBER() OVER (
                ORDER BY T.DiffOfColumns)
        FROM dbo.MyTableWithDiff AS T
        JOIN RecursiveCTE AS R
            ON R.DiffOfColumns < T.DiffOfColumns
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT ca.*
FROM RecursiveCTE rcte
CROSS APPLY (
    SELECT mt.Id, mt.RangeFrom, mt.RangeTo
    FROM dbo.MyTableWithDiff mt
    WHERE mt.DiffOfColumns = rcte.DiffOfColumns
    AND mt.RangeFrom >= 50000000 - rcte.DiffOfColumns AND mt.RangeFrom <= 50000000
) ca
OPTION (MAXRECURSION 0);

すべての個別の値のインデックスシークとともに通常の再帰部分を確認できます。

クエリプラン1

このアプローチの欠点は、の明確な値が多すぎると遅くなり始めることですDiffOfColumns。同じテストを行いましょうが、のCRYPT_GEN_RANDOM(2)代わりに使用しますCRYPT_GEN_RANDOM(1)

DROP TABLE IF EXISTS dbo.MyTableBigDiff;

CREATE TABLE dbo.MyTableBigDiff
(
Id        INT IDENTITY PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom)
);

WITH RandomNumbers
     AS (SELECT TOP 10000000 ABS(CRYPT_GEN_RANDOM(4)%100000000) AS Num
         FROM   sys.all_objects o1,
                sys.all_objects o2,
                sys.all_objects o3,
                sys.all_objects o4)
INSERT INTO dbo.MyTableBigDiff
            (RangeFrom,
             RangeTo)
SELECT Num,
       Num + 1 + CRYPT_GEN_RANDOM(2) -- note the 2
FROM   RandomNumbers;


ALTER TABLE dbo.MyTableBigDiff ADD DiffOfColumns AS RangeTo-RangeFrom;

CREATE INDEX IXDIFF ON dbo.MyTableBigDiff (DiffOfColumns,RangeFrom) INCLUDE (RangeTo);

同じクエリは、再帰部分から65536行を検出し、マシンで823 msのCPUを使用します。PAGELATCH_SHの待機があり、その他の悪いことが起こっています。diff値をバケット化して一意の値の数を制御し、のバケット化を調整することにより、パフォーマンスを改善できますCROSS APPLY。このデータセットでは、256個のバケットを試します。

ALTER TABLE dbo.MyTableBigDiff ADD DiffOfColumns_bucket256 AS CAST(CEILING((RangeTo-RangeFrom) / 256.) AS INT);

CREATE INDEX [IXDIFF😎] ON dbo.MyTableBigDiff (DiffOfColumns_bucket256, RangeFrom) INCLUDE (RangeTo);

余分な行を取得しないようにする1つの方法(現在、真の値ではなく丸められた値と比較している)は、以下をフィルタリングすることRangeToです。

CROSS APPLY (
    SELECT mt.Id, mt.RangeFrom, mt.RangeTo
    FROM dbo.MyTableBigDiff mt
    WHERE mt.DiffOfColumns_bucket256 = rcte.DiffOfColumns_bucket256
    AND mt.RangeFrom >= 50000000 - (256 * rcte.DiffOfColumns_bucket256)
    AND mt.RangeFrom <= 50000000
    AND mt.RangeTo >= 50000000
) ca

私のマシンでは、完全なクエリに6ミリ秒かかります。


8

範囲を表す別の方法の1つは、線上の点です。

以下は、geometryデータ型として表される範囲を持つすべてのデータを新しいテーブルに移行します。

CREATE TABLE MyTable2
(
Id INT IDENTITY PRIMARY KEY,
Range GEOMETRY NOT NULL,
RangeFrom AS Range.STPointN(1).STX,
RangeTo   AS Range.STPointN(2).STX,
CHECK (Range.STNumPoints() = 2 AND Range.STPointN(1).STY = 0 AND Range.STPointN(2).STY = 0)
);

SET IDENTITY_INSERT MyTable2 ON

INSERT INTO MyTable2
            (Id,
             Range)
SELECT ID,
       geometry::STLineFromText(CONCAT('LINESTRING(', RangeFrom, ' 0, ', RangeTo, ' 0)'), 0)
FROM   MyTable

SET IDENTITY_INSERT MyTable2 OFF 


CREATE SPATIAL INDEX index_name   
ON MyTable2 ( Range )  
USING GEOMETRY_GRID  
WITH (  
BOUNDING_BOX = ( xmin=0, ymin=0, xmax=110000000, ymax=1 ),  
GRIDS = (HIGH, HIGH, HIGH, HIGH),  
CELLS_PER_OBJECT = 16); 

値を含む範囲を見つけるための同等のクエリ50,000,000は次のとおりです。

SELECT Id,
       RangeFrom,
       RangeTo
FROM   MyTable2
WHERE  Range.STContains(geometry::STPointFromText ('POINT (50000000 0)', 0)) = 1 

この読み取りは10,951、元のクエリの改善を示しています。

Table 'MyTable2'. Scan count 0, logical reads 505, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'extended_index_1797581442_384000'. Scan count 4, logical reads 17, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

ただし、経過時間に関しては、元の製品と比べて大幅な改善はありません。典型的な実行結果は、250 ms対252 msです。

実行計画は次のように複雑です

ここに画像の説明を入力してください

書き換えの信頼性が向上する唯一のケースは、コールドキャッシュを使用する場合です。

したがって、この場合は期待はずれであり、この書き換えを推奨することは困難ですが、否定的な結果を公開することも有用です。


5

新しいロボットの大君たちへのオマージュとして、私はここで新しいRとPythonの機能が役立つかどうかを確認することにしました。少なくとも、作業して正しい結果を返すスクリプトについては、答えはノーです。より良い知識を持っている人が来たら、私をたたいてください。私の料金は合理的です。

これを行うには、4つのコアと16 GBのRAMを備えたVMをセットアップします。これは、200 MBまでのデータセットを処理するには十分だと思います。

ボストンには存在しない言語から始めましょう!

R

EXEC sp_execute_external_script 
@language = N'R', 
@script = N'
tweener = 50000000
MO = data.frame(MartinIn)
MartinOut <- subset(MO, RangeFrom <= tweener & RangeTo >= tweener, select = c("Id","RangeFrom","RangeTo"))
', 
@input_data_1_name = N'MartinIn',
@input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable',
@output_data_1_name = N'MartinOut',
@parallel = 1
WITH RESULT SETS ((ID INT, RangeFrom INT, RangeTo INT));

これは悪い時期でした。

Table 'MyTable'. Scan count 1, logical reads 22400

 SQL Server Execution Times:
   CPU time = 3219 ms,  elapsed time = 5349 ms.

実行計画は、中央のオペレータが私たちの名前を呼び出す必要があり、なぜ私にはわからないものの、かなりつまらないです。

ナッツ

次に、クレヨンでコーディングしましょう!

Python

EXEC sp_execute_external_script 
@language = N'Python', 
@script = N'
import pandas as pd
MO = pd.DataFrame(MartinIn)
tweener = 50000000
MartinOut = MO[(MO.RangeFrom <= tweener) & (MO.RangeTo >= tweener)]
', 
@input_data_1_name = N'MartinIn',
@input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable',
@output_data_1_name = N'MartinOut',
@parallel = 1
WITH RESULT SETS ((ID INT, RangeFrom INT, RangeTo INT));

あなたがそれがRより悪くなることができないと思ったとき:

Table 'MyTable'. Scan count 1, logical reads 22400

 SQL Server Execution Times:
   CPU time = 3797 ms,  elapsed time = 10146 ms.

別の口汚い実行計画

ナッツ

うーんとハマー

これまでのところ、私は感心していません。このVMを削除するのが待ちきれません。


1
たとえば、パラメータを渡すこともできますが、DECLARE @input INT = 50000001; EXEC dbo.sp_execute_external_script @language = N'R', @script = N'OutputDataSet <- InputDataSet[which(x >= InputDataSet$RangeFrom & x <= InputDataSet$RangeTo) , ]', @parallel = 1, @input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable;', @params = N'@x INT', @x = 50000001 WITH RESULT SETS ( ( Id INT NOT NULL, RangeFrom INT NOT NULL, RangeTo INT NOT NULL ));パフォーマンスはあまり良くありません。SQLでできないこと、たとえば何かを予測したい場合にRを使用します。
wBob

4

計算列を使用してかなり良いソリューションを見つけましたが、単一の値に対してのみ有効です。そうは言っても、魔法の価値があれば、それで十分かもしれません。

指定されたサンプルから始めて、テーブルを変更します。

ALTER TABLE dbo.MyTable
    ADD curtis_jackson 
        AS CONVERT(BIT, CASE 
                            WHEN RangeTo >= 50000000
                            AND RangeFrom < 50000000
                            THEN 1 
                            ELSE 0 
                        END);

CREATE INDEX IX1_redo 
    ON dbo.MyTable (curtis_jackson) 
        INCLUDE (RangeFrom, RangeTo);

クエリは次のようになります。

SELECT *
FROM MyTable
WHERE curtis_jackson = 1;

開始クエリと同じ結果を返します。実行プランをオフにした場合の統計は次のとおりです(簡潔にするために省略されています)。

Table 'MyTable'. Scan count 1, logical reads 3...

SQL Server Execution Times:
  CPU time = 0 ms,  elapsed time = 0 ms.

そして、ここにあるクエリプランは

ナッツ


インデックスがオンの場合、計算カラム/フィルターインデックスの模倣を克服できませんWHERE (50000000 BETWEEN RangeFrom AND RangeTo) INCLUDE (..)か?
ypercubeᵀᴹ

3
@yper-crazyhat-c​​ubeᵀᴹ-はい。CREATE INDEX IX1_redo ON dbo.MyTable (curtis_jackson) INCLUDE (RangeFrom, RangeTo) WHERE RangeTo >= 50000000 AND RangeFrom <= 50000000動作します。そして、クエリはSELECT * FROM MyTable WHERE RangeTo >= 50000000 AND RangeFrom <= 50000000;それを使用しています-ので、その後はない貧しい人々カーティスのための多くの必要性は
マーティン・スミス

3

私の解決策は、間隔に既知の最大幅Wがあるという観察に基づいています。サンプルデータの場合、これは1バイトまたは256整数です。したがって、指定された検索パラメーター値Pに対して、結果セットに含まれる可能性がある最小のRangeFromはP-Wであることがわかります。これを述語に追加すると、

declare @P int = 50000000;
declare @W int = 256;

select
    *
from MyTable
where @P between RangeFrom and RangeTo
and RangeFrom >= (@P - @W);

元のセットアップとクエリを使用すると、マシン(64ビットWindows 10、4コアハイパースレッドi7、2.8GHz、16GB RAM)が13行を返します。そのクエリは、(RangeFrom、RangeTo)インデックスの並列インデックスシークを使用します。修正されたクエリは、同じインデックスで並列インデックスシークも実行します。

元のクエリと改訂されたクエリの測定値は次のとおりです。

                          Original  Revised
                          --------  -------
Stats IO Scan count              9        6
Stats IO logical reads       11547        6

Estimated number of rows   1643170  1216080
Number of rows read        5109666       29
QueryTimeStats CPU             344        2
QueryTimeStats Elapsed          53        0

元のクエリの場合、読み取られる行の数は、@ P以下の行の数に等しくなります。クエリオプティマイザー(QO)には代替手段はありませんが、これらの行が述語を満たすかどうかを事前に判断できないため、すべてを読み取ります。(RangeFrom、RangeTo)の複数列インデックスは、最初のインデックスキーと適用可能な2番目のインデックスキーの間に相関関係がないため、RangeToと一致しない行を削除するのに役立ちません。たとえば、最初の行の間隔は短くて削除されますが、2番目の行の間隔は長くなって返されますが、その逆も同様です。

1つの失敗した試行で、チェック制約を通じてその確実性を提供しようとしました。

alter table MyTable with check
add constraint CK_MyTable_Interval
check
(
    RangeTo <= RangeFrom + 256
);

違いはありませんでした。

データ分布に関する外部の知識を述語に組み込むことで、QOが結果セットの一部となることのできない低い値のRangeFrom行をスキップし、インデックスの先頭の列を許容される行に移動させることができます。これは、クエリごとに異なるシーク述部に表示されます。

ミラー引数では、RangeToの上限はP + Wですです。ただし、これは有用ではありません。RangeFromとRangeToの間に相関関係がなく、複数列インデックスの末尾の列で行を削除できるためです。したがって、この句をクエリに追加してもメリットはありません。

このアプローチは、間隔サイズが小さいという利点を最大限に活用します。可能な間隔サイズが大きくなると、スキップされる値の低い行の数が減少しますが、一部はスキップされます。データ範囲と同じ間隔の制限のある場合、このアプローチは元のクエリよりも悪くはありません(これは私が認める冷静さです)。

この回答に1つずつのオフバイエラーが存在する可能性があることをおIびします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.