SQL: рекурсивное получение родительских записей с использованием общих табличных выражений

Предположим, вам нужно следить за таблицами, в которых продажа состоит из продуктов, а продукт может быть помещен в несколько категорий. При этом категории имеют иерархическую структуру, например:

Man
 Shoes
  Sport
  Casual
 Watches
Women
 Shoes
  Sport
  Casual
 Watches

Таблицы:

Sale:
    id name 
    1  Sale1

Product:
    id saleidfk name 
    1  1        a
    2  1        b
    3  1        c
    4  1        d
    5  1        e

ProductCategory :
    productid categoryid 
    1         3
    2         3           
    3         4
    4         5
    5         10     

Category:
    id ParentCategoryIdFk name 
    1  null               Men
    2  1                  Shoes
    3  2                  Sport
    4  2                  Casual
    5  1                  Watches
    6  null               Women
    7  6                  Shoes
    8  7                  Sport
    9  7                  Casual
    10 6                 Watches

Вопрос:

Теперь на моем веб-сайте я хочу создать элемент управления, в котором отображаются только категории определенной продажи и где категории заполнены продуктами продажи. Я также хочу включить иерархическую структуру категорий. Итак, если у нас есть конечный узел, рекурсивно переходим к верхнему узлу.

Так что с sale1 у меня должен быть запрос со следующим результатом:

Men
  Shoes
    Sport
    Casual
  Watches
Women
  Watches

person Martijn B    schedule 11.04.2010    source источник
comment
возможно, вы можете получить базовые шаблоны для получения родительских записей, начиная с дочерних, используя этот подход: stackoverflow.com/questions/1104977/   -  person Michael Buen    schedule 12.04.2010
comment
или, если вы хотите сверху вниз, проверьте шаблон здесь: stackoverflow.com/questions/1686340   -  person Michael Buen    schedule 12.04.2010


Ответы (5)


Попробуйте что-то вроде этого — базовый CTE для получения иерархического списка ваших категорий будет похож на этот:

WITH Categories AS
(
    SELECT Cat.ID, Cat.NAME, Cat.ParentCategoryID, CAST('none' AS VARCHAR(50)) AS 'ParentCategory', 1 AS 'Level'
    FROM dbo.MBCategory Cat
    WHERE Cat.ParentCategoryID IS NULL

    UNION ALL

    SELECT Cat.ID, Cat.NAME, Cat.ParentCategoryID, c2.NAME AS 'ParentCategory', LEVEL + 1
    FROM dbo.MBCategory CAT
    INNER JOIN Categories c2 ON cat.ParentCategoryID = c2.ID
)
SELECT * FROM Categories

Теперь вам нужно присоединить другие ваши таблицы к этому CTE, чтобы в конце получить следующий запрос:

WITH Categories AS
(
    SELECT Cat.ID, Cat.NAME, Cat.ParentCategoryID, CAST('none' AS VARCHAR(50)) AS 'ParentCategory', 1 AS 'Level'
    FROM dbo.MBCategory Cat
    WHERE Cat.ParentCategoryID IS NULL

    UNION ALL

    SELECT Cat.ID, Cat.NAME, Cat.ParentCategoryID, c2.NAME AS 'ParentCategory', LEVEL + 1
    FROM dbo.MBCategory CAT
    INNER JOIN Categories c2 ON cat.ParentCategoryID = c2.ID
)
SELECT DISTINCT s.*, c.*
FROM dbo.Sale s
INNER JOIN dbo.Product p ON p.SaleID = s.ID
INNER JOIN dbo.ProductCategory pc ON p.ID = pc.ProductID
INNER JOIN Categories c ON pc.CategoryID = c.ID
ORDER BY Level

Это дает мне результирующий вывод примерно так:

ID  Name   CatID  CatName  ParentCatID  ParentCatName Level
 1  Sale1    5    Watches      1            Men         2
 1  Sale1   10    Watches      6            Women       2 
 1  Sale1    3    Sport        2            Shoes       3
 1  Sale1    3    Sport        2            Shoes       3
 1  Sale1    4    Casual       2            Shoes       3
person marc_s    schedule 11.04.2010
comment
Это близко, но есть недостающие биты. Потому что я не знаю, например, есть ли у обуви родитель. - person Martijn B; 11.04.2010
comment
Я сомневаюсь, что вы сможете заставить всю эту иерархию (до любого количества уровней) работать в SQL-запросе ....... - person marc_s; 12.04.2010

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

CategoryAncestor
ID   categoryid   ancestorid    
1         1            1      -- Men, obligatory self reference (makes queries easier)
2         2            2      -- Shoes, self reference 
3         2            1      -- Shoes is a subcategory of Men
4         3            3      -- Sport, self reference 
5         3            2      -- Sport is a subcategory of Shoes
6         3            1      -- Sport is ALSO a subcategory of Men
-- etc.

Это создаст немного больше накладных расходов SQL при вставке новых категорий или их удалении, но позволит вам выполнять запросы намного быстрее.

Следующее, что вы, возможно, захотите сделать, это добавить столбцы ранга и уровня в категории (опять же, больше работы при создании и удалении категорий):

id ParentCategoryIdFk name       level    rank
1  null               Men           0       1
2  1                  Shoes         1       2
3  2                  Sport         2       3
4  2                  Casual        2       4
5  1                  Watches       1       5
6  null               Women         0       6
7  6                  Shoes         1       7
8  7                  Sport         2       8
9  7                  Casual        2       9
10 6                 Watches        1      10

Столбец rank определяет порядок сортировки.

Затем вы можете просто запустить следующий запрос:

SELECT * FROM Category c
  WHERE c.id IN (
    SELECT ancestorid FROM CategoryAncestor ca, ProductCategory pc, Product p
      WHERE p.id = pc.productid 
        AND pc.categoryid = ca.categoryid
        AND p.saleidfk = 1
    )
  ORDER BY rank

Надеюсь это поможет.

person Zach Smith    schedule 11.04.2010
comment
Спасибо за ваше время, ваша правда, но я не в состоянии изменить это сейчас. Спасибо! - person Martijn B; 12.04.2010

Это не особенно эффективно, но если то, что вы хотите сделать, это эффективно «взорвать» всю иерархию и получить результаты последовательно от родителя к листу, что-то вроде этого сделает это:

WITH CategoryHierarchy AS
(
    SELECT
        ID, ParentCategoryIdFk, 0 AS Level,
        ROW_NUMBER() OVER (ORDER BY ID) AS SubTreeID
    FROM Category
    WHERE CategoryID IN
    (
        SELECT pc.CategoryID
        FROM Sale s
        INNER JOIN Product p
            ON p.saleidfk = s.id
        INNER JOIN ProductCategory pc
            ON pc.productid = p.id
        WHERE s.id = @SaleID
    )

    UNION ALL

    SELECT c.ID, c.ParentCategoryIdFk, h.Level + 1, h.SubTreeID
    FROM CategoryHierarchy h
    INNER JOIN Category c
        ON c.ID = h.ParentID
)
SELECT c.ID, c.ParentCategoryIdFk AS ParentID, c.Name
FROM CategoryHierarchy h
INNER JOIN Category c
    ON c.ID = h.ID
ORDER BY h.SubTreeID ASC, h.Level DESC

Это должно дать вам результаты, подобные следующим:

ID | ParentID | Name
---+----------+----------
1  |     NULL | Men
2  |        1 | Shoes
3  |        2 | Sport
---+----------+----------
1  |     NULL | Men
2  |        1 | Shoes
4  |        2 | Casual
---+----------+----------
1  |     NULL | Men
5  |        1 | Watches
---+----------+----------
6  |     NULL | Women
10 |        6 | Watches

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

Если вы не хотите, чтобы он был полностью разобран, вы можете использовать другой rownum, чтобы возвращать только первый экземпляр каждого родителя:

WITH CategoryHierarchy AS
(
    SELECT
        ID, ParentCategoryIdFk, 0 AS Level,
        ROW_NUMBER() OVER (ORDER BY ID) AS SubTreeID
    FROM Category
    WHERE CategoryID IN
    (
        SELECT pc.CategoryID
        FROM Sale s
        INNER JOIN Product p
            ON p.saleidfk = s.id
        INNER JOIN ProductCategory pc
            ON pc.productid = p.id
        WHERE s.id = @SaleID
    )

    UNION ALL

    SELECT c.ID, c.ParentCategoryIdFk, h.Level + 1, h.SubTreeID
    FROM CategoryHierarchy h
    INNER JOIN Category c
        ON c.ID = h.ParentID
),
Filter_CTE AS
(
    SELECT
        ID, Level, SubTreeID
        ROW_NUMBER() OVER (PARTITION BY ID ORDER BY SubTreeID) AS RowNum
    FROM CategoryHierarchy
)
SELECT c.ID, c.ParentCategoryIdFk AS ParentID, c.Name
FROM Filter_CTE f
INNER JOIN Category c
    ON c.ID = f.ID
WHERE f.RowNum = 1
ORDER BY f.SubTreeID ASC, f.Level DESC

... даст вам результаты, подобные:

ID | ParentID | Name
---+----------+----------
1  |     NULL | Men
2  |        1 | Shoes
3  |        2 | Sport
4  |        2 | Casual
5  |        1 | Watches
6  |     NULL | Women
10 |        6 | Watches

Примечание. Будьте осторожны со второй версией, так как она не обязательно возвращает результаты в иерархическом порядке. Так уж получилось, что эта версия работает, потому что сами идентификаторы находятся в иерархическом порядке. Вы можете обойти это ограничение, но это значительно усложнит и без того сложный запрос.

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

person Aaronaught    schedule 12.04.2010
comment
Спасибо, проверю и начну с этого. - person Martijn B; 12.04.2010

Немного сумбурно, но:

DROP TABLE #Sale
GO
DROP TABLE #PRoduct
GO
DROP TABLE #ProductCategory
GO
DROP TABLE #Category
GO
CREATE TABLE #Sale 
(
    ID INT,
    Name VARCHAR(20)        
    )
GO
INSERT INTO #Sale SELECT 1, 'Sale1'
GO
CREATE TABLE #Product 
(
ID INT,
saleidfk INT,
name VARCHAR(20)
)
GO
INSERT INTO #Product 
SELECT 1,1,'a'
UNION
SELECT 2,1,'b'
UNION
SELECT 3,1,'c'
UNION
SELECT 4,1,'d'
UNION
SELECT 5,1,'e'
UNION
SELECT 6,1,'f'
GO
CREATE TABLE #ProductCategory 
(
ProductID INT,
CategoryID INT
)
GO
INSERT INTO #ProductCategory
SELECT 1,3
UNION
SELECT 2,3
UNION
SELECT 3,4
UNION
SELECT 4,5
UNION
SELECT 5,10
UNION
SELECT 6,10
GO
CREATE TABLE #Category 
(
ID INT,
ParentCategoryFK INT,
Name varchar(20)
)
GO
INSERT INTO #Category
SELECT 1,NULL,'Men'
UNION
SELECT 2,1,'Shoes'
UNION
SELECT 3,2,'Sport'
UNION
SELECT 4,2,'Casual'
UNION
SELECT 5,1,'Watches'
UNION
SELECT 6,NULL,'Women'
UNION
SELECT 7,6,'Shoes'
UNION
SELECT 8,7,'Sport'
UNION
SELECT 9,7,'Casual'
UNION
SELECT 10,6,'Watches'


GO

WITH Categories (CategoryName,CategoryID, [Level], SortOrder)  AS 
( 
    SELECT  Cat.Name,cat.id, 1 AS [Level], CONVERT(VARCHAR(MAX), ROW_NUMBER() OVER (order by cat.Name) ) AS SortOrder
    FROM #Category Cat 
    WHERE Cat.ParentCategoryFK IS NULL 

    UNION ALL

    SELECT CAT.Name,cat.ID, [Level] + 1, c2.SortOrder + CONVERT(VARCHAR(MAX), ROW_NUMBER() OVER (order by cat.Name)) 
    FROM #Category CAT 
    INNER JOIN Categories c2 ON cat.ParentCategoryFK = c2.CategoryID
) 
SELECT #Sale.Name, Categories.CategoryName, #Product.name,Categories.Level,Categories.SortOrder FROM
Categories 
LEFT JOIN
#ProductCategory ON #ProductCategory.CategoryID = Categories.CategoryID
LEFT JOIN
#Product ON #Product.ID = #ProductCategory.ProductID
LEFT JOIN
#Sale ON #Product.saleidfk = #Sale.ID
ORDER BY Categories.SortOrder, #Product.name

Следует отметить, что для того, чтобы получить полную иерархию смысла, вам нужны категории независимо от того, есть ли в них продукты или нет. Также varchar для SortOrder позволяет отображать иерархию в правильном порядке.

person Nat    schedule 12.04.2010

Думаю, я слишком поздно, но для будущих сверстников, пытающихся сделать то же самое, я думаю, это сработает. :) (Просто сделал это для родительской иерархии одного конкретного элемента, но внутреннее соединение с листьями сделает тот же трюк)

with 
hierarchy (id, parentId, level)
as
(
    select c.id, c.parentId, 0 as level
    from categories c
    where parentId = 0
    union all
    select c.id, c.parentId, level + 1
    from categories c
    inner join hierarchy p on c.parentId = p.id
),
parents (id, parentId, level)
as
(
    select l.id, l.parentId, l.level
    from hierarchy l
        [where id = *leafid* | inner join *insert_your_leaves_here*]
    union all
    select p.id, p.parentId, p.level
    from hierarchy p
    inner join parents l on p.id = l.parentId
)

select * from parents
person Lars-Erik    schedule 27.09.2012