web-dev-qa-db-ja.com

SQLServerを使用して部分的な単語を含む

バーコードが部分的に一致する巨大なカタログで多くの製品検索を実行しています。

単純なlikeクエリから始めました

_select * from products where barcode like '%2345%'_

ただし、テーブル全体をスキャンする必要があるため、時間がかかりすぎます。全文検索は、containsを使用してここで支援できると考えました。

select * from products where contains(barcode, '2345')

しかし、containsはテキストを部分的に含む単語の検索をサポートしていないようですが、完全なWordの一致またはプレフィックスのみをサポートしています。 (ただし、この例では「123456」を探しています)。

17
Guy Korland

私の答えは、@ DenisReznikは正しかったです:)

さて、見てみましょう。
私は長年バーコードと大きなカタログを扱ってきましたが、この質問に興味を持っていました。

だから私は自分でいくつかのテストを行いました。

テストデータを格納するテーブルを作成しました。

_CREATE TABLE [like_test](
    [N] [int] NOT NULL PRIMARY KEY,
    [barcode] [varchar](40) NULL
) 
_

バーコードには多くの種類があり、数字のみが含まれているもの、文字も含まれているもの、および非常に複雑なものがあることは知っています。

バーコードがランダムな文字列であるとしましょう。
ランダムな英数字データの1000万件のレコードを入力しました:

_insert into like_test
select (select count(*) from like_test)+n, REPLACE(convert(varchar(40), NEWID()), '-', '') barcode 
from FN_NUMBERS(10000000)
_

FN_NUMBERS()は、DBで使用する関数(一種のtally_table)であり、レコードをすばやく取得できます。

私はそのような1000万のレコードを得ました:

_N   barcode
1   1C333262C2D74E11B688281636FAF0FB
2   3680E11436FC4CBA826E684C0E96E365
3   7763D29BD09F48C58232C7D33551E6C9
_

検索する変数を宣言しましょう:

_declare @s varchar(20) = 'D34F15' -- random alfanumeric string 
_

結果を比較するために[〜#〜] like [〜#〜]で基本的な試してみましょう:

_select * from like_test where barcode like '%'+@s+'%'
_

私のワークステーションでは、クラスター化インデックスの完全スキャンに24.4秒かかります。非常に遅い。

SSMSは、バーコード列にインデックスを追加することを提案します。

_CREATE NONCLUSTERED INDEX [ix_barcode] ON [like_test] ([barcode]) INCLUDE ([N])
_

500Mbのインデックス、私は選択を再試行しますが、今回は非クラスター化インデックスシークに対して24.0秒です。2%未満の改善、ほぼ同じ結果です。 SSMSが想定する75%からはかなり離れています。このインデックスは本当に価値がないように思えます。たぶん私のSSD Samsung 840が違いを生んでいるのかもしれません。
当面は、インデックスをアクティブにしました。

[〜#〜] charindex [〜#〜]ソリューションを試してみましょう:

_select * from like_test where charindex(@s, barcode) > 0
_

今回は23.5秒で完了しましたが、LIKEよりもはるかに優れているわけではありません。

次に、Binary Collat​​ionを使用すると処理が速くなるという@DenisReznikの提案を確認してみましょう。

_select * from like_test
where barcode collate Latin1_General_BIN like '%'+@s+'%' collate Latin1_General_BIN 
_

うわー、それはうまくいくようです!わずか4.5秒でこれは印象的です! 5倍良い..
では、CHARINDEXと照合は一緒にどうでしょうか?試してみよう:

_select * from like_test
where charindex(@s collate Latin1_General_BIN, barcode collate Latin1_General_BIN)>0
_

信じられない! 2.4秒、10倍良い.

さて、これまでのところ、CHARINDEXはLIKEよりも優れており、バイナリ照合は通常の文字列照合よりも優れていることに気付いたので、これからはCHARINDEXとCollat​​ionについてのみ説明します。

さて、さらに良い結果を得るために他に何ができるでしょうか?たぶん、非常に長い文字列を減らしてみることができます。スキャンは常にスキャンです。

まず、SUBSTRINGを使用して論理文字列をカットし、8文字のバーコードで仮想的に機能します。

_select * from like_test
where charindex(
        @s collate Latin1_General_BIN, 
        SUBSTRING(barcode, 12, 8) collate Latin1_General_BIN
      )>0
_

素晴らしい! 1.8秒..同じ結果でSUBSTRING(barcode, 1, 8)(文字列の先頭)とSUBSTRING(barcode, 12, 8)(文字列の中央)の両方を試しました。

次に、バーコード列のサイズを物理的に縮小しようとしましたが、SUBSTRING()を使用する場合とほとんど同じです

最後に、バーコード列のインデックスを削除しようとし、上記のすべてのテストを繰り返しました...ほとんど同じ結果で、ほとんど違いがないことに驚きました。
インデックスのパフォーマンスは3〜5%向上しますが、500Mbのディスクスペースと、カタログを更新した場合のメンテナンスコストがかかります。

当然のことながら、_where barcode = @s_のような直接キールックアップの場合、インデックスを使用すると20〜50ミリ秒かかります。インデックスがない場合、照合構文_where barcode collate Latin1_General_BIN = @s collate Latin1_General_BIN_を使用して1.1秒未満にすることはできません。

これは面白かった。
これがお役に立てば幸いです

16
MtwStark

私はしばしばcharindexを使用しますが、これと同じくらい頻繁にこの非常に議論があります。

結局のところ、構造によっては、実際にパフォーマンスが大幅に向上する場合があります。

http://cc.davelozinski.com/sql/like-vs-substring-vs-leftright-vs-charindex

6

FTSインデックスの作成-あなたのケースに適したオプションです。実装方法は次のとおりです。

1)テーブル用語を作成します。

CREATE TABLE Terms
(
    Id int IDENTITY NOT NULL,
    Term varchar(21) NOT NULL,
    CONSTRAINT PK_TERMS PRIMARY KEY (Term),
    CONSTRAINT UK_TERMS_ID UNIQUE (Id)
)

注:テーブル定義のインデックス宣言は、2014の機能です。これより古いバージョンを使用している場合は、それをCREATE TABLEステートメントから取り出して、個別に作成してください。

2)バーコードをグラムにカットし、それぞれをテーブル用語に保存します。例:barcode = '123456'、テーブルには6つの行が必要です: '123456'、 '23456'、 '3456'、 '456'、 '56'、 '6'。

3)テーブルBarcodeIndexを作成します。

CREATE TABLE BarcodesIndex
(
    TermId int NOT NULL,
    BarcodeId int NOT NULL,
    CONSTRAINT PK_BARCODESINDEX PRIMARY KEY (TermId, BarcodeId),
    CONSTRAINT FK_BARCODESINDEX_TERMID FOREIGN KEY (TermId) REFERENCES Terms (Id),
    CONSTRAINT FK_BARCODESINDEX_BARCODEID FOREIGN KEY (BarcodeId) REFERENCES Barcodes (Id)
)

4)バーコードのペア(TermId、BarcodeId)をテーブルBarcodeIndexに保存します。 TermIdは2番目のステップで生成されたか、Termsテーブルに存在します。 BarcodeId-バーコード(またはバーコードに使用する名前)テーブルに格納されているバーコードの識別子です。バーコードごとに、BarcodeIndexテーブルに6行あるはずです。

5)次のクエリを使用して、パーツごとにバーコードを選択します。

SELECT b.* FROM Terms t
INNER JOIN BarcodesIndex bi
    ON t.Id = bi.TermId
INNER JOIN Barcodes b
    ON bi.BarcodeId = b.Id
WHERE t.Term LIKE 'SomeBarcodePart%'

このソリューションでは、バーコードの同様の部分がすべて近くに格納されるため、SQL Serverはインデックスレンジスキャン戦略を使用して、条件テーブルからデータをフェッチします。用語テーブルの用語は、このテーブルをできるだけ小さくするために一意である必要があります。これは、アプリケーションロジックで行うことができます。存在を確認->用語が存在しない場合は新規に挿入します。または、条件テーブルのクラスター化インデックスのオプションIGNORE_DUP_KEYを設定します。 BarcodesIndexテーブルは、用語とバーコードを参照するために使用されます。

このソリューションの外部キーと制約が考慮事項であることに注意してください。個人的には、私が怪我をするまでは、外部キーを用意することを好みます。

3
Denis Reznik

さらにテストし、@ DenisReznikを読んで話し合った後、仮想列をバーコードテーブルに追加してバーコードを分割するのが最善の方法だと思います。

2番目から4番目までの開始位置の列のみが必要です。1番目は元のバーコード列を使用し、最後はまったく役に立たないためです(レコードの60%が一致する場合、6の1文字はどのような部分一致ですか? ?):

CREATE TABLE [like_test](
    [N] [int] NOT NULL PRIMARY KEY,
    [barcode] [varchar](6) NOT NULL,
    [BC2]  AS (substring([BARCODE],(2),(5))),
    [BC3]  AS (substring([BARCODE],(3),(4))),
    [BC4]  AS (substring([BARCODE],(4),(3))),
    [BC5]  AS (substring([BARCODE],(5),(2)))
) 

次に、この仮想列にインデックスを追加します。

CREATE NONCLUSTERED INDEX [IX_BC2] ON [like_test2] ([BC2]);
CREATE NONCLUSTERED INDEX [IX_BC3] ON [like_test2] ([BC3]);
CREATE NONCLUSTERED INDEX [IX_BC4] ON [like_test2] ([BC4]);
CREATE NONCLUSTERED INDEX [IX_BC5] ON [like_test2] ([BC5]);
CREATE NONCLUSTERED INDEX [IX_BC6] ON [like_test2] ([barcode]);

これで、このクエリで部分一致を簡単に見つけることができます

declare @s varchar(40) 
declare @l int

set @s = '654'
set @l = LEN(@s)

select N from like_test 
where 1=0
OR ((barcode = @s) and (@l=6)) -- to match full code (rem if not needed)
OR ((barcode like @s+'%') and (@l<6)) -- to match strings up to 5 chars from beginning
or ((BC2 like @s+'%') and (@l<6)) -- to match strings up to 5 chars from 2nd position
or ((BC3 like @s+'%') and (@l<5)) -- to match strings up to 4 chars from 3rd position
or ((BC4 like @s+'%') and (@l<4)) -- to match strings up to 3 chars from 4th position
or ((BC5 like @s+'%') and (@l<3)) -- to match strings up to 2 chars from 5th position

これは[〜#〜] hell [〜#〜]高速です!

  • 6文字の検索文字列の場合15-20ミリ秒(完全なコード)
  • 5文字25ミリ秒の検索文字列(20〜80)
  • 4文字の検索文字列の場合50ミリ秒(40-130)
  • 3文字65ミリ秒の検索文字列(50〜150)
  • 2文字の検索文字列の場合200ミリ秒(190〜260)

テーブルに使用される追加のスペースはありませんが、eachインデックスには最大200Mb(100万バーコードの場合)がかかります

支払い注意
Microsoft SQL Server Express(64ビット)とMicrosoft SQL Server Enterprise(64ビット)でテストされた後者のオプティマイザは少し優れていますが、主な違いは次のとおりです。

エクスプレス版では、抽出する必要があります[〜#〜] only [〜#〜]文字列を検索するときの主キー、SELECTに他の列を追加すると、オプティマイザはインデックスを使用しなくなりますが、それは完全なクラスター化インデックススキャンに行くので、次のようなものが必要になります

;with
k as (-- extract only primary key
    select N from like_test
    where 1=0
    OR ((barcode = @s) and (@l=6))
    OR ((barcode like @s+'%') and (@l<6))
    or ((BC2 like @s+'%') and (@l<6))
    or ((BC3 like @s+'%') and (@l<5))
    or ((BC4 like @s+'%') and (@l<4))
    or ((BC5 like @s+'%') and (@l<3))
)
select N 
from like_test t
where exists (select 1 from k where k.n = t.n)

標準(エンタープライズ)エディションでは[〜#〜] have [〜#〜]

    select * from like_test -- take a look at the star
    where 1=0
    OR ((barcode = @s) and (@l=6))
    OR ((barcode like @s+'%') and (@l<6))
    or ((BC2 like @s+'%') and (@l<6))
    or ((BC3 like @s+'%') and (@l<5))
    or ((BC4 like @s+'%') and (@l<4))
    or ((BC5 like @s+'%') and (@l<3))
2
MtwStark

更新しました:

フルテキスト検索は次の目的で使用できることがわかります。

フルテキスト検索-MSDN

  1. 1つ以上の特定の単語または語句(単純な用語)
  2. 指定したテキストで始まる単語またはフレーズ(接頭辞)
  3. 特定の語の屈折形(世代用語)
  4. 別の単語または語句に近接する単語または語句(近接用語)
  5. 特定の単語のシノニム(シソーラス)
  6. 加重値を使用した単語または語句(加重用語)

これらのいずれかがクエリの要件によって満たされていますか?一貫したパターン( '1%'など)なしで、説明したとおりにパターンを検索する必要がある場合、SQLでSARGを使用する方法がない可能性があります。

  • Booleanステートメントを使用できます

_C++_の観点から見ると、_B-Trees_は、プレオーダー、インオーダー、ポストオーダーtraversalsからアクセスされ、Booleanステートメントを使用して_B-Tree_。文字列比較よりもはるかに高速に処理され、ブール値は少なくとも改善されたパフォーマンスを提供します。

これは、次の2つのオプションで確認できます。

[〜#〜] patindex [〜#〜]

  • PATINDEXは文字列用に設計されているため、列が数値でない場合のみ。
  • 文字列よりも処理が簡単な整数(CHARINDEXなど)を返します。

CHARINDEXはソリューションです

  • CHARINDEXはINTの検索に問題はなく、再び数値を返します。
  • 組み込みの追加のケースが必要になる場合があります(つまり、最初の数値は常に無視されます)が、次のように追加できます:CHARINDEX('200', barcode) > 1

私が言っていることの証明、古い_[AdventureWorks2012].[Production].[TransactionHistory]_に戻りましょう。必要なアイテムの数を含むTransactionIDがあり、楽しみのために、最後に200を持つすべてのtransactionIDが必要であると想定します。

_-- WITH LIKE
SELECT TOP 1000 [TransactionID]
      ,[ProductID]
      ,[ReferenceOrderID]
      ,[ReferenceOrderLineID]
      ,[TransactionDate]
      ,[TransactionType]
      ,[Quantity]
      ,[ActualCost]
      ,[ModifiedDate]
  FROM [AdventureWorks2012].[Production].[TransactionHistory]
  WHERE TransactionID LIKE '%200'

-- WITH CHARINDEX(<delimiter>, <column>) > 3
SELECT TOP 1000 [TransactionID]
      ,[ProductID]
      ,[ReferenceOrderID]
      ,[ReferenceOrderLineID]
      ,[TransactionDate]
      ,[TransactionType]
      ,[Quantity]
      ,[ActualCost]
      ,[ModifiedDate]
  FROM [AdventureWorks2012].[Production].[TransactionHistory]
  WHERE CHARINDEX('200', TransactionID) > 3
_

注CHARINDEXは検索で値200200を削除するため、コードを適切に調整する必要がある場合があります。しかし、結果を見てください。

Amazing Awesomeness

  • 明らかに、ブール値と数値はより高速な比較です。
  • LIKEは文字列比較を使用しますが、これも処理がはるかに遅くなります。

違いの大きさに少し驚きましたが、基本は同じです。 IntegersおよびBooleanステートメントは常に文字列比較よりも処理が高速です。

0
clifton_h

多くの制約を含まないため、文字列内の文字列を検索する必要があります。文字列内の文字列を検索するようにインデックスを最適化する方法があった場合、それは組み込まれているだけです。

特定の答えを出すのを困難にする他のこと:

  • 「巨大」と「長すぎる」の意味が明確ではありません。

  • アプリケーションの動作については明確ではありません。 1,000の新製品を追加するときに、バッチで検索していますか?ユーザーが検索ボックスに部分的なバーコードを入力することを許可していますか?

私はあなたの場合に役立つかもしれないし、そうでないかもしれないいくつかの提案をすることができます。

一部のクエリを高速化

たくさんのナンバープレートを含むデータベースがあります。時には、将校がプレートの最後の3文字で検索したい場合があります。これをサポートするには、ナンバープレートを逆に格納し、次にLIKE ('ZYX%')を使用してABCXYZと一致させます。検索を実行するとき、検索には(含む)検索が遅い(オプション)か、インデックスのために[開始/終了]を実行するオプションがあります。これは、特にこれが一般的なニーズである場合、問題を解決することがあります(これで十分な場合もあります)。

並列クエリ

インデックスは、データを編成するために機能します。編成がないため、インデックスは文字列内の文字列を支援できません。速度が最適化の焦点であると思われるため、並行して検索する方法でデータを保存/クエリできます。例:10ミリオンの行を順次検索するのに10秒かかる場合、10の並列プロセス(つまり、100万のプロセスを検索)を使用すると、10秒から1秒になります(kind'a-sort'a)。 。スケールアウトと考えてください。これには、単一のSQLインスタンス内(データのパーティション化を試す)または複数のSQL Server(オプションの場合)にわたって、さまざまなオプションがあります。

ボーナス:RAID設定を使用していない場合、効率的に並行して読み取ることができるため、読み取りに役立ちます。

ボトルネックを減らす

「巨大な」データセットの検索に「時間がかかりすぎる」理由の1つは、すべてのデータをディスクから読み取る必要があるためです。ディスクをスキップして、インメモリテーブルを使用できます。 「巨大」が定義されていないため、これは機能しない可能性があります。

0
Robert Paulsen