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

У меня есть таблица продуктов, содержащая FK для категории, таблица категорий создана таким образом, что каждая категория может иметь родительскую категорию, например:

Computers
    Processors
        Intel
            Pentium
            Core 2 Duo
        AMD
            Athlon

Мне нужно сделать запрос выбора, который, если выбранная категория - «Процессоры», будет возвращать продукты, которые находятся в Intel, Pentium, Core 2 Duo, Amd и т. Д.

Я подумал о создании своего рода «кеша», который будет хранить все категории в иерархии для каждой категории в базе данных и включать «IN» в предложение where. Это лучшее решение?


person Bruno    schedule 13.10.2008    source источник
comment
Куда делись все голоса за ответы?   -  person MDCore    schedule 13.10.2008


Ответы (9)


Лучшее решение для этого - на этапе проектирования базы данных. Таблица категорий должна быть вложенным набором. Статья Управление иерархическими данными в 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 соответственно.

person MDCore    schedule 13.10.2008
comment
Отличное краткое описание плюсов и минусов каждого подхода. - person Cory House; 01.10.2009
comment
Спасибо тебе за это. Я бы никогда не придумал идею вложенных множеств. Это так безумие, что это может сработать! - person Steve; 20.11.2009
comment
Отличный совет! Я хотел бы добавить, что в моем случае, если я использую этот код, я получу продукты только из дочерних категорий, но если у категории нет дочерних элементов, она не будет показывать свои собственные продукты, так что в основном то, что я сделал : WHERE ((Categories.lft ›2 and category.rgt‹ 11) OR products.category_id = X) X - идентификатор записи процессора. - person Alexandru Trandafir Catalin; 12.10.2013

Похоже на задание для общего табличного выражения ... что-то вроде:

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 для возврата продуктов.

person Steven Robbins    schedule 13.10.2008

Я делал аналогичные вещи в прошлом, сначала запрашивая идентификаторы категорий, а затем запрашивая продукты «В» этих категориях. Получение категорий - это сложная задача, и у вас есть несколько вариантов:

  • Если уровень вложенности категорий известен или вы можете найти верхнюю границу: создайте ужасно выглядящий SELECT с большим количеством JOIN. Это быстро, но некрасиво, и вам нужно установить ограничение на уровни иерархии.
  • Если у вас относительно небольшое количество общих категорий, запросите их все (только идентификаторы, родители), соберите идентификаторы тех, которые вам интересны, и выполните SELECT .... IN для продуктов. Для меня это был подходящий вариант.
  • Выполняйте запросы вверх / вниз по иерархии, используя серию SELECT. Просто, но относительно медленно.
  • Я считаю, что в последних версиях SQLServer есть некоторая поддержка рекурсивных запросов, но я не использовал их.

Сохраненные процедуры могут помочь, если вы не хотите делать это на стороне приложения.

person Draemon    schedule 13.10.2008

Что вы хотите найти, так это транзитивное замыкание отношения категории «родитель». Я полагаю, что нет ограничений на глубину иерархии категорий, поэтому вы не можете сформулировать один SQL-запрос, который находит все категории. Что бы я сделал (в псевдокоде), так это:

categoriesSet = empty set
while new.size > 0:
  new = select * from categories where parent in categoriesSet
  categoriesSet = categoriesSet+new

Так что просто продолжайте опрашивать детей, пока их больше не найдут. Это ведет себя хорошо с точки зрения скорости, если у вас нет вырожденной иерархии (скажем, 1000 категорий, каждая дочерняя для другой) или большого количества общих категорий. Во втором случае вы всегда можете работать с временными таблицами, чтобы уменьшить объем передачи данных между вашим приложением и базой данных.

person Simon    schedule 13.10.2008

Может быть, что-то вроде:

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)

[РЕДАКТИРОВАТЬ] Если глубина категории больше единицы, это сформирует ваш самый сокровенный запрос. Я подозреваю, что вы можете разработать хранимую процедуру, которая будет детализировать таблицу до тех пор, пока идентификаторы, возвращаемые внутренним запросом, не будут иметь потомков - возможно, лучше иметь атрибут, который отмечает категорию как конечный узел в иерархии - тогда выполнить внешний запрос по этим идентификаторам.

person tvanfosson    schedule 13.10.2008
comment
верно, но в моей схеме БД нет ограничений на глубину категории ... возможно, решение для кеширования - лучший вариант на данный момент ... в любом случае спасибо! - person Bruno; 13.10.2008

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 из продуктов и замените простым SELECT * FROM r

person Jonas Lincoln    schedule 13.10.2008

Это должно повторяться по всем «дочерним» категориям, начиная с данной категории.

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
person Community    schedule 13.10.2008

Мне нравится использовать временную таблицу стека для иерархических данных. вот примерный пример -

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

здесь есть хорошее объяснение текст ссылки

person cheeves    schedule 15.10.2008

Мой ответ на другой вопрос, заданный пару дней назад, применим здесь ... рекурсия в SQL

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

person Tom H    schedule 15.10.2008