この質問は IP範囲検索の最適化? に似ていますが、SQL Server 2000に限定されています。
以下のように構造化され、入力されたテーブルに一時的に1000万個の範囲が格納されているとします。
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万行近くが読み取られたことを示しています。
このパフォーマンスを改善できますか?テーブルまたは追加のインデックスの再構築は問題ありません。
列ストアは、テーブルの半分をスキャンする非クラスター化インデックスと比較して、ここでは非常に大量です。非クラスター化列ストアインデックスはほとんどの利点を提供しますが、順序付けされたデータをクラスター化列ストアインデックスに挿入することはさらに優れています。
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ミリ秒かかります。
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
の個別の値がすべてわかっている場合は、DiffOfColumns
の範囲フィルターを使用してRangeTo
のすべての値に対してシークを実行し、関連するすべてのデータを取得できます。たとえば、DiffOfColumns
= 2であることがわかっている場合、RangeFrom
に許可される値は49999998、49999999、50000000のみです。再帰を使用して、DiffOfColumns
および256しかないため、データセットに適しています。以下のクエリは私のマシンで約6 msかかります:
_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);
_
すべての個別の値のインデックスシークとともに、通常の再帰部分を確認できます。
このアプローチの欠点は、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ミリ秒のCPUが使用されます。 PAGELATCH_SH待機とその他の悪いことが起こっています。差分値をバケット化して一意の値の数を制御下に保ち、_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ミリ秒かかります。
Paul Whiteは、 Itzik Ben Ganによる興味深い記事 へのリンクを含む同様の質問への回答を指摘しました。これは、これを効率的に実行できるようにする「静的リレーショナル間隔ツリー」モデルについて説明しています。
要約すると、このアプローチには、行の間隔値に基づいて計算された(「フォークノード」)値を格納することが含まれます。別の範囲と交差する範囲を検索する場合、一致する行に必要な可能なフォークノード値を事前計算し、これを使用して最大31のシーク操作で結果を見つけることができます(以下は0から最大の符号付き32の範囲の整数をサポートしています)ビット整数)
これに基づいて、次のように表を再構成しました。
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 stats。
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を使用します。
範囲を表す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ミリ秒対252ミリ秒です。
実行計画は以下のようにより複雑です
書き換えが確実にパフォーマンスを向上させる唯一のケースは、コールドキャッシュです。
したがって、この場合はがっかりし、この書き換えを推奨することは困難ですが、否定的な結果の公開も役立つ可能性があります。
新しいロボットオーバーロードへのオマージュとして、新しいRとPythonの機能のいずれかがここで私たちを助けることができるかどうかを確認することにしました。うまくいき、正しい結果を返すことができました。知識が豊富な人がいれば、遠慮なく私を殴ってください。
これを行うには、4つのコアと16 GBのRAMを備えたVMをセットアップしました。これで約200MBのデータセットを処理するには十分だと思います。
ボストンには存在しない言語から始めましょう!
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.
実行計画 は、非常に興味深いものではありませんが、中間の演算子が私たちに名前を呼ばなければならない理由はわかりません。
次は、クレヨンでコーディング!
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を削除するのが待ちきれません。
計算列を使用してかなり良い解決策を見つけましたが、それは単一の値に対してのみ良いです。そうは言っても、魔法の価値があれば、それで十分でしょう。
指定したサンプルから始めて、テーブルを変更します。
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.
そして、これが query plan です。
私の解決策は、間隔に既知の最大幅[〜#〜] 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.8 GHz、16 GB 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の間に相関関係がないためです。したがって、この句をクエリに追加してもメリットはありません。
このアプローチは、間隔のサイズが小さいことから、ほとんどのメリットを得られます。可能な間隔サイズが大きくなると、スキップされる低い値の行の数は減少しますが、一部はスキップされます。限定的なケースでは、データ範囲と同じくらいの間隔で、このアプローチは元のクエリ(これは私が認める寒さの快適さ)よりも悪くはありません。
この回答に存在する可能性のあるオフバイワンエラーをお詫び申し上げます。