Сложный SQL-запрос - поиск элементов, соответствующих нескольким различным внешним ключам - PullRequest
2 голосов
/ 26 октября 2010

Итак, представьте, что у вас есть таблица Products (ID int, Name nvarchar(200)) и две другие таблицы ProductsCategories (ProductID int, CategoryID int) и InvoiceProducts (InvoiceID int, ProductID int).

Мне нужно написать запрос для создания набора продуктов, которые соответствуют заданному набору идентификаторов счетов и идентификаторов категорий, чтобы список продуктов соответствовал всем указанным категориям и всем указанным счетам, не возвращаясь к динамическому SQL. Представьте, что мне нужно найти список продуктов, которые относятся как к категориям 1 и 2, так и к счетам 3 и 4.

Для начала я написал хранимую процедуру, которая принимает идентификаторы категорий и идентификаторов счетов-фактур в виде строк и разбирает их на таблицы:

 CREATE PROCEDURE dbo.SearchProducts (@categories varchar(max), @invoices varchar(max))
 AS BEGIN
      with catids as (select cast([value] as int) from dbo.split(@categories, ' ')),
           invoiceids as (select cast([value] as int) from dbo.split(@invoices, ' '))
           select * from products --- insert awesomeness here
 END

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


Обновление: Это пример написанного мной запроса, который дает ожидаемые результаты. Я упускаю какие-либо возможности оптимизации? Как магические операции с матрицей единорога от ниндзя?

with catids as (select distinct cast([value] as int) [value] from dbo.split(@categories, ' ')),
  invoiceids as (select distinct cast([value] as int) [value] from dbo.split(@invoices, ' '))

  select pc.ProductID from ProductsCategories pc (nolock)
    inner join catids c on c.value = pc.CategoryID 
    group by pc.ProductID 
    having COUNT(*) = (select COUNT(*) from catids)  
  intersect
  select ip.ProductID from InvoiceProducts ip (nolock)
    inner join invoiceids i on i.value = ip.InvoiceID 
    group by ip.ProductID 
    having COUNT(*) = (select COUNT(*) from invoiceids)   

Ответы [ 5 ]

1 голос
/ 27 октября 2010

При условии, что у вас есть уникальные индексы (ProductID, CategoryID) и (ProductID, InvoiceID):

SELECT  ProductID
FROM    (
        SELECT  ProductID
        FROM    ProductInvoice
        WHERE   InvoiceID IN (1, 2)
        UNION ALL
        SELECT  ProductID
        FROM    ProductCategory pc
        WHERE   CategoryID IN (3, 4)
        ) q
GROUP BY
        ProductID
HAVING  COUNT(*) = 4

или, если ваши значения передаются в CSV строках:

WITH    catids(value) AS
        (
        SELECT  DISTINCT CAST([value] AS INT)
        FROM    dbo.split(@categories, ' '))
        ), 
        (
        SELECT  DISTINCT CAST([value] AS INT)
        FROM    dbo.split(@invoices, ' '))
        )
SELECT  ProductID
FROM    (
        SELECT  ProductID
        FROM    ProductInvoice
        WHERE   InvoiceID IN
                (
                SELECT  value
                FROM    invoiceids
                )
        UNION ALL
        SELECT  ProductID
        FROM    ProductCategory pc
        WHERE   CategoryID IN
                (
                SELECT  value
                FROM    catids
                )
        ) q
GROUP BY
        ProductID
HAVING  COUNT(*) = 
        (
        SELECT  COUNT(*)
        FROM    catids
        ) + 
        (
        SELECT  COUNT(*)
        FROM    invoiceids
        )

Обратите внимание, что в SQL Server 2008 вы можете передавать табличные параметры в хранимые процедуры.

0 голосов
/ 26 октября 2010

ProductCategories должен иметь кластеризованный индекс на (CategoryId, ProductId), а InvoiceProducts должен иметь один на (InvoiceId, ProductId) оптимально.Это позволит находить идентификаторы продуктов с данными CategoryId и InvoiceId, используя данные только в кластеризованных индексах.

Вы можете использовать функцию для возврата таблицы целых чисел с заданной строкой.Google "CsvToInt" и нажмите на первую ссылку из SqlTeam, чтобы увидеть код.

Тогда вы можете:

SELECT *
FROM Products
WHERE ID IN (SELECT DISTINCT ProductId 
        FROM ProductCategories
        WHERE CategoryId in dbo.CsvToInt(@categories)
    ) AND ID IN (SELECT DISTINCT ProductId 
        FROM InvoiceProducts
        WHERE InvoiceId in dbo.CsvToInt(@invoices)
    )
0 голосов
/ 26 октября 2010

Я бы начал с чего-то вроде этого, используя ваши значения идентификатора таблицы из параметров. Временные таблицы могут помочь со скоростью подзапроса.

select p.*
from
(
    select pc.*
    from catids c
    inner join ProductsCategories pc
        on pc.CategoryID = c.value
) catMatch
inner join
(
    select pin.*
    from invoiceids i
    inner join ProductsInvoices pin
        on pin.InvoiceID = i.value
) invMatch
    on invMatch.ProductID = catMatch.ProductID
inner join Products p
    on p.ID = invMatch.ProductID
0 голосов
/ 26 октября 2010

Как насчет рекурсивного CTE?

Сначала добавьте номера строк в таблицы критериев, затем псевдо SQL, если хотите:

;WITH cte AS(
Base case: Select productid, criteria from products left join criteria where row_number = 1 if it matches criteria from both row 1s or one is null.
UNION ALL
Recursive case: Select n+1 criteria row from products left join criteria where row_number = cte.row_number + 1 AND matches criteria from both row_number + 1 or one or the other (but not both) is null
)
SELECT *
WHERE criteria = maximum id from criteria table.

Это даст вам способ выполнить ANDпо нескольким критериям и должны хорошо работать.

Имеет ли это какой-либо смысл вообще?В последнее время я сделал довольно крутые вещи с CTE, и могу уточнить при необходимости.

Удален cte-код, потому что он был неправильным и не стоило исправления, так как существовало гораздо лучшее решение.

0 голосов
/ 26 октября 2010

Передайте их как параметр XML, сохраните их во временную таблицу и объедините.

...