web-dev-qa-db-fra.com

Le comptage de l'occurrence des mots dans le tableau est lent

Considérez ces tableaux simplifiés:

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

La table words contient environ 9000 enregistrements, chacun contenant un seul mot ('téléphone', 'canapé', 'maison', 'chien', ...) La table items contient environ 12000 enregistrements, chacun avec un corps de texte ne dépassant pas 256 caractères.

Maintenant, je dois mettre à jour la table words, en comptant le nombre d'enregistrements dans la table items qui contiennent (au moins une fois) le texte dans le champ Word . Je dois tenir compte des mots partiels, donc tous ces 4 enregistrements doivent être comptés pour le mot chien:

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

Ce dernier exemple devrait compter comme un seul enregistrement (contient le terme "chien" au moins une fois).

Je peux utiliser cette requête:

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

Mais, c'est extrêmement lent, cela prendra plus de 10 minutes pour terminer sur le serveur pas trop lourd que j'ai pour cela.

Les index AFAIK n'aideront pas, je fais des recherches COMME. Je pense également que le texte intégral ne m'aidera pas, car je recherche des mots commençant, terminant ou contenant mon terme de recherche. Je peux me tromper ici.

Des conseils sur la façon d'accélérer cela?

6
palloquin

Le meilleur moyen que j'ai trouvé pour accélérer les recherches de caractères génériques LIKE est d'utiliser des n-grammes. Je décris la technique et fournit un exemple d'implémentation dans Trigram Wildcard String Search dans SQL Server .

L'idée de base d'une recherche de trigrammes est assez simple:

  1. Conserver les sous-chaînes à trois caractères (trigrammes) des données cibles.
  2. Divisez le ou les termes de recherche en trigrammes.
  3. Faites correspondre les trigrammes de recherche avec les trigrammes stockés (recherche d'égalité).
  4. Intersectez les lignes qualifiées pour trouver des chaînes qui correspondent à tous les trigrammes.
  5. Appliquez le filtre de recherche d'origine à l'intersection très réduite.

Il peut convenir à vos besoins, mais sachez:

La recherche de trigrammes n'est pas une panacée. Les exigences de stockage supplémentaires, la complexité de la mise en œuvre et l'impact sur les performances de mise à jour pèsent lourdement sur lui.

Tester

J'ai effectué un test rapide en utilisant la Complete Works of Shakespeare pour remplir la colonne body de la table items avec 15 838 lignes. J'ai chargé la table words avec 7 669 mots uniques du même texte.

Les structures de trigrammes construites en environ 2 secondes et l'instruction de mise à jour suivante terminée en 5 secondes sur mon ordinateur portable de milieu de gamme:

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

Une sélection du tableau des mots mis à jour:

sample

Les scripts trigrammes modifiés de mon article sont ci-dessous:

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;

La seule autre modification consistait à ajouter un index clusterisé à la table items:

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

Êtes-vous sûr d'en avoir besoin pour être plus rapide? Vous avez annulé la requête après 10 minutes, mais vous n'avez pas vraiment de moyen de juger des progrès. Que se passe-t-il si la requête a été effectuée à 90% + lorsque vous l'avez annulée? À quelle vitesse avez-vous vraiment besoin de la requête? À quelle fréquence exécuterez-vous une mise à jour comme celle-ci?

Je pose ces questions car je peux obtenir un UPDATE similaire pour terminer sur ma machine en 144 secondes lors de l'exécution à MAXDOP 1. La requête convient également très bien au parallélisme des requêtes. Lorsque je force l'exécution de la requête à MAXDOP 8 il se termine en 20 secondes sur ma machine.

Notez que le classement peut avoir beaucoup d'importance ici. Les nombres ci-dessus sont avec le classement SQL_Latin1_General_CP1_CS_AS. Si je change le classement des colonnes en Latin1_General_CI_AS alors le code est environ huit fois plus lent. De plus, mes données de test et mon matériel sont peut-être sensiblement différents des vôtres. Je suggère toujours d'estimer la durée totale d'exécution de votre requête, puis de décider si vous devez essayer une solution plus exotique. Vous pouvez le faire en créant une table temporaire avec 1% des lignes dans dbo.words et voir combien de temps le UPDATE prend pour la petite table. Si vous multipliez le temps d'exécution de la requête par 100, cela devrait être une assez bonne estimation de la réalité.

Dans le code ci-dessous, j'ai utilisé CHARINDEX au lieu de LIKE car c'est plus rapide lorsque l'on vérifie simplement l'occurrence d'une chaîne dans une autre chaîne. Si nécessaire, la requête UPDATE peut être encouragée à s'exécuter en parallèle avec une indication d'utilisation non documentée ENABLE_PARALLEL_PLAN_PREFERENCE . Voici la requête:

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

Données de test:

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

Je ne peux pas penser à une manière SQL, mais si vous êtes prêt à sortir des sentiers battus, il existe une approche différente qui peut être viable. Votre jeu de données est assez petit. 256 * 12000 + 32*9000 = 3360000. C'est un peu plus de 3 Mo; ces données s'intègrent facilement même dans le cache CPU de la plupart des processeurs modernes. Ainsi, vous pouvez écrire une petite application dans le langage de programmation de votre choix qui sélectionne simplement toutes les données, effectue le calcul et met à jour les données. Cela ne devrait prendre que quelques secondes.

Si c'est encore trop lent, vérifiez quel type de boucle est le plus rapide - d'abord sur les mots puis sur les éléments, ou inversement. Si la surcharge de votre langage de programmation est suffisamment grande pour que les données pas tout à fait tiennent dans le cache CPU, l'une d'elles sera plus rapide que l'autre.

0
Vilx-