Creating natural hierarchical order using recursive SQL - sql

I have a table holding categories with an inner parent child relationship.
The table looks like this:
ID | ParentID | OrderID
---+----------+---------
1 | Null | 1
2 | Null | 2
3 | 2 | 1
4 | 1 | 1
OrderID is the order inside the current level.
I want to create a recursive SQL query to create the natural order of the table.
Meaning the output will be something like:
ID | Order
-----+-------
1 | 100
4 | 101
2 | 200
3 | 201
Appreciate any help.
Thanks

I am not really sure what you mean by "natural order", but the following query generates the results you want for this data:
with t as (
select v.*
from (values (1, NULL, 1), (2, NULL, 2), (3, 2, 1), (4, 1, 1)) v(ID, ParentID, OrderID)
)
select t.*,
(100 * coalesce(tp.orderid, t.orderid) + (case when t.parentid is null then 0 else 1 end)) as natural_order
from t left join
t tp
on t.parentid = tp.id
order by natural_order;

Related

Using join to include null values in same table

Following is my table structure.
AttributeMaster - This table is a master attribute table that will be available for each and every request.
AttrMasterId
AttrName
1
Expense Items
2
Business Reason
AttributeValue - When the user fills the data from grid, if a column is empty we don't store its value in the database.
For each request, there are multiple line items (TaskId). Every task must have attributes from attribute master. Now, if the user doesn't an attribute, then we don't store it in the database.
AttrValId
RequestId
TaskId
AttrMasterId
AttrValue
RecordStatus
1
200
1
1
Furniture
A
2
200
2
1
Infra
A
3
200
2
2
Relocation
A
In the above scenario, for request 200, for task Id - 1, I only have value for one attribute.
For task Id - 2, I have both attributes filled.
The query result should give me 4 rows, 2 for each task ID, with null placeholders in AttrValue column.
select * from AttributeMaster cam
left join AttributeValue cav on cam.AttrMasterId = cav.AttrMasterId
and cav.requestId = 36498 and cav.recordStatus = 'A'
right outer join (select distinct AttrMasterId from attrValue cav1 where cav1.requestId = 36498 ) ctI on cti.AttrMasterId = cav.AttrMasterId;
So far, I've tried different joins, tried to self join attribute value table as above, still no results to fill the empty rows.
Any help or pointers would be appreciated. Thanks.
Edit 1:
Expected Output is as follows:
RequestId
TaskId
AttrMasterId
AttrValue
RecordStatus
200
1
1
Furniture
A
200
1
2
NULL
NULL
200
2
1
Infra
A
200
2
2
Relocation
A
Working Fiddle for SQL Server
Since there really should be a Task table, I added that as a CTE term in the first solution. The second form just uses your existing tables directly, with the same result.
WITH Task (TaskId) AS (
SELECT DISTINCT TaskId FROM AttributeValue
)
, pairs (TaskId, AttrMasterId) AS (
SELECT Task.TaskId, AttributeMaster.AttrMasterId
FROM AttributeMaster CROSS JOIN Task
)
SELECT pairs.*
, AttributeMaster.*
, cav.*
FROM pairs
JOIN AttributeMaster
ON pairs.AttrMasterId = AttributeMaster.AttrMasterId
LEFT JOIN AttributeValue AS cav
ON pairs.AttrMasterId = cav.AttrMasterId AND pairs.TaskId = cav.TaskId
AND cav.requestId = 200 AND cav.recordStatus = 'A'
ORDER BY pairs.TaskId, pairs.AttrMasterId
;
+--------+--------------+--------------+-----------------+-----------+-----------+--------+--------------+------------+--------------+
| TaskId | AttrMasterId | AttrMasterId | AttrName | AttrValId | RequestId | TaskId | AttrMasterId | AttrValue | RecordStatus |
+--------+--------------+--------------+-----------------+-----------+-----------+--------+--------------+------------+--------------+
| 1 | 1 | 1 | Expense Items | 1 | 200 | 1 | 1 | Furniture | A |
| 1 | 2 | 2 | Business Reason | NULL | NULL | NULL | NULL | NULL | NULL |
| 2 | 1 | 1 | Expense Items | 2 | 200 | 2 | 1 | Infra | A |
| 2 | 2 | 2 | Business Reason | 3 | 200 | 2 | 2 | Relocation | A |
+--------+--------------+--------------+-----------------+-----------+-----------+--------+--------------+------------+--------------+
The second form is without the added Task CTE term...
WITH pairs AS (
SELECT DISTINCT AttributeValue.TaskId, AttributeMaster.AttrMasterId
FROM AttributeMaster CROSS JOIN AttributeValue
)
SELECT pairs.*
, AttributeMaster.*
, cav.*
FROM pairs
JOIN AttributeMaster
ON pairs.AttrMasterId = AttributeMaster.AttrMasterId
LEFT JOIN AttributeValue AS cav
ON pairs.AttrMasterId = cav.AttrMasterId AND pairs.TaskId = cav.TaskId
AND cav.requestId = 200 AND cav.recordStatus = 'A'
ORDER BY pairs.TaskId, pairs.AttrMasterId
;
Here is another solution that does not require a CTE.
This also uses the TaskID like #jon-armstrong 's answer
declare #AttributeMaster table (MasterID int, Name varchar(50))
declare #AttributeValues table (ValueID int, RequestID int, TaskID int, MasterID int, Value varchar(50), Status varchar(1))
insert into #AttributeMaster (MasterID, Name)
values (1, 'Expense'), (2, 'Business')
insert into #AttributeValues (ValueID, RequestID, TaskID, MasterID, Value, Status)
values (1, 200, 1, 1, 'Furniture', 'A'),
(2, 200, 2, 1, 'Infra', 'A'),
(3, 200, 2, 2, 'Relocation', 'A')
select t.RequestID, t.TaskID, t.MasterID, v.Value, v.Status, t.Name
from ( select distinct m.MasterID, v.TaskID, v.RequestID, m.Name
from #AttributeMaster m cross join #AttributeValues v
) t
left join #AttributeValues v on v.MasterID = t.MasterID and v.TaskID = t.TaskID
and v.RequestID = 200 and v.Status = 'A'
order by t.TaskID, t.MasterID
the result is
RequestID TaskID MasterID Value Status Name
200 1 1 Furniture A Expense
200 1 2 NULL NULL Business
200 2 1 Infra A Expense
200 2 2 Relocation A Business

How to convert JSONB array of pair values to rows and columns?

Given that I have a jsonb column with an array of pair values:
[1001, 1, 1002, 2, 1003, 3]
I want to turn each pair into a row, with each pair values as columns:
| a | b |
|------|---|
| 1001 | 1 |
| 1002 | 2 |
| 1003 | 3 |
Is something like that even possible in an efficient way?
I found a few inefficient (slow) ways, like using LEAD(), or joining the same table with the value from next row, but queries take ~ 10 minutes.
DDL:
CREATE TABLE products (
id int not null,
data jsonb not null
);
INSERT INTO products VALUES (1, '[1001, 1, 10002, 2, 1003, 3]')
DB Fiddle: https://www.db-fiddle.com/f/2QnNKmBqxF2FB9XJdJ55SZ/0
Thanks!
This is not an elegant approach from a declarative standpoint, but can you please see whether this performs better for you?
with indexes as (
select id, generate_series(1, jsonb_array_length(data) / 2) - 1 as idx
from products
)
select p.id, p.data->>(2 * i.idx) as a, p.data->>(2 * i.idx + 1) as b
from indexes i
join products p on p.id = i.id;
This query
SELECT j.data
FROM products
CROSS JOIN jsonb_array_elements(data) j(data)
should run faster if you just need to unpivot all elements within the query as in the demo.
Demo
or even remove the columns coming from products table :
SELECT jsonb_array_elements(data)
FROM products
OR
If you need to return like this
| a | b |
|------|---|
| 1001 | 1 |
| 1002 | 2 |
| 1003 | 3 |
as unpivoting two columns, then use :
SELECT MAX(CASE WHEN mod(rn,2) = 1 THEN data->>(rn-1)::int END) AS a,
MAX(CASE WHEN mod(rn,2) = 0 THEN data->>(rn-1)::int END) AS b
FROM
(
SELECT p.data, row_number() over () as rn
FROM products p
CROSS JOIN jsonb_array_elements(data) j(data)) q
GROUP BY ceil(rn/2::float)
ORDER BY ceil(rn/2::float)
Demo

SQL SERVER 2017 - How do I query to retrieve a group of data only if all of the data inside that group are marked as completed?

I have some observation tables like below. The observation data might be in individual form or grouped form which is determined by the observation category table.
cat table (which holds category data)
id | title | is_groupable
-------------------------------------------
1 | Cat 1 | 1
2 | Cat 2 | 1
3 | Cat 3 | 0
4 | Cat 4 | 0
5 | Cat 5 | 1
obs table (Holds observation data, groupable data are indicated by is_groupable of cat table, and the data is grouped in respect to index of obs table. and is_completed field indicates if some action has been taken on that or not)
id | cat_id | index | is_completed | created_at
------------------------------------------------------
1 | 3 | 100 | 0 | 2017-12-01
2 | 4 | 400 | 1 | 2017-12-02
// complete action taken group indicated by 1 in is_completed field
3 | 1 | 200 | 1 | 2017-12-1
4 | 1 | 200 | 1 | 2017-12-1
// not complete action taken group
5 | 2 | 300 | 0 | 2017-12-1
6 | 2 | 300 | 1 | 2017-12-1
7 | 2 | 300 | 0 | 2017-12-1
// complete action taken group
8 | 5 | 400 | 1 | 2017-12-1
9 | 5 | 400 | 1 | 2017-12-1
10 | 5 | 400 | 1 | 2017-12-1
For the sake of easeness in understanding i have separated the set of data as completed or not using the comment above in obs table.
Now what I want to achieve is retrieve the set of data in a group format from obs table. In the above case the set of groups are
{3,4}
{5,6,7}
{8,9,10}
i want to get set {3,4} and {8,9,10} in my result since every data in the group are flagged as is_completed: 1
I don't need {5,6,7} set of data because it has only 6 is flagged as completed, 5 and 7 are not taken action and hence not completed.
What I have done till now is
(Lets ignore the individual case, because It is very easy and already completed and for the group case as well, Im able to retrieve the group data, if ignoring the action taken case, ie I able to group them and retrieve the sets irrespective of taken action or not.)
(SELECT
null AS id,
cat.is_groupable AS is_grouped,
cat.title,
cat.id AS category_id,
o.index,
o.date,
null AS created_at,
null AS is_action_taken,
(
-- individual observation
SELECT
oi.id AS "observation.id",
oi.category_id AS "observation.category_id",
oi.index AS "observation.index",
oi.created_at AS "observation.created_at",
-- action taken flag according to is_completed
CAST(
CASE
WHEN ((oi.is_completed) > 0) THEN 1
ELSE 0
END AS BIT
) AS "observation.is_action_taken",
-- we might do some sort of comparison here
CAST(
(
CASE
--
-- Check if total count == completed count
WHEN (
SELECT COUNT(obs.id)
FROM obs
WHERE obs.category_id = cat.id AND obs.index = o.index
) = (
SELECT COUNT(obs.id)
FROM obs
WHERE obs.category_id = cat.id AND oi.index = o.index
AND oi.is_action_taken = 1
) then 1
else 0
end
) as bit
) as all_completed_in_group
FROM observations oi
WHERE oi.category_id = cat.id
AND oi.index = o.index
FOR JSON PATH
) AS observations
FROM obs o
INNER JOIN cat ON cat.id = o.category_id
WHERE cat.is_groupable = 1
GROUP BY cat.id, cat.name, o.index, cat.is_groupable, o.created_at
)
Let's not get over on if this query executes successfully or not. I just want idea, if there is any better approach than this one, or if this approach is correct or not.
Hopefully this is what you need. To check the group completeness I just used AND NOT EXISTS for the group that has an is_completed = '0'. The inner join is used to get the corresponding obs id's. The algorithm is put in a CTE (common table expression). Then I use STUFF on the CTE to get the output.
DECLARE #cat TABLE (id int, title varchar(100), is_groupable bit)
INSERT INTO #cat VALUES
(1, 'Cat 1', 1), (2, 'Cat 2', 1), (3, 'Cat 3', 0), (4, 'Cat 4', 0), (5, 'Cat 5', 1)
DECLARE #obs TABLE (id int, cat_id int, [index] int, is_completed bit, created_at date)
INSERT INTO #obs VALUES
(1, 3, 100, 0, '2017-12-01'), (2, 4, 400, 1, '2017-12-02')
-- complete action taken group indicated by 1 in is_completed field
,(3, 1, 200, 1, '2017-12-01'), (4, 1, 200, 1, '2017-12-01')
-- not complete action taken group
,(5, 2, 300, 0, '2017-12-01'), (6, 2, 300, 1, '2017-12-01'), (7, 2, 300, 0, '2017-12-01')
-- complete action taken group
,(8, 5, 400, 1, '2017-12-01'), (9, 5, 400, 1, '2017-12-01'), (10, 5, 400, 1, '2017-12-01')
;
WITH cte AS
(
SELECT C.id [cat_id]
,O2.id [obs_id]
FROM #cat C INNER JOIN #obs O2 ON C.id = O2.cat_id
WHERE C.is_groupable = 1 --is a group
AND NOT EXISTS (SELECT *
FROM #obs O
WHERE O.cat_id = C.id
AND O.is_completed = '0'
--everything in the group is_completed
)
)
--Stuff will put everything on one row
SELECT DISTINCT
'{'
+ STUFF((SELECT ',' + CAST(C2.obs_id as varchar)
FROM cte C2
WHERE C2.cat_id = C1.cat_id
FOR XML PATH('')),1,1,'')
+ '}' AS returnvals
FROM cte C1
Produces output:
returnvals
{3,4}
{8,9,10}
I would try this approach, the trick is using the sum of completed to be checked against the total present for the group.
;WITH aux AS (
SELECT o.cat_id, COUNT(o.id) Tot, SUM(CONVERT(int, o.is_completed)) Compl, MIN(CONVERT(int, c.is_groupable)) is_groupable
FROM obs o INNER JOIN cat c ON o.cat_id = c.id
GROUP BY o.cat_id
)
, res AS (
SELECT o.*, a.is_groupable
FROM obs o INNER JOIN aux a ON o.cat_id = a.cat_id
WHERE (a.Tot = a.Compl AND a.is_groupable = 1) OR a.Tot = 1
)
SELECT CONVERT(nvarchar(10), id) id, CONVERT(nvarchar(10), cat_id) cat_id
INTO #res
FROM res
SELECT * FROM #res
SELECT m.cat_id, LEFT(m.results,Len(m.results)-1) AS DataGroup
FROM
(
SELECT DISTINCT r2.cat_id,
(
SELECT r1.id + ',' AS [text()]
FROM #res r1
WHERE r1.cat_id = r2.cat_id
ORDER BY r1.cat_id
FOR XML PATH ('')
) results
FROM #res r2
) m
ORDER BY DataGroup
DROP TABLE #res
The conversion is due to bit datatype

SQL 2000 Left Join Top 1 of 0 to many relationship

This question has been asked multiple times on SO but all the answers refer to SQL 2005 or later (e.g. OUTER APPLY) and we are still using SQL 2000 (for corporate reasons too complex to go into here!)
I have a table of Things and a table of Widgets with a 0 to Many relationship:
CREATE TABLE Things ( ThingId INT, ThingName VARCHAR(50) )
CREATE TABLE Widgets ( WidgetId INT, ThingId INT, WidgetName VARCHAR(50) )
INSERT INTO Things VALUES ( 1, 'Thing 1' )
INSERT INTO Things VALUES ( 2, 'Thing 2' )
INSERT INTO Things VALUES ( 3, 'Thing 3' )
INSERT INTO Widgets VALUES ( 1, 2, 'Thing 2 Widget 1' )
INSERT INTO Widgets VALUES ( 2, 2, 'Thing 2 Widget 2' )
INSERT INTO Widgets VALUES ( 3, 3, 'Thing 3 Widget 1' )
A standard LEFT OUTER JOIN returns the expected 4 rows
SELECT * FROM Things t LEFT OUTER JOIN Widgets w ON t.ThingId = w.ThingId
ThingId | ThingName | WidgetId | ThingId | WidgetName
---------+-----------+----------+---------+------------------
1 | Thing 1 | NULL | NULL | NULL
2 | Thing 2 | 1 | 2 | Thing 2 Widget 1
2 | Thing 2 | 2 | 2 | Thing 2 Widget 2
3 | Thing 3 | 3 | 3 | Thing 3 Widget 1
However, I only want the newest Widget for each Thing, i.e.:
ThingId | ThingName | WidgetId | ThingId | WidgetName
---------+-----------+----------+---------+------------------
1 | Thing 1 | NULL | NULL | NULL
2 | Thing 2 | 2 | 2 | Thing 2 Widget 2
3 | Thing 3 | 3 | 3 | Thing 3 Widget 1
My starting point was:
SELECT * FROM Things t LEFT OUTER JOIN (SELECT TOP 1 * FROM Widgets subw WHERE subw.ThingId = t.ThingId ORDER BY subw.WidgetId DESC) w ON t.ThingId = w.ThingId
But this is not valid because the parent t.ThingId does not exist in the sub query.
Can this be achieved using SQL 2000?
If (ThingId, WidgetId) combination is unique in table Widgets, then this will work correctly:
SELECT t.*, w.*
FROM
dbo.Things AS t
LEFT OUTER JOIN
( SELECT ThingId, MAX(WidgetId) AS WidgetId
FROM dbo.Widgets
GROUP BY ThingId
) AS
subw
ON subw.ThingId = t.ThingId
LEFT OUTER JOIN
dbo.Widgets AS w
ON w.ThingId = subw.ThingId
AND w.WidgetId = subw.WidgetId ;

Selecting the same row multiple times

I have a table that has some children of a master object. Any child can occur more than once, and there is a Occurences column that contains that number, so the data in the table is something like:
ChildID | ParentID | Occurences
-------------------------------
1 | 1 | 2
2 | 1 | 2
3 | 2 | 1
4 | 2 | 3
I need to get a list of all the children, with each child appearing the corect number of times in the result, something like
IDENT | ChildID | ParentID
--------------------------
1 | 1 | 1
2 | 1 | 1
3 | 2 | 1
4 | 2 | 1
5 | 3 | 2
6 | 4 | 2
7 | 4 | 2
8 | 4 | 2
I can do this with a cursor that loops the table and inserts as many rows as neccessary, but I don't think that that is the best solution possible.
Thanks for the help
Create script included:
DECLARE #Children TABLE (ChildID int, ParentID int, Occurences int)
INSERT #Children
SELECT 1, 1, 2 UNION ALL
SELECT 2, 1, 2 UNION ALL
SELECT 3, 2, 1 UNION ALL
SELECT 4, 2, 3
;with C as
(
select ChildID,
ParentID,
Occurences - 1 as Occurences
from #Children
union all
select ChildID,
ParentID,
Occurences - 1 as Occurences
from C
where Occurences > 0
)
select row_number() over(order by ChildID) as IDENT,
ChildID,
ParentID
from C
order by IDENT
;WITH CTEs
AS
(
SELECT 1 [Id]
UNION ALL
SELECT [Id] + 1 FROM CTEs WHERE [Id] < 100
)
SELECT ROW_NUMBER() OVER(ORDER BY c1.ChildID, c1.ParentID) [rn]
, c1.ChildID, c1.ParentID
FROM CTEs ct
JOIN #Children c1 ON c1.Occurences >= ct.[Id]
Another way to generate sequence is using predefined table, e.g. master.dbo.spt_values:
SELECT ROW_NUMBER() OVER(ORDER BY c1.ChildID, c1.ParentID) [rn]
, c1.ChildID, c1.ParentID
FROM master.dbo.spt_values ct
JOIN #Children c1 ON c1.Occurences > ct.number
AND ct.type = 'P'