#sql #sql-server #recursion #common-table-expression #recursive-query
#sql #sql-сервер #рекурсия #common-table-expression #рекурсивный запрос
Вопрос:
Это текущий дизайн базы данных SQL Server, с которой я работаю.
У меня есть два стола:
- Рецепты
- ингредиенты рецепта
Рецепты состоят из ингредиентов рецепта, но ингредиентом может быть другой рецепт. Теоретически существует бесконечное количество уровней, поскольку в каждом рецепте может быть другой ингредиент, который также является рецептом.
В приведенном выше примере данных рецепт свежей сальсы (ID 3047) состоит из 7 ингредиентов. Шесть — это сырье, но один — это другой рецепт (идентификатор рецепта 3008). Этот идентификатор рецепта ссылается на другой рецепт в таблице «рецепты».
Там нет иерархии, и я не думаю, что смогу создать иерархию.
Цель состоит в том, чтобы извлечь все элементы рецепта для конкретного рецепта, которые имеют «вспомогательные» рецепты и «вспомогательные» рецепты и т.д.
Казалось бы, рекурсивный поиск был бы ответом, но поскольку иерархии нет, это, похоже, не работает.
Вот моя попытка запроса (переменная списка RecipeItem — это список всех recipeitem, которые также являются рецептами, созданными в предыдущем запросе):
<cfquery name="whatever">
WITH MenuPrepOfPreps (recipe_id, depth, otherRecipe_id, recipe_name)
AS
(
SELECT r.recipe_id,
0 as depth,
ri.otherRecipe_id,
r.recipe_name
FROM menu_recipes r
JOIN menu_recipeItems ri
ON ri.otherRecipe_id = r.recipe_id
WHERE ri.otherRecipe_id in (#recipeItemList#)
UNION ALL
-- recursive members
SELECT
mop.recipe_id,
mop.depth 1 as depth,
ri.otherRecipe_id,
r.recipe_name
FROM menu_recipes r
JOIN menu_recipeItems ri
ON ri.otherRecipe_id = r.recipe_id
INNER JOIN MenuPrepOfPreps AS MOP
ON ri.otherRecipe_id = MOP.recipe_id
)
SELECT top(6)recipe_id, recipe_name
FROM MenuPrepOfPreps
GROUP BY recipe_id, recipe_name
</cfquery>
Он продолжает создавать бесконечный цикл. Когда я ограничиваю результаты первыми несколькими строками (верхние 6), это дает желаемый результат.
Возможно, что дизайн базы данных неверен, поэтому это может никогда не сработать.
Любая помощь приветствуется.
[ОБНОВЛЕННЫЙ ЗАПРОС НА ОСНОВЕ ПРЕДЛОЖЕННОГО РЕШЕНИЯ @NewBie20200101 С ИЗМЕНЕНИЯМИ ИМЕН ПЕРЕМЕННЫХ / СТОЛБЦОВ]
<cfquery name="whatever">
WITH MenuPrepOfPreps AS
(
SELECT otherrecipe_id,
CASE
when
otherRecipe_id = 0 then null
else
otherRecipe_id
end
as sub_recipe
FROM menu_recipeItems as a -- anchor
UNION ALL
SELECT
a.otherrecipe_id,
CASE
when
b.otherRecipe_id = 0 then null
else
b.otherRecipe_id
end
as sub_recipe
FROM menu_recipeItems as b
where b.recipe_id = a.otherRecipe_id --recursion
and a.otherRecipe_id is null --stopper
), allrecipeitems as (
SELECT recipe_id, sub_recipe
FROM MenuPrepOfPreps
)
Select
c.recipe_id,
d.otherRecipe_id
From MENU_recipes c
INNER JOIN MENU_recipeItems d on c.recipe_id = d.otherRecipe_id
Where c.recipe in (#recipelist#)
</cfquery>
Не работает и выдает следующую ошибку:
Не удалось связать многосоставный идентификатор «a.otherRecipe_id».
Комментарии:
1. Есть ли у вас что-нибудь для защиты от циклов в вашем графике рецептов?
2. Я так не думаю, поэтому я думаю, что это не работает… он просто продолжает цикл и по какой-то причине возвращает огромный или бесконечный набор результатов
3. Разве вы все равно не идете в неправильном направлении. В настоящее время вы начинаете с рецептов в нижней части дерева и возвращаетесь назад, похоже, что ваш список рецептов — это корневые рецепты?
4. Объединение в рекурсивной части не имеет смысла. Он дважды присоединяется к
otherRecipe_id
5. Вот тут-то я и думаю, что это не удается, но если у меня есть список, из которого я выбираю, я не знаю, как это сделать, если только я не пройдусь по списку и не выполню запрос столько раз, сколько будет длинен список, что не имеет смысла. Я не уверен, что вы подразумеваете под «неправильным направлением», это просто вопрос установки предложения «IN» где-то в другом месте? В части рекурсивного элемента? Но тогда он будет выбирать все рецепты, а не те, которые я хочу? Как вы можете видеть, я не совсем ясно выразился.
Ответ №1:
Не уверен, что это сработает:
With preppreprep as (
Select
Recipeid,
Case when otherrecipeId = 0 then null else otherrecipeID end as otherrecipeID, ——remove 0 it might be a problem
From
Recipeitems as a ——————anchor
Left outer join recipeitems as b on a.otherrecipeID = b.recipeID
Union all
Select
C.recipeid,
Case when c.otherrecipeID = 0 the null else c.other recipeID end as otherrecipeID, also remove 0
From preprepprep as c
Left outer join recipeitems as d
Where c.recipeid = d.otherrecipeID———--recursion
), allrecipeitems as (
Select
RecipeID,
OtherrecipeID
From preprepprep
)
Select
C.RecipeID
D.OtherRecipeID
From recipe c
Inner recipeitems d on c.recipeid = d.recipeid
Where c.recipe in (##)
——extract unpacked sub recipes based on recipe
Если вы считаете, что существует более 99 уровней, добавьте параметр max recursion 0
Комментарии:
1. Не уверен, понимаю ли я, где это будет выполняться? Это весь запрос без рекурсии?
2. Извините, это моя первая попытка ответить на вопросы, лол. Это заменит весь ваш запрос, но вам нужно изменить слова. Рекурсия происходит внутри скобок. Он должен продолжать присоединяться до тех пор, пока не останется ничего, к чему можно присоединиться (вот почему изменили 0 на null), и за скобками вы просто выделяете все
3. Хорошо, я попробую это после моей поездки на велосипеде … 🙂
4. Ложусь спать 🙂 Я еще раз проверю почту, когда встану, посмотрю, как у тебя дела
5. @NewBie20200101 пожалуйста, проверьте параметры форматирования кода в редакторе и убедитесь, что ваши запросы легко читаются. Также, пожалуйста, убедитесь, что вы объяснили, что вы сделали, а не просто написали запрос, таким образом OP изучает мышление, лежащее в основе решения.
Ответ №2:
Я понял это сам с помощью всех участников (спасибо). Во-первых, как упоминал @Charlieface, я подходил к этому с неправильного направления. С помощью этого решения я начал с нижней части иерархии и работал до тех пор, пока в иерархии не осталось данных для получения.
Последний запрос содержит несколько фильтров и группировок, чтобы получить именно ту информацию, которая мне нужна.
Если кому-то интересно, вот решение:
WITH MenuPrepOfPreps (recipe_id, otherRecipe_id, depth, recipe_name)
AS (
SELECT
mria.recipe_id, mria.otherRecipe_id, 0, mr.recipe_name
FROM
menu_recipeItems mria, menu_recipes mr
where
mria.otherrecipe_id <> 0
and mria.recipe_id = mr.recipe_id
UNION ALL
SELECT
MenuPrepOfPreps.recipe_id, mri.otherrecipe_id, MenuPrepOfPreps.depth 1, mr.recipe_name
FROM
menu_recipeItems mri, MenuPrepOfPreps, menu_recipes mr
WHERE MenuPrepOfPreps.otherrecipe_id = mri.recipe_id
AND MRI.otherrecipe_id <> 0
AND mr.recipe_id = mri.recipe_id
)
SELECT
mopsA.otherrecipe_id, mopsA.recipe_id, mopsA.depth, mra.recipe_name AS thePrepRecipeName
FROM
MenuPrepOfPreps mopsA, MENU_recipes MRA
WHERE
mopsA.recipe_id in (#recipelist#)
AND mopsA.otherRecipe_id = MRA.recipe_id
AND MRA.recipeType_id = 2
GROUP BY mra.recipe_name, mopsA.otherrecipe_id, mopsA.recipe_id, mopsA.depth
Мне нужно будет посмотреть, как это работает с большим набором данных, чем тот набор образцов, который у меня есть сейчас, но пока это происходит довольно быстро.
Ответ №3:
Я бы создал табличную функцию, которая возвращает весь лист recipeItem
для одного recipe_id
(что выравнивает иерархию), без рекурсии, но с циклом, который разрешает один уровень рекурсии за раз (вместо следования каждой отдельной ветви). Если у вас есть миллион вспомогательных рецептов глубиной до 10 уровней, для получения данных требуется 12 итераций (10 (глубина) 1 (определяет, что больше ничего не изменилось) 1 (чтобы получить все листья из рецептов).
Преимущество этого подхода также в том, что не нужно устанавливать никаких ограничений, если в базе данных определен бесконечный цикл, это нас нисколько не беспокоит.
Здесь приведен код для функции с табличным значением:
CREATE FUNCTION [dbo].[GetRecipeItemsFlat](@recipe_id INT)
RETURNS @recipeItems TABLE (recipeItem_id INT NOT NULL)
AS
BEGIN
DECLARE @recipeList TABLE (recipe_id INT);
DECLARE @previousCount INT = 0;
DECLARE @currentCount INT = 1;
-- Get all recipe_ids recursively until infinity (no endless loop possible)
INSERT INTO @recipeList SELECT @recipe_id;
WHILE (@previousCount < @currentCount) BEGIN
-- Adding not yet added child recipe_ids
INSERT INTO @recipeList SELECT DISTINCT otherRecipe_id FROM recipeItem WHERE recipe_id IN (SELECT recipe_id FROM @recipeList) AND (otherRecipe_id IS NOT NULL) AND (otherRecipe_id != 0) AND (otherRecipe_id NOT IN (SELECT recipe_id FROM @recipeList));
SET @previousCount = @currentCount ;
SET @currentCount = (SELECT COUNT(*) FROM @recipeList);
END
INSERT INTO @recipeItems SELECT recipeItem_ID FROM recipeItem WHERE recipe_id IN (SELECT * FROM @recipeList) AND (rawMaterial_id IS NOT NULL) AND (rawMaterial_id != 0);
RETURN;
END
И тогда я бы создал представление, которое предоставляет весь лист recipeItem
для recipe
подобного:
CREATE VIEW recipeItemFlat
AS
SELECT c.recipe_id, c.recipeItem_id, c.ItemQuantity, c.rawMaterial_id
FROM recipe a
CROSS APPLY dbo.GetRecipeItemsFlat(recipe_id) b
INNER JOIN recipeItem c ON b.recipeItem_id = c.recipeItem_id;
И тогда запрос на получение всех элементов для рецепта 3047 тривиален:
SELECT * FROM recipeItemFlat WHERE recipe_id = 3047
Существует одна возможная проблема с этим решением, которое я обычно использую для разрешений прав и ролей и тому подобных вещей, где это не имеет значения, но здесь это может:
Если существует рецепт A, в котором есть два рецепта B и C, и оба B и C имеют рецепт D, то D будет только один раз в результате, и сложение количеств даст неверный результат. Если это важно, используйте не DISTINCT
для определения конца цикла в функции с табличным значением, а максимальный уровень, как вы делали в своем примере.
Комментарии:
1. Я также заметил, что вы используете 0 в
otherRecipe_id
случае, если его нет. Я бы также использовалnull
, как вы делаете в столбцеrawMaterial_id
, а затем установил пользовательское ограничение следующим образом: ИЗМЕНИТЬ ТАБЛИЦУ dbo.RecipeItem ДОБАВИТЬ ОГРАНИЧЕНИЕ recipeItem_CK01 ПРОВЕРИТЬ (((otherRecipe_id РАВНО НУЛЮ) И (rawMaterial_id НЕ РАВНО НУЛЮ)) ИЛИ ((otherRecipe_id НЕ равно НУЛЮ) И (rawMaterial_id РАВНО НУЛЮ))) ПЕРЕЙТИ2. Я обязательно попробую это решение и опубликую здесь результаты … спасибо!