如何在SQL Server中达到条件之前无限地查询查询

时间:2017-08-17 20:01:22

标签: sql sql-server

在SQL Server中

我有一个名为Recipe的数据库,这个表包含食谱和进入食谱的材料。我需要一个显示配方所有基础材料的查询。不幸的是,其中一些材料实际上是配方中的其他配方。例如:你有配方AA01,其中包含2种基础材料和配方BB01。您现在必须查找该配方中的材料以在AA01中查找基础材料。诀窍是,你可以在食谱中拥有无限量的食谱。这可以是一个搜索阶梯。 你不知道你需要走多远的梯子。

我提出了一个查询的想法,该查询搜索并找到下一个材料并循环,直到它最终找到基础材料。有时它可能需要循环一次才能找到一种材料,有时可能需要循环5次以降低5级。

不幸的是,下面的代码我无法循环,所以它只找到第一层。我无法自己进行查询循环。

SELECT 
    Recipe.RecipeID, 
    Recipe_1.RecipeID, 
    Recipe_1.MaterialID  
FROM Recipe 
LEFT JOIN Recipe AS Recipe_1 ON Recipe.MaterialID = Recipe_1.RecipeID
ORDER BY Recipe.RecipeID;

解决方案是将此代码放在循环中,或者让它以递归方式调用自身,直到达到找到所有基础材料的级别。我附上了一张RECIPE表示例的图片,代码生成的内容,以及我需要它无限生成(更多级别)的内容。您可以看到突出显示的行是实际的行中缺少的行,这些材料需要2个级别才能最终得到基础材质。我用硬编码来搜索最多5层,但显然可以有更多。我如何拥有SQL循环并分配层本身?

here

是否可以循环连续循环查询?

2 个答案:

答案 0 :(得分:1)

这看起来正在产生您正在寻找的结果......

SET NOCOUNT ON;

IF OBJECT_ID('tempdb..#Recipe', 'U') IS NOT NULL 
DROP TABLE #Recipe;

CREATE TABLE #Recipe (
    Recipe VARCHAR(5) NOT NULL,
    Material VARCHAR(5) NOT NULL 
    );

INSERT #Recipe (Recipe, Material) VALUES 
    ('aa01', 'B1'),
    ('aa01', 'B2'),
    ('aa01', 'bb01'),
    ('bb01', 'B1'),
    ('bb01', 'cc01'),
    ('cc01', 'B3'),
    ('cc01', 'B4'),
    ('B1', 'B1'),
    ('B2', 'B2'),
    ('B3', 'B3'),
    ('B4', 'B4');

--SELECT * FROM #Recipe r;

--====================================================================================

IF OBJECT_ID('tempdb..#RecursiveOutput', 'U') IS NOT NULL 
DROP TABLE #RecursiveOutput;

WITH 
    cte_Recursion AS (  
        SELECT 
            r.Recipe,
            x = r.Material,
            Material = CAST(r.Material AS VARCHAR(8000)),
            NodeLevel = 1,
            MaterialLevel = CAST('m1.Material' AS VARCHAR(8000))
        FROM
            #Recipe r
        UNION ALL
        SELECT 
            cr.Recipe, 
            x = r.Material,
            Material = CAST(CONCAT(cr.Material, '>', r.Material) AS VARCHAR(8000)),
            NodeLevel = cr.NodeLevel + 1,
            MaterialLevel = CAST(CONCAT('m', cr.NodeLevel + 1, '.Material, ', cr.MaterialLevel) AS VARCHAR(8000))
        FROM 
            cte_Recursion cr
            JOIN #Recipe r
                ON cr.x = r.Recipe
        WHERE 1 = 1
            AND cr.Recipe <> r.Recipe
            AND r.Recipe <> r.Material
        )
SELECT 
    cr.Recipe, 
    cr.Material, 
    cr.NodeLevel,
    cr.MaterialLevel
    INTO #RecursiveOutput
FROM
    cte_Recursion cr;

-------------------------------------

DECLARE 
    @Split VARCHAR(8000) = '',
    @Material VARCHAR(8000) = '',
    @Level VARCHAR(8000) = '',
    @SelectList VARCHAR(8000) = '', 
    @MaxNode INT = 0,
    @DeBug BIT = 0;     -- set to 0 to execute & set to 1 to print...


SELECT
    @Split = CONCAT(@Split, '
    CROSS APPLY ( VALUES (NULLIF(CHARINDEX(''>'', ro.Material, ', CASE WHEN ro.NodeLevel = 1 THEN '1' ELSE CONCAT('s', ro.NodeLevel - 1, '.Split + 1') END, '), 0)) ) s', ro.NodeLevel, ' (Split)'),
    @Material = CONCAT(@Material, '
    CROSS APPLY ( VALUES (SUBSTRING(ro.Material, ', CASE WHEN ro.NodeLevel = 1 THEN '1, ISNULL(s1.Split -1,' ELSE CONCAT('s', ro.NodeLevel - 1, '.Split + 1, ISNULL(s', ro.NodeLevel, '.Split - s', ro.NodeLevel - 1, '.Split - 1,') END, ' 1000))) ) m', ro.NodeLevel, ' (Material)'),
    @Level = CONCAT(@Level, CASE WHEN ro.NodeLevel = 1 THEN '' ELSE CONCAT('
    CROSS APPLY ( VALUES (CAST(COALESCE(', ro.MaterialLevel, ') AS VARCHAR(20))) ) L', ro.NodeLevel, ' ([Level ', ro.NodeLevel, ' Material])') END),
    @SelectList =  CONCAT(@SelectList, CASE WHEN ro.NodeLevel = 1 THEN '' ELSE CONCAT(',
    L', ro.NodeLevel, '.[Level ', ro.NodeLevel, ' Material]') END),
    @MaxNode = CASE WHEN ro.NodeLevel > @MaxNode THEN ro.NodeLevel ELSE @MaxNode END
FROM 
    #RecursiveOutput ro
GROUP BY 
    ro.NodeLevel,
    ro.MaterialLevel;

DECLARE @sql VARCHAR(MAX) = CONCAT('
SELECT DISTINCT
   ro.Recipe,
    [Level 1 Material] = CAST(m1.Material AS VARCHAR(20))', 
    @SelectList, '
FROM
    #RecursiveOutput ro', 
    @Split, 
    @Material,
    @Level, '
WHERE
    EXISTS (SELECT 1 FROM #Recipe r WHERE L', @MaxNode, '.[Level ', @MaxNode, ' Material] = r.Recipe AND r.Recipe = r.Material);')

IF @DeBug = 1
BEGIN 
    PRINT(@sql);
END;
ELSE 
BEGIN
    EXEC(@sql);
END; 

结果...

Recipe Level 1 Material     Level 2 Material     Level 3 Material
------ -------------------- -------------------- --------------------
aa01   B1                   B1                   B1
aa01   B2                   B2                   B2
aa01   bb01                 B1                   B1
aa01   bb01                 cc01                 B3
aa01   bb01                 cc01                 B4
B1     B1                   B1                   B1
B2     B2                   B2                   B2
B3     B3                   B3                   B3
B4     B4                   B4                   B4
bb01   B1                   B1                   B1
bb01   cc01                 B3                   B3
bb01   cc01                 B4                   B4
cc01   B3                   B3                   B3
cc01   B4                   B4                   B4

编辑:下面是与上面相同的解决方案,但是为了消除在早期版本的SQL Server中使用CONCAT功能而编写...

SET NOCOUNT ON;

IF OBJECT_ID('tempdb..#Recipe', 'U') IS NOT NULL 
DROP TABLE #Recipe;

CREATE TABLE #Recipe (
    Recipe VARCHAR(5) NOT NULL,
    Material VARCHAR(5) NOT NULL 
    );

INSERT #Recipe (Recipe, Material) VALUES 
    ('aa01', 'B1'),
    ('aa01', 'B2'),
    ('aa01', 'bb01'),
    ('bb01', 'B1'),
    ('bb01', 'cc01'),
    ('cc01', 'B3'),
    ('cc01', 'B4'),
    ('B1', 'B1'),
    ('B2', 'B2'),
    ('B3', 'B3'),
    ('B4', 'B4');

--SELECT * FROM #Recipe r;

--====================================================================================

IF OBJECT_ID('tempdb..#RecursiveOutput', 'U') IS NOT NULL 
DROP TABLE #RecursiveOutput;

WITH 
    cte_Recursion AS (  
        SELECT 
            r.Recipe,
            x = r.Material,
            Material = CAST(r.Material AS VARCHAR(8000)),
            NodeLevel = 1,
            MaterialLevel = CAST('m1.Material' AS VARCHAR(8000))
        FROM
            #Recipe r
        UNION ALL
        SELECT 
            cr.Recipe, 
            x = r.Material,
            Material = CAST(cr.Material + '>' + r.Material AS VARCHAR(8000)),
            NodeLevel = cr.NodeLevel + 1,
            MaterialLevel = CAST('m' + CAST(cr.NodeLevel + 1 AS VARCHAR(10)) + '.Material, ' + cr.MaterialLevel AS VARCHAR(8000))
        FROM 
            cte_Recursion cr
            JOIN #Recipe r
                ON cr.x = r.Recipe
        WHERE 1 = 1
            AND cr.Recipe <> r.Recipe
            AND r.Recipe <> r.Material
        )
SELECT 
    cr.Recipe, 
    cr.Material, 
    cr.NodeLevel,
    cr.MaterialLevel
    INTO #RecursiveOutput
FROM
    cte_Recursion cr;

-------------------------------------

DECLARE 
    @Split VARCHAR(8000) = '',
    @Material VARCHAR(8000) = '',
    @Level VARCHAR(8000) = '',
    @SelectList VARCHAR(8000) = '', 
    @MaxNode INT = 0,
    @DeBug BIT = 0;     -- set to 0 to execute & set to 1 to print...


SELECT
    @Split = @Split + '
    CROSS APPLY ( VALUES (NULLIF(CHARINDEX(''>'', ro.Material, ' + CASE WHEN ro.NodeLevel = 1 THEN '1' ELSE 's' + CAST(ro.NodeLevel - 1 AS VARCHAR(10)) + '.Split + 1' END + '), 0)) ) s' + CAST(ro.NodeLevel AS VARCHAR(10)) + ' (Split)',

    @Material = @Material + '
    CROSS APPLY ( VALUES (SUBSTRING(ro.Material, ' + CASE WHEN ro.NodeLevel = 1 THEN '1, ISNULL(s1.Split -1,' ELSE 's' + CAST(ro.NodeLevel - 1 AS VARCHAR(10)) + '.Split + 1, ISNULL(s' + CAST(ro.NodeLevel AS VARCHAR(10)) + '.Split - s' 
            + CAST(ro.NodeLevel - 1 AS VARCHAR(10)) + '.Split - 1,' END + ' 1000))) ) m' + CAST(ro.NodeLevel as VARCHAR(10)) + ' (Material)',

    @Level = @Level + CASE WHEN ro.NodeLevel = 1 THEN '' ELSE '
    CROSS APPLY ( VALUES (CAST(COALESCE(' + ro.MaterialLevel + ') AS VARCHAR(20))) ) L' + CAST(ro.NodeLevel AS VARCHAR(10)) + ' ([Level ' + CAST(ro.NodeLevel as VARCHAR(10)) + ' Material])' END,

    @SelectList =  @SelectList + CASE WHEN CAST(ro.NodeLevel as VARCHAR(10)) = 1 THEN '' ELSE ',
    L' + CAST(ro.NodeLevel AS VARCHAR(10)) + '.[Level ' + CAST(ro.NodeLevel AS VARCHAR(10)) + ' Material]' END,

    @MaxNode = CASE WHEN ro.NodeLevel > @MaxNode THEN ro.NodeLevel ELSE @MaxNode END
FROM 
    #RecursiveOutput ro
GROUP BY 
    ro.NodeLevel,
    ro.MaterialLevel;

DECLARE @sql VARCHAR(MAX) = '
SELECT DISTINCT
   ro.Recipe,
    [Level 1 Material] = CAST(m1.Material AS VARCHAR(20))' +
    @SelectList + '
FROM
    #RecursiveOutput ro' +
    @Split +
    @Material +
    @Level + '
WHERE
    EXISTS (SELECT 1 FROM #Recipe r WHERE L' + CAST(@MaxNode AS VARCHAR(10)) + '.[Level ' + CAST(@MaxNode AS VARCHAR(10)) + ' Material] = r.Recipe AND r.Recipe = r.Material);'

IF @DeBug = 1
BEGIN 
    PRINT(@sql);
END;
ELSE 
BEGIN
    EXEC(@sql);
END;

HTH, 杰森

答案 1 :(得分:0)

简单:)

  1. Level = 1

  2. 使用顶级配方(AA01)的成分创建表变量,并使用Level标记记录(在表变量的单独列中)

  3. 获得的记录数大于零

    1. 提高等级

    2. 获取上一步(Level - 1)中找到的成分中具有父配方的所有成分,并将它们添加到临时表变量中。

    3. 循环

  4. 此时你有结果。表中的水平也表示给定成分的深度。

  5. 这可以避免递归,并且在计算上非常便宜。

    确保在AFTER INSERT触发器中添加成分时检测到圆形路径(如果检测到成分则无法添加),因此配方AA01中的配方BB01没有子配方AA01。但这也可以通过上述方法实现,因此这不是一个大问题。

    此代码应该这样做(未经测试,请报告问题,如果有的话):

    DECLARE @RecipeID nchar(100) = 'AA01'; -- input - recipe you are searching for
    
    -- initializations
    DECLARE @RecipeDepth int = 0;
    DECLARE @IngredientsFound TABLE (
        RecipeID NCHAR(100),
        MaterialID NCHAR(100),
        RecipeDepth int);
    DECLARE @MAX_RECIPEDEPTH int = 100; -- constant for failsafe purposes
    
    -- 0th step
    INSERT INTO @IngredientsFound
        SELECT RecipeID, MaterialID, @RecipeDepth
            FROM Recipe
            WHERE RecipeID = @RecipeID;
    
    -- following steps
    WHILE (@@ROWCOUNT > 0 AND @RecipeDepth <= @MAX_RECIPEDEPTH)
    BEGIN
        SET @RecipeDepth += 1;
        INSERT INTO @IngredientsFound
            SELECT RecipeID, MaterialID, @RecipeDepth
                FROM Recipe
                WHERE RecipeID IN (SELECT MaterialID
                                        FROM @IngredientsFound
                                        WHERE RecipeDepth = @RecipeDepth - 1);
    END;
    
    -- result
    SELECT RecipeID, MaterialID, RecipeDepth
        FROM @IngredientsFound;
    
    • 而不是Level我使用RecipeDepth,因为前一个似乎是保留字或keyowrd
    • 我添加了一个100级深度的安全防护装置,以防止在有圆形参考时无限循环
    • 我用;完成了所有命令,因为将来他们打算强制要求