Выберите товары, категория которых относится к любой категории в иерархии

StackOverflow https://stackoverflow.com/questions/197362

Вопрос

У меня есть таблица 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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top