Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

Trigram Wildcard – wyszukiwanie ciągów znaków w SQL Server

Wyszukiwanie danych ciągu w celu znalezienia dowolnego dopasowania podciągu może być kosztowną operacją w programie SQL Server. Zapytania w postaci Column LIKE '%match%' nie może korzystać z możliwości wyszukiwania indeksu b-drzewa, więc procesor zapytań musi zastosować predykat do każdego wiersza indywidualnie. Ponadto każdy test musi poprawnie stosować pełny zestaw skomplikowanych reguł sortowania. Łącząc wszystkie te czynniki, nie jest niespodzianką, że tego typu wyszukiwania mogą być czasochłonne i wymagające dużej ilości zasobów.

Wyszukiwanie pełnotekstowe to potężne narzędzie do dopasowywania językowego, a nowsze statystyczne wyszukiwanie semantyczne doskonale nadaje się do znajdowania dokumentów o podobnym znaczeniu. Ale czasami naprawdę wystarczy znaleźć ciągi, które zawierają określony podciąg – podciąg, który może nawet nie być słowem, w dowolnym języku.

Jeśli wyszukiwane dane nie są duże lub wymagania dotyczące czasu odpowiedzi nie są krytyczne, użyj LIKE '%match%' równie dobrze może być odpowiednim rozwiązaniem. Ale w dziwnej sytuacji, gdy potrzeba superszybkiego wyszukiwania przewyższa wszystkie inne względy (w tym przestrzeń dyskową), możesz rozważyć niestandardowe rozwiązanie przy użyciu n-gramów. Konkretną odmianą omawianą w tym artykule jest trzyznakowy trygram.

Wyszukiwanie symboli wieloznacznych za pomocą trygramów

Podstawowa idea wyszukiwania trygramów jest dość prosta:

  1. Utrzymuj trzyznakowe podciągi (trygramy) danych docelowych.
  2. Podziel wyszukiwane hasła na trygramy.
  3. Dopasuj trygramy wyszukiwania do zapisanych trygramów (wyszukiwanie równości)
  4. Przetnij zakwalifikowane wiersze, aby znaleźć ciągi pasujące do wszystkich trygramów
  5. Zastosuj oryginalny filtr wyszukiwania do znacznie ograniczonego skrzyżowania

Przeanalizujemy przykład, aby zobaczyć dokładnie, jak to wszystko działa i jakie są kompromisy.

Przykładowa tabela i dane

Poniższy skrypt tworzy przykładową tabelę i wypełnia ją milionem wierszy danych łańcuchowych. Każdy ciąg ma długość 20 znaków, przy czym pierwsze 10 znaków to cyfry. Pozostałe 10 znaków to mieszanka cyfr i liter od A do F, wygenerowana za pomocą NEWID() . Nie ma nic strasznie specjalnego w tych przykładowych danych; technika trygramów jest dość ogólna.

-- The test table
CREATE TABLE dbo.Example 
(
    id integer IDENTITY NOT NULL,
    string char(20) NOT NULL,
 
    CONSTRAINT [PK dbo.Example (id)]
        PRIMARY KEY CLUSTERED (id)
);
GO
-- 1 million rows
INSERT dbo.Example WITH (TABLOCKX)
    (string)
SELECT TOP (1 * 1000 * 1000)
    -- 10 numeric characters
    REPLACE(STR(RAND(CHECKSUM(NEWID())) * 1e10, 10), SPACE(1), '0') +
    -- plus 10 mixed numeric + [A-F] characters
    RIGHT(NEWID(), 10)
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2
OPTION (MAXDOP 1);

Zajmuje to około 3 sekundy do tworzenia i wypełniania danych na moim skromnym laptopie. Dane są pseudolosowe, ale jako wskazówka będzie wyglądać mniej więcej tak:

Próbka danych

Generowanie trygramów

Poniższa funkcja wbudowana generuje różne trygramy alfanumeryczne z podanego ciągu wejściowego:

--- Generate trigrams from a string
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]%';

Jako przykład jego użycia, następujące wywołanie:

SELECT
    GT.trigram
FROM dbo.GenerateTrigrams('SQLperformance.com') AS GT;

Tworzy następujące trygramy:

Trygramy SQLperformance.com

W tym przypadku plan wykonania jest dość bezpośrednim tłumaczeniem T-SQL:

  • Generowanie wierszy (łączenie krzyżowe stałych skanów)
  • Numerowanie wierszy (projekt segmentów i sekwencji)
  • Ograniczanie potrzebnych liczb na podstawie długości ciągu (góra)
  • Usuń trygramy ze znakami niealfanumerycznymi (filtr)
  • Usuń duplikaty (sortowanie odrębne)

Plan generowania trygramów

Ładowanie trygramów

Następnym krokiem jest utrwalenie trygramów dla przykładowych danych. Trygramy będą przechowywane w nowej tabeli, wypełnionej za pomocą utworzonej właśnie funkcji wbudowanej:

-- Trigrams for Example table
CREATE TABLE dbo.ExampleTrigrams
(
    id integer NOT NULL,
    trigram char(3) NOT NULL
);
GO
-- Generate trigrams
INSERT dbo.ExampleTrigrams WITH (TABLOCKX)
    (id, trigram)
SELECT
    E.id,
    GT.trigram
FROM dbo.Example AS E
CROSS APPLY dbo.GenerateTrigrams(E.string) AS GT;

Zajmuje to około 20 sekund do wykonania na moim laptopie SQL Server 2016. Ten konkretny przebieg dał 17 937 972 wiersze trygramów dla 1 miliona wierszy 20-znakowych danych testowych. Plan wykonania zasadniczo pokazuje plan funkcji, który jest oceniany dla każdego wiersza tabeli Przykład:

Wypełnianie tabeli trygramów

Ponieważ ten test został przeprowadzony na SQL Server 2016 (ładowanie tabeli sterty, na poziomie zgodności bazy danych 130, z TABLOCK wskazówka), plan korzysta z równoległego wstawiania. Wiersze są rozdzielane między wątki przez równoległe skanowanie tabeli Przykład i pozostają w tym samym wątku później (bez wymiany na partycje).

Operator Sort może wyglądać nieco imponująco, ale liczby pokazują całkowitą liczbę posortowanych wierszy we wszystkich iteracjach sprzężenia w pętli zagnieżdżonej. W rzeczywistości istnieje milion oddzielnych rodzajów, każdy po 18 rzędów. Przy stopniu równoległości równym czterem (w moim przypadku dwa rdzenie hiperwątkowe) w dowolnym momencie występują maksymalnie cztery małe sortowania, a każda instancja sortowania może ponownie wykorzystać pamięć. To wyjaśnia, dlaczego maksymalne wykorzystanie pamięci w tym planie wykonania wynosi zaledwie 136 KB (chociaż przyznano 2152 KB).

Tabela trygramów zawiera jeden wiersz na każdy odrębny trygram w każdym wierszu tabeliźródłowej (identyfikowany przez id ):

Przykład tabeli trygramów

Teraz tworzymy klastrowany indeks b-drzewa do obsługi wyszukiwania dopasowań trygramów:

-- Trigram search index
CREATE UNIQUE CLUSTERED INDEX
    [CUQ dbo.ExampleTrigrams (trigram, id)]
ON dbo.ExampleTrigrams (trigram, id)
WITH (DATA_COMPRESSION = ROW);

Zajmuje to około 45 sekund , chociaż częściowo wynika to z rozsypywania się (moja instancja jest ograniczona do 4 GB pamięci). Instancja z większą dostępną pamięcią mogłaby prawdopodobnie ukończyć budowanie indeksu równoległego z minimalnym logowaniem znacznie szybciej.

Spis planu budowy

Zauważ, że indeks jest określony jako unikalny (przy użyciu obu kolumn w kluczu). Mogliśmy stworzyć nieunikalny indeks klastrowy na samym trygramie, ale SQL Server i tak dodałby 4-bajtowe ujednolicacze do prawie wszystkich wierszy. Gdy weźmiemy pod uwagę, że uniquifiers są przechowywane w części wiersza o zmiennej długości (z powiązanym obciążeniem), bardziej sensowne jest dołączenie id w kluczu i skończ z tym.

Kompresja wierszy jest określona, ​​ponieważ zmniejsza rozmiar tabeli trygramów z 277 MB do 190 MB (dla porównania przykładowa tabela ma 32 MB). Jeśli nie używasz przynajmniej SQL Server 2016 SP1 (gdzie kompresja danych jest dostępna dla wszystkich edycji), możesz w razie potrzeby pominąć klauzulę kompresji.

Jako ostateczną optymalizację utworzymy również zindeksowany widok tabeli trygramów, aby szybko i łatwo znaleźć, które trygramy są najczęściej i najmniej powszechne w danych. Ten krok można pominąć, ale jest on zalecany ze względu na wydajność.

-- Selectivity of each trigram (performance optimization)
CREATE VIEW dbo.ExampleTrigramCounts
WITH SCHEMABINDING
AS
SELECT ET.trigram, cnt = COUNT_BIG(*)
FROM dbo.ExampleTrigrams AS ET
GROUP BY ET.trigram;
GO
-- Materialize the view
CREATE UNIQUE CLUSTERED INDEX
    [CUQ dbo.ExampleTrigramCounts (trigram)]
ON dbo.ExampleTrigramCounts (trigram);

Zindeksowany widok planu budynku

To zajmuje tylko kilka sekund. Rozmiar zmaterializowanego widoku jest niewielki, zaledwie 104 KB .

Wyszukiwanie trygramów

Podany ciąg wyszukiwania (np. '%find%this%' ), nasze podejście będzie polegać na:

  1. Wygeneruj pełny zestaw trygramów dla wyszukiwanego ciągu
  2. Użyj widoku indeksowanego, aby znaleźć trzy najbardziej selektywne trygramy
  3. Znajdź identyfikatory pasujące do wszystkich dostępnych trygramów
  4. Pobierz ciągi według identyfikatora
  5. Zastosuj pełny filtr do wierszy zakwalifikowanych do trygramu

Znajdowanie selektywnych trygramów

Pierwsze dwa kroki są dość proste. Mamy już funkcję do generowania trygramów dla dowolnego ciągu. Znalezienie najbardziej selektywnego z tych trygramów można osiągnąć, dołączając do zindeksowanego widoku. Poniższy kod otacza implementację naszej przykładowej tabeli w innej funkcji wbudowanej. Obraca trzy najbardziej selektywne trygramy w jednym rzędzie w celu ułatwienia późniejszego użycia:

-- Most selective trigrams for a search string
-- Always returns a row (NULLs if no trigrams found)
CREATE FUNCTION dbo.Example_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.ExampleTrigramCounts AS ETC
            WITH (NOEXPAND)
            ON ETC.trigram = GT.trigram
        ORDER BY
            ETC.cnt ASC
    ) AS BT;

Jako przykład:

SELECT
    EGBT.trigram1,
    EGBT.trigram2,
    EGBT.trigram3 
FROM dbo.Example_GetBestTrigrams('%1234%5678%') AS EGBT;

zwraca (dla moich przykładowych danych):

Wybrane trygramy

Plan wykonania to:

Plan wykonania GetBestTrigrams

Jest to znany wcześniej plan generowania trygramów, po którym następuje przeglądanie indeksowanego widoku dla każdego trygramu, sortowanie według liczby dopasowań, numerowanie wierszy (Projekt sekwencyjny), ograniczanie zestawu do trzech wierszy (Góra), a następnie obracanie wynik (Stream Aggregate).

Znajdowanie identyfikatorów pasujących do wszystkich trygramów

Następnym krokiem jest znalezienie identyfikatorów wierszy tabeli Przykład, które pasują do wszystkich trygramów innych niż null pobranych w poprzednim etapie. Zmarszczka polega na tym, że możemy mieć dostępne zero, jeden, dwa lub trzy trygramy. Poniższa implementacja otacza niezbędną logikę w funkcji wieloinstrukcji, zwracając kwalifikujące się identyfikatory w zmiennej tabeli:

-- Returns Example ids matching all provided (non-null) trigrams
CREATE FUNCTION dbo.Example_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.ExampleTrigrams AS ET1 
                WHERE ET1.trigram = @Trigram1
                INTERSECT
                SELECT ET2.id
                FROM dbo.ExampleTrigrams AS ET2
                WHERE ET2.trigram = @Trigram2
                INTERSECT
                SELECT ET3.id
                FROM dbo.ExampleTrigrams AS ET3
                WHERE ET3.trigram = @Trigram3
                OPTION (MERGE JOIN);
            END;
            ELSE
            BEGIN
                -- 2 trigrams available
                INSERT @IDs (id)
                SELECT ET1.id
                FROM dbo.ExampleTrigrams AS ET1 
                WHERE ET1.trigram = @Trigram1
                INTERSECT
                SELECT ET2.id
                FROM dbo.ExampleTrigrams AS ET2
                WHERE ET2.trigram = @Trigram2
                OPTION (MERGE JOIN);
            END;
        END;
        ELSE
        BEGIN
            -- 1 trigram available
            INSERT @IDs (id)
            SELECT ET1.id
            FROM dbo.ExampleTrigrams AS ET1 
            WHERE ET1.trigram = @Trigram1;
        END;
    END;
 
    RETURN;
END;

Szacowany plan wykonania dla tej funkcji przedstawia strategię:

Plan Trigram Match IDs

Jeśli dostępny jest jeden trygram, wykonywane jest pojedyncze wyszukiwanie w tablicy trygramów. W przeciwnym razie wykonywane są dwa lub trzy wyszukiwania i przecięcie identyfikatorów znalezione za pomocą wydajnego łączenia jeden-do-wielu. W tym planie nie ma operatorów zajmujących pamięć, więc nie ma szans na mieszanie lub sortowanie.

Kontynuując przykładowe wyszukiwanie, możemy znaleźć id pasujące do dostępnych trygramów, stosując nową funkcję:

SELECT EGTMID.id 
FROM dbo.Example_GetBestTrigrams('%1234%5678%') AS EGBT
CROSS APPLY dbo.Example_GetTrigramMatchIDs
    (EGBT.trigram1, EGBT.trigram2, EGBT.trigram3) AS EGTMID;

Zwraca to zestaw podobny do następującego:

Pasujące identyfikatory

Rzeczywisty plan (po wykonaniu) nowej funkcji pokazuje kształt planu z użyciem trzech trygramów wejściowych:

Rzeczywisty plan dopasowania identyfikatora

To dość dobrze pokazuje moc dopasowania trygramów. Chociaż każdy z trzech trygramów identyfikuje około 11 000 wierszy w tabeli Przykład, pierwsze przecięcie zmniejsza ten zestaw do 1004 wierszy, a drugie przecięcie zmniejsza go do tylko 7 .

Pełna implementacja wyszukiwania trygramów

Teraz, gdy mamy identyfikatory pasujące do trygramów, możemy wyszukać pasujące wiersze w tabeli Przykład. Nadal musimy zastosować pierwotny warunek wyszukiwania jako ostatnią kontrolę, ponieważ trygramy mogą generować fałszywie pozytywne (ale nie fałszywie negatywne). Ostatnią kwestią do rozwiązania jest to, że poprzednie etapy mogły nie znaleźć żadnych trygramów. Może to być na przykład spowodowane tym, że wyszukiwany ciąg zawiera zbyt mało informacji. Ciąg wyszukiwania '%FF%' nie można użyć wyszukiwania trygramów, ponieważ dwa znaki nie wystarczą do wygenerowania nawet jednego trygramu. Aby sprawnie obsłużyć ten scenariusz, nasze wyszukiwanie wykryje ten stan i wróci do wyszukiwania bez trygramów.

Następująca końcowa funkcja inline implementuje wymaganą logikę:

-- Search implementation
CREATE FUNCTION dbo.Example_TrigramSearch
(
    @Search varchar(255)
)
RETURNS table
WITH SCHEMABINDING
AS
RETURN
    SELECT
        Result.string
    FROM dbo.Example_GetBestTrigrams(@Search) AS GBT
    CROSS APPLY
    (
        -- Trigram search
        SELECT
            E.id,
            E.string
        FROM dbo.Example_GetTrigramMatchIDs
            (GBT.trigram1, GBT.trigram2, GBT.trigram3) AS MID
        JOIN dbo.Example AS E
            ON E.id = MID.id
        WHERE
            -- At least one trigram found 
            GBT.trigram1 IS NOT NULL
            AND E.string LIKE @Search
 
        UNION ALL
 
        -- Non-trigram search
        SELECT
            E.id,
            E.string
        FROM dbo.Example AS E
        WHERE
            -- No trigram found 
            GBT.trigram1 IS NULL
            AND E.string LIKE @Search
    ) AS Result;

Kluczową cechą jest zewnętrzne odniesienie do GBT.trigram1 po obu stronach UNION ALL . Przekładają się one na filtry z wyrażeniami początkowymi w planie wykonania. Filtr startowy wykonuje swoje poddrzewo tylko wtedy, gdy jego warunek ma wartość prawda. Efekt netto jest taki, że tylko jedna część unii zostanie wykonana, w zależności od tego, czy znaleźliśmy trygram, czy nie. Odpowiednia część planu wykonania to:

Efekt filtra uruchamiania

Albo Example_GetTrigramMatchIDs zostanie wykonana funkcja (a wyniki zostaną wyszukane w przykładzie przy użyciu wyszukiwania według identyfikatora) lub skanowanie indeksu klastrowego przykładu z resztą LIKE predykat zostanie uruchomiony, ale nie jedno i drugie.

Wydajność

Poniższy kod testuje wydajność wyszukiwania trygramów względem równoważnego LIKE :

SET STATISTICS XML OFF
DECLARE @S datetime2 = SYSUTCDATETIME();
 
SELECT F2.string
FROM dbo.Example AS F2
WHERE
    F2.string LIKE '%1234%5678%'
OPTION (MAXDOP 1);
 
SELECT ElapsedMS = DATEDIFF(MILLISECOND, @S, SYSUTCDATETIME());
GO
SET STATISTICS XML OFF
DECLARE @S datetime2 = SYSUTCDATETIME();
 
SELECT ETS.string
FROM dbo.Example_TrigramSearch('%1234%5678%') AS ETS;
 
SELECT ElapsedMS = DATEDIFF(MILLISECOND, @S, SYSUTCDATETIME());

Oba generują te same wiersze wyników, ale LIKE zapytanie działa przez 2100ms , podczas gdy wyszukiwanie trygramów trwa 15 ms .

Możliwa jest jeszcze lepsza wydajność. Ogólnie rzecz biorąc, wydajność poprawia się, gdy trygramy stają się bardziej selektywne i mniej liczne (poniżej maksymalnej liczby trzech w tej implementacji). Na przykład:

SET STATISTICS XML OFF
DECLARE @S datetime2 = SYSUTCDATETIME();
 
SELECT ETS.string
FROM dbo.Example_TrigramSearch('%BEEF%') AS ETS;
 
SELECT ElapsedMS = DATEDIFF(MILLISECOND, @S, SYSUTCDATETIME());

To wyszukiwanie zwróciło 111 wierszy do siatki SSMS w 4 ms . LIKE odpowiednik trwał 1950ms .

Utrzymanie trygramów

Jeśli tabela docelowa jest statyczna, nie ma oczywiście problemu z synchronizacją tabeli podstawowej i powiązanej tabeli trygramów. Podobnie, jeśli wyniki wyszukiwania nie muszą być zawsze w pełni aktualne, zaplanowane odświeżanie tabeli trygramów może działać dobrze.

W przeciwnym razie możemy użyć dość prostych wyzwalaczy, aby zsynchronizować dane wyszukiwania trygramu z bazowymi ciągami. Ogólną ideą jest generowanie trygramów dla usuniętych i wstawionych wierszy, a następnie odpowiednio dodawanie lub usuwanie ich w tabeli trygramów. Poniższe wyzwalacze wstawiania, aktualizowania i usuwania pokazują ten pomysł w praktyce:

-- Maintain trigrams after Example inserts
CREATE TRIGGER MaintainTrigrams_AI
ON dbo.Example
AFTER INSERT
AS
BEGIN
    IF @@ROWCOUNT = 0 RETURN;
    IF TRIGGER_NESTLEVEL(@@PROCID, 'AFTER', 'DML') > 1 RETURN;
    SET NOCOUNT ON;
    SET ROWCOUNT 0;
 
    -- Insert related trigrams
    INSERT dbo.ExampleTrigrams
        (id, trigram)
    SELECT
        INS.id, GT.trigram
    FROM Inserted AS INS
    CROSS APPLY dbo.GenerateTrigrams(INS.string) AS GT;
END;
-- Maintain trigrams after Example deletes
CREATE TRIGGER MaintainTrigrams_AD
ON dbo.Example
AFTER DELETE
AS
BEGIN
    IF @@ROWCOUNT = 0 RETURN;
    IF TRIGGER_NESTLEVEL(@@PROCID, 'AFTER', 'DML') > 1 RETURN;
    SET NOCOUNT ON;
    SET ROWCOUNT 0;
 
    -- Deleted related trigrams
    DELETE ET
        WITH (SERIALIZABLE)
    FROM Deleted AS DEL
    CROSS APPLY dbo.GenerateTrigrams(DEL.string) AS GT
    JOIN dbo.ExampleTrigrams AS ET
        ON ET.trigram = GT.trigram
        AND ET.id = DEL.id;
END;
-- Maintain trigrams after Example updates
CREATE TRIGGER MaintainTrigrams_AU
ON dbo.Example
AFTER UPDATE
AS
BEGIN
    IF @@ROWCOUNT = 0 RETURN;
    IF TRIGGER_NESTLEVEL(@@PROCID, 'AFTER', 'DML') > 1 RETURN;
    SET NOCOUNT ON;
    SET ROWCOUNT 0;
 
    -- Deleted related trigrams
    DELETE ET
        WITH (SERIALIZABLE)
    FROM Deleted AS DEL
    CROSS APPLY dbo.GenerateTrigrams(DEL.string) AS GT
    JOIN dbo.ExampleTrigrams AS ET
        ON ET.trigram = GT.trigram
        AND ET.id = DEL.id;
 
    -- Insert related trigrams
    INSERT dbo.ExampleTrigrams
        (id, trigram)
    SELECT
        INS.id, GT.trigram
    FROM Inserted AS INS
    CROSS APPLY dbo.GenerateTrigrams(INS.string) AS GT;
END;

Wyzwalacze są dość wydajne i będą obsługiwać zarówno zmiany jedno-, jak i wielowierszowe (w tym wiele akcji dostępnych podczas korzystania z MERGE oświadczenie). Zindeksowany widok tabeli trygramów będzie automatycznie obsługiwany przez SQL Server bez konieczności pisania kodu wyzwalacza.

Operacja wyzwalania

Jako przykład uruchom instrukcję, aby usunąć dowolny wiersz z tabeli Przykład:

-- Single row delete
DELETE TOP (1) dbo.Example;

Plan wykonania po wykonaniu (rzeczywisty) zawiera wpis dla wyzwalacza po usunięciu:

Usuń plan wykonania wyzwalacza

Żółta sekcja planu odczytuje wiersze z usuniętych pesudo-table, generuje trygramy dla każdego usuniętego przykładowego ciągu (używając znanego planu podświetlonego na zielono), a następnie lokalizuje i usuwa powiązane wpisy tablicy trygramów. Ostatnia sekcja planu, pokazana na czerwono, jest automatycznie dodawana przez SQL Server, aby zapewnić aktualność zindeksowanego widoku.

Plan wyzwalacza wstawiania jest bardzo podobny. Aktualizacje są obsługiwane przez wykonanie usunięcia, a następnie wstawienia. Uruchom następujący skrypt, aby zobaczyć te plany i potwierdzić, że nowe i zaktualizowane wiersze można zlokalizować za pomocą funkcji wyszukiwania trygramów:

-- Single row insert
INSERT dbo.Example (string) 
VALUES ('SQLPerformance.com');
 
-- Find the new row
SELECT ETS.string
FROM dbo.Example_TrigramSearch('%perf%') AS ETS;
 
-- Single row update
UPDATE TOP (1) dbo.Example 
SET string = '12345678901234567890';
 
-- Multi-row insert
INSERT dbo.Example WITH (TABLOCKX)
    (string)
SELECT TOP (1000)
    REPLACE(STR(RAND(CHECKSUM(NEWID())) * 1e10, 10), SPACE(1), '0') +
    RIGHT(NEWID(), 10)
FROM master.dbo.spt_values AS SV1;
 
-- Multi-row update
UPDATE TOP (1000) dbo.Example 
SET string = '12345678901234567890';
 
-- Search for the updated rows
SELECT ETS.string 
FROM dbo.Example_TrigramSearch('12345678901234567890') AS ETS;

Przykład scalania

Następny skrypt pokazuje MERGE instrukcja używana do jednoczesnego wstawiania, usuwania i aktualizowania tabeli Przykład:

-- MERGE demo
DECLARE @MergeData table 
(
    id integer UNIQUE CLUSTERED NULL,
    operation char(3) NOT NULL,
    string char(20) NULL
);
 
INSERT @MergeData 
    (id, operation, string)
VALUES 
    (NULL, 'INS', '11223344556677889900'),  -- Insert
    (1001, 'DEL', NULL),                    -- Delete
    (2002, 'UPD', '00000000000000000000');  -- Update
 
DECLARE @Actions table 
(
    action$ nvarchar(10) NOT NULL, 
    old_id integer NULL, 
    old_string char(20) NULL, 
    new_id integer NULL, 
    new_string char(20) NULL
);
 
MERGE dbo.Example AS E
USING @MergeData AS MD
    ON MD.id = E.id
WHEN MATCHED AND MD.operation = 'DEL' 
    THEN DELETE
WHEN MATCHED AND MD.operation = 'UPD' 
    THEN UPDATE SET E.string = MD.string
WHEN NOT MATCHED AND MD.operation = 'INS'
    THEN INSERT (string) VALUES (MD.string)
OUTPUT $action, Deleted.id, Deleted.string, Inserted.id, Inserted.string
INTO @Actions (action$, old_id, old_string, new_id, new_string);
 
SELECT * FROM @Actions AS A;

Wynik pokaże coś takiego:

Wyjście akcji

Ostateczne przemyślenia

Być może istnieje pewien zakres, aby przyspieszyć duże operacje usuwania i aktualizacji poprzez bezpośrednie odwoływanie się do identyfikatorów zamiast generowania trygramów. Nie jest to tutaj zaimplementowane, ponieważ wymagałoby to nowego indeksu nieklastrowego w tabeli trygramów, podwajając zajętą ​​(już znaczącą) przestrzeń dyskową. Tabela trygramów zawiera jedną liczbę całkowitą i char(3) na rząd; indeks nieklastrowany w kolumnie liczb całkowitych zyskałby char(3) kolumna na wszystkich poziomach (dzięki uprzejmości indeksu klastrowego i konieczności, aby klucze indeksu były unikalne na każdym poziomie). Należy również wziąć pod uwagę miejsce w pamięci, ponieważ wyszukiwanie trygramów działa najlepiej, gdy wszystkie odczyty pochodzą z pamięci podręcznej.

Dodatkowy indeks uczyniłby kaskadową integralność referencyjną opcją, ale często jest to więcej kłopotów, niż jest to warte.

Wyszukiwanie trygramów nie jest panaceum. Dodatkowe wymagania dotyczące pamięci masowej, złożoność implementacji i wpływ na wydajność aktualizacji mają na to duży wpływ. Technika ta jest również bezużyteczna w przypadku wyszukiwań, które nie generują żadnych trygramów (minimum 3 znaki). Chociaż pokazana tutaj podstawowa implementacja może obsługiwać wiele typów wyszukiwania (zaczyna się, zawiera, kończy się wieloma symbolami wieloznacznymi), nie obejmuje ona wszystkich możliwych wyrażeń wyszukiwania, które mogą działać z LIKE . Działa dobrze dla ciągów wyszukiwania, które generują trygramy typu AND; potrzeba więcej pracy, aby obsłużyć ciągi wyszukiwania, które wymagają obsługi typu OR, lub bardziej zaawansowanych opcji, takich jak wyrażenia regularne.

Wszystko to powiedziawszy, jeśli Twoja aplikacja naprawdę musi mieć szybkie wyszukiwanie ciągów znaków wieloznacznych, n-gramy są czymś, co należy poważnie rozważyć.

Powiązane treści:Jeden ze sposobów na uzyskanie indeksu wyszukiwania wiodącego %wildcard autorstwa Aarona Bertranda.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jaka jest różnica między Scope_Identity(), Identity(), @@Identity i Ident_Current()?

  2. MS Access wywołanie procedury składowanej SQL Server

  3. Jak NULLIF() działa w SQL Server

  4. 5 najlepszych narzędzi do modelowania danych dla SQL Server

  5. Znajdź indeks ostatniego wystąpienia podciągu za pomocą T-SQL