Get Nested JSON from SQL table - sql

Below is my table data looks like.
Below is SQL Query for Create table
CREATE TABLE [dbo].[CategoryMaster](
[CategoryId] [int] NOT NULL,
[ParentId] [int] NULL,
[Name] [varchar](50) NULL,
CONSTRAINT [PK_CategoryMaster] PRIMARY KEY CLUSTERED
(
[CategoryId] 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
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (1, NULL, N'Toys & Games')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (2, 1, N'Art And Crafts')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (3, 1, N'Baby & Toddler Toys')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (4, 1, N'Bikes, Trikes & Ride-Ons')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (5, 2, N'Aprons & Smocks')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (6, 2, N'Blackboards & Whiteboards')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (7, 2, N'Clay & Dough')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (8, 1, N'Pretend Play')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (9, 8, N'Kitchen Toys')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (10, 9, N'Cooking Appliances')
GO
INSERT [dbo].[CategoryMaster] ([CategoryId], [ParentId], [Name]) VALUES (11, 9, N'Cookware')
GO
ALTER TABLE [dbo].[CategoryMaster] WITH CHECK ADD CONSTRAINT [FK_CategoryMaster_CategoryMaster] FOREIGN KEY([ParentId])
REFERENCES [dbo].[CategoryMaster] ([CategoryId])
GO
ALTER TABLE [dbo].[CategoryMaster] CHECK CONSTRAINT [FK_CategoryMaster_CategoryMaster]
GO
I had tried many queries but not able to get desired result. can anyone please help me out from this situation?
I want output like below.

You can create a recursive function like below:
CREATE FUNCTION Create_Json(#CategoryId INT, #IsRoot INT)
RETURNS VARCHAR(MAX)
BEGIN
DECLARE #Json NVARCHAR(MAX) = '{}', #Name NVARCHAR(MAX), #Children NVARCHAR(MAX)
SET #Json = (SELECT P.[Name],JSON_QUERY(dbo.Create_Json(P.CategoryId, 2) ) AS Children
FROM [dbo].[CategoryMaster] AS P
WHERE P.ParentId = #CategoryId
FOR JSON AUTO);
IF(#IsRoot = 1)
BEGIN
SELECT #Name = P.[Name] FROM [dbo].[CategoryMaster] AS P WHERE P.CategoryId = #CategoryId
SET #Json = '"result": {"Name":"' + #Name + '","Children":' + CAST(#Json AS NVARCHAR(MAX)) + '}'
SET #IsRoot = 0
END
RETURN #Json
END
and call it like:
select dbo.Create_Json(1, 1)
Please find the db<>fiddle here.

Related

SQL Recursive Count

I have two tables I am joining with the following structure:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ContentDivider](
[Id] [int] IDENTITY(1,1) NOT NULL,
[ParentId] [int] NULL,
[Name] [nvarchar](128) NOT NULL,
CONSTRAINT [PK_ContentDivider] PRIMARY KEY CLUSTERED
(
[Id] 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].[CustomPage] Script Date: 23-03-2020 17:46:09 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[CustomPage](
[Id] [int] IDENTITY(1,1) NOT NULL,
[ContentDividerId] [int] NOT NULL,
[Name] [nvarchar](128) NOT NULL,
CONSTRAINT [PK_CustomPage] PRIMARY KEY CLUSTERED
(
[Id] 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
A ContentDivider can have n ContentDividers as Children and can have m CustomPages as children as well.
I want a View that counts the Display the current CustomDivider and the COunt for all the CustomPages as Children of the current ContentDivider.
My Test data:
SET IDENTITY_INSERT [dbo].[ContentDivider] ON
GO
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (1, NULL, N'TopLevel1')
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (2, NULL, N'TopLevel2')
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (3, NULL, N'TopLevel3')
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (4, 1, N'SecondLevel1')
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (5, 1, N'SecondLevel2')
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (6, 1, N'SecondLevel3')
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (7, 4, N'ThirdLevel1')
INSERT [dbo].[ContentDivider] ([Id], [ParentId], [Name]) VALUES (8, 4, N'ThirdLevel2')
GO
SET IDENTITY_INSERT [dbo].[ContentDivider] OFF
GO
SET IDENTITY_INSERT [dbo].[CustomPage] ON
GO
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (1, 1, N'Level1_1')
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (2, 1, N'Level1_2')
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (3, 2, N'Level1_3')
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (4, 2, N'Level1_4')
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (5, 4, N'Level1_5')
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (6, 5, N'Level1_6')
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (7, 7, N'Level1_7')
INSERT [dbo].[CustomPage] ([Id], [ContentDividerId], [Name]) VALUES (8, 8, N'Level1_8')
GO
SET IDENTITY_INSERT [dbo].[CustomPage] OFF
GO
And the View I want to extend:
SELECT dbo.ContentDivider.ParentId, dbo.ContentDivider.Name, dbo.ContentDivider.Id, COUNT(DISTINCT dbo.CustomPage.Id) AS CustomPageCount
FROM dbo.ContentDivider LEFT OUTER JOIN
dbo.CustomPage ON dbo.ContentDivider.Id = dbo.CustomPage.ContentDividerId
GROUP BY dbo.ContentDivider.ParentId, dbo.ContentDivider.Name, dbo.ContentDivider.Id
As for now the view counts the custompages directly underneath the contentdivider. I would like all the CustomPages as children counted.
Any suggestions?
The respected result would be:
View
this sounds like a perfect situation for recursive cte ;)
So, if I understood correctly, your expected result would be Toplevel1 with 6 pages and Toplevel 2 with 2 pages since all the other levels are somewhere beneath these two mentioned levels?
The cte might look something like this (maybe you habe to include the max recursion option):
WITH cte AS(
SELECT 1 lvl, ID AS ParentID, ID, Name
FROM dbo.ContentDivider cd
WHERE ParentId IS NULL
UNION ALL
SELECT c.lvl+1 AS lvl, c.ParentID, cd.ID, cd.Name
FROM dbo.ContentDivider cd
INNER JOIN cte c ON cd.ParentID = c.ID
)
SELECT c.ParentID, cd.Name, COUNT(DISTINCT cp.Id) AS CustomPageCount
FROM cte c
JOIN dbo.ContentDivider cd ON cd.ID = c.ParentID
LEFT OUTER JOIN dbo.CustomPage cp ON cp.ContentDividerId = c.id
GROUP BY c.ParentId, cd.Name
This leads to all pages being assigned to its top level.
See fiddle for details: http://sqlfiddle.com/#!18/f1a44/28/1
Edit: Since you need the details down to DividerID, I extended my example in the fiddle. First of all, I fetch the PageCount per ID in one cte and additionally the PageCount aggregated on level (ParentID and all its IDs) - this done you don't need the count and grouping in the following ctes.
In my query I then check, if my current rows ID is a top-level of any kind and assign the corresponding PageCount to this row.
WITH cteCnt AS(
SELECT cd.ID, COUNT(DISTINCT cp.Id) AS CustomPageCount
FROM dbo.ContentDivider cd
LEFT OUTER JOIN dbo.CustomPage cp ON cp.ContentDividerId = cd.id
GROUP BY cd.ID
),
cteTop AS(
SELECT cd.ID, COUNT(DISTINCT cp.Id) AS CustomPageCount
FROM dbo.ContentDivider cd
LEFT OUTER JOIN dbo.CustomPage cp ON cp.ContentDividerId = cd.id
GROUP BY cd.ID
UNION ALL
SELECT cd.ParentID, COUNT(DISTINCT cp.Id) AS CustomPageCount
FROM dbo.ContentDivider cd
LEFT OUTER JOIN dbo.CustomPage cp ON cp.ContentDividerId = cd.id
WHERE cd.ParentID IS NOT NULL
GROUP BY cd.ParentID
),
cteTopSum AS(
SELECT ID, SUM(CustomPageCount) AS CustomPageCount
FROM ctetop
GROUP BY ID
),
cte AS(
SELECT 1 lvl, cd.ID AS ParentID, cd.ID AS ParentIDx, cd.ID, cd.Name, cnt.CustomPageCount
FROM dbo.ContentDivider cd
INNER JOIN cteCnt cnt ON cnt.ID = cd.ID
WHERE ParentId IS NULL
UNION ALL
SELECT c.lvl+1 AS lvl, c.ParentID, cd.ParentID AS ParentIDx, cd.ID, cd.Name, cnt.CustomPageCount
FROM dbo.ContentDivider cd
INNER JOIN cteCnt cnt ON cnt.ID = cd.ID
INNER JOIN cte c ON cd.ParentID = c.ID
),
cteOut AS(
SELECT *
,SUM(CustomPageCount) OVER (PARTITION BY ParentID) x
,SUM(CustomPageCount) OVER (PARTITION BY ParentIDx) y
FROM cte c
)
SELECT CASE WHEN co.ParentIDx = co.ID THEN NULL ELSE co.ParentIDx END AS ParentID, co.ID, co.Name, CASE WHEN co.ID = co.ParentID THEN co.X ELSE ts.CustomPageCount END CustomPageCount
FROM cteOut co
LEFT JOIN cteTopSum ts ON ts.ID = co.ID
ORDER BY 1, 2
See new fiddle for details: http://sqlfiddle.com/#!18/f1a44/185/1
I'm mot sure, if there is a prettier / nicer way to solve this, but seemingly this seems to solve the problem.
However, I did NOT check if it works if any number of sublevels or whatsoever - if you find any issues, feel free to comment.

Generate missing rows using JOIN / CROSS JOIN

I have 3 tables which are used to fill dynamic from value.
The table is represented by schema below.
User can add as many rows as possible, and the rows are shown to end user by template from FormField table.
The data that is saved in FormValues are only for not null values and any values that missing are not saved.
Now the problem is I have to generate report as expected below.
I have tried various combination of join / cross join, but none of them works as expected.
I am able to achieve this in C# via loops, but not able to do the same via SQL Server.
Script attached for DB
USE [SampleDb]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[FormTemplate](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_FormTemplate] PRIMARY KEY CLUSTERED
(
[Id] ASC
)
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[FormField](
[Id] [int] IDENTITY(1,1) NOT NULL,
[FormId] [int] NOT NULL,
[FieldName] [nvarchar](50) NULL,
[FieldType] [nvarchar](50) NULL,
CONSTRAINT [PK_FormField] PRIMARY KEY CLUSTERED
(
[Id] ASC
)
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[FormTemplate] Script Date: 3/2/2020 10:10:22 PM ******/
/****** Object: Table [dbo].[FormValue] Script Date: 3/2/2020 10:10:22 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[FormValue](
[FormId] [int] NOT NULL,
[FieldId] [int] NOT NULL,
[RowIndex] [int] NOT NULL,
[FormValue] [nvarchar](50) NULL,
CONSTRAINT [PK_FormValue] PRIMARY KEY CLUSTERED
(
[FormId] ASC,
[FieldId] ASC,
[RowIndex] ASC
)
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[FormField] ON
GO
INSERT [dbo].[FormField] ([Id], [FormId], [FieldName], [FieldType]) VALUES (1, 1, N'FirstName', N'string')
GO
INSERT [dbo].[FormField] ([Id], [FormId], [FieldName], [FieldType]) VALUES (2, 1, N'LastName', N'string')
GO
INSERT [dbo].[FormField] ([Id], [FormId], [FieldName], [FieldType]) VALUES (3, 1, N'Place', N'string')
GO
INSERT [dbo].[FormField] ([Id], [FormId], [FieldName], [FieldType]) VALUES (4, 1, N'age', N'int')
GO
INSERT [dbo].[FormField] ([Id], [FormId], [FieldName], [FieldType]) VALUES (5, 1, N'dob', N'date')
GO
SET IDENTITY_INSERT [dbo].[FormField] OFF
GO
SET IDENTITY_INSERT [dbo].[FormTemplate] ON
GO
INSERT [dbo].[FormTemplate] ([Id], [Name]) VALUES (1, N'Sample')
GO
SET IDENTITY_INSERT [dbo].[FormTemplate] OFF
GO
INSERT [dbo].[FormValue] ([FormId], [FieldId], [RowIndex], [FormValue]) VALUES (1, 1, 1, N'fname1')
GO
INSERT [dbo].[FormValue] ([FormId], [FieldId], [RowIndex], [FormValue]) VALUES (1, 2, 1, N'lname1')
GO
INSERT [dbo].[FormValue] ([FormId], [FieldId], [RowIndex], [FormValue]) VALUES (1, 2, 3, N'lname3')
GO
INSERT [dbo].[FormValue] ([FormId], [FieldId], [RowIndex], [FormValue]) VALUES (1, 4, 5, N'20')
GO
INSERT [dbo].[FormValue] ([FormId], [FieldId], [RowIndex], [FormValue]) VALUES (1, 5, 3, N'10/10/2020')
GO
This answers the original version of the question -- and then some. But I don't update answers to conform to evolving questions.
You seem to want cross joins on three "tables". The third needs to generate the rowindex values:
select f.id as formtemplateid, ff.id as formfieldid, v.rowindex,
fv.value
from formtemplate f cross join
formfield ff cross join
(values (1), (2), (3), (4), (5)) v(rowindex) left join
formvalues fv
on fv.formtemplateid = f.id and
fv.formfieldid = ff.id and
fv.rowindex = v.rowindex
order by f.id, ff.id, v.rowindex;
EDIT:
You can generate up to 100 numbers using a recursive CTE by doing:
with n as (
select 1 as n
union all
select n + 1
from n
where n < 10 -- "10" is however many you want
)
select n.n
from n;

Top three records for each branch

I need to write query for top three record(count and sum) for each branch of company and I have branch costumers and contracts tables
First of all, create a table like this:
CREATE TABLE [dbo].[Branches](
[Id] [int] NOT NULL,
[CompanyId] [int] NULL,
[Something] [int] NULL,
CONSTRAINT [PK_Table_5] PRIMARY KEY CLUSTERED
(
[Id] 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
Then, insert some data into it:
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (1, 1, 10)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (2, 1, 15)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (3, 1, 20)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (4, 1, 25)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (5, 1, 22)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (6, 2, 50)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (7, 2, 32)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (8, 2, 30)
GO
INSERT [dbo].[Branches] ([Id], [CompanyId], [Something]) VALUES (9, 2, 10)
GO
In the end, your query will be like this:
SELECT result.Companyid, SUM(result.Something) [Sum], (SELECT COUNT(*) FROM Branches WHERE CompanyId = result.companyid) AS [Count]
FROM (
SELECT companyid, Something, Rank()
OVER (PARTITION BY CompanyId
ORDER BY id DESC ) AS Rank
FROM Branches
) result WHERE Rank <= 3
GROUP BY CompanyId
Schema:
First data:
Result:

how to get table from first table when data is not there in second table

i have requirement where i need to show data of both tables when both the ID's are same.when id is present in first table and not there in second table i need to show data from first table
CREATE TABLE [dbo].[TEST](
[ID] [int] NULL,
[Name] [varchar](10) NULL,
[Status] [char](1) NULL,
[CreatedDate] [datetime] NULL
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Test_History](
[ID] [int] NULL,
[Name] [varchar](10) NULL,
[Status] [char](1) NULL,
[CreatedDate] [datetime] NULL
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[Test_History] Script Date: 06/19/2015 19:01:49 ******/
INSERT [dbo].[Test_History] ([ID], [Name], [Status], [CreatedDate]) VALUES (1, N'Mohan', N'A', CAST(0x0000A4BC01347E88 AS DateTime))
INSERT [dbo].[Test_History] ([ID], [Name], [Status], [CreatedDate]) VALUES (1, N'Mohan', N'I', CAST(0x0000A4BC0134A390 AS DateTime))
INSERT [dbo].[Test_History] ([ID], [Name], [Status], [CreatedDate]) VALUES (2, N'Rohan', N'A', CAST(0x0000A4BC01391FCC AS DateTime))
/****** Object: Table [dbo].[TEST] Script Date: 06/19/2015 19:01:49 ******/
INSERT [dbo].[TEST] ([ID], [Name], [Status], [CreatedDate]) VALUES (2, N'Rohan', N'I', CAST(0x0000A4BC0138D584 AS DateTime))
INSERT [dbo].[TEST] ([ID], [Name], [Status], [CreatedDate]) VALUES (1, N'Mohan', N'A', CAST(0x0000A4BC013072DC AS DateTime))
INSERT [dbo].[TEST] ([ID], [Name], [Status], [CreatedDate]) VALUES (3, N'Raj', N'A', CAST(0x0000A4BC0138DED7 AS DateTime))
INSERT [dbo].[TEST] ([ID], [Name], [Status], [CreatedDate]) VALUES (4, N'Krishna', N'A', CAST(0x0000A4BC0138EE31 AS DateTime))
so far i have tried my query to achieve the result
select T.ID,COALESCE(T.ID,TT.ID),T.Name,COALESCE(T.Name,TT.Name),T.status,COALESCE(T.status,TT.status)
from Test T LEFT JOIN (Select TOP 1 ID,MIN(Name)name,Status from Test_History
GROUP BY ID,status
)TT
ON T.ID = TT.ID
where T.ID = 3
Id = 1 and 2 present show i will get data from both tables
Id = 3 and 4 not present in the table
so using coalesce i will get the data
from first table and show in 2nd table column also
but is there any other way like both tables are same structure
i'm thinking of
Declare #tablename varchar(10)
IF EXISTS (SELECT 1 from TESt where id = #id)
IF COunt there in both tables
SET #tablename = Test
ELSE
SET #tablename = Test_history
select * from #tablename where id = #ID
can i get any solution like this
You can use EXCEPT.
Here is an example:
SELECT a,b
FROM (
VALUES (1, 2), (3, 4), (5, 6), (7, 8), (9, 10)
) AS MyTable(a, b)
EXCEPT
SELECT a,b
FROM (
VALUES (1, 2), (7, 8), (9, 10)
) AS MyTable(a, b);
This will return all rows of the upper statement, which are not in the second statement.
First: Thanks for the excellent setup for the data related to the question!
If your real question was if table variables can be used as described in your question, the answer is no; or more accurately that its not worth it.
Not recommended:
declare #TableName TABLE (
[ID] [int] NULL,
[Name] [varchar](10) NULL,
[Status] [char](1) NULL,
[CreatedDate] [datetime] NULL)
IF EXISTS (SELECT 1 from TESt where id = #id)
INSERT INTO #TableName SELECT * FROM dbo.TEST WHERE ID = #ID
ELSE INSERT INTO #TableName SELECT * FROM dbo.[Test_History] WHERE ID = #ID
select * from #tablename where id = #ID
Here's the solution I prefer:
DECLARE #ID INT = 3;
SELECT * FROM [dbo].[TEST] ss WHERE ss.id = #id
UNION ALL SELECT * FROM [dbo].[Test_History] th WHERE th.id = #id
and not exists ( SELECT * FROM [dbo].[TEST] ss WHERE ss.id = #id);
UNION ALL performs surprisingly well - don't forget the ALL keyword, and I am assuming that ID is a PK or AK.
If I'm understanding correctly and you want to display all records that match between the two tables and only records from first table when the id does not exist in the second in the same result set, then all you need is a simple left join:
SELECT *
FROM dbo.test t
LEFT OUTER JOIN Test_History th
ON t.id = th.id
WHERE t.id = #id

Sql Recursion via CTE over LINQ SelectMany

Firstly I'm fairly well versed in LINQ queries, but a complete novice at writing direct SQL queries.
I want to be able to do the following:
For any given ItemId, loop through it's child Items until the very base child items have been selected.
Each item belongs to a container, which has a base (or parent) container Id specified (or NULL if it is the base container). A container can only have one parent container, but it can have multiple child containers.
Currently I've been doing something like the following:
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();
}
}
This runs fine and fairly speedy, however when the ContainerId is quite high up the chain, I've noticed the ridiculous number of queries to the database caused by the SelectMany. For example, if 1000 Items belonged across 100 Items in a parent container, and those 100 items belonged across 10 Items in that containers parent container and finally those 10 belong to 1 item at the top of the chain, the Select Many will run 10 + 100 queries, flattening the results each time to retrieve the base 1000 items - AFAIK to be expected.
I have therefore suspected (after much research) that a Sql CTE may be a better option, not only hitting the database a little more gently, but possibly faster too - is this a bad assumption?
I am however struggling to get to grips with the CTE syntax and am hoping someone out there can shed their wisdom on my problem and help me out.
Re-creating the scenario
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
The following SQL Query correctly returns the immediate child items:
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
)
Results
Item Id 17 belongs to container 4. Container 4's base Container is container 2, Container 2's base container is container 1, and container 1's base container is NULL.
The above query returns ItemIds 13, 14 and 15 (which is correct) within container 2. However I need this query to automatically then look for a base container for container 2 and get all the ItemId's for the base items of Items 13, 14 and 15 (which should yield Item Ids 1 to 9 in this scenario).
Notes
As an item can be attached (through itemrelationships) to items from unrelated containers, the check for the current item(s) container's base container MUST be present.
If the ItemId passed is within a base container then the query should simply return the ItemId passed.
A CTE is preferred as the results will actually be used as part of another query against another table (but that is outside of the scope of this question).
I hope someone can help and thank you in advance for your efforts.
I am not sure I understand completely your model and the place of ItemRelationships table, so the query might require some tweaking - however it should give you an idea how to use recursive 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
As you may see - recursive CTEs consists of two parts. First (base) part - you select your row for given #itemID and in second (recursive) part you join your base part to tables to get child item.
This will run until there is nothing selected in recursive part - or some other condition you may impose is met.