特定のテーブルの多くの文字列フィールドを高速に検索するために、トライグラムを使用しようとしています。
それらを保持するための個別のテーブルと、それらを検索するためのクエリを作成しました(テーブル値関数での使用を目的としています)。
CREATE TABLE [dbo].[SearchTrigramTwoFieldKey]
(
[Ordinal] BIGINT NOT NULL,
[SearchCategoryId] INTEGER NOT NULL CONSTRAINT [FK__SearchTrigramTwoFieldKey_SearchCategoryId_To_dbo.SearchCategory_Id] FOREIGN KEY([SearchCategoryId]) REFERENCES [dbo].[SearchCategory]([Id]),
[SearchCategoryColumnId] INTEGER NOT NULL CONSTRAINT [FK__SearchTrigramTwoFieldKey_SearchCategoryColumnId_To_dbo.SearchCategoryColumn_Id] FOREIGN KEY([SearchCategoryColumnId]) REFERENCES [dbo].[SearchCategoryColumn]([Id]),
[TableId] INTEGER NOT NULL CONSTRAINT [FK__SearchTrigramTwoFieldKey_TableId_To_dbo.Table_Id] FOREIGN KEY([TableId]) REFERENCES [dbo].[Table]([Id]),
[RecordId1] BIGINT NOT NULL,
[RecordId2] BIGINT NOT NULL,
[Trigram] NVARCHAR(3) NOT NULL,
[IsLastTrigram] BIT NOT NULL,
[RecordColumnTrigramCount] INTEGER NOT NULL,
CONSTRAINT [PK__SearchTrigramTwoFieldKey_SearchCategoryId_SearchCategoryColumnId_TableId_RecordId1_RecordId2_Ordinal]
PRIMARY KEY
(
[SearchCategoryId] ASC,
[SearchCategoryColumnId] ASC,
[TableId] ASC,
[RecordId1] ASC,
[RecordId2] ASC,
[Ordinal] ASC
),
)
CREATE UNIQUE NONCLUSTERED INDEX [UNCI__SearchTrigramTwoFieldKey_IsLastTrigram] ON [dbo].[SearchTrigramTwoFieldKey]
(
[SearchCategoryId] ASC,
[SearchCategoryColumnId] ASC,
[TableId] ASC,
[RecordId1] ASC,
[RecordId2] ASC,
[IsLastTrigram] ASC
)
WHERE ([IsLastTrigram]=(1))
最後の2つのフィールドは、このテーブルに対する検索クエリで実行する必要がある計算量を削減して、パフォーマンスを向上させるための試みです。インデックスは、不良データに対する予防策です。
すべてのトライグラムを挿入した後、このテーブルには約6000万のレコードがあります。この数はほぼ確実に時間の経過とともに増加します。
それを検索するために、次のクエリを作成しました。
--Setting up query parameters:
DECLARE @SearchCategoryId INTEGER = 3
DECLARE @SearchCategoryColumnIds AS TABLE([Value] INTEGER NOT NULL)
DECLARE @searchValues AS TABLE([Value] NVARCHAR(4000))
INSERT INTO @searchValues([Value])
VALUES('Land'), ('Ireland')
--The query itself:
SELECT ROW_NUMBER() OVER (ORDER BY COUNT(CASE WHEN IsExactMatch = 1 THEN 1 END) DESC,
COUNT(*) DESC,
MIN(CASE WHEN IsExactMatch = 0 THEN MinMatchDistanceRowOrder END)) AS [MatchOrder],
RecordId1,
RecordId2
FROM
(
SELECT RecordId1, RecordId2,
IIF(MIN([T].T2Ordinal) = 1 AND MAX(CAST(T.T2IsLastTrigram AS INTEGER)) = 1, 1, 0) AS IsExactMatch,
ROW_NUMBER() OVER (ORDER BY MIN(T.T2TrigramCount - T1TrigramCount)) AS MinMatchDistanceRowOrder,
[SearchValue]
FROM
(SELECT T1.SearchValueNumber,
T1.SearchValue,
LAG(T1.Ordinal) OVER (PARTITION BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T1.SearchValueNumber ORDER BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T1.Ordinal)
AS T1OrdinalLag,
T1.Ordinal AS T1Ordinal,
LEAD(T1.Ordinal) OVER (PARTITION BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T1.SearchValueNumber ORDER BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T1.Ordinal)
AS T1OrdinalLead,
T1.NgramCount AS T1TrigramCount,
LAG(T2.Ordinal) OVER (PARTITION BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T1.SearchValueNumber ORDER BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T2.Ordinal, T2.Trigram)
AS T2OrdinalLag,
T2.Ordinal AS T2Ordinal,
LEAD(T2.Ordinal) OVER (PARTITION BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T1.SearchValueNumber ORDER BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T2.Ordinal, T2.Trigram)
AS T2OrdinalLead,
T2.IsLastTrigram AS T2IsLastTrigram,
MIN(T2.Ordinal) OVER (PARTITION BY T2.SearchCategoryId, T2.SearchCategoryColumnId, T2.TableId, T2.RecordId1, T2.RecordId2, T1.SearchValueNumber)
AS MinOrdinal,
T2.RecordColumnTrigramCount AS T2TrigramCount,
T2.SearchCategoryId,
T2.SearchCategoryColumnId,
T2.TableId,
T2.RecordId1,
T2.RecordId2
FROM dbo.SearchTrigramTwoFieldKey AS T2
INNER JOIN
(
SELECT [Value] FROM @SearchCategoryColumnIds
UNION ALL
SELECT NULL) AS scc ON NOT EXISTS(SELECT TOP 1 [Value] FROM @SearchCategoryColumnIds) OR T2.SearchCategoryColumnId = [Value]
INNER JOIN
(
SELECT SearchValueNumber, SearchValue, ngrams.Ordinal, ngrams.Ngram, ngrams.IsLastNgram, ngrams.NgramCount
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY [Value]) AS SearchValueNumber, *
FROM
(
SELECT DISTINCT [Value] AS SearchValue, *
FROM @searchValues
) AS T
) AS [sv]
CROSS APPLY dbo.fnGenerateNgrams([sv].[Value], DEFAULT) AS ngrams
) AS T1 ON T1.Ngram = T2.Trigram
WHERE T2.SearchCategoryId = @SearchCategoryId) AS T
WHERE
(
( T1OrdinalLead IS NULL OR T1OrdinalLead = T1Ordinal+1)
OR (T1OrdinalLag IS NULL OR T1OrdinalLag = T1Ordinal-1)
)
AND
(
( T2OrdinalLead IS NULL OR T2OrdinalLead = T2Ordinal+1)
OR (T2OrdinalLag IS NULL OR T2OrdinalLag = T2Ordinal-1)
)
AND T2TrigramCount >= T1TrigramCount
GROUP BY SearchCategoryId, SearchCategoryColumnId, TableId, RecordId1, RecordId2, [SearchValue]
HAVING COUNT(*) >= (SELECT TOP 1 NGramCount FROM dbo.fnGenerateNgrams([SearchValue], DEFAULT))
) AS T
GROUP BY RecordId1, RecordId2
HAVING COUNT(DISTINCT [SearchValue]) = (SELECT COUNT(DISTINCT [Value]) FROM @searchValues)
ORDER BY MatchOrder ASC
OPTION(RECOMPILE)
クエリに関するいくつかのメモ:
このクエリは、このテーブルをクエリする唯一のクエリです。更新されたデータを更新するために、設定された期間ごとにデータの挿入と削除が行われますが、それらの速度は現時点では特に問題ではありません。
実行時間は、指定された検索値に応じて大きく異なります。たとえ単一の値であっても(6秒程度のものもあれば、2語で5分ほどかかるものもあります)、疑っています(確かではありません)。最終的に完全に一致していなくても、トライグラムのどれだけのデータsomeが一致するかが原因です。
SSMSとPlan Explorerで実行プランを見ると、時間を浪費しているようなものだと思いますが、インデックスを使用してこれを適切に修正する方法がわかりません。
これらは、実行速度を向上させるためにトライグラムテーブルでこれまでに作成したインデックス(そのプライマリクラスター化インデックスと上記の一意の非クラスター化インデックスに加えて)です。
CREATE NONCLUSTERED INDEX [NCI__SearchTgramTwoFieldKey_SearchCategoryColumnId_TableId_RecordId1_RecordId2_Ordinal_IsLastTgram_RecordColumnTgramCount_Tgram] ON [dbo].[SearchTrigramTwoFieldKey]
(
[SearchCategoryColumnId] ASC,
[TableId] ASC,
[RecordId1] ASC,
[RecordId2] ASC,
[Ordinal] ASC,
[IsLastTrigram] ASC,
[RecordColumnTrigramCount] ASC,
[Trigram] ASC
)
CREATE NONCLUSTERED INDEX [NCI__SearchTrigramTwoFieldKey_SearchCategoryColumnId_TableId_RecordId1_RecordId2] ON [dbo].[SearchTrigramTwoFieldKey]
(
[SearchCategoryColumnId] ASC,
[TableId] ASC,
[RecordId1] ASC,
[RecordId2] ASC
)
CREATE NONCLUSTERED INDEX [NCI__SearchTrigramTwoFieldKey_SearchCategoryColumnId_TableId_RecordId1_RecordId2_Ordinal] ON [dbo].[SearchTrigramTwoFieldKey]
(
[SearchCategoryColumnId] ASC,
[TableId] ASC,
[RecordId1] ASC,
[RecordId2] ASC,
[Ordinal] ASC
)
CREATE NONCLUSTERED INDEX [NCI__SearchTrigramTwoFieldKey_SearchCategoryId_Trigram__Include_IsLastTrigram_RecordColumnTrigramCount] ON [dbo].[SearchTrigramTwoFieldKey]
(
[SearchCategoryId] ASC,
[Trigram] ASC
)
INCLUDE ( [IsLastTrigram], RecordColumnTrigramCount])
これらの4つのインデックスのうち、最後に作成したのは、私が作成を推奨したインデックスだけです。その他はすべて、パフォーマンスを向上させるための実験的なものです。
実行計画: https://www.brentozar.com/pastetheplan/?id=HyFZDlTDI
私の努力にもかかわらず、パフォーマンスはまだ私が望むところから遠いです。可能な限り実行時間を高速化したいのですが、1つ以上の検索語が1秒もかからない最良のシナリオでは、それがどれほど実行可能かわかりません。
これに適切に対処する方法を理解するのに十分な索引付けの知識がありません(その索引付けisこれに対処する正しい方法を想定しています)。ここでパフォーマンスを向上させるために何ができるか(そしてなぜパフォーマンスが向上するのか)を、適切なインデックス付けによって、または可能な場合は機能を維持しながらクエリを向上させることによって学びたいと思っています。
クエリとテーブル定義が含まれていますが、それらが、私が存在していないことに気づかされた恐ろしい(しかし修正可能な)非効率性が明らかになった場合に備えています。
ここでの索引付けは(主な)問題ではないと思います。
その実行プランには、タイミングに関連するいくつかの奇妙で厄介なことが含まれています。 1つ目は、期間とCPUの違いです。
<QueryTimeStats CpuTime="93275" ElapsedTime="315874" />
クエリは5分間実行されましたが、(DOP 1で)1.5分のCPU時間しか使用していませんでした。この違いは、SQL Serverが一部の共有リソースで待機中であり、クエリの実行が進行していないことを意味します。
一部の待機統計は、実行計画で取得されます。
<WaitStats>
<Wait WaitType="RESOURCE_GOVERNOR_IDLE" WaitTimeMs="103626" WaitCount="35266" />
<Wait WaitType="PAGELATCH_EX" WaitTimeMs="77512" WaitCount="2742411" />
<Wait WaitType="PAGELATCH_SH" WaitTimeMs="66027" WaitCount="2037681" />
<Wait WaitType="SOS_SCHEDULER_YIELD" WaitTimeMs="7798" WaitCount="2440" />
<Wait WaitType="RESERVED_MEMORY_ALLOCATION_EXT" WaitTimeMs="41" WaitCount="38422" />
</WaitStats>
103秒以上のRESOURCE_GOVERNOR_IDLE
待機があります。通常は、サーバーの構成を確認し、次のようなクエリを使用してCPUの割り当てが行われる限り、過度に上限が設定されていないことを確認することをお勧めします。
SELECT
rgrp.[name],
rgrp.min_cpu_percent,
rgrp.max_cpu_percent,
rgrp.cap_cpu_percent
FROM sys.dm_resource_governor_resource_pools rgrp;
Azure SQL Databaseを使用しているため、代わりに、より多くのコンピューティングを備えた階層にアップグレードする必要があります。プランXMLでもこれに気づきました。
NonParallelPlanReason="EstimatedDOPIsOne"
最小のvCoreオプションは2だと思うので、これは最小のDTUモデル製品(S3未満)を使用していることを意味します。
RESOURCE_GOVERNOR_IDLE
の待機がより許容できるレベルに減少するまで、データベースを一度に1層ずつスケールアップしてみます。
注:これはSOS_SCHEDULER_YIELD
の7秒にも貢献している可能性があります。
また、143秒のラッチ待機があります。通常、これはある種のtempdbの競合であると思われますが、このクエリでtempdbが使用されていることを示す証拠はあまりありません(〜200 MBのハッシュスピルが1つあり、小さなスプールがいくつかあります)。
CPUキャップの問題を考えると、この予想外に高いレベルのラッチ待機は、使用されているAzureサービス層にも関連していると思います。
上記の246秒の待機時間を差し引くと、クエリの実行時間が315秒から69秒に短縮されます。それはまだ驚くべきことではありませんが、確かに5分よりも優れています。上位のAzureサービス層では、並列実行のメリットも得られ、ランタイムがさらに短縮されます。
この問題で「ハードウェアを投げる」ことを望まず、別のアプローチに興味がある場合、Paul Whiteは非常にパフォーマンスを重視したトライグラム検索関数を作成し、ここで共有しました。 SQLでのトライグラムワイルドカード文字列検索サーバー
もちろん、これは基本的にアプローチ全体を変えるものなので、これまでに行った作業を破棄/書き換えすることの利点とAzureの費用を増やすことの利点を比較検討する必要があります。
Conor Cunninghamは、実験として、行モードで新しいバッチモードを使用するか、テーブルに列ストアインデックスを作成して、バッチモードを活用することを提案しました。
...行ストアに最近の互換レベルでバッチモードを追加したので、それを考慮してください。ただし、DOPが高いほど、より多くのメリットが得られます。また、列ストアインデックスは、検討すべき実験かもしれません...