it-swarm.com.de

Das Zählen des Auftretens von Wörtern in der Tabelle ist langsam

Betrachten Sie diese vereinfachten Tabellen:

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

Die Tabelle words enthält ungefähr 9000 Datensätze, die jeweils ein einzelnes Wort enthalten ('Telefon', 'Sofa', 'Haus', 'Hund', ...). Die Tabelle items enthält ungefähr 12000 Datensätze Datensätze mit jeweils einem Textkörper von maximal 256 Zeichen.

Jetzt muss ich die Tabelle words aktualisieren und zählen, wie viele Datensätze in der Tabelle items enthalten sind, die (mindestens einmal) den Text im Feld Word enthalten . Ich muss Teilwörter berücksichtigen, daher sollten alle diese 4 Datensätze für das Wort Hund gezählt werden:

'This is my dog'  
'I really like the movie dogma'  
'my cousin has sheepdogs'  
'dog dog dog doggerdy dog dog'

Das letzte Beispiel sollte als nur ein Datensatz gelten (enthält mindestens einmal den Begriff "Hund").

Ich kann diese Abfrage verwenden:

UPDATE dbo.words
SET hits = (SELECT COUNT(*) FROM dbo.items WHERE body like '%' + Word + '%')

Dies ist jedoch extrem langsam. Auf dem nicht zu schweren Server, den ich dafür habe, dauert dies über 10 Minuten.

AFAIK-Indizes helfen nicht, ich mache LIKE-Suchen. Ich denke auch, dass Volltext mir nicht hilft, da ich nach Wörtern suche, die meinen Suchbegriff beginnen, beenden oder enthalten. Ich könnte mich hier irren.

Irgendwelche Ratschläge, wie man das beschleunigt?

6
palloquin

Der beste Weg, den ich gefunden habe, um die Suche nach führenden Platzhaltern LIKE zu beschleunigen, ist die Verwendung von n-Gramm. Ich beschreibe die Technik und stelle eine Beispielimplementierung in Trigram Wildcard String Search in SQL Server bereit.

Die Grundidee einer Trigrammsuche ist ganz einfach:

  1. Behalten Sie dreistellige Teilzeichenfolgen (Trigramme) der Zieldaten bei.
  2. Teilen Sie die Suchbegriffe in Trigramme auf.
  3. Ordnen Sie Suchtrigramme den gespeicherten Trigrammen zu (Gleichheitssuche).
  4. Schneiden Sie die qualifizierten Zeilen, um Zeichenfolgen zu finden, die allen Trigrammen entsprechen.
  5. Wenden Sie den ursprünglichen Suchfilter auf die stark reduzierte Kreuzung an.

Es kann für Ihre Bedürfnisse geeignet sein, aber beachten Sie:

Trigrammsuche ist kein Allheilmittel. Die zusätzlichen Speicheranforderungen, die Komplexität der Implementierung und die Auswirkungen auf die Update-Leistung wirken sich stark negativ aus.

Prüfung

Ich habe einen Schnelltest mit Complete Works of Shakespeare durchgeführt, um die Spalte body der Tabelle items mit 15.838 Zeilen zu füllen. Ich habe die Tabelle words mit 7.669 eindeutigen Wörtern aus demselben Text geladen.

Die in ca. 2 Sekunden erstellten Trigrammstrukturen und die folgende Update-Anweisung wurden in 5 Sekunden auf meinem Midrange-Laptop abgeschlossen:

UPDATE dbo.words WITH (TABLOCK)
SET hits = 
(
    SELECT COUNT_BIG(*) 
    FROM dbo.Items_TrigramSearch
        ('%' + Word +'%') AS ITS
);

Eine Auswahl der aktualisierten Worttabelle:

(sample

Die geänderten Trigrammskripte aus meinem Artikel sind unten:

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;

Die einzige andere Änderung bestand darin, der Tabelle items einen Clustered-Index hinzuzufügen:

CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.items (id);
9
Paul White 9

Sind Sie sicher, dass es schneller sein muss? Sie haben die Abfrage nach 10 Minuten abgebrochen, haben aber keine Möglichkeit, den Fortschritt zu beurteilen. Was ist, wenn die Abfrage zu 90% erledigt war, als Sie sie abgebrochen haben? Wie schnell muss die Abfrage wirklich sein? Wie oft werden Sie ein solches Update ausführen?

Ich stelle diese Fragen, weil ich ein ähnliches UPDATE in 144 Sekunden auf meinem Computer beenden kann, wenn ich mit MAXDOP 1 Ausführe. Die Abfrage passt auch ziemlich gut zur Abfrageparallelität. Wenn ich erzwinge, dass die Abfrage mit MAXDOP 8 Ausgeführt wird, wird sie auf meinem Computer in 20 Sekunden beendet.

Beachten Sie, dass die Sortierung hier eine große Rolle spielen kann. Die obigen Zahlen beziehen sich auf die Sortierung SQL_Latin1_General_CP1_CS_AS. Wenn ich die Spaltenkollatierung in Latin1_General_CI_AS Ändere, ist der Code ungefähr achtmal langsamer. Außerdem unterscheiden sich meine Testdaten und Hardware möglicherweise erheblich von Ihren. Ich empfehle weiterhin, die Gesamtlaufzeit Ihrer Abfrage zu schätzen und dann zu entscheiden, ob Sie eine exotischere Lösung ausprobieren müssen. Sie können dies tun, indem Sie eine temporäre Tabelle mit 1% der Zeilen in dbo.words Erstellen und sehen, wie lange UPDATE für die kleinere Tabelle dauert. Wenn Sie die Ausführungszeit der Abfrage mit 100 multiplizieren, sollte dies eine ziemlich gute Schätzung für die reale Sache sein.

Im folgenden Code habe ich CHARINDEX anstelle von LIKE verwendet, da dies schneller ist, wenn nur nach dem Auftreten einer Zeichenfolge in einer anderen Zeichenfolge gesucht wird. Bei Bedarf kann die Abfrage UPDATE dazu ermutigt werden, parallel zu einem undokumentierten Verwendungshinweis ausgeführt zu werden ENABLE_PARALLEL_PLAN_PREFERENCE . Hier ist die Abfrage:

UPDATE #words
SET hits = (SELECT COUNT(*) FROM #items WHERE CHARINDEX(Word, body) > 0)
OPTION (MAXDOP 1);

Testdaten:

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;
4
Joe Obbish

Ich kann mir keinen SQL-Weg vorstellen, aber wenn Sie bereit sind, über den Tellerrand hinaus zu denken, gibt es einen anderen Ansatz, der möglicherweise praktikabel ist. Ihr Datensatz ist ziemlich klein. 256 * 12000 + 32*9000 = 3360000. Das ist etwas mehr als 3 MB; Diese Daten passen problemlos auch in den CPU-Cache der meisten modernen CPUs. So können Sie eine kleine Anwendung in der Programmiersprache Ihrer Wahl schreiben, die einfach alle Daten auswählt, die Berechnung durchführt und die Daten zurück aktualisiert. Dies sollte nur einige Sekunden dauern.

Wenn es immer noch zu langsam ist, überprüfen Sie, welche Art von Schleife schneller ist - zuerst über Wörter, dann über Elemente oder umgekehrt. Wenn der Overhead Ihrer Programmiersprache groß genug ist, dass die Daten nicht ganz in den CPU-Cache passen, ist einer davon schneller als der andere.

0
Vilx-