it-swarm.com.de

Die Verwendung von SQLServer enthält teilweise Wörter

Wir führen viele Produkte in einem riesigen Katalog mit teilweise passenden Barcodes.

Wir haben mit einer einfachen like-Abfrage begonnen

select * from products where barcode like '%2345%'

Dies dauert jedoch viel zu lange, da dafür ein vollständiger Tabellenscan erforderlich ist. Wir dachten, eine Volltextsuche könnte uns hier helfen, indem wir inhaltsverzeichnisse verwenden.

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

Es scheint jedoch so zu sein, als ob das Finden von Wörtern, die teilweise einen Text enthalten, nicht unterstützt wird, jedoch nur ein vollständiger Wortvergleich oder ein Präfix. (Aber in diesem Beispiel suchen wir nach '123456').

15
Guy Korland

Meine Antwort lautet: @DenisReznik hatte recht :)

ok, lass uns einen Blick darauf werfen.
Ich habe viele Jahre mit Barcodes und großen Katalogen gearbeitet und war neugierig auf diese Frage. 

Also habe ich selbst einige Tests gemacht. 

Ich habe eine Tabelle zum Speichern von Testdaten erstellt:

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

Ich weiß, dass es viele Arten von Barcodes gibt, einige enthalten nur Zahlen, andere enthalten auch Buchstaben und andere können sogar sehr komplex sein. 

Nehmen wir an, unser Barcode ist eine zufällige Zeichenfolge.
Ich habe es mit 10 Millionen Datensätzen von zufälligen alfanumerischen Daten gefüllt:

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

FN_NUMBERS () ist nur eine Funktion, die ich in meinen DBs verwende (eine Art von tally_table) , Um Datensätze schnell abzurufen.

Ich habe 10 Millionen Platten davon bekommen:

N   barcode
1   1C333262C2D74E11B688281636FAF0FB
2   3680E11436FC4CBA826E684C0E96E365
3   7763D29BD09F48C58232C7D33551E6C9

Lassen Sie uns eine Var deklarieren, nach der gesucht werden soll: 

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

Nehmen wir eine Basis mitWIE, um die Ergebnisse mit zu vergleichen:

select * from like_test where barcode like '%'[email protected]+'%'

Auf meiner Workstation dauert es 24,4 Sekunden für einen vollständigen Index-Cluster-Cluster. Sehr langsam. 

SSMS schlägt vor, einen Index für die Barcode-Spalte hinzuzufügen:

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

500Mb Index, ich versuche die Auswahl erneut, diesmal 24,0 Sekunden für den nicht gruppierten Index. Weniger als 2% besser, fast dasselbe Ergebnis. Sehr weit von den 75%, die von SSMS angenommen werden. Es scheint mir, dass dieser Index es wirklich nicht wert ist. Vielleicht macht meine SSD Samsung 840 den Unterschied ..
Für den Moment lasse ich den Index aktiv.

Versuchen wir es mit derCHARINDEX-Lösung:

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

Diesmal dauerte es 23,5 Sekunden, nicht wirklich viel besser als bei LIKE.

Lassen Sie uns nun den Vorschlag von @DenisReznik überprüfen, dass die Verwendung der Binary Collation die Dinge beschleunigen sollte.

select * from like_test
where barcode collate Latin1_General_BIN like '%'[email protected]+'%' collate Latin1_General_BIN 

WOW, es scheint zu funktionieren! Nur 4,5 Sekunden ist das beeindruckend! 5 mal besser ..
Also, was ist mit CHARINDEX und Collation zusammen? Lass es uns versuchen:

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

Unglaublich! 2,4 Sekunden, 10 Mal besser .. 

Ok, bisher habe ich erkannt, dass CHARINDEX besser ist als LIKE, und dass Binary Collation besser ist als normale String-Sortierung. Von nun an werde ich nur noch mit CHARINDEX und Collation fortfahren. 

Können wir noch etwas tun, um noch bessere Ergebnisse zu erzielen? Vielleicht können wir versuchen, unsere sehr langen Zeichenfolgen zu reduzieren. Ein Scan ist immer ein Scan. 

Versuchen Sie zunächst, einen logischen String-Schnitt mit SUBSTRING auszuführen, um Barcodes mit 8 Zeichen virtuell zu bearbeiten:

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

Fantastisch! 1,8 Sekunden .. Ich habe sowohl SUBSTRING(barcode, 1, 8) (Kopf der Zeichenfolge) als auch SUBSTRING(barcode, 12, 8) (Mitte der Zeichenfolge) mit den gleichen Ergebnissen versucht. 

Dann habe ich versucht, die Größe der Barcode-Spalte zu reduzieren, fast keinen Unterschied zu SUBSTRING ().

Schließlich habe ich versucht, den Index auf die Barcode-Spalte zu löschen und ALLE Tests oben wiederholt zu haben.
Der Index ist um 3-5% besser, kostet jedoch 500 MB Speicherplatz und Wartungskosten, wenn der Katalog aktualisiert wird.

Für eine direkte Suche nach Schlüsseln wie where barcode = @s mit dem Index dauert es natürlich 20-50 Millisekunden. Ohne Index können wir mit der Kollatierungssyntax where barcode collate Latin1_General_BIN = @s collate Latin1_General_BIN nicht weniger als 1,1 Sekunden erhalten.

Das war interessant.
Ich hoffe das hilft

12
MtwStark

Ich benutze oft charindex und habe genau so oft diese Debatte. 

Es stellt sich heraus, dass Sie abhängig von Ihrer Struktur tatsächlich eine erhebliche Leistungssteigerung erzielen können. 

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

5

Die gute Option hier für Ihren Fall - Erstellen Ihres FTS-Index. So könnte es umgesetzt werden:

1) Erstellen Sie Tabellenbegriffe:

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)
)

Hinweis: Die Indexdeklaration in der Tabellendefinition ist eine Funktion von 2014. Wenn Sie eine niedrigere Version haben, holen Sie sie einfach aus der CREATE TABLE-Anweisung und erstellen Sie sie separat.

2) Schneiden Sie die Barcodes in Gramm um und speichern Sie sie in einer Tabelle. Zum Beispiel: barcode = '123456', Ihre Tabelle sollte 6 Zeilen enthalten: '123456', '23456', '3456', '456', '56', '6'.

3) Erstellen Sie eine Tabelle 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) Speichern Sie ein Paar (TermId, BarcodeId) für den Barcode in der Tabelle BarcodeIndex. TermId wurde im zweiten Schritt generiert oder ist in der Tabelle "Terms" enthalten. BarcodeId - ist eine Kennung des Barcodes, die in der Barcodes-Tabelle (oder dem Namen, den Sie dafür verwenden) gespeichert ist. Für jeden Barcode sollten 6 Zeilen in der BarcodeIndex-Tabelle enthalten sein.

5) Wählen Sie die Barcodes anhand ihrer Teile mit der folgenden Abfrage aus:

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%'

Diese Lösung erzwingt, dass alle ähnlichen Teile von Barcodes in der Nähe gespeichert werden. SQL Server verwendet daher die Indexbereich-Scan-Strategie, um Daten aus der Tabelle "Terms" abzurufen. Die Begriffe in der Tabelle "Begriffe" sollten eindeutig sein, um diese Tabelle so klein wie möglich zu machen. Dies kann in der Anwendungslogik erfolgen: Existenz prüfen -> neu einfügen, wenn kein Begriff vorhanden ist. Oder indem Sie die Option IGNORE_DUP_KEY für den gruppierten Index der Tabelle "Terms" festlegen. Die BarcodesIndex-Tabelle dient zum Referenzieren von Begriffen und Barcodes. 

Bitte beachten Sie, dass Fremdschlüssel und Einschränkungen in dieser Lösung die Betrachtungspunkte sind. Ich persönlich bevorzuge Fremdschlüssel, bis sie mir weh tun. 

2
Denis Reznik

Nach weiteren Tests und Lesen und Gesprächen mit @DenisReznik denke ich, dass die beste Option darin bestehen könnte, virtuelle Spalten zur Barcodetabelle hinzuzufügen, um den Barcode zu teilen. 

Wir brauchen nur Spalten für Startpositionen vom 2. bis zum 4. Platz, da wir für die 1. Spalte die ursprüngliche Barcode-Spalte verwenden werden. Die letzte Barcodespalte wird meiner Meinung nach überhaupt nicht nützlich sein (welche Art von partieller Übereinstimmung ist 1 Zeichen auf 6, wenn 60% der Datensätze übereinstimmen ?):

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)))
) 

und dann Indizes für diese virtuellen Spalten hinzufügen:

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]);

jetzt können wir mit dieser Abfrage einfach partielle Übereinstimmungen finden

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

das istHELLfast! 

  • für Suchstrings von 6 Zeichen 15-20 Millisekunden (vollständiger Code)
  • für Suchstrings mit 5 Zeichen 25 Millisekunden (20-80)
  • für Suchstrings mit 4 Zeichen 50 Millisekunden (40-130)
  • für Suchstrings mit 3 Zeichen 65 Millisekunden (50-150)
  • für Suchstrings mit 2 Zeichen 200 Millisekunden (190-260)

Es wird kein zusätzlicher Speicherplatz für die Tabelle verwendet, aber der Index " Each " benötigt bis zu 200 MB (für 1 Million Barcodes).

PAY ACHTUNG
Getestet auf einem Microsoft SQL Server Express (64-Bit) und Microsoft SQL Server Enterprise (64-Bit) ist der Optimierer des letzteren etwas besser, aber der Hauptunterschied besteht darin, dass: 

bei der Express-Edition müssen SieONLYden Primärschlüssel extrahieren, wenn Sie Ihre Zeichenfolge durchsuchen. Wenn Sie weitere Spalten in SELECT hinzufügen, verwendet das Optimierungsprogramm keine Indizes mehr, aber es wird für einen vollständigen Clustered-Index-Scan verwendet Sie werden so etwas brauchen 

;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)

bei der Standardausgabe (Enterprise) haben Sie, um darauf zuzugreifen 

    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))
1
MtwStark

AKTUALISIERTE:

Wir wissen, dass FULL-TEXT-Suchen für Folgendes verwendet werden können:

Volltextsuche - MSDN

  1. Ein oder mehrere bestimmte Wörter oder Ausdrücke (einfacher Begriff)
  2. Ein Wort oder ein Satz, bei dem die Wörter mit dem angegebenen Text beginnen (Präfixbegriff)
  3. Flexionsformen eines bestimmten Wortes (Generationsbegriff)
  4. Ein Wort oder ein Satz in der Nähe eines anderen Wortes oder Satzes (Näherungsbegriff)
  5. Synonyme Formen eines bestimmten Wortes (Thesaurus)
  6. Wörter oder Ausdrücke mit gewichteten Werten (gewichteter Begriff)

Werden einige davon durch Ihre Suchanforderungen erfüllt? Wenn Sie wie beschrieben nach Mustern suchen müssen, ohne ein konsistentes Muster (z. B. '1%'), gibt es möglicherweise keine Möglichkeit für SQL, eine SARG zu verwenden.

  • Sie könnten Boolean-Anweisungen verwenden

Aus einer C++-Perspektive kann auf B-Trees von traversals aus Pre-Order, In-Order und Post-Order zugegriffen werden, und mit Boolean-Anweisungen wird der B-Tree durchsucht. Booleans werden wesentlich schneller verarbeitet als Zeichenfolgenvergleiche und bieten zumindest eine verbesserte Leistung.

Wir können dies in den folgenden zwei Optionen sehen:

PATINDEX

  • Nur wenn Ihre Spalte nicht numerisch ist, da PATINDEX für Strings ausgelegt ist.
  • Gibt eine Ganzzahl (wie CHARINDEX) zurück, die einfacher zu verarbeiten ist als Zeichenfolgen.

CHARINDEX ist eine Lösung

  • CHARINDEX hat kein Problem bei der Suche nach INTs und gibt erneut eine Zahl zurück.
  • Möglicherweise müssen einige zusätzliche Fälle eingebaut werden (d. H. Die erste Zahl wird immer ignoriert), Sie können sie jedoch wie folgt hinzufügen: CHARINDEX('200', barcode) > 1.

Als Beweis für das, was ich sage, gehen wir zurück zum alten [AdventureWorks2012].[Production].[TransactionHistory]. Wir haben eine Transaktions-ID, die die Anzahl der Elemente enthält, die wir wünschen, und lässt zum Spaß annehmen, dass Sie jede Transaktions-ID wünschen, die am Ende 200 hat.

-- 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

Hinweis: CHARINDEX entfernt den Wert 200200 bei der Suche. Daher müssen Sie möglicherweise Ihren Code entsprechend anpassen. Aber schauen Sie sich die Ergebnisse an:

 Amazing Awesomeness

  • Boolesche Werte und Zahlen sind eindeutig schnellere Vergleiche.
  • LIKE verwendet Stringvergleiche, die wiederum viel langsamer verarbeitet werden können.

Ich war etwas überrascht, wie groß der Unterschied ist, aber die Grundlagen sind die gleichen. Integers- und Boolean-Anweisungen sind always schneller zu verarbeiten als Stringvergleiche.

0
clifton_h

Sie enthalten nicht viele Einschränkungen, dh, Sie möchten nach einem String in einem String suchen. Wenn es einen Weg gab, einen Index für die Suche nach einem String in einem String zu optimieren, wäre er nur eingebaut!

Andere Dinge, die es schwierig machen, eine bestimmte Antwort zu geben: 

  • Es ist nicht klar, was "riesig" und "zu lang" bedeutet. 

  • Es ist nicht klar, wie Ihre Anwendung funktioniert. Suchen Sie stapelweise, während Sie 1.000 neue Produkte hinzufügen? Erlauben Sie einem Benutzer, einen partiellen Barcode in ein Suchfeld einzugeben?

Ich kann einige Vorschläge machen, die in Ihrem Fall hilfreich sein könnten oder nicht.

Beschleunigen Sie einige Anfragen

Ich habe eine Datenbank mit vielen Kennzeichen. Manchmal möchte ein Offizier nach den letzten 3 Zeichen der Platte suchen. Um dies zu unterstützen, speichere ich das Nummernschild in umgekehrter Reihenfolge und verwende dann LIKE ('ZYX%'), um ABCXYZ zuzuordnen. Bei der Suche haben sie die Möglichkeit, eine 'Enthält'-Suche (wie Sie) durchzuführen, die langsam ist, oder eine Option,' Begins/Ends with 'auszuführen, die aufgrund des Index super ist. Dies würde Ihr Problem gelegentlich lösen (was gut genug sein kann), insbesondere wenn dies ein häufiges Bedürfnis ist.

Parallele Abfragen

Ein Index funktioniert, weil er Daten organisiert, ein Index kann nicht mit einer Zeichenfolge in einer Zeichenfolge helfen, da keine Organisation vorhanden ist. Die Geschwindigkeit scheint Ihr Hauptanliegen der Optimierung zu sein, so dass Sie Ihre Daten auf eine Weise speichern/abfragen können, die parallel durchsucht wird. Beispiel: Wenn es 10 Sekunden dauert, um nacheinander 10 Millionen Zeilen zu suchen, dann brauchen Sie 10 parallele Prozesse (also Prozesssuche 1 Million) von 10 Sekunden bis 1 Sekunde (art'a-sort'a). . Betrachten Sie es als Skalierung. Dafür gibt es verschiedene Optionen, entweder innerhalb Ihrer einzelnen SQL-Instanz (versuchen Sie die Datenpartitionierung) oder auf mehreren SQL-Servern (falls dies eine Option ist). 

BONUS: Wenn Sie sich nicht in einem RAID-Setup befinden, kann dies beim Lesen helfen, da das Lesen parallel erfolgt.

Engpässe reduzieren

Ein Grund für die Suche nach "großen" Datensätzen ist "zu lang", weil alle Daten von der Festplatte gelesen werden müssen, was immer langsam ist. Sie können die Festplatte überspringen und InMemory-Tabellen verwenden. Da "riesig" nicht definiert ist, funktioniert dies möglicherweise nicht.

0
Robert Paulsen