【问题标题】:Recursive CTE Problem递归 CTE 问题
【发布时间】:2023-04-10 15:21:02
【问题描述】:

我试图在 SQL Server 中使用递归 CTE 从包含基础树结构的表中构建谓词公式。 例如,我的表格如下所示:

Id  |  Operator/Val |  ParentId
--------------------------
1   | 'OR'          |  NULL 
2   | 'AND'         |  1
3   | 'AND'         |  1
4   | '>'           |  2
5   | 'a'           |  4
6   | 'alpha'       |  4
...

...表示 ((a > alpha) AND (b > beta)) OR ((c > gamma) AND (a

ParentId是对父节点同表中Id的引用。

我想编写一个查询,它将从表中构建这个字符串。有可能吗?

谢谢

【问题讨论】:

  • 请在原始数据和预期输出之间放置一些中间步骤以进一步解释这一点。我看不到你如何从一个到另一个,抱歉
  • 当我有机会时,我需要为您创建一个图像 - 基本上它将谓词表示为一个树结构,然后在表中展平,每个节点都有一个指向其父节点的指针。
  • 酷!我一直在你的典型类别大纲上做这种事情,但我从未想过使用 CTE 来处理语法树。
  • 您打算如何处理 Id 列足以指定树结构的左右排序的假设?
  • 好点 Nat - 我曾计划为树中的左/右排序添加另一列,但想保持问题的简单性,然后在此基础上进行构建。

标签: sql-server recursive-cte


【解决方案1】:

对于生产环境,如果性能和递归深度限制(32 级)不成问题,您可能希望使用递归函数以简化操作。

但是,这是一个非常干净且非常有效的 CTE 解决方案(请注意,它将接受任意数量的“树”并为每个没有父项的项目返回一个结果):

DECLARE @tbl TABLE
  (
   id int PRIMARY KEY
          NOT NULL,
   op nvarchar(max) NOT NULL,
   parent int
  ) ;
INSERT INTO @tbl
    SELECT 1, 'OR', NULL    UNION ALL
    SELECT 2, 'AND', 1    UNION ALL
    SELECT 3, 'AND', 1    UNION ALL
    SELECT 4, '>', 2    UNION ALL
    SELECT 5, 'a', 4    UNION ALL
    SELECT 6, 'alpha', 4    UNION ALL
    SELECT 7, '>', 2    UNION ALL
    SELECT 8, 'b', 7    UNION ALL
    SELECT 9, 'beta', 7    UNION ALL
    SELECT 10, '>', 3    UNION ALL
    SELECT 11, 'c', 10    UNION ALL
    SELECT 12, 'gamma', 10    UNION ALL
    SELECT 13, '>', 3    UNION ALL
    SELECT 14, 'd', 13    UNION ALL
    SELECT 15, 'delta', 13 ;

WITH  nodes -- A CTE which sets a flag to 1 for non-leaf nodes
        AS (
            SELECT t.*, CASE WHEN p.parent IS NULL THEN 0
                             ELSE 1
                        END node
              FROM @tbl t 
              LEFT JOIN (
                         SELECT DISTINCT parent
                          FROM @tbl
                        ) p ON p.parent = T.id
           ),
      rec -- the main recursive run to determine the sort order and add meta information
        AS (
            SELECT id rootId, node lvl, CAST(0 AS float) sort, CAST(0.5 AS float) offset, *
              FROM nodes
              WHERE parent IS NULL
            UNION ALL
            SELECT r.rootId, r.lvl+t.node, r.sort+r.offset*CAST((ROW_NUMBER() OVER (ORDER BY t.id)-1)*2-1 AS float),
                r.offset/2, t.*
              FROM rec r 
              JOIN 
                nodes t ON r.id = t.parent
           ),
      ranked -- ranking of the result to sort and find the last item
        AS (
            SELECT rootId, ROW_NUMBER() OVER (PARTITION BY rootId ORDER BY sort) ix,
                COUNT(1) OVER (PARTITION BY rootId) cnt, lvl, op
              FROM rec
           ),
      concatenated -- concatenate the string, adding ( and ) as needed
        AS (
            SELECT rootId, ix, cnt, lvl, CAST(REPLICATE('(', lvl)+op AS nvarchar(max)) txt
              FROM ranked
              WHERE ix = 1
            UNION ALL
            SELECT r.rootId, r.ix, r.cnt, r.lvl,
                c.txt+COALESCE(REPLICATE(')', c.lvl-r.lvl), '')+' '+COALESCE(REPLICATE('(', r.lvl-c.lvl), '')+r.op
                +CASE WHEN r.ix = r.cnt THEN REPLICATE(')', r.lvl)
                      ELSE ''
                 END
              FROM ranked r 
              JOIN 
                concatenated c ON (r.rootId = c.rootId)
                                  AND (r.ix = c.ix+1)
           )
  SELECT rootId id, txt
    FROM concatenated
    WHERE ix = cnt
    OPTION (MAXRECURSION 0);

【讨论】:

  • 感谢您的反馈!赞成票会将这个答案放在两个不完整/无效的答案之上。 ;)
【解决方案2】:

我发现了一些东西,但它看起来很糟糕。使用递归基金可以更轻松地做到这一点......

DECLARE @Table TABLE(
        ID INT,
        Op VARCHAR(20),
        ParentID INT
)

INSERT INTO @Table SELECT 1,'OR',NULL 
INSERT INTO @Table SELECT 2,'AND',1
INSERT INTO @Table SELECT 3,'AND',1

INSERT INTO @Table SELECT 4,'>',2
INSERT INTO @Table SELECT 5,'a',4
INSERT INTO @Table SELECT 6,'alpha',4
INSERT INTO @Table SELECT 7,'>',2
INSERT INTO @Table SELECT 8,'b',7
INSERT INTO @Table SELECT 9,'beta',7

INSERT INTO @Table SELECT 10,'>',3
INSERT INTO @Table SELECT 11,'c',10
INSERT INTO @Table SELECT 12,'gamma',10
INSERT INTO @Table SELECT 13,'<',3
INSERT INTO @Table SELECT 14,'a',13
INSERT INTO @Table SELECT 15,'delta',13

;WITH Vals AS (
        SELECT  t.*,
                1 Depth
        FROM    @Table t LEFT JOIN
                @Table parent ON t.ID = parent.ParentID
        WHERE   parent.ParentID IS NULL 
        UNION ALL
        SELECT  t.*,
                v.Depth + 1
        FROM    @Table t INNER JOIN
                Vals v ON v.ParentID = t.ID
),
ValLR AS(
        SELECT  DISTINCT 
                vLeft.ID LeftID,
                vLeft.Op LeftOp,
                vRight.ID RightID,
                vRight.Op RightOp,
                vLeft.ParentID OperationID,
                vLeft.Depth
        FROM    Vals vLeft INNER JOIN
                Vals vRight ON  vLeft.ParentID = vRight.ParentID
                            AND vLeft.ID < vRight.ID
        WHERE   (vRight.ID IS NOT NULL)
),
ConcatVals AS(
        SELECT  CAST('(' + LeftOp + ' ' + Op + ' ' + RightOp + ')' AS VARCHAR(500)) ConcatOp,
                t.ID OpID,
                v.Depth,
                1 CurrentDepth
        FROM    ValLR v INNER JOIN
                @Table t ON v.OperationID = t.ID
        WHERE   v.Depth = 1
        
        UNION ALL       
        SELECT  CAST('(' + cL.ConcatOp + ' ' + t.Op + ' {' + CAST(v.RightID AS VARCHAR(10)) + '})' AS VARCHAR(500)) ConcatOp,
                t.ID OpID,
                v.Depth,
                cL.CurrentDepth + 1
        FROM    ValLR v INNER JOIN
                @Table t ON v.OperationID = t.ID INNER JOIN
                ConcatVals cL ON v.LeftID = cL.OpID
        WHERE   v.Depth = cL.CurrentDepth + 1
),
Replaces AS(
        SELECT  REPLACE(
                            c.ConcatOp,
                            SUBSTRING(c.ConcatOp,PATINDEX('%{%', c.ConcatOp), PATINDEX('%}%', c.ConcatOp) - PATINDEX('%{%', c.ConcatOp) + 1),
                            (SELECT ConcatOp FROM ConcatVals WHERE OpID = CAST(SUBSTRING(c.ConcatOp,PATINDEX('%{%', c.ConcatOp) + 1, PATINDEX('%}%', c.ConcatOp) - PATINDEX('%{%', c.ConcatOp) - 1)  AS INT))
                        ) ConcatOp,
                1 Num
        FROM    ConcatVals c
        WHERE   Depth = (SELECT MAX(Depth) FROM ConcatVals)
        UNION ALL
        SELECT  REPLACE(
                            r.ConcatOp,
                            SUBSTRING(r.ConcatOp,PATINDEX('%{%', r.ConcatOp), PATINDEX('%}%', r.ConcatOp) - PATINDEX('%{%', r.ConcatOp) + 1),
                            (SELECT ConcatOp FROM ConcatVals WHERE OpID = CAST(SUBSTRING(r.ConcatOp,PATINDEX('%{%', r.ConcatOp) + 1, PATINDEX('%}%', r.ConcatOp) - PATINDEX('%{%', r.ConcatOp) - 1)  AS INT))
                        ) ConcatOp,
                Num + 1
        FROM    Replaces r
        WHERE   PATINDEX('%{%', r.ConcatOp) > 0
)
SELECT  TOP 1
        *
FROM    Replaces
ORDER BY Num DESC

输出

ConcatOp                                                        
----------------------------------------------------------------
(((a > alpha) AND (b > beta)) OR ((c > gamma) AND (a < delta))) 

如果你更想看递归函数,请给我留言,我们可以看看。

编辑:递归函数

看看这有多容易

CREATE TABLE TableValues (
        ID INT,
        Op VARCHAR(20),
        ParentID INT
)

INSERT INTO TableValues SELECT 1,'OR',NULL 
INSERT INTO TableValues SELECT 2,'AND',1
INSERT INTO TableValues SELECT 3,'AND',1

INSERT INTO TableValues SELECT 4,'>',2
INSERT INTO TableValues SELECT 5,'a',4
INSERT INTO TableValues SELECT 6,'alpha',4
INSERT INTO TableValues SELECT 7,'>',2
INSERT INTO TableValues SELECT 8,'b',7
INSERT INTO TableValues SELECT 9,'beta',7

INSERT INTO TableValues SELECT 10,'>',3
INSERT INTO TableValues SELECT 11,'c',10
INSERT INTO TableValues SELECT 12,'gamma',10
INSERT INTO TableValues SELECT 13,'<',3
INSERT INTO TableValues SELECT 14,'a',13
INSERT INTO TableValues SELECT 15,'delta',13

GO

CREATE FUNCTION ReturnMathVals (@ParentID INT, @Side VARCHAR(1))
RETURNS VARCHAR(500)
AS 
BEGIN
    DECLARE @RetVal VARCHAR(500)

    IF (@ParentID IS NULL)
    BEGIN
        SELECT  @RetVal = ' (' + dbo.ReturnMathVals(ID,'L') + Op + dbo.ReturnMathVals(ID,'R') + ') '
        FROM    TableValues 
        WHERE   ParentID IS NULL
    END
    ELSE
    BEGIN
        SELECT  TOP 1 @RetVal = ' (' + dbo.ReturnMathVals(ID,'L') + Op + dbo.ReturnMathVals(ID,'R') + ') '
        FROM    TableValues 
        WHERE   ParentID = @ParentID
        ORDER BY CASE WHEN @Side = 'L' THEN ID ELSE -ID END
        
        SET @RetVal = ISNULL(@RetVal, (SELECT TOP 1 Op FROM TableValues WHERE ParentID = @ParentID ORDER BY CASE WHEN @Side = 'L' THEN ID ELSE -ID END))
    END
    
    RETURN @RetVal
END
GO

SELECT  dbo.ReturnMathVals(NULL, NULL)
GO
DROP FUNCTION ReturnMathVals
DROP TABLE TableValues

【讨论】:

【解决方案3】:

是的,可以这样做,但问题不在于 CTE,请使用 PIVOT 检查 从此链接了解更多信息

http://msdn.microsoft.com/en-us/library/ms177410.aspx

本文档中的一些示例与您的问题类似

【讨论】:

    【解决方案4】:

    我不知道如何进行双重递归,但希望其中的一个中间 CTE 能让你走上正轨:

    SET NOCOUNT ON
    
    DECLARE @tree AS TABLE
        (
         Id int NOT NULL
        ,Operator varchar(10) NOT NULL
        ,ParentId int
        )
    
    INSERT  INTO @tree
    VALUES  (1, 'OR', NULL)
    INSERT  INTO @tree
    VALUES  (2, 'AND', 1)
    INSERT  INTO @tree
    VALUES  (3, 'AND', 1)
    INSERT  INTO @tree
    VALUES  (4, '>', 2)
    INSERT  INTO @tree
    VALUES  (5, 'a', 4)
    INSERT  INTO @tree
    VALUES  (6, 'alpha', 4)
    INSERT  INTO @tree
    VALUES  (7, '>', 2)
    INSERT  INTO @tree
    VALUES  (8, 'b', 7)
    INSERT  INTO @tree
    VALUES  (9, 'beta', 7)
    INSERT  INTO @tree
    VALUES  (10, '>', 3)
    INSERT  INTO @tree
    VALUES  (11, 'c', 10)
    INSERT  INTO @tree
    VALUES  (12, 'gamma', 10)
    INSERT  INTO @tree
    VALUES  (13, '>', 3)
    INSERT  INTO @tree
    VALUES  (14, 'd', 13)
    INSERT  INTO @tree
    VALUES  (15, 'delta', 13) ;
    WITH    lhs_selector
              AS (
                  SELECT    ParentId
                           ,MIN(Id) AS Id
                  FROM      @tree
                  GROUP BY  ParentId
                 ),
            rhs_selector
              AS (
                  SELECT    ParentId
                           ,MAX(Id) AS Id
                  FROM      @tree
                  GROUP BY  ParentId
                 ),
            leaf_selector
              AS (
                  SELECT    Id
                  FROM      @tree AS leaf
                  WHERE     NOT EXISTS ( SELECT *
                                         FROM   @tree
                                         WHERE  ParentId = leaf.Id )
                 ),
            recurse
              AS (
                  SELECT    operator.Id
                           ,CASE WHEN lhs_is_leaf.Id IS NOT NULL THEN NULL
                                 ELSE lhs.Id
                            END AS LhsId
                           ,CASE WHEN rhs_is_leaf.Id IS NOT NULL THEN NULL
                                 ELSE rhs.Id
                            END AS RhsId
                           ,CASE WHEN COALESCE(lhs_is_leaf.Id, rhs_is_leaf.Id) IS NULL
                                 THEN '({' + CAST(lhs.Id AS varchar) + '} ' + operator.Operator + ' {'
                                      + CAST(rhs.Id AS varchar) + '})'
                                 ELSE '(' + lhs.Operator + ' ' + operator.Operator + ' ' + rhs.Operator + ')'
                            END AS expression
                  FROM      @tree AS operator
                  INNER JOIN lhs_selector
                            ON lhs_selector.ParentID = operator.Id
                  INNER JOIN rhs_selector
                            ON rhs_selector.ParentID = operator.Id
                  INNER JOIN @tree AS lhs
                            ON lhs.Id = lhs_selector.Id
                  INNER JOIN @tree AS rhs
                            ON rhs.Id = rhs_selector.Id
                  LEFT JOIN leaf_selector AS lhs_is_leaf
                            ON lhs_is_leaf.Id = lhs.Id
                  LEFT JOIN leaf_selector AS rhs_is_leaf
                            ON rhs_is_leaf.Id = rhs.Id
                 )
        SELECT  *
               ,REPLACE(REPLACE(op.expression, '{' + CAST(op.LhsId AS varchar) + '}', lhs.expression),
                        '{' + CAST(op.RhsId AS varchar) + '}', rhs.expression) AS final_expression
        FROM    recurse AS op
        LEFT JOIN recurse AS lhs
                ON lhs.Id = op.LhsId
        LEFT JOIN recurse AS rhs
                ON rhs.Id = op.RhsId
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-17
      • 1970-01-01
      • 2014-08-13
      相关资源
      最近更新 更多