0

Во-первых, я довольно хорошо разбираюсь в запросах LINQ, но новичок в написании прямых SQL-запросов.Рекурсия Sql через CTE над LINQ SelectMany

Я хочу, чтобы иметь возможность сделать следующее:

  • Для любого заданного ItemId, петля через это дочерние элементы до тех пор, пока не будут выбраны очень базовые элементы ребенка.

Каждый элемент принадлежит контейнеру, в котором указан идентификатор контейнера базы (или родительского) (или NULL, если это базовый контейнер). Контейнер может иметь только один родительский контейнер, но может иметь несколько дочерних контейнеров.

В настоящее время я делаю что-то вроде следующий:

using (MyEntities db = new MyEntities()) 
{ 
    var theItem = db.Find(itemId); 
    var theContainer = theItem.Container.BaseContainer; 
    var theBaseItems = theItem.BaseItems.Where(bi => bi.ContainerId == theContainer.ContainerId).ToList(); 

    while (theContainer.BaseContainerId != null) 
    { 
     theContainer = theContainer.BaseContainer; 
     theBaseItems = theBaseItems.SelectMany(bi => bi.BaseItems.Where(i => i.ContainerId == theContainer.ContainerId)).ToList(); 
    } 
} 

Это прекрасно работает и довольно быстрым, но когда ContainerId довольно высоко цепь, я заметил смешное количество запросов к базы данных, вызванной SelectMany. Например, если 1000 элементов принадлежали 100 элементам в родительском контейнере, а эти 100 элементов принадлежали 10 элементам в родительском контейнере контейнеров, и, наконец, эти 10 принадлежат одному элементу в верхней части цепочки, Select Many будет запускать 10 + 100 запросов, каждый раз сглаживая результаты для извлечения базовых 1000 предметов - ожидается AFAIK.

Я подозреваю (после долгих исследований), что Sql CTE может быть лучшим вариантом, а не только ударить по базе данных немного более мягко, но, возможно, и быстрее - это плохое предположение?

Я, однако, изо всех сил пытаюсь справиться с синтаксисом CTE, и я надеюсь, что кто-то там может пролить мудрость на мою проблему и помочь мне.

Воссоздание сценарий

USE [TestDatabase] 
GO 
/****** Object: Table [dbo].[Containers] Script Date: 20/05/2013 14:17:20 ******/ 
SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
CREATE TABLE [dbo].[Containers](
    [ContainerId] [int] IDENTITY(1,1) NOT NULL, 
    [BaseContainerId] [int] NULL, 
    [Name] [nvarchar](50) NOT NULL, 
CONSTRAINT [PK_Containers] PRIMARY KEY CLUSTERED 
(
    [ContainerId] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) ON [PRIMARY] 

GO 
/****** Object: Table [dbo].[ItemRelationships] Script Date: 20/05/2013 14:17:20 ******/ 
SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
CREATE TABLE [dbo].[ItemRelationships](
    [ChildItemId] [int] NOT NULL, 
    [ParentItemId] [int] NOT NULL, 
CONSTRAINT [PK_ItemRelationships] PRIMARY KEY CLUSTERED 
(
    [ChildItemId] ASC, 
    [ParentItemId] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) ON [PRIMARY] 

GO 
/****** Object: Table [dbo].[Items] Script Date: 20/05/2013 14:17:20 ******/ 
SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
CREATE TABLE [dbo].[Items](
    [ItemId] [int] IDENTITY(1,1) NOT NULL, 
    [ContainerId] [int] NOT NULL, 
    [Name] [nvarchar](50) NOT NULL, 
CONSTRAINT [PK_Items] PRIMARY KEY CLUSTERED 
(
    [ItemId] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) ON [PRIMARY] 

GO 
SET IDENTITY_INSERT [dbo].[Containers] ON 

INSERT [dbo].[Containers] ([ContainerId], [BaseContainerId], [Name]) VALUES (1, NULL, N'Level 1') 
INSERT [dbo].[Containers] ([ContainerId], [BaseContainerId], [Name]) VALUES (2, 1, N'Level 2') 
INSERT [dbo].[Containers] ([ContainerId], [BaseContainerId], [Name]) VALUES (3, 1, N'Level 2b') 
INSERT [dbo].[Containers] ([ContainerId], [BaseContainerId], [Name]) VALUES (4, 2, N'Level 3') 
INSERT [dbo].[Containers] ([ContainerId], [BaseContainerId], [Name]) VALUES (5, NULL, N'TypeB') 
SET IDENTITY_INSERT [dbo].[Containers] OFF 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (1, 13) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (2, 13) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (3, 13) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (4, 14) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (5, 14) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (6, 14) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (7, 15) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (8, 15) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (9, 15) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (10, 16) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (11, 16) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (12, 16) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (13, 17) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (14, 17) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (15, 17) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (1007, 13) 
INSERT [dbo].[ItemRelationships] ([ChildItemId], [ParentItemId]) VALUES (1008, 17) 
SET IDENTITY_INSERT [dbo].[Items] ON 

INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (1, 1, N'A') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (2, 1, N'B') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (3, 1, N'C') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (4, 1, N'D') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (5, 1, N'E') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (6, 1, N'F') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (7, 1, N'G') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (8, 1, N'H') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (9, 1, N'I') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (10, 1, N'J') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (11, 1, N'K') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (12, 1, N'L') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (13, 2, N'A2') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (14, 2, N'A2') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (15, 2, N'C2') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (16, 3, N'D2B') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (17, 4, N'A3') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (1007, 5, N'TypeB1') 
INSERT [dbo].[Items] ([ItemId], [ContainerId], [Name]) VALUES (1008, 5, N'TypeB2') 
SET IDENTITY_INSERT [dbo].[Items] OFF 
ALTER TABLE [dbo].[Containers] WITH CHECK ADD CONSTRAINT [FK_Containers_Containers] FOREIGN KEY([BaseContainerId]) 
REFERENCES [dbo].[Containers] ([ContainerId]) 
GO 
ALTER TABLE [dbo].[Containers] CHECK CONSTRAINT [FK_Containers_Containers] 
GO 
ALTER TABLE [dbo].[ItemRelationships] WITH CHECK ADD CONSTRAINT [FK_ItemRelationships_ChildItems] FOREIGN KEY([ParentItemId]) 
REFERENCES [dbo].[Items] ([ItemId]) 
GO 
ALTER TABLE [dbo].[ItemRelationships] CHECK CONSTRAINT [FK_ItemRelationships_ChildItems] 
GO 
ALTER TABLE [dbo].[ItemRelationships] WITH CHECK ADD CONSTRAINT [FK_ItemRelationships_ParentItems] FOREIGN KEY([ChildItemId]) 
REFERENCES [dbo].[Items] ([ItemId]) 
GO 
ALTER TABLE [dbo].[ItemRelationships] CHECK CONSTRAINT [FK_ItemRelationships_ParentItems] 
GO 
ALTER TABLE [dbo].[Items] WITH CHECK ADD CONSTRAINT [FK_Items_Containers] FOREIGN KEY([ContainerId]) 
REFERENCES [dbo].[Containers] ([ContainerId]) 
ON UPDATE CASCADE 
ON DELETE CASCADE 
GO 
ALTER TABLE [dbo].[Items] CHECK CONSTRAINT [FK_Items_Containers] 
GO 
USE [master] 
GO 
ALTER DATABASE [TestDatabase] SET READ_WRITE 
GO 

Следующий SQL Query правильно возвращает непосредственные дочерние элементы:

DECLARE @itemId BIGINT = 17; 

SELECT 
    Items.ItemId 
FROM 
    [TestDatabase].[dbo].[ItemRelationships] 
INNER JOIN 
    [TestDatabase].[dbo].Items 
ON 
    ItemRelationships.ChildItemId = Items.ItemId 
INNER JOIN 
    [TestDatabase].[dbo].[Containers] 
ON 
    Items.ContainerId = Containers.ContainerId 
WHERE 
    ItemRelationships.ParentItemId = @itemId 
AND 
    Items.ContainerId = 
    (
    SELECT 
     BaseContainerId 
    FROM 
     [TestDatabase].[dbo].[Items] 
    INNER JOIN 
     [TestDatabase].[dbo].[Containers] 
    ON 
     Items.ContainerId = Containers.ContainerId 
    WHERE 
     Items.ItemId = @itemId 
    ) 

Результаты

Item Id 17 принадлежит к контейнеру 4 Контейнер 4 контейнера Контейнер - контейнер 2, основание контейнера 2 контейнер - контейнер 1, а контейнер контейнера 1 - NULL.

Вышеуказанный запрос возвращает ItemIds 13, 14 и 15 (что является правильным) в контейнере 2. Однако мне нужен этот запрос, чтобы автоматически искать базовый контейнер для контейнера 2 и получить все ItemId для базовых элементов Items 13, 14 и 15 (в этом сценарии должны указываться идентификаторы с 1 по 9).

Примечание

  • Как элемент может быть присоединен (через itemrelationships) к элементам из неродственных контейнеров, проверка для текущего элемента базового контейнер (ов) контейнера должна присутствовать.
  • Если ItemId прошел в базовом контейнере, тогда запрос должен просто вернуть переданный ItemId.
  • CTE предпочтительнее, поскольку результаты будут фактически использоваться как часть другого запроса к другой таблице (но это выходит за рамки этого вопроса).

Я надеюсь, что кто-то может помочь и поблагодарить вас заранее за ваши усилия.

ответ

1

Я не уверен, что полностью понимаю вашу модель и место таблицы ItemRelationships, поэтому для запроса может потребоваться некоторая настройка - однако она должна дать вам представление о том, как использовать рекурсивный CTE.

DECLARE @itemID INT 
Set @itemID = 17 

;WITH CTE_Containers AS 
(
    SELECT c.ContainerId, c.BaseContainerId, i.ItemID AS ChildItemId, NULL AS ParentItemID, i.Name 
    FROM Items i 
    INNER JOIN Containers c ON i.ContainerId = c.ContainerId 
    WHERE i.ItemId = @itemID 

    UNION ALL 

    SELECT c.ContainerId, c.BaseContainerId, ir.ChildItemId, ir.ParentItemId, i.Name 
    FROM CTE_Containers cte 
    INNER JOIN dbo.Containers c ON cte.BaseContainerId = c.ContainerId 
    INNER JOIN dbo.ItemRelationships ir ON ir.ParentItemId = cte.ChildItemId 
    INNER JOIN dbo.Items i ON ir.ChildItemId = i.ItemID 
) 
SELECT * FROM CTE_Containers 

Как вы можете видеть - рекурсивные CTE состоят из двух частей. Первая (базовая) часть - вы выбираете свою строку для данного @itemID, а во второй (рекурсивной) части вы присоединяете свою базовую часть к таблицам для получения дочернего элемента.

Это будет работать до тех пор, пока в рекурсивной части ничего не будет выбрано - или какое-либо другое условие, которое вы можете наложить, выполнено.

+0

Очень полезно, и отличное начало - спасибо. Однако при тестировании с некоторыми дополнительными данными, которые я хочу обеспечить, во избежание возврата возвращается несколько дополнительных записей, которые не должны включаться в окончательные результаты. Я обновил сценарий создания базы данных для предоставления этих записей - по существу, идентификаторы предметов 1007 и 1008 не должны включаться, так как их контейнер не является частью иерархии иерархии контейнеров Item ID 17. Примечание 1007 и 1008 связаны с идентификаторами 13 и 17, чтобы имитировать это. –

+0

Отмечено в качестве ответа, в то же время не доставляя точных результатов, предоставляя ценную информацию (с простым объяснением) в CTE. Спасибо. –

+0

@JonBellamy Привет, надеюсь, что у вас есть решение, в котором вы нуждаетесь. Для exculding дочерних элементов, не принадлежащих дочерним контейнерам, - добавьте 'WHERE c.ContainerId = i.ContainerId' в рекурсивную часть CTE –

Смежные вопросы