Selecione produtos onde a categoria pertence a nenhuma categoria na hierarquia
-
10-07-2019 - |
Pergunta
Eu tenho uma tabela produtos que contém uma FK para uma categoria, a tabela Categorias é criado de uma forma que cada categoria pode ter uma categoria pai, exemplo:
Computers
Processors
Intel
Pentium
Core 2 Duo
AMD
Athlon
Eu preciso fazer uma consulta seleção que se a categoria selecionada é Processors, ele irá devolver os produtos que está em Intel, Pentium, Core 2 Duo, AMD, etc ...
Eu pensei sobre a criação de uma espécie de "cache" que irá armazenar todas as categorias na hierarquia para cada categoria no db e incluem o "IN" na cláusula onde. É esta a melhor solução?
Solução
A melhor solução para isso é na fase de projeto de banco de dados. Suas categorias necessidades de mesa para ser um Nested Set . O artigo Gerenciamento de dados hierárquicos em MySQL não é que o MySQL específico ( apesar do título), e dá uma grande visão geral dos diferentes métodos de armazenar uma hierarquia em uma tabela de banco de dados.
Sumário Executivo:
conjuntos aninhados
- Seleciona são fáceis para qualquer profundidade
- Inserções e exclusões são difíceis
Padrão PARENT_ID baseado hierarquia
- Seleciona são baseados em junções internas (de modo a obter rápido cabeludo)
- Inserções e exclusões são fáceis
Assim, com base no seu exemplo, se sua tabela de hierarquia era um conjunto aninhado sua consulta seria algo parecido com isto:
SELECT * FROM products
INNER JOIN categories ON categories.id = products.category_id
WHERE categories.lft > 2 and categories.rgt < 11
a 2 e 11 são a esquerda e direita, respectivamente, do registro Processors
.
Outras dicas
parece um trabalho para uma expressão de tabela comum .. algo ao longo das linhas de:
with catCTE (catid, parentid)
as
(
select cat.catid, cat.catparentid from cat where cat.name = 'Processors'
UNION ALL
select cat.catid, cat.catparentid from cat inner join catCTE on cat.catparentid=catcte.catid
)
select distinct * from catCTE
Isso deve selecionar a categoria cujo nome é 'Processors' e qualquer de descendentes, deve ser capaz de usar isso em uma cláusula IN para puxar para trás os produtos.
Eu tenho feito coisas semelhantes no passado, primeira consulta para as IDs de categoria, em seguida, consultando para os produtos "em" dessas categorias. Obtendo as categorias é o pouco difícil, e você tem algumas opções:
- Se o nível de aninhamento de categorias é conhecido ou você pode encontrar um limite superior: Construir um SELECT de aparência horrível com lotes de junções. Isto é rápido, mas feio e você precisa definir um limite sobre os níveis da hierarquia.
- Se você tem um número relativamente pequeno de categorias no total, consulta todos eles (apenas ids, pais), recolher os ids de aqueles que se preocupam, e fazer um SELECT .... IN para os produtos. Esta foi a opção adequada para mim.
- Consulta para cima / baixo na hierarquia usando uma série de SELECTs. Simples, mas relativamente lento.
- Eu acredito que as versões recentes do SQLServer tem algum suporte para consultas recursivas, mas não usei-los eu mesmo.
Os procedimentos armazenados podem ajudar se você não quer fazer isso do lado do aplicativo.
O que você quer encontrar é o fechamento transitivo da categoria relação "pai". Suponho que não há nenhuma limitação à categoria de profundidade hierarquia, então você não pode formular uma única consulta SQL, que encontra todas as categorias. O que eu faria (em pseudocódigo) é o seguinte:
categoriesSet = empty set
while new.size > 0:
new = select * from categories where parent in categoriesSet
categoriesSet = categoriesSet+new
Então, basta manter a consulta para crianças até não mais são encontrados. Este se comporta bem em termos de velocidade, a menos que você tem uma hierarquia degenerado (digamos, 1000 categorias, cada uma criança de outro), ou um grande número de categorias no total. No segundo caso, você poderia sempre trabalhar com tabelas temporárias para manter a transferência de dados entre seu aplicativo e o pequeno banco de dados.
Talvez algo como:
select *
from products
where products.category_id IN
(select c2.category_id
from categories c1 inner join categories c2 on c1.category_id = c2.parent_id
where c1.category = 'Processors'
group by c2.category_id)
[EDIT] Se a profundidade categoria é maior do que um presente formaria sua consulta mais interna. Eu suspeito que você poderia projetar um procedimento armazenado que iria detalhar na tabela até que os ids retornados pela consulta interna não ter filhos - provavelmente melhor ter um atributo que marca uma categoria como um nó terminal na hierarquia - em seguida, executar a consulta externa sobre os ids.
CREATE TABLE #categories (id INT NOT NULL, parentId INT, [name] NVARCHAR(100))
INSERT INTO #categories
SELECT 1, NULL, 'Computers'
UNION
SELECT 2, 1, 'Processors'
UNION
SELECT 3, 2, 'Intel'
UNION
SELECT 4, 2, 'AMD'
UNION
SELECT 5, 3, 'Pentium'
UNION
SELECT 6, 3, 'Core 2 Duo'
UNION
SELECT 7, 4, 'Athlon'
SELECT *
FROM #categories
DECLARE @id INT
SET @id = 2
; WITH r(id, parentid, [name]) AS (
SELECT id, parentid, [name]
FROM #categories c
WHERE id = @id
UNION ALL
SELECT c.id, c.parentid, c.[name]
FROM #categories c JOIN r ON c.parentid=r.id
)
SELECT *
FROM products
WHERE p.productd IN
(SELECT id
FROM r)
DROP TABLE #categories
A última parte do exemplo não é realmente a trabalhar se você estiver executando-o diretamente como esta. Basta remover a escolha dos produtos e substituto com um simples * SELECT FROM r
Isso deve recurse para baixo todas as categorias 'criança' a partir de uma determinada categoria.
DECLARE @startingCatagoryId int
DECLARE @current int
SET @startingCatagoryId = 13813 -- or whatever the CatagoryId is for 'Processors'
CREATE TABLE #CatagoriesToFindChildrenFor
(CatagoryId int)
CREATE TABLE #CatagoryTree
(CatagoryId int)
INSERT INTO #CatagoriesToFindChildrenFor VALUES (@startingCatagoryId)
WHILE (SELECT count(*) FROM #CatagoriesToFindChildrenFor) > 0
BEGIN
SET @current = (SELECT TOP 1 * FROM #CatagoriesToFindChildrenFor)
INSERT INTO #CatagoriesToFindChildrenFor
SELECT ID FROM Catagory WHERE ParentCatagoryId = @current AND Deleted = 0
INSERT INTO #CatagoryTree VALUES (@current)
DELETE #CatagoriesToFindChildrenFor WHERE CatagoryId = @current
END
SELECT * FROM #CatagoryTree ORDER BY CatagoryId
DROP TABLE #CatagoriesToFindChildrenFor
DROP TABLE #CatagoryTree
Eu gosto de usar uma tabela temporária de pilha para dados hierárquicos. aqui está um exemplo áspero -
-- create a categories table and fill it with 10 rows (with random parentIds)
CREATE TABLE Categories ( Id uniqueidentifier, ParentId uniqueidentifier )
GO
INSERT
INTO Categories
SELECT NEWID(),
NULL
GO
INSERT
INTO Categories
SELECT TOP(1)NEWID(),
Id
FROM Categories
ORDER BY Id
GO 9
DECLARE @lvl INT, -- holds onto the level as we move throught the hierarchy
@Id Uniqueidentifier -- the id of the current item in the stack
SET @lvl = 1
CREATE TABLE #stack (item UNIQUEIDENTIFIER, [lvl] INT)
-- we fill fill this table with the ids we want
CREATE TABLE #tmpCategories (Id UNIQUEIDENTIFIER)
-- for this example we’ll just select all the ids
-- if we want all the children of a specific parent we would include it’s id in
-- this where clause
INSERT INTO #stack SELECT Id, @lvl FROM Categories WHERE ParentId IS NULL
WHILE @lvl > 0
BEGIN -- begin 1
IF EXISTS ( SELECT * FROM #stack WHERE lvl = @lvl )
BEGIN -- begin 2
SELECT @Id = [item]
FROM #stack
WHERE lvl = @lvl
INSERT INTO #tmpCategories
SELECT @Id
DELETE FROM #stack
WHERE lvl = @lvl
AND item = @Id
INSERT INTO #stack
SELECT Id, @lvl + 1
FROM Categories
WHERE ParentId = @Id
IF @@ROWCOUNT > 0
BEGIN -- begin 3
SELECT @lvl = @lvl + 1
END -- end 3
END -- end 2
ELSE
SELECT @lvl = @lvl - 1
END -- end 1
DROP TABLE #stack
SELECT * FROM #tmpCategories
DROP TABLE #tmpCategories
DROP TABLE Categories
há uma boa explicação aqui link de texto
A minha resposta a outra pergunta de um par de dias atrás, se aplica aqui ... recursão em SQL
Existem alguns métodos no livro que eu ligado, que deve cobrir a sua situação muito bem.