Сопоставление приоритетов SQL

Я пытаюсь выполнить сопоставление приоритетов для таблицы в хранимой процедуре. Требования немного сложно объяснить, но, надеюсь, это будет иметь смысл. Допустим, у нас есть таблица с названием books, с полями id, author, title, date и pages.

У нас также есть хранимая процедура, которая будет сопоставлять запрос с ОДНОЙ строкой в ​​таблице.

Вот подпись процедуры:

create procedure match
  @pAuthor varchar(100)
  ,@pTitle varchar(100)
  ,@pDate varchar(100)
  ,@pPages varchar(100)

 as

 ...

Правила приоритета следующие:

  • Во-первых, попробуйте сопоставить все 4 параметра. Если мы найдем возврат матча.
  • Затем попробуйте сопоставить любые 3 параметра. Здесь 1-й параметр имеет наивысший приоритет, а 4-й — низший. Если мы найдем какие-либо совпадения, верните совпадение.
  • Затем мы проверяем, совпадают ли какие-либо два параметра и, наконец, соответствует ли хотя бы один из них (по-прежнему следуя правилам приоритета порядка параметров).

Я реализовал это в каждом конкретном случае. Например:

 select @lvId = id 
 from books
 where
  author = @pAuthor 
 ,title = @pTitle 
 ,date = @pDate 
 ,pages = @pPages

if @@rowCount = 1 begin
  select @lvId
  return
end

 select @lvId = id 
  from books
 where
  author = @pAuthor 
 ,title = @pTitle 
 ,date = @pDate 

 if @@rowCount = 1 begin
  select @lvId
  return
end

....

Однако для каждого нового столбца в таблице количество отдельных проверок вырастает на порядок 2. Мне бы очень хотелось обобщить это на X кол-во столбцов; однако у меня возникли проблемы с придумыванием схемы.

Спасибо за чтение, и я могу предоставить любую дополнительную информацию, необходимую.


Добавлено:

Дэйв и другие, я попытался реализовать ваш код, и он задыхается от первого пункта Order by, где мы добавляем все подсчеты. Это дает мне недопустимую ошибку имени столбца. Когда я комментирую общее количество и упорядочиваю только отдельные псевдонимы, процедура компилируется нормально.

У кого-нибудь есть идеи?

Это в Microsoft Sql Server 2005


person Charlie White    schedule 22.12.2008    source источник
comment
Что случилось с использованием AND для соединения условий в предложении WHERE?   -  person Jonathan Leffler    schedule 23.12.2008
comment
@JL Это должно быть смешно?   -  person    schedule 23.12.2008


Ответы (7)


Я считаю, что ответы, над которыми вы работаете, на сегодняшний день самые простые. Но я также считаю, что в SQL-сервере всегда будет полное сканирование таблиц. (В Oracle вы можете использовать индексы Bitmap, если таблица не подвергалась большому количеству одновременных DML)

Более сложным решением, но гораздо более эффективным было бы создание собственного индекса. Не индекс SQL Server, а ваш собственный.

Создайте таблицу (хэш-индекс) с 3 столбцами (поиск-хэш, ранг, ряд)

Скажем, у вас есть 3 столбца для поиска. А, Б, С

Для каждой строки, добавленной в книги, вы будете вставлять 7 строк в hash_index либо с помощью триггера, либо с помощью процедуры CRUD.

Сначала вы будете

insert into hash_index 
SELECT HASH(A & B & C), 7 , ROWID
FROM Books

Где & — оператор конкатенации, а HASH — функция

затем вы вставите хэши для A и B, A и C и B и C. Теперь у вас есть некоторая гибкость, вы можете дать им одинаковый ранг или, если A и B лучше соответствуют B и C, вы можете дать им более высокий ранг.

А затем вставьте хэши для A отдельно и B и C с одинаковым выбором ранга... все одинаковое число или все разные... можно даже сказать, что совпадение на A лучше, чем совпадение на B и C , Это решение дает вам большую гибкость.

Конечно, это добавит много накладных расходов на INSERT, но если DML в книгах низкий или производительность не имеет значения, все в порядке.

Теперь, когда вы приступите к поиску, вы создадите функцию, которая возвращает таблицу HASH для ваших @A, @B и @C. у вас будет небольшая таблица из 7 значений, которые вы присоедините к хешу поиска в таблице хэш-индексов. Это даст вам все возможные совпадения и, возможно, некоторые ложные совпадения (это просто природа хэшей). Вы получите этот результат, закажите desc в столбце рангов. Затем верните первый идентификатор строки обратно в таблицу book и убедитесь, что все значения @A @B @C действительно находятся в этой строке. На случай, если это не так, и вы получили ложное срабатывание, вам нужно будет проверить следующий rowid.

Каждая из этих операций в этом «ролле сама» выполняется очень быстро.

  • Хеширование ваших 3 значений в небольшую табличную переменную из 7 строк = очень быстро.
  • объединение их по индексу в вашей таблице Hash_index = очень быстрый поиск по индексу
  • Цикл по набору результатов приведет к доступу к 1 или, возможно, 2 или 3 таблицам по rowid = очень быстро

Конечно, все это вместе может быть медленнее, чем FTS... Но FTS будет продолжать становиться все медленнее и медленнее. Будет размер, который FTS медленнее, чем этот. Вам придется поиграть с ним.

person Community    schedule 23.12.2008

У меня нет времени писать запрос, но я думаю, что эта идея сработает.

Для вашего предиката используйте «author = @pAuthor OR title = @ptitle ...", чтобы получить все строки-кандидаты.

Используйте выражения CASE или что угодно, чтобы создать виртуальные столбцы в результирующем наборе, например:

SELECT CASE WHEN author = @pAuthor THEN 1 ELSE 0 END author_match,
       ...

Затем добавьте этот порядок и верните первую строку:

ORDER BY (author_match+title_match+date_match+page_match) DESC,
         author_match DESC,
         title_match DESC,
         date_match DESC
         page_match DESC

Вам все еще нужно расширять его для каждого нового столбца, но только немного.

person Dave Costa    schedule 22.12.2008
comment
Дэйв, спасибо, это выглядит многообещающе. Я немного поиграю и отчитаюсь. - person Charlie White; 22.12.2008

Вы не объясняете, что должно произойти, если более одного результата соответствует любому заданному набору достигнутых параметров, поэтому вам нужно будет изменить это, чтобы учесть эти бизнес-правила. Прямо сейчас я настроил его так, чтобы возвращались книги, которые соответствуют более поздним параметрам, а не те, которые не соответствуют. Например, совпадение по автору, названию и страницам будет предшествовать совпадению по автору и названию.

Ваша СУБД может иметь другой способ обработки «TOP», поэтому вам также может потребоваться настроить его.

SELECT TOP 1
     author,
     title,
     date,
     pages
FROM
     Books
WHERE
     author = @author OR
     title = @title OR
     date = @date OR
     pages = @pages OR
ORDER BY
     CASE WHEN author = @author THEN 1 ELSE 0 END +
     CASE WHEN title = @title THEN 1 ELSE 0 END +
     CASE WHEN date = @date THEN 1 ELSE 0 END +
     CASE WHEN pages = @pages THEN 1 ELSE 0 END DESC,

     CASE WHEN author = @author THEN 8 ELSE 0 END +
     CASE WHEN title = @title THEN 4 ELSE 0 END +
     CASE WHEN date = @date THEN 2 ELSE 0 END +
     CASE WHEN pages = @pages THEN 1 ELSE 0 END DESC
person Tom H    schedule 22.12.2008
comment
Я думаю, что это не совсем то, чего он хочет, потому что совпадение по автору + заголовку лучше, чем совпадение по названию + дате + страницам. Я думаю, он хотел, чтобы 3 матча всегда побеждали 2 матча. - person Dave Costa; 22.12.2008
comment
@Dave, правильно, мне нужно 3 совпадения, прежде чем 2. @Tom, мы можем предположить, что найдем только одно совпадение. Выберите Top 1 будет работать нормально. - person Charlie White; 22.12.2008
comment
Я внес некоторые изменения, чтобы исправить эту проблему, но без предложения WHERE производительность может быть проблемой. Хотя я еще подумаю. - person Tom H; 22.12.2008
comment
Как и в моем ответе, вы можете добавить WHERE author=@author OR title=@title и т. д., чтобы отфильтровать набор строк, которые необходимо отсортировать. - person Dave Costa; 22.12.2008
comment
Том Х. Единственный способ, который я могу придумать, с этим решением для повышения производительности предложения WHERE — это использование индексов Bitmap, которые недоступны в MSSS. - person ; 23.12.2008

      select id, 
               CASE WHEN @pPages = pages 
                    THEN 1 ELSE 0 
               END
             +  Case WHEN @pAuthor=author 
                    THEN 1 ELSE 0 
                END AS 
             /* +  Do this for each attribute. If each of your 
attributes are just as important as the other 
for example matching author is jsut as a good as matching title then 
leave the values alone, if different matches are more 
important then change the values */ as MatchRank  
        from books 

        where  author = @pAuthor OR
               title = @pTitle OR
               date = @pDate

     ORDER BY  MatchRank DESC

Отредактировано

Когда я запускаю этот запрос (модифицированный только для одной из моих собственных таблиц), он отлично работает в SQL2005.

Я бы порекомендовал предложение where, но вы захотите поиграть с ним, чтобы увидеть влияние на производительность. Вам нужно будет использовать предложение OR, иначе вы потеряете потенциальные совпадения.

person JoshBerke    schedule 22.12.2008

Что касается предложения Order By, которое не удалось скомпилировать:

Как сказал рекурсивный (в комментарии), псевдоним не может быть в выражениях, которые используются в предложениях Order By. чтобы обойти это, я использовал подзапрос, который возвращал строки, а затем упорядочивал их во внешнем запросе. Таким образом, я могу использовать псевдоним в предложении order by. Немного медленнее, но намного чище.

person Charlie White    schedule 23.12.2008
comment
Перестаньте публиковать ответы. Отредактируйте исходный пост или добавьте к нему комментарии. Теперь, когда тебе за 50, я думаю, ты можешь комментировать. - person ; 23.12.2008

Хорошо, позвольте мне повторить мое понимание вашего вопроса: вам нужна хранимая процедура, которая может принимать переменное количество параметров и возвращать верхнюю строку, которая соответствует параметрам в взвешенном порядке предпочтения, переданном в SQL Server 2005.

В идеале он будет использовать предложения WHERE для предотвращения полного сканирования таблиц, а также использовать преимущества индексов и будет "короткое замыкание" поиска - вы не хотите искать все возможные комбинации, если одна из них может быть найдена раньше. Возможно, мы также можем разрешить другие компараторы, кроме =, такие как >= для дат, LIKE для строк и т. д.

Один из возможных способов — передать параметры в виде XML, как в эта статья и использовать хранимые процедуры .Net, но давайте пока оставим простой ванильный T-SQL.

Мне это кажется бинарным поиском по параметрам: искать все параметры, затем отбрасывать последний, затем отбрасывать предпоследний, но включать последний и т. д.

Давайте передадим параметры в виде строки с разделителями, поскольку хранимые процедуры не позволяют передавать массивы в качестве параметров. Это позволит нам получить переменное количество параметров в нашей хранимой процедуре, не требуя хранимой процедуры для каждого варианта параметров.

Чтобы разрешить какое-либо сравнение, мы передадим весь список предложений WHERE, например так: заголовок типа '%something%'

Передача нескольких параметров означает их разграничение в строке. Мы будем использовать символ тильды ~ для разделения параметров, например: author = 'Chris Latta'~title like '%something%'~pages >= 100

Тогда это просто вопрос выполнения двоичного взвешенного поиска первой строки, которая соответствует нашему упорядоченному списку параметров (надеюсь, хранимая процедура с комментариями не требует пояснений, но если нет, дайте мне знать). Обратите внимание, что вам всегда гарантируется результат (при условии, что в вашей таблице есть хотя бы одна строка), поскольку последний поиск не имеет параметров.

Вот код хранимой процедуры:

CREATE PROCEDURE FirstMatch
@SearchParams VARCHAR(2000)
AS
BEGIN
    DECLARE @SQLstmt NVARCHAR(2000)
    DECLARE @WhereClause NVARCHAR(2000)
    DECLARE @OrderByClause NVARCHAR(500)
    DECLARE @NumParams INT
    DECLARE @Pos INT
    DECLARE @BinarySearch INT
    DECLARE @Rows INT

    -- Create a temporary table to store our parameters
    CREATE TABLE #params 
    (
        BitMask int,             -- Uniquely identifying bit mask
        FieldName VARCHAR(100),  -- The field name for use in the ORDER BY clause
        WhereClause VARCHAR(100) -- The bit to use in the WHERE clause
    )

    -- Temporary table identical to our result set (the books table) so intermediate results arent output
    CREATE TABLE #junk
    (
        id INT,
        author VARCHAR(50),
        title VARCHAR(50),
        printed DATETIME,
        pages INT
    )

    -- Ill use tilde ~ as the delimiter that separates parameters
    SET @SearchParams = LTRIM(RTRIM(@SearchParams))+ '~'
    SET @Pos = CHARINDEX('~', @SearchParams, 1)
    SET @NumParams = 0

    -- Populate the #params table with the delimited parameters passed
    IF REPLACE(@SearchParams, '~', '') <> ''
    BEGIN
        WHILE @Pos > 0
        BEGIN
            SET @NumParams = @NumParams + 1
            SET @WhereClause = LTRIM(RTRIM(LEFT(@SearchParams, @Pos - 1)))
            IF @WhereClause <> ''
            BEGIN
                -- This assumes your field names dont have spaces and that you leave a space between the field name and the comparator
                INSERT INTO #params (BitMask, FieldName, WhereClause) VALUES (POWER(2, @NumParams - 1), LTRIM(RTRIM(LEFT(@WhereClause, CHARINDEX(' ', @WhereClause, 1) - 1))), @WhereClause) 
            END
            SET @SearchParams = RIGHT(@SearchParams, LEN(@SearchParams) - @Pos)
            SET @Pos = CHARINDEX('~', @SearchParams, 1)
        END
    END 

    -- Set the binary search to search from all parameters down to one in order of preference
    SET @BinarySearch = POWER(2, @NumParams) 
    SET @Rows = 0
    WHILE (@BinarySearch > 0) AND (@Rows = 0)
    BEGIN
        SET @BinarySearch = @BinarySearch - 1
        SET @WhereClause = ' WHERE '
        SET @OrderByClause = ' ORDER BY '
        SELECT @OrderByClause = @OrderByClause + FieldName + ', ' FROM #params WHERE (@BinarySearch & BitMask) = BitMask ORDER BY BitMask
        SET @OrderByClause = LEFT(@OrderByClause, LEN(@OrderByClause) - 1) -- Remove the trailing comma
        SELECT @WhereClause = @WhereClause + WhereClause + ' AND ' FROM #params WHERE (@BinarySearch & BitMask) = BitMask ORDER BY BitMask
        SET @WhereClause = LEFT(@WhereClause, LEN(@WhereClause) - 4) -- Remove the trailing AND

        IF @BinarySearch = 0
        BEGIN
            -- If nothing found so far, return the top row in the order of the parameters fields
            SET @WhereClause = ''
            -- Use the full order sequence of fields to return the results
            SET @OrderByClause = ' ORDER BY '
            SELECT @OrderByClause = @OrderByClause + FieldName + ', ' FROM #params ORDER BY BitMask
            SET @OrderByClause = LEFT(@OrderByClause, LEN(@OrderByClause) - 1) -- Remove the trailing comma
        END

        -- Find out if there are any results for this search
        SET @SQLstmt = 'SELECT TOP 1 id, author, title, printed, pages INTO #junk FROM books' + @WhereClause + @OrderByClause
        Exec (@SQLstmt)

        SET @Rows = @@RowCount
    END

    -- Stop the result set being eaten by the junk table
    SET @SQLstmt = REPLACE(@SQLstmt, 'INTO #junk ', '')

    -- Uncomment the next line to see the SQL you are producing
    --PRINT @SQLstmt

    -- This gives the result set
    Exec (@SQLstmt)
END

Эта хранимая процедура вызывается так:

FirstMatch 'author = ''Chris Latta''~pages > 100~title like ''%something%'''

Вот и все — полностью расширяемый, оптимизированный поиск лучших результатов в порядке взвешенного предпочтения. Это была интересная задача, и она показывает, чего можно добиться с помощью родного T-SQL.

Пара небольших проблем с этим:

  • он полагается на то, что вызывающая сторона знает, что они должны оставить пробел после имени поля, чтобы параметр работал правильно.
  • вы не можете иметь имена полей с пробелами в них - поправимо с некоторыми усилиями
  • предполагается, что соответствующий порядок сортировки всегда восходящий
  • следующий программист, которому придется взглянуть на эту процедуру, подумает, что вы сумасшедший :)
person Chris Latta    schedule 23.12.2008
comment
Я не думаю, что это переменное количество параметров как таковое (все вызовы будут передавать все 4 параметра). Если все 4 совпадают, то строки должны быть возвращены. Если со всеми 4 ничего нет, то должны быть возвращены строки, соответствующие «лучшим 3» параметрам, а затем те, которые соответствуют двум лучшим параметрам; тогда лучший. - person Jonathan Leffler; 23.12.2008
comment
Да, но он говорит, что для каждого нового столбца количество отдельных проверок увеличивается в два раза. Мое решение не меняется с дополнительными (или меньшими) столбцами, допускает взвешенный поиск, использует индексы, замыкает набор результатов и допускает операторы, отличные от равных. Он делает то, что просит ОП. - person Chris Latta; 23.12.2008

Попробуй это:

ALTER PROCEDURE match  
  @pAuthor varchar(100)  
 ,@pTitle varchar(100)  
 ,@pDate varchar(100)  
 ,@pPages varchar(100)  
-- exec match 'a title', 'b author', '1/1/2007', 15  
AS

SELECT  id,

        CASE WHEN author = @pAuthor THEN 1 ELSE 0 END
        + CASE WHEN title = @pTitle THEN 1 ELSE 0 END
        + CASE WHEN bookdate = @pDate THEN 1 ELSE 0 END
        + CASE WHEN pages = @pPages THEN 1 ELSE 0 END AS matches,

        CASE WHEN author = @pAuthor THEN 4 ELSE 0 END
        + CASE WHEN title = @pTitle THEN 3 ELSE 0 END
        + CASE WHEN bookdate = @pDate THEN 2 ELSE 0 END
        + CASE WHEN pages = @pPages THEN 1 ELSE 0 END AS score
FROM books
WHERE author = #pAuthor 
    OR title = @pTitle 
    OR bookdate = @PDate 
    OR pages = @pPages
ORDER BY matches DESC, score DESC

Однако это, конечно, вызывает сканирование таблицы. Вы можете избежать этого, сделав его объединением CTE и 4 предложений WHERE, по одному для каждого свойства - будут дубликаты, но вы все равно можете просто взять TOP 1.

РЕДАКТИРОВАТЬ: добавлено предложение WHERE... OR. Я чувствовал бы себя более комфортно, если бы это было

SELECT ... FROM books WHERE author = @pAuthor
UNION
SELECT ... FROM books WHERE title = @pTitle
UNION
...
person dkretz    schedule 23.12.2008