Выберите товары, категория которых относится к любой категории в иерархии
-
10-07-2019 - |
Вопрос
У меня есть таблица products, которая содержит FK для категории, таблица Categories создана таким образом, что каждая категория может иметь родительскую категорию, например:
Computers
Processors
Intel
Pentium
Core 2 Duo
AMD
Athlon
Мне нужно сделать запрос select, чтобы, если выбранной категорией являются Процессоры, он возвращал продукты Intel, Pentium, Core 2 Duo, Amd и т.д...
Я думал о создании своего рода "кэша", который будет хранить все категории в иерархии для каждой категории в БД и включать "IN" в предложение where.Является ли это лучшим решением?
Решение
Лучшее решение для этого находится на стадии проектирования базы данных.Ваша таблица категорий должна быть Вложенный Набор.Статья Управление иерархическими данными в MySQL не является настолько специфичным для MySQL (несмотря на название) и дает отличный обзор различных методов хранения иерархии в таблице базы данных.
Краткое изложение:
Вложенные наборы
- Выбор прост для любой глубины
- Вставки и удаления - это сложная задача
Стандартная иерархия на основе parent_id
- Выбор основан на внутренних соединениях (так что быстро набирайтесь смелости).
- Вставки и удаления выполняются легко
Итак, основываясь на вашем примере, если бы ваша иерархическая таблица была вложенным набором, ваш запрос выглядел бы примерно так:
SELECT * FROM products
INNER JOIN categories ON categories.id = products.category_id
WHERE categories.lft > 2 and categories.rgt < 11
цифры 2 и 11 являются левыми и правыми соответственно от Processors
запись.
Другие советы
Похоже на работу для общего табличного выражения ... что-то вроде:
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
Это должно выбрать категорию с именем «Процессоры» и любые ее потомки, должно быть в состоянии использовать это в предложении IN для возврата продуктов.
Я делал подобные вещи в прошлом, сначала запрашивая идентификаторы категорий, затем запрашивая товары "В" этих категориях.Получение категорий - это сложная задача, и у вас есть несколько вариантов:
- Если уровень вложенности категорий известен или вы можете найти верхнюю границу:Создайте ужасно выглядящий SELECT с большим количеством соединений.Это быстро, но некрасиво, и вам нужно установить ограничение на уровни иерархии.
- Если у вас относительно небольшое количество общих категорий, запросите их все (только идентификаторы, родительские), соберите идентификаторы тех, которые вам интересны, и выполните ВЫБОР....В разделе для продуктов.Для меня это был подходящий вариант.
- Выполняйте запросы вверх / вниз по иерархии, используя серию выборок.Простой, но относительно медленный.
- Я полагаю, что последние версии SQLServer имеют некоторую поддержку рекурсивных запросов, но сам ими не пользовался.
Хранимые процедуры могут помочь, если вы не хотите делать это на стороне приложения.
Что вы хотите найти, так это транзитивное закрытие категории " parent " связь. Я полагаю, что нет никаких ограничений на глубину иерархии категорий, поэтому вы не можете сформулировать один SQL-запрос, который находит все категории. Что бы я сделал (в псевдокоде) это:
categoriesSet = empty set
while new.size > 0:
new = select * from categories where parent in categoriesSet
categoriesSet = categoriesSet+new
Так что продолжайте опрашивать детей, пока больше не найдете. Это ведет себя хорошо с точки зрения скорости, если у вас нет вырожденной иерархии (скажем, 1000 категорий, каждая дочерняя по отношению к другой) или большого количества общих категорий. Во втором случае вы всегда можете работать с временными таблицами, чтобы обеспечить небольшую передачу данных между вашим приложением и базой данных.
Может быть что-то вроде:
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)
[РЕДАКТИРОВАТЬ] Если глубина категории больше единицы, это сформирует ваш самый внутренний запрос. Я подозреваю, что вы могли бы разработать хранимую процедуру, которая будет детализироваться в таблице до тех пор, пока у идентификаторов, возвращаемых внутренним запросом, не будет дочерних элементов - вероятно, лучше иметь атрибут, который помечает категорию как конечный узел в иерархии, - тогда выполнить внешний запрос на эти идентификаторы.
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
Последняя часть примера на самом деле не работает, если вы работаете прямо так. Просто удалите выбор из продуктов и замените его простым SELECT * FROM r
Это должно охватить все «дочерние» категории, начиная с данной категории.
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
Мне нравится использовать временную таблицу стека для иерархических данных. вот грубый пример -
-- 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
здесь есть хорошее объяснение текст ссылки а> р>
Мой ответ на другой вопрос пару дней назад применяется здесь ... рекурсия в SQL
В книге, на которую я ссылаюсь, есть несколько методов, которые должны хорошо освещать вашу ситуацию.