次の簡略化されたテーブルを考えてみます。
CREATE TABLE dbo.words
(
id bigint NOT NULL IDENTITY (1, 1),
Word varchar(32) NOT NULL,
hits int NULL
)
CREATE TABLE dbo.items
(
id bigint NOT NULL IDENTITY (1, 1),
body varchar(256) NOT NULL,
)
words
テーブルには約9000レコードが保持され、それぞれに1つの単語( 'phone'、 'sofa'、 'house'、 'dog'、...)が保持されますitems
テーブルには約12000のレコードが保持されますそれぞれ256文字以下の本文テキストを持つレコード。
次に、words
テーブルを更新し、_Wordフィールドのテキストを(少なくとも1回)保持するitems
テーブル内のレコード数をカウントする必要があります。 。部分的な単語を説明する必要があるので、これらの4つのレコードすべてをWordとしてカウントする必要がありますdog:
'This is my dog'
'I really like the movie dogma'
'my cousin has sheepdogs'
'dog dog dog doggerdy dog dog'
最後の例は、1つのレコードとして数える必要があります(「犬」という用語が少なくとも1回含まれています)。
このクエリを使用できます:
UPDATE dbo.words
SET hits = (SELECT COUNT(*) FROM dbo.items WHERE body like '%' + Word + '%')
しかし、これは非常に遅く、私が持っているそれほど重いサーバーでは完了までに10分以上かかります。
AFAIKインデックスは役に立ちません。私はLIKE検索を行っています。また、検索用語の先頭、末尾、または検索語を含む単語を探しているため、フルテキストは役に立たないと思います。私はここで間違っている可能性があります。
これをスピードアップする方法についてアドバイスはありますか?
主要なワイルドカードLIKE
検索を高速化するために私が見つけた最良の方法は、n-gramを使用することです。 SQL Serverでのトライグラムワイルドカード文字列検索 でテクニックを説明し、サンプル実装を提供します。
トライグラム検索の基本的な考え方は非常に簡単です。
- ターゲットデータの3文字の部分文字列(トライグラム)を保持します。
- 検索語をトライグラムに分割します。
- 保存されたトライグラムに対して検索トライグラムを照合します(等価検索)。
- 修飾された行を交差させて、すべてのトライグラムに一致する文字列を見つけます。
- 大幅に削減された交差に元の検索フィルターを適用します。
それはあなたのニーズに適しているかもしれませんが、注意してください:
トライグラム検索は万能薬ではありません。追加のストレージ要件、実装の複雑さ、および更新パフォーマンスへの影響はすべて、それに対する重荷になります。
Complete Works of Shakespeareを使用してクイックテストを実行し、body
テーブルのitems
列に15,838行を設定しました。 words
テーブルに、同じテキストから7,669の一意の単語をロードしました。
私のミッドレンジラップトップでは、約2秒で構築されたトライグラム構造と次の更新ステートメントが5秒で完了しました。
UPDATE dbo.words WITH (TABLOCK)
SET hits =
(
SELECT COUNT_BIG(*)
FROM dbo.Items_TrigramSearch
('%' + Word +'%') AS ITS
);
更新された単語テーブルの選択:
私の記事から変更されたトライグラムスクリプトは以下のとおりです。
CREATE FUNCTION dbo.GenerateTrigrams (@string varchar(255))
RETURNS table
WITH SCHEMABINDING
AS RETURN
WITH
N16 AS
(
SELECT V.v
FROM
(
VALUES
(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0),(0),(0),(0),(0),(0),(0)
) AS V (v)),
-- Numbers table (256)
Nums AS
(
SELECT n = ROW_NUMBER() OVER (ORDER BY A.v)
FROM N16 AS A
CROSS JOIN N16 AS B
),
Trigrams AS
(
-- Every 3-character substring
SELECT TOP (CASE WHEN LEN(@string) > 2 THEN LEN(@string) - 2 ELSE 0 END)
trigram = SUBSTRING(@string, N.n, 3)
FROM Nums AS N
ORDER BY N.n
)
-- Remove duplicates and ensure all three characters are alphanumeric
SELECT DISTINCT
T.trigram
FROM Trigrams AS T
WHERE
-- Binary collation comparison so ranges work as expected
T.trigram COLLATE Latin1_General_BIN2 NOT LIKE '%[^A-Z0-9a-z]%';
GO
-- Trigrams for items table
CREATE TABLE dbo.ItemsTrigrams
(
id integer NOT NULL,
trigram char(3) NOT NULL
);
GO
-- Generate trigrams
INSERT dbo.ItemsTrigrams WITH (TABLOCKX)
(id, trigram)
SELECT
E.id,
GT.trigram
FROM dbo.items AS E
CROSS APPLY dbo.GenerateTrigrams(E.body) AS GT;
GO
-- Trigram search index
CREATE UNIQUE CLUSTERED INDEX
[CUQ dbo.ItemsTrigrams (trigram, id)]
ON dbo.ItemsTrigrams (trigram, id)
WITH (DATA_COMPRESSION = ROW);
GO
-- Selectivity of each trigram (performance optimization)
CREATE OR ALTER VIEW dbo.ItemsTrigramCounts
WITH SCHEMABINDING
AS
SELECT ET.trigram, cnt = COUNT_BIG(*)
FROM dbo.ItemsTrigrams AS ET
GROUP BY ET.trigram;
GO
-- Materialize the view
CREATE UNIQUE CLUSTERED INDEX
[CUQ dbo.ItemsTrigramCounts (trigram)]
ON dbo.ItemsTrigramCounts (trigram);
GO
-- Most selective trigrams for a search string
-- Always returns a row (NULLs if no trigrams found)
CREATE FUNCTION dbo.Items_GetBestTrigrams (@string varchar(255))
RETURNS table
WITH SCHEMABINDING AS
RETURN
SELECT
-- Pivot
trigram1 = MAX(CASE WHEN BT.rn = 1 THEN BT.trigram END),
trigram2 = MAX(CASE WHEN BT.rn = 2 THEN BT.trigram END),
trigram3 = MAX(CASE WHEN BT.rn = 3 THEN BT.trigram END)
FROM
(
-- Generate trigrams for the search string
-- and choose the most selective three
SELECT TOP (3)
rn = ROW_NUMBER() OVER (
ORDER BY ETC.cnt ASC),
GT.trigram
FROM dbo.GenerateTrigrams(@string) AS GT
JOIN dbo.ItemsTrigramCounts AS ETC
WITH (NOEXPAND)
ON ETC.trigram = GT.trigram
ORDER BY
ETC.cnt ASC
) AS BT;
GO
-- Returns Example ids matching all provided (non-null) trigrams
CREATE FUNCTION dbo.Items_GetTrigramMatchIDs
(
@Trigram1 char(3),
@Trigram2 char(3),
@Trigram3 char(3)
)
RETURNS @IDs table (id integer PRIMARY KEY)
WITH SCHEMABINDING AS
BEGIN
IF @Trigram1 IS NOT NULL
BEGIN
IF @Trigram2 IS NOT NULL
BEGIN
IF @Trigram3 IS NOT NULL
BEGIN
-- 3 trigrams available
INSERT @IDs (id)
SELECT ET1.id
FROM dbo.ItemsTrigrams AS ET1
WHERE ET1.trigram = @Trigram1
INTERSECT
SELECT ET2.id
FROM dbo.ItemsTrigrams AS ET2
WHERE ET2.trigram = @Trigram2
INTERSECT
SELECT ET3.id
FROM dbo.ItemsTrigrams AS ET3
WHERE ET3.trigram = @Trigram3
OPTION (MERGE JOIN);
END;
ELSE
BEGIN
-- 2 trigrams available
INSERT @IDs (id)
SELECT ET1.id
FROM dbo.ItemsTrigrams AS ET1
WHERE ET1.trigram = @Trigram1
INTERSECT
SELECT ET2.id
FROM dbo.ItemsTrigrams AS ET2
WHERE ET2.trigram = @Trigram2
OPTION (MERGE JOIN);
END;
END;
ELSE
BEGIN
-- 1 trigram available
INSERT @IDs (id)
SELECT ET1.id
FROM dbo.ItemsTrigrams AS ET1
WHERE ET1.trigram = @Trigram1;
END;
END;
RETURN;
END;
GO
-- Search implementation
CREATE FUNCTION dbo.Items_TrigramSearch
(
@Search varchar(255)
)
RETURNS table
WITH SCHEMABINDING
AS
RETURN
SELECT
Result.body
FROM dbo.Items_GetBestTrigrams(@Search) AS GBT
CROSS APPLY
(
-- Trigram search
SELECT
E.id,
E.body
FROM dbo.Items_GetTrigramMatchIDs
(GBT.trigram1, GBT.trigram2, GBT.trigram3) AS MID
JOIN dbo.Items AS E
ON E.id = MID.id
WHERE
-- At least one trigram found
GBT.trigram1 IS NOT NULL
AND E.body LIKE @Search
UNION ALL
-- Non-trigram search
SELECT
E.id,
E.body
FROM dbo.Items AS E
WHERE
-- No trigram found
GBT.trigram1 IS NULL
AND E.body LIKE @Search
) AS Result;
その他の唯一の変更は、クラスター化インデックスをitems
テーブルに追加することでした。
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.items (id);
もっと速くする必要がありますか? 10分後にクエリをキャンセルしましたが、実際に進行状況を判断する方法がありません。キャンセルしたときにクエリが90%以上完了した場合はどうなりますか?クエリは本当にどれほど速く必要ですか?このような更新をどのくらいの頻度で実行しますか?
MAXDOP 1
で実行すると、同様のUPDATE
が144秒で私のマシンで終了するため、これらの質問をします。クエリは、クエリの並列処理にも適しています。クエリをMAXDOP 8
で強制的に実行すると、私のマシンでは20秒で終了します。
ここでは照合がかなり重要になることに注意してください。上記の数値は、照合順序SQL_Latin1_General_CP1_CS_AS
を使用したものです。列の照合順序をLatin1_General_CI_AS
に変更すると、コードは約8倍遅くなります。さらに、おそらく私のテストデータとハードウェアはあなたのものとはかなり異なります。それでも、クエリの合計実行時間を見積もり、よりエキゾチックなソリューションを試す必要があるかどうかを判断することをお勧めします。これを行うには、dbo.words
の行の1%を含む一時テーブルを作成し、小さいテーブルでUPDATE
にかかる時間を確認します。クエリの実行時間に100を掛けると、実際の見積もりはかなり良くなります。
以下のコードでは、CHARINDEX
ではなくLIKE
を使用しました。これは、別の文字列内の文字列の出現をチェックするだけの方が速いためです。必要に応じて、文書化されていない使用のヒント ENABLE_PARALLEL_PLAN_PREFERENCE
と並行してUPDATE
クエリを実行することを推奨できます。これがクエリです:
UPDATE #words
SET hits = (SELECT COUNT(*) FROM #items WHERE CHARINDEX(Word, body) > 0)
OPTION (MAXDOP 1);
テストデータ:
CREATE TABLE #items
(
body varchar(256) NOT NULL
)
INSERT INTO #items WITH (TABLOCK)
SELECT TOP (12000) text
FROM sys.messages
WHERE LEN(text) <= 256
AND CAST(text AS VARCHAR(256)) = CAST(text AS NVARCHAR(256))
ORDER BY LEN(text) DESC;
CREATE TABLE #words
(
id bigint NOT NULL IDENTITY (1, 1),
Word varchar(32) NOT NULL,
hits int NULL,
PRIMARY KEY (id)
)
INSERT INTO #words (Word, hits)
SELECT DISTINCT TOP (9000) LEFT(Word, 32), NULL
FROM (
SELECT LEFT(body, CHARINDEX(' ', body)) Word
FROM #items
UNION ALL
SELECT LEFT(body, -1 + CHARINDEX(' ', body)) a
FROM #items
UNION ALL
SELECT RIGHT(body, CHARINDEX(' ', REVERSE(body)))
FROM #items
UNION ALL
SELECT RIGHT(body, -1 + CHARINDEX(' ', REVERSE(body)))
FROM #items
) q;
私はSQLの方法を考えることはできませんが、箱から出して考えたければ、実行可能な別のアプローチがあります。データセットはかなり小さいです。 256 * 12000 + 32*9000 = 3360000
。これは3MB強です。このデータは、ほとんどの最新のCPUのCPUキャッシュ内にも簡単に収まります。したがって、すべてのデータを選択し、計算を実行し、データを更新する、選択したプログラミング言語で小さなアプリケーションを作成できます。これには数秒かかります。
それでも遅すぎる場合は、どの種類のループが速いかを確認してください。最初に単語に対して、次にアイテムに対して、またはその逆です。プログラミング言語のオーバーヘッドがデータ完全ではないがCPUキャッシュに収まるほど大きい場合、これらの1つは他よりも高速になります。