【问题标题】:Select products where the category belongs to any category in the hierarchy选择类别属于层次结构中任何类别的产品
【发布时间】:2010-09-16 21:02:34
【问题描述】:

我有一个产品表,其中包含一个类别的 FK,类别表的创建方式是每个类别都可以有一个父类别,例如:

Computers
    Processors
        Intel
            Pentium
            Core 2 Duo
        AMD
            Athlon

我需要进行选择查询,如果选择的类别是处理器,它将返回 Intel、Pentium、Core 2 Duo、Amd 等中的产品...

我考虑过创建某种“缓存”,它将为数据库中的每个类别存储层次结构中的所有类别,并在 where 子句中包含“IN”。这是最好的解决方案吗?

【问题讨论】:

  • 答案票都去哪儿了?

标签: sql sql-server select hierarchy


【解决方案1】:

最好的解决方案是在数据库设计阶段。您的类别表需要是一个嵌套集。文章 Managing Hierarchical Data in 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 记录的左侧和右侧。

【讨论】:

  • 对每种方法的 + 和 - 的出色总结描述。
  • 谢谢你。我永远不会想出嵌套集的想法。太疯狂了,它可能会起作用!
  • 很棒的提示!我想补充一点,如果我使用该代码,我只会获得子类别的产品,但如果该类别没有子类别,则不会显示它自己的产品,所以基本上我所做的: WHERE ((categories.lft > 2 and categories.rgt
【解决方案2】:

看起来像是通用表表达式的工作......类似于以下内容:

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 子句中使用它来拉回产品。

【讨论】:

    【解决方案3】:

    我过去做过类似的事情,首先查询类别 id,然后查询这些类别的产品“IN”。获取类别很难,您有几个选择:

    • 如果类别的嵌套级别已知,或者您可以找到一个上限:构建一个包含大量 JOIN 的看起来很糟糕的 SELECT。这速度很快,但很难看,您需要对层次结构的级别进行限制。
    • 如果您的总类别数量相对较少,请全部查询(仅 ids、parents),收集您关心的类别的 id,然后为产品执行 SELECT....IN。这对我来说是合适的选择。
    • 使用一系列 SELECT 向上/向下查询层次结构。简单,但相对较慢。
    • 我相信最新版本的 SQLServer 对递归查询有一些支持,但我自己没有使用过。

    如果您不想在应用端执行此操作,存储过程可以提供帮助。

    【讨论】:

      【解决方案4】:

      您想要找到的是类别“父”关系的传递闭包。我想类别层次结构深度没有限制,因此您无法制定一个查找所有类别的 SQL 查询。我会做的(在伪代码中)是这样的:

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

      因此,请继续查询孩子,直到找不到更多孩子为止。这在速度方面表现良好,除非您有退化的层次结构(例如,1000 个类别,每个类别都是另一个的子类别)或大量的总类别。在第二种情况下,您始终可以使用临时表来保持应用程序和数据库之间的数据传输较小。

      【讨论】:

        【解决方案5】:

        可能是这样的:

        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)
        

        [编辑] 如果类别深度大于 1,这将形成您最内层的查询。我怀疑您可以设计一个存储过程,该过程将在表中向下钻取,直到内部查询返回的 id 没有子级 - 最好有一个属性将类别标记为层次结构中的终端节点 - 然后对这些 id 执行外部查询。

        【讨论】:

        • 对,但是我的数据库架构中的类别深度没有限制......也许“缓存”解决方案是目前最好的选择......无论如何,谢谢!
        【解决方案6】:
        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 替换

        【讨论】:

          【解决方案7】:

          这应该从给定类别开始递归所有“子”类别。

          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
          

          【讨论】:

            【解决方案8】:

            我喜欢将堆栈临时表用于分层数据。 这是一个粗略的例子 -

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

            这里有很好的解释link text

            【讨论】:

              【解决方案9】:

              我几天前对另一个问题的回答在这里适用...recursion in SQL

              我已经链接了书中的一些方法,它们应该很好地涵盖您的情况。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2020-10-20
                • 1970-01-01
                • 1970-01-01
                • 2015-03-02
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多