T-SQL (2008): Сравнение разнородных фраз (текстовые строки) - WHILE цикл / оптимизация запросов? - PullRequest
0 голосов
/ 19 февраля 2019

Отказ от ответственности : я не являюсь администратором этой базы данных и не имею разрешения на запись (я не могу создавать хранимые процедуры или представления).Кроме того, я застрял на SQL Server 2008 / уровень совместимости 100.

Фон : у меня есть две таблицы, которые имеют общий идентификатор, но один столбец в каждой таблице является текстовой строкой иможет сильно различаться между таблицами, даже если они описывают одно и то же.Поскольку вполне вероятно, что несколько ключевых слов являются общими для каждой строки, я создал запрос, чтобы разбить каждое слово на строку временной таблицы (один столбец для каждого источника) и использовать DIFFERENCE () для определения очень нечеткоспособ, является ли достаточным количество слов, чтобы предположить, что фразы являются вероятным совпадением.

Я присоединяюсь к исходным таблицам и таблицам сравнения по нескольким другим значениям;даже при прочих равных условиях мне все еще нужно вручную проверить, действительно ли данная строка из источника действительно существует в сравнении, основанном на этой строке (перед началом цикла WHILE набор поиска уже настолько узок, насколько это возможно).

Проблема : Там, где я застреваю, я оптимизирую это для быстрого запуска (таблицы объединяются на ~ 30 тыс. Строк).В настоящее время для возврата всех результатов требуется около 30 минут.Я могу сделать это в сегментах, которые занимают меньше минуты, чтобы вернуть пару тысяч результатов за раз, но я действительно надеюсь, что упускаю что-то очевидное, что замедляет мою производительность.Я ни в коем случае не эксперт, и это один из моих первых запросов с использованием цикла WHILE.Это кажется мне слишком запутанным / сложным со всеми временными таблицами и переменными в игре, но, возможно, я просто слишком долго на это смотрел.Заранее спасибо!

Пример запроса:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 
SET NOCOUNT ON

IF OBJECT_ID('tempdb..#Phrases_A') IS NOT NULL
    BEGIN DROP TABLE #Phrases_A END;
IF OBJECT_ID('tempdb..#Phrases_B') IS NOT NULL
    BEGIN DROP TABLE #Phrases_B END;

CREATE TABLE #Phrases_A -- TEST TABLE A
    (
        ID INT IDENTITY(1,1)
        , [PHRASE A] VARCHAR(8000)
    );

CREATE TABLE #Phrases_B -- TEST TABLE B
    (
        ID INT IDENTITY(1,1)
        , [PHRASE B] VARCHAR(8000)
    );

INSERT INTO #Phrases_A ([PHRASE A]) -- TEST DATA TABLE A
VALUES
    ('the quick brown fox jumped over the lazy dog')
    , ('the awkward aardvark ate an ant')
    , ('small phrase')
    , ('abbr. wrds.')
    , ('Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.')
    , ('This is a potentially unique phrase.')
    , ('This is another non-conformist group of words.')
    , ('This phrase is dissimmilar to its counterpart')
    , ('This phrase matches its counterpart exactly')

INSERT INTO #Phrases_B ([PHRASE B])-- TEST DATA TABLE B
VALUES
    ('the assiduous hound caught the lethargic fox')
    , ('the captivated capybara canoodled a cat')
    , ('this is a not-so-small phrase')
    , ('not abbreviated words')
    , ('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.')
    , ('This is a standardized phrase.')
    , ('This is a standardized phrase.')
    , ('Who cleans the CERN supercollider?')
    , ('This phrase matches its counterpart exactly')

-------------------------------

DECLARE @Increment AS INT, @ID_Limit AS INT, @TimeStamp AS DATETIME;

SET @Increment = 1 -- Used to increment loop while value is less than the max value of a given table (see @ID_Limit)
SET @TimeStamp = SYSDATETIME() -- Used to display seconds elapsed for current loop since execution time

IF OBJECT_ID('tempdb..#TextDump') IS NOT NULL
    BEGIN DROP TABLE #TextDump END;
IF OBJECT_ID('tempdb..#TextDumpSRC') IS NOT NULL
    BEGIN DROP TABLE #TextDumpSRC END;
IF OBJECT_ID('tempdb..#TextDumpCMP') IS NOT NULL
    BEGIN DROP TABLE #TextDumpCMP END;
IF OBJECT_ID('tempdb..#TextMatch') IS NOT NULL
    BEGIN DROP TABLE #TextMatch END;

CREATE TABLE #TextMatch -- Used to evaluate phrases and return SOUNDEX likeness / liklihood of match (note that this is outside of loop)
    (
        ID INT,
        [Phrase Match Data] VARCHAR(8000),
        [Phrase Match Strength] DECIMAL(5,2)
    );

SET @ID_Limit = (SELECT MAX(a.ID) FROM #Phrases_A a) -- Stops loop from running after all rows from source table are complete

-------------------------------

WHILE @Increment <= @ID_Limit
BEGIN

    -- Table dump for given loop
    IF OBJECT_ID('tempdb..#TextDump') IS NOT NULL
        BEGIN DROP TABLE #TextDump END;
    IF OBJECT_ID('tempdb..#TextDumpSRC') IS NOT NULL
        BEGIN DROP TABLE #TextDumpSRC END;
    IF OBJECT_ID('tempdb..#TextDumpCMP') IS NOT NULL
        BEGIN DROP TABLE #TextDumpCMP END;
    IF OBJECT_ID('tempdb..#TextDumpSRCMerge') IS NOT NULL
        BEGIN DROP TABLE #TextDumpSRCMerge END;
    IF OBJECT_ID('tempdb..#TextDumpCMPMerge') IS NOT NULL
        BEGIN DROP TABLE #TextDumpCMPMerge END;

    -- Recreate tables for loop evaluation
    CREATE TABLE #TextDump (ID INT, SRC VARCHAR(8000), CMP VARCHAR(8000));
    CREATE TABLE #TextDumpSRC (SRC VARCHAR(8000));
    CREATE TABLE #TextDumpCMP (CMP VARCHAR(8000));
    CREATE TABLE #TextDumpSRCMerge (ID INT, SRC VARCHAR(8000));
    CREATE TABLE #TextDumpCMPMerge (ID INT, CMP VARCHAR(8000));

    DECLARE @VerifyPrint NVARCHAR(36), @SplitSRC NVARCHAR(256), @SplitCMP NVARCHAR(256), @MatchData AS VARCHAR(8000), @SRC_Count AS INT, @CMP_Count AS INT;

    INSERT INTO #TextDump (SRC, CMP) -- Parses potential dynamic SQL pitfall characters and common articles out and joins source tables on common value, in this case ID.
        SELECT 
            LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(' ' + a.[PHRASE A], '''', ''), '"',''), ',',''), '.',''), '-',' '), ' a ',' '), ' an ',' '), ' the ',' ')))
            , LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(' ' + b.[PHRASE B], '''', ''), '"',''), ',',''), '.',''), '-',' '), ' a ',' '), ' an ',' '), ' the ',' ')))
        FROM #Phrases_A a
        JOIN #Phrases_B b ON b.ID = a.ID
        WHERE a.ID = @Increment

    -- @Split = Dynamic SQL that populates rows of table #TextDump with individual words from current phrase
    SET @SplitSRC = N'INSERT INTO #TextDumpSRC (SRC) VALUES (''' + REPLACE((SELECT SRC FROM #TextDump), ' ' , '''' + '), (''') + ''')';
    SET @SplitCMP = N'INSERT INTO #TextDumpCMP (CMP) VALUES (''' + REPLACE((SELECT CMP FROM #TextDump), ' ' , '''' + '), (''') + ''')';

    -- This is just a way to view time elapsed for individual loop evaluations as query runs (useful for large tables)
    SET @VerifyPrint = CONVERT(NVARCHAR(6), @Increment) + ' PASS; RUNTIME = ' + CONVERT(VARCHAR(24), ABS(DATEDIFF(SECOND, SYSDATETIME(), @TimeStamp))) + ' SEC'

    -- Populates #TextDump rows with individual words from Phrase_A (source)
    EXECUTE sp_executesql @SplitSRC;

    -- Inserts Phrase_A words into their own table while removing words less than 2 chars long
    INSERT INTO #TextDumpSRCMERGE (ID, SRC)
        SELECT @Increment, src.SRC
        FROM #TextDumpSRC src
        WHERE LEN(src.SRC) > 2; --Can be changed as needed

    -- Populates #TextDump rows with individual words from Phrase_B (comparison)
    EXECUTE sp_executesql @SplitCMP;

    -- Inserts Phrase_B words into their own table while removing words less than 2 chars long
    INSERT INTO #TextDumpCMPMerge (ID, CMP)
        SELECT @Increment, cmp.CMP
        FROM #TextDumpCMP cmp
        WHERE LEN(cmp.CMP) > 2; --Can be changed as needed

    -- JOINS words from Phrase_A (SRC) and Phrase_B (CMP) where there's an above medium SOUNDEX likeness; also gives some text feedback on what was joined and strength of match
    SELECT @MatchData = COALESCE(@MatchData + ' ', '') + s.SRC + ' :: ' + c.CMP + ' = ' + CAST(CAST((SUM(DIFFERENCE(s.SRC, c.CMP))/4.0)*100 AS INT) AS VARCHAR(4)) + '%, '
    FROM #TextDumpSRCMerge s
        JOIN #TextDumpCMPMerge c ON DIFFERENCE(s.SRC, c.CMP) >= 3
    WHERE s.ID = @Increment
    GROUP BY s.ID, s.SRC, c.CMP

    SELECT @SRC_Count = (SELECT COUNT(s.ID) FROM #TextDumpSRCMerge s)

    SELECT @CMP_Count = (SELECT COUNT(c.ID) FROM #TextDumpCMPMerge c)

    -- Adds results from this loop iteration to master table, which will ultimately be selected outside the loop
    INSERT INTO #TextMatch (ID, [Phrase Match Data], [Phrase Match Strength])
        SELECT 
            s.ID 
            , @MatchData
            , COALESCE(SUM(DIFFERENCE(s.SRC, c.CMP)/((@SRC_Count + @CMP_Count)*2.0)), 0)
        FROM #TextDumpSRCMerge s
        INNER JOIN #TextDumpCMPMerge c 
            ON DIFFERENCE(s.SRC, c.CMP) >= 3
        WHERE s.ID = @Increment
        GROUP BY s.ID;

    -- Prints the loop time elapsed to Messages tab
    RAISERROR (@VerifyPrint, 1, 1)

    -- Updates the increment value
    SET @Increment = @Increment + 1

    -- Resets the @MatchData variable, though this is likely unnecessary
    SET @MatchData = ''

END;

-------------------------------

SELECT * 
FROM #Phrases_A a
JOIN #Phrases_B b ON b.ID = a.ID 
JOIN #TextMatch m ON m.ID = a.ID

1 Ответ

0 голосов
/ 21 февраля 2019

Используя разделитель строк, вы можете определить, какой процент слов из фразы A содержится в фразе B. Если вы хотите, чтобы этот рефакторинг сравнивал их друг с другом (вместо только от A до B), дайте мне знать.

Использовался сплиттер Джеффа Модена

;with a as(
select
    a.*
    ,aSplit.*
from #Phrases_A a
cross apply 
    dbo.DelimitedSplit8K([Phrase A], ' ') aSplit),

b as(
select
    b.*
    ,bSplit.*
from #Phrases_B b
cross apply 
    dbo.DelimitedSplit8K([Phrase B], ' ') bSplit)

select distinct
    a.ID
    ,a.[PHRASE A]
    ,b.[PHRASE B]
    ,PercentMatched = cast(count(case when a.Item = b.Item then 1 end) / (count(distinct a.Item) * 1.0) * 100 as decimal(5,2))
from a
left join b on b.ID = a.ID 
group by
    a.ID
    ,a.[PHRASE A]
    ,b.[PHRASE B]
...