SQL Recursive Menu Sorting - sql-server-2005

I've got a simple table that I'm using to represent a hierarchy of categories.
CREATE TABLE [dbo].[Categories](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Title] [varchar](256) NOT NULL,
[ParentID] [int] NOT NULL,
CONSTRAINT [PK_Categories] 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
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('All', 0)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('Banking', 8)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('USAA Checking', 2)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('USAA Mastercard', 2)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('Medical', 8)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('Jobs', 8)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('Archive', 1)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('Active', 1)
INSERT INTO [MDS].[dbo].[Categories]([Title],[ParentID]) VALUES ('BoA Amex', 2)
Everything is fine except for selecting the entire tree. Here is my query, I removed my ORDER BY because it doesn't work:
WITH CategoryTree (ID, Title, Level, ParentID) AS
(
SELECT r.ID, r.Title, 0 Level, r.ParentID
FROM Categories r
WHERE r.ParentID = 0
UNION ALL
SELECT c.ID, c.Title, p.Level + 1 AS Level, c.ParentID
FROM Categories c
INNER JOIN CategoryTree p
ON p.ID = c.ParentID
)
SELECT ID,
REPLICATE('-----', Level) + Title AS Title,
ParentID
FROM CategoryTree
Results:
ID Title ParentID
1 All 0
7 -----Archive 1
8 -----Active 1
2 ----------Banking 8
5 ----------Medical 8
6 ----------Jobs 8
3 ---------------USAA Checking 2
4 ---------------USAA Mastercard 2
9 ---------------BoA Amex 2
The result I want is this:
ID Title ParentID
1 All 0
8 -----Active 1
2 ----------Banking 8
9 ---------------BoA Amex 2
3 ---------------USAA Checking 2
4 ---------------USAA Mastercard 2
6 ----------Jobs 8
5 ----------Medical 8
7 -----Archive 1
What is killing me is I got this working perfectly before but then I forgot to back up the DB and lost it in a server upgrade.
I looked at the HierarchyID type in 2008 but it just seems like a big pain in the ass if you care about order of children at the same level.

Ok, got it :) -- This seems to work here.
DECLARE #Categories TABLE (
ID int PRIMARY KEY
,Title varchar(256)
,ParentID int
)
INSERT INTO #Categories
VALUES
(1, 'All', 0)
,(2,'Banking', 8)
,(3,'USAA Checking', 2)
,(4,'USAA Mastercard', 2)
,(5,'Medical', 8)
,(6,'Jobs', 8)
,(7,'Archive', 1)
,(8,'Active', 1)
,(9,'BoA Amex', 2)
;
WITH CategoryTree
AS (SELECT r.ID, r.Title, 0 Level, r.ParentID,
CAST(r.Title AS VARCHAR(1000)) AS "Path"
FROM #Categories r
WHERE r.ParentID = 0
UNION ALL
SELECT c.ID, c.Title, p.Level + 1 AS Level, c.ParentID,
CAST((p.path + '/' + c.Title) AS VARCHAR(1000)) AS "Path"
FROM #Categories c
INNER JOIN CategoryTree p
ON p.ID = c.ParentID
)
SELECT ID, REPLICATE('-----', Level) + Title AS Title, [Path]
FROM CategoryTree
ORDER BY [Path]

Related

Recursively Conditionally get parent Id

I have two tables: Budget Line and Expense. They are structured in such a way that an expense must have either a parent record in the budget line table, or a parent record in the expense table. I need to select all child-most expenses for each budget line.
For example - BudgetLine:
Id
Description
1
TEST123
2
OTHERTEST
Expense:
Id
ParentId
ParentType
Description
1
1
BudgetLine
Group of Expenses
2
1
Expense
Expense # 1
3
1
Expense
Expense # 2
4
2
BudgetLine
Expense 3
Desired result:
BudgetLineId
ExpenseId
Description
1
2
Expense # 1
1
3
Expense # 2
2
4
Expense # 3
I am looking to omit expenses in the result only if they are the only sub-child. Note that an expense may have many children, grandchildren, etc.
I have tried the following, and researching various recursive CTE methods:
WITH RCTE AS
(
SELECT Expense.Id, Expense.ParentId, Expense.ParentType, 1 AS Lvl, Expense.Id as startId FROM Expense
UNION ALL
SELECT rh.Id, rh.ParentId, rh.ParentType, Lvl+1 AS Lvl, rc.Id as startId FROM dbo.Expense rh
INNER JOIN RCTE rc ON rh.Id = rc.ParentId and rc.ParentType = 'Expense'
),
FilteredRCTE AS
(
SELECT startId, MAX(LVL) AS Lvl
FROM RCTE
GROUP BY startID
),
RecursiveData AS
(
SELECT FilteredRCTE.startId AS ExpenseId, RCTE.ParentId AS BudgetLineId
FROM FilteredRCTE
JOIN RCTE ON FilteredRCTE.startId = RCTE.startId AND FilteredRCTE.Lvl = RCTE.Lvl
)
SELECT *
FROM RecursiveData
Which did in-fact obtain all the child Expenses and their associated parent BudgetLine, but it also included the middle-tier expenses (such as item 1 in the example) and I cannot figure out how to filter those middle-tier items out.
Here is a script to create tables / insert sample data:
CREATE TABLE [dbo].[BudgetLine]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[Description] [varchar](500) NULL,
) ON [PRIMARY]
GO
INSERT INTO dbo.BudgetLine VALUES ('TEST123')
INSERT INTO dbo.BudgetLine VALUES ('OTHERTEST')
CREATE TABLE [dbo].[Expense]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[ParentId] [int] NOT NULL,
[ParentType] [varchar](100) NOT NULL,
[Description] [varchar](max) NULL,
) ON [PRIMARY]
GO
INSERT INTO dbo.Expense VALUES ('1', 'BudgetLine', 'Group of Expenses')
INSERT INTO dbo.Expense VALUES ('1', 'Expense', 'Expense # 1')
INSERT INTO dbo.Expense VALUES ('1', 'Expense', 'Expense # 2')
INSERT INTO dbo.Expense VALUES ('2', 'BudgetLine', 'Expense # 3')
Maybe I have oversimplified, but the following returns your desired results, by checking that there is no other expense row connected to the current row.
WITH RCTE AS
(
SELECT E.Id ExpenseId, E.ParentId, E.ParentType
FROM #Expense E
UNION ALL
SELECT RH.Id, RH.ParentId, RH.ParentType
FROM #Expense RH
INNER JOIN RCTE RC ON RH.Id = RC.ParentId AND RC.ParentType = 'Expense'
)
SELECT *
FROM RCTE R1
WHERE NOT EXISTS (
SELECT 1
FROM RCTE R2
WHERE R2.ParentId = R1.ExpenseId AND R2.ParentType = 'Expense'
);

CTE arithmetic shift operator causes arithmetic overflow error

When executing a CTE expression to query for an ordered child parent relation by using a shift, it fails with
Arithmetic overflow error converting expression to data type bigint
The problem is that the shift value becomes big very easily. I know I could increase the datatype to support 38 numeric values but I would still hit this number when having deep parent child relations. I'm wondering if there are any other method to order the results, so I would not hit this limit.
Here is a sample script that shows the increase of the shift parameter.
CREATE TABLE [dbo].[ParentChild] (
[Id] [int] IDENTITY(1,1) NOT NULL,
[ParentId] [int] NULL,
[Name] [nvarchar](150) NOT NULL
CONSTRAINT [PK_Dialog] PRIMARY KEY CLUSTERED
(
[Id] ASC
))
GO
ALTER TABLE [dbo].[ParentChild] WITH CHECK ADD CONSTRAINT [FK_ParentChild_ParentId] FOREIGN KEY([ParentId])
REFERENCES [dbo].[ParentChild] ([Id])
GO
ALTER TABLE [dbo].[ParentChild] CHECK CONSTRAINT [FK_ParentChild_ParentId]
GO
set identity_insert [dbo].[ParentChild] on
insert into [dbo].[ParentChild] ([Id], [ParentId],[Name])
values
(1, NULL, '1'),
(2, NULL, '2'),
(3, 1, '1.1'),
(4, 1, '1.2'),
(5, 2, '2.1'),
(6, 5, '2.1.1')
set identity_insert [dbo].[ParentChild] off
-- without shift
with Parent as (
select d1.[Id], d1.[ParentId], d1.[Name], 0 AS [Level]
FROM [dbo].[ParentChild] as d1
WHERE d1.[ParentId] IS NULL
UNION ALL
SELECT d2.Id, d2.ParentId, d2.[Name], [Level] + 1
FROM [dbo].[ParentChild] as d2
INNER JOIN Parent d1 ON d1.[Id] = d2.ParentId
)
select p.Id, p.ParentId, p.[Name], [Level]
from Parent p
group by p.Id, p.ParentId, p.[Name], [Level];
-- desired
with Parent as (
select d1.[Id], d1.[ParentId], d1.[Name], 0 AS [Level],
CAST(row_number() over(order by id) as DECIMAL(38,0)) as [shift]
FROM [dbo].[ParentChild] as d1
WHERE d1.[ParentId] IS NULL
UNION ALL
SELECT d2.Id, d2.ParentId, d2.[Name], [Level] + 1,
CAST([shift] * 100 + row_number() over(order by d2.id) as DECIMAL(38,0))
FROM [dbo].[ParentChild] as d2
INNER JOIN Parent d1 ON d1.[Id] = d2.ParentId
)
select p.Id, p.ParentId, p.[Name], [Level], [shift]
from Parent p
group by p.Id, p.ParentId, p.[Name], [Level], [shift]
order by cast([shift] as varchar(50))
Output without the shift parameter
Id ParentId Name Level
1 NULL 1 0
2 NULL 2 0
3 1 1.1 1
4 1 1.2 1
5 2 2.1 1
6 5 2.1.1 2
Output with the shift parameter (desired)
Id ParentId Name Level shift
1 NULL 1 0 1
3 1 1.1 1 101
4 1 1.2 1 102
2 NULL 2 0 2
5 2 2.1 1 201
6 5 2.1.1 2 20101
Assuming we can make shift a string rather than a maths-supporting data type, we can just do this:
with Parent as (
select d1.[Id], d1.[ParentId], d1.[Name], 0 AS [Level],
CONVERT(varchar(max),row_number() over(order by id)) as [shift]
FROM [dbo].[ParentChild] as d1
WHERE d1.[ParentId] IS NULL
UNION ALL
SELECT d2.Id, d2.ParentId, d2.[Name], [Level] + 1,
shift + RIGHT('0' + CONVERT(varchar(2),row_number() over(order by d2.id)),2)
FROM [dbo].[ParentChild] as d2
INNER JOIN Parent d1 ON d1.[Id] = d2.ParentId
)
select p.Id, p.ParentId, p.[Name], [Level], [shift]
from Parent p
group by p.Id, p.ParentId, p.[Name], [Level], [shift]
order by shift
It produces different results if the row numbers can ever exceed 100 but that seems to lead to problems with this representation anyway (ambiguous encodings).

SQL Server 2008: many-to-many relationship: Concatenation in SELECT query [duplicate]

This question already has answers here:
How to make a query with group_concat in sql server [duplicate]
(4 answers)
Closed 4 years ago.
There are 3 tables:
Project
Tool
LinkProjectTool
I need a query that lists everything in the Project table plus an extra column called ProjectTools. This column should contain a comma delimited string with all the tool names belonging to each project.
The data is:
Table Project:
ID Name Client
------------------------
0 table Anna
1 chair Bobby
2 workbench James
3 window Jenny
4 shelves Matthew
Table Tool:
ID Name
------------------------
0 hammer
1 measuring tape
2 pliers
3 scissors
4 spanner
5 saw
6 screwdriver
Table LinkProjectTool:
IDProject IDTool
-------------------
0 0
0 3
2 1
2 4
2 5
The result should be:
ID Name Client ProjectTools
-------------------------------------------------------------
0 table Anna hammer, scissors
1 chair Bobby
2 workbench James measuring tape, spanner, saw
3 window Jenny
4 shelves Matthew
Here are the queries I used to create these tables:
CREATE TABLE [dbo].[Project]
(
[ID] [int] NOT NULL,
[Name] [nvarchar](15) NOT NULL,
[Client] [nvarchar](15) NULL
)
INSERT INTO [dbo].[Project]
(ID, Name, Client)
VALUES
(0, 'table', 'Anna'),
(1, 'chair', 'Bobby'),
(2, 'workbench', 'James'),
(3, 'window', 'Jenny'),
(4, 'shelves', 'Matthew')
CREATE TABLE [dbo].[Tool](
[ID] [tinyint] IDENTITY(0,1) NOT NULL,
[Name] [nvarchar](30) NULL,
CONSTRAINT [PK_Tool] 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]
INSERT INTO [dbo].Tool
(Name)
VALUES
('hammer'),
('measuring tape'),
('pliers'),
('scissors'),
('spanner'),
('saw'),
('screwdriver')
CREATE TABLE [dbo].LinkProjectTool
(
[IDProject] [int] NOT NULL,
[IDTool] [tinyint] NULL
)
INSERT INTO [dbo].LinkProjectTool
(IDProject, IDTool)
VALUES
(0, 0),
(0, 3),
(2, 1),
(2, 4),
(2, 5)
Could you, please, help?
Thank you.
You can use STUFF function alongside with FOR XML (see this answer for a more detailed explanation on how they work).
Assuming you want the project tools to be separated by a comma and a blank space, you can use the following query:
SELECT DISTINCT p.ID, p.Name, p.Client,
ProjectTools = STUFF((
SELECT ', ' + t.Name
FROM Tool t
WHERE t.ID IN (SELECT IDTool FROM LinkProjectTool WHERE IdProject = p.ID)
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '')
FROM Project p LEFT OUTER JOIN LinkProjectTool lpt ON p.Id = lpt.IDProject
ORDER BY p.ID

Select Data with Order By Respecting Ancestor

I have a table MyStackFiles that has 3 columns:
FileID (The primary key)
FileName
OriginalFileID (This can be either 0 if there is no original file or one of the other file IDs)
My goal is to select the whole data sorted by name. In addition, I need to always have the original files appear before their children. In other words, the desired result will start with the first alphabetical file whose OriginalFileID is 0 followed by all its children (if available) alphabetically. The following SQL script creates the sample data and illustrates exactly what I'm trying to achieve. Notice that the last select command is the desired output.
What is the query that can return the desired result?
The Script:
-------------------------- Creating the Example Schema --------------------------
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.Tables WHERE Table_Name = 'MyStackFiles')
Drop table MyStackFiles
GO
CREATE TABLE [dbo].[MyStackFiles](
[FileID] [int] IDENTITY(1,1) NOT NULL,
[FileName] [varchar](50) NULL,
[OriginalFileID] [int] NOT NULL DEFAULT (0),
CONSTRAINT [PK_MyStackFiles] PRIMARY KEY CLUSTERED
(
[FileID] 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 Into the Sample Data --------------------------
INSERT INTO MyStackFiles(FileName) values ('S')
INSERT INTO MyStackFiles(FileName) values ('G')
INSERT INTO MyStackFiles(FileName, OriginalFileID) values ('E', 1)
INSERT INTO MyStackFiles(FileName) values ('F')
INSERT INTO MyStackFiles(FileName, OriginalFileID) values ('Q', 2)
INSERT INTO MyStackFiles(FileName, OriginalFileID) values ('N', 3)
INSERT INTO MyStackFiles(FileName) values ('A')
INSERT INTO MyStackFiles(FileName, OriginalFileID) values ('X', 1)
INSERT INTO MyStackFiles(FileName) values ('W')
------------------------------------------------------------------------------
GO
-------------------------- Simple select sorted by FileName --------------------------
SELECT * From MyStackFiles ORDER BY FileName
-------------------------- A representation of the desired result --------------------------
SELECT * FROM MyStackFiles WHERE FileID = 7 UNION ALL -- We insert "A" (respecting the alphabetical order) since its OriginalFileID is 0
SELECT * FROM MyStackFiles WHERE FileID = 4 UNION ALL -- Then we insert F.
SELECT * FROM MyStackFiles WHERE FileID = 2 UNION ALL -- Then we insert G. G has children so we insert them
SELECT * FROM MyStackFiles WHERE FileID = 5 UNION ALL -- Q is the only child of G. We insert it
SELECT * FROM MyStackFiles WHERE FileID = 1 UNION ALL -- Now we insert S. Notice that S has two children (E and X)
SELECT * FROM MyStackFiles WHERE FileID = 3 UNION ALL -- E is before X alphabetically so it gets inserted first
SELECT * FROM MyStackFiles WHERE FileID = 6 UNION ALL -- E happens to have children so we insert them right away (in a depth first fashion)
SELECT * FROM MyStackFiles WHERE FileID = 8 UNION ALL -- Now we insert the other child of S which is X
SELECT * FROM MyStackFiles WHERE FileID = 9 -- Finally we insert W the only file left
--Drop Table MyStackFiles
I'm open to any schema modification if that helps find an efficient query.
I'm using the technique called Recursive CTE to try to solve your problem:
with t (RowID, FileID, FileName, OriginalFileID)
as (
select convert(varchar(max), row_number() over (order by s.FileName)), s.*
from MyStackFiles s
where s.OriginalFileID = 0
union all
select t.RowID + '.' + convert(varchar(max), row_number() over (order by s.FileName)), s.*
from MyStackFiles s
inner join t on t.FileID = s.OriginalFileID
)
select FileID, FileName, OriginalFileID from t
order by RowID
A temporary column RowID is created on-the-fly to chain up the ancestor's RowID to the row's row_number, so that for instance the file "N" will have RowID = '4.1.1', the file "X" will have RowID = '4.2', and this is the column to sort that fits your sorting requirement.

How to get result from parent child table

Work on SQL-Server. My table structure is below
CREATE TABLE [dbo].[AgentInfo](
[AgentID] [int] NOT NULL,
[ParentID] [int] NULL,
CONSTRAINT [PK_AgentInfo] PRIMARY KEY CLUSTERED
(
[AgentID] 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].[AgentInfo] ([AgentID], [ParentID]) VALUES (1, -1)
INSERT [dbo].[AgentInfo] ([AgentID], [ParentID]) VALUES (2, -1)
INSERT [dbo].[AgentInfo] ([AgentID], [ParentID]) VALUES (3, 1)
INSERT [dbo].[AgentInfo] ([AgentID], [ParentID]) VALUES (4, 2)
Required output
Use my below syntax get required output but not satisfied. Is there any better way to get the required output
--get parent child list
---step--1
SELECT *
INTO #temp1
FROM ( SELECT a.AgentID ,
a.ParentID,
a.AgentID AS BaseAgent
FROM dbo.AgentInfo a WHERE ParentID=-1
UNION ALL
SELECT a.ParentID ,
0 as AgentID,
a.AgentID AS BaseAgent
FROM dbo.AgentInfo a WHERE ParentID!=-1
UNION ALL
SELECT a.AgentID ,
a.ParentID,
a.AgentID AS BaseAgent
FROM dbo.AgentInfo a
WHERE ParentID!=-1
) AS d
SELECT * FROM #temp1
DROP TABLE #temp1
Help me to improve my syntax. If you have any questions please ask.
You could use a recursive SELECT, see the examples in the documentation for WITH, starting with example D.
The general idea within the recursive WITH is: You have a first select that is the starting point, and then a UNION ALL and a second SELECT which describes the step from on level to the next, where the previous level can either be the result of the first select or the result of the previous run of the second SELECT.
You can try this, to get a tree of the elements:
WITH CTE_AgentInfo(AgentID, ParentID, BaseAgent)
AS(
SELECT
AgentID,
ParentID,
AgentID AS BaseAgent
FROM AgentInfo
WHERE ParentID = -1
UNION ALL
SELECT
a.AgentID,
a.ParentID,
a.AgentID AS BaseAgent
FROM AgentInfo a
INNER JOIN CTE_AgentInfo c ON
c.AgentID = a.ParentID
)
SELECT * FROM CTE_AgentInfo
And here is an SQLFiddle demo to see it.
Try something like this:
WITH Merged (AgentId, ParentId) AS (
SELECT AgentId, ParentId FROM AgentInfo WHERE ParentId = -1
UNION ALL
SELECT AgentInfo.AgentId, AgentInfo.ParentId FROM AgentInfo INNER JOIN Merged ON AgentInfo.AgentId = Merged.ParentId
)
SELECT * FROM Merged
You can use a Common Table Expression to do this.
The sql statement will then look like this:
WITH [Parents]([AgentID], [ParentID], [BaseAgent])
AS
(
SELECT
[AgentID],
[ParentID],
[AgentID] AS [BaseAgent]
FROM [AgentInfo]
WHERE [ParentID] = -1
UNION ALL
SELECT
[ai].[AgentID],
[ai].[ParentID],
[p].[BaseAgent]
FROM [AgentInfo] [ai]
INNER JOIN [Parents] [p]
ON [ai].[ParentID] = [p].[AgentID]
)
SELECT *
FROM [Parents]
ORDER BY
[BaseAgent] ASC,
[AgentID] ASC
But, the results are different from your desired output, since every Agent is only listed once.
The output is:
AGENTID PARENTID BASEAGENT
1 -1 1
3 1 1
2 -1 2
4 2 2
The Fiddle is over here.
And here is a nice post on working with hierarchies: What are the options for storing hierarchical data in a relational database?