Pergunta

Imagine que você tem uma mesa de Products (ID int, Name nvarchar(200)), e duas outras mesas, ProductsCategories (ProductID int, CategoryID int) e InvoiceProducts (InvoiceID int, ProductID int).

Preciso escrever uma consulta para produzir um conjunto de produtos que correspondam a um determinado conjunto de IDs de fatura e IDs de categoria, de modo que a lista de produtos corresponda a todas as categorias especificadas e todas as faturas especificadas, sem voltar ao SQL dinâmico. Imagine que preciso encontrar uma lista de produtos que estejam nas categorias 1 e 2 e nas faturas 3 e 4.

Como começo, escrevi um procedimento armazenado que aceita os IDs de categoria e os IDs de fatura como cordas e analisam-os em tabelas:

 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

As diferentes soluções que eu tenho parecem horríveis e pior. A melhor coisa que encontrei é gerar uma visão composta por junções à esquerda de todos os critérios, mas isso parece muito caro e não resolve a questão de combinar todas as diferentes chaves especificadas.


Atualizar: Este é um exemplo de consulta que escrevi que gera os resultados esperados. Estou perdendo alguma oportunidade de otimização? Como operações mágicas de matriz unicórnio por ninjas?

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)   
Foi útil?

Solução

Desde que você tenha índices únicos em ambos (ProductID, CategoryID) e (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

ou, se seus valores forem passados CSV cordas:

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
        )

Observe que em SQL Server 2008 Você pode passar os parâmetros com valor de tabela para os procedimentos armazenados.

Outras dicas

Eu começaria com algo assim, utilizando seus valores de identificação entre os parâmetros. As tabelas de temperatura podem ajudar na velocidade da subconeração.

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

As categorias de produtos devem ter um índice em cluster (categoryId, productId) e os produtos de fatura devem ter um (InvoiceId, ProductId) de maneira ideal. Isso permitirá encontrar IDs de produtos, dado o categoryID e o InvoiceId usando os dados apenas nos índices clusterados.

Você pode usar uma função para retornar uma tabela de INTs com uma string. Google "CSVToint" e clique no primeiro link da SQLTeam para ver o código.

Então você poderia:

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

Que tal um CTE recursivo?

Primeiro, adicione números de linha às tabelas de critérios, depois alguns pseudo -sql se você quiser:

;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.

Isso lhe dará uma maneira de executar e em vários critérios, e deve ter um bom desempenho.

Isso faz algum sentido? Ultimamente, fiz algumas coisas rápidas muito legais com CTEs e posso elaborar, se necessário.

Removeu o código CTE porque estava errado e não vale a pena consertar ter uma solução muito melhor por aí.

Passe -os como parâmetro XML, armazene -os para uma tabela temporária e junte -se.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top