在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循环并分配层本身?
是否可以循环连续循环查询?
答案 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)
简单:)
Level = 1
使用顶级配方(AA01)的成分创建表变量,并使用Level
标记记录(在表变量的单独列中)
获得的记录数大于零
提高等级
获取上一步(Level - 1
)中找到的成分中具有父配方的所有成分,并将它们添加到临时表变量中。
循环
此时你有结果。表中的水平也表示给定成分的深度。
这可以避免递归,并且在计算上非常便宜。
确保在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 ;
完成了所有命令,因为将来他们打算强制要求