How to find grandparent parent child relationship for View in SQL - sql-server

With a table that looks like this:
ParentID
AgencyID
CompanyName
NULL
1
ABC Agency
NULL
2
Another Agency
2
3
Agency 3
3
4
Agency 4
The goal here is to develop a database view that shows the parent and grand-parent for a given agency. Some agencies only have a parent (no grand-parent), and still others are stand-alone - they don't have a parent. We want the view to look like this:
- GrandParentAgencyNo
- GrandParentName
- ParentAgencyNo
- ParentName
- AgencyNo
- AgencyName
- NumberOfChildren
- NumberOfGrandChildren
We could use that to find all the children for a given agency:
select * from AgencyView where ParentAgencyNo = "ABC123"
if an agency doesn't have a parent, the above result should look like this:
- GrandParentAgencyNo: 1
- GrandParentName: ABC Agency
- ParentAgencyNo: 1
- ParentName: ABC Agency
- AgencyNo: 1
- AgencyName: ABC Agency
- NumberOfChildren: 0
- NumberOfGrandChildren: 0
I tried writing recursive functions similar to the one below (including other queries that included trying to find the grandparent) with no luck. I am unfamiliar with recursion and always seem to hit the max recursion rate in SQL Server.
with A(Id, ParentId) as
(
select AgencyId, ParentAgencyId from Agency
union all
select e.AgencyId, p.ParentId from Agency e
join A p on e.ParentAgencyId = p.Id
)
select * from A
OPTION (MAXRECURSION 32767)

I don't use recursion unless I have to, usually due to an unknown or large number of levels. I would simply use left joins and case statements to get parents and grandparents and a sub-select for the child counts.
SELECT A.Agencyid, A.CompanyName
, CASE WHEN p.AgencyId is not null then p.AgencyId
else A.AgencyID END AS ParentAgencyId
, CASE WHEN p.AgencyId is not null then p.CompanyName
else A.CompanyNameEND ParentCompanyName
, CASE WHEN gp.AgencyId is not null then gp.AgencyId
WHEN p.AgencyId is not null then p.AgencyId
else A.AgencyID END GrandParentAgencyId
, CASE WHEN gp.AgencyId is not null then gp.CompanyName
WHEN p.AgencyId is not null then p.CompanyName
else A.CompanyNameEND GrandParentCompanyName
, (SELECT count(*) FROM Agency where ParentId = A.AgencyId) Children
, (SELECT count(*) FROM Agency C
JOIN Agency GC ON C.AgencyId = GC.ParentId
where C.ParentId = A.AgencyId) GrandChildren
FROM Agency A
LEFT JOIN Agency P ON P.AgencyId = A.ParentId
LEFT JOIN Agency GP ON GP.AgencyId = P.ParentId
WHERE A.AgencyId = 1

Related

How to Left Inner Join two queries in Sybase?

I have two queries that should be joined together. Here is my query 1:
SELECT
t1.rec_id,
t1.category,
t1.name,
t1.code,
CASE
WHEN t1.name= 'A' THEN SUM(t1.amount)
WHEN t1.name = 'D' THEN SUM(t1.amount)
WHEN t1.name = 'H' THEN SUM(t1.amount)
WHEN t1.name = 'J' THEN SUM(t1.amount)
END AS Amount
FROM Table1 t1
GROUP BY t1.name, t1.rec_id, t1.category, t1.code
Query 1 produce this set of results:
Rec ID Category Name Code Amount
1 1 A MIX 70927.00
1 3 D MIX 19922.00
1 2 H MIX 55104.00
1 4 J MIX 76938.00
Then I have query 2:
SELECT
CASE
WHEN t2.category_id = 1 THEN SUM(t2.sum)
WHEN t2.category_id = 2 THEN SUM(t2.sum)
WHEN t2.category_id = 3 THEN SUM(t2.sum)
WHEN t2.category_id = 4 THEN SUM(t2.sum)
END AS TotalSum
FROM Table2 t2
INNER JOIN Table1 t1
ON t1.amnt_id = t2.amnt_id
AND t2.unique_id = #unique_id
GROUP BY t2.category_id
The result set of query 2 is this:
TotalSum
186013.00
47875.00
12136.00
974602.00
All I need is this result set that combines query 1 and query 2:
Rec ID Category Name Code Amount TotalSum
1 1 A MIX 70927.00 186013.00
1 3 D MIX 19922.00 47875.00
1 2 H MIX 55104.00 12136.00
1 4 J MIX 76938.00 974602.00
As you can see there is connection between table 1 and table 2. That connection is amnt_id. However, I tried doing LEFT INNER JOIN on query 1 and then simply using same logic with case statement to get the total sum for table 2. Unfortunately Sybase version that I use does not support Left Inner Join. I'm wondering if there is other way to join these two queries? Thank you
I wondered if the CASE statement makes sense in the first query because it sums in every row. Are there other values for the name column except A, D, H, J? If not you can change the CASE statement to SUM(t1.amount) AS Amount. Also the GROUP BY in the first query seems dubious to me: you are grouping by the record id column - that means you are not grouping at all but instead return every row. If that is what you really want you can omit the SUM at all and just return the pure amount column.
As far as I understood your problem and your data structure: the values in Table2 are kind of category sums and the values in Table1 are subsets. You would like to see the category sum for every category in Table1 next to the single amounts?
You would typically use a CTE (common table expression, "WITH clause") but ASE doesn't support CTEs, so we have to work with joins. I recreated your tables in my SQL Anywhere database and put together this example. In a nutshell: both queries are subqueries in an outer query and are left joined on the category id:
SELECT *
FROM
(
SELECT
t1.rec_id,
t1.category,
t1.name,
t1.code,
CASE
WHEN t1.name= 'A' THEN SUM(t1.amount)
WHEN t1.name = 'D' THEN SUM(t1.amount)
WHEN t1.name = 'H' THEN SUM(t1.amount)
WHEN t1.name = 'J' THEN SUM(t1.amount)
END AS Amount
FROM Table1 t1
GROUP BY t1.rec_id, t1.name, t1.category, t1.code
) AS t1
LEFT JOIN
(
SELECT category_id, SUM(sum) FROM
table2
GROUP BY category_id
) AS totals(category_id, total_sum)
ON totals.category_id = t1.category;
This query gives me:
Rec ID Category Name Code Amount Category_id total_sum
2 3 D MIX 19922.00 3 47875.00
3 2 H MIX 55104.00 2 12136.00
1 1 A MIX 70927.00 1 186013.00
4 4 J MIX 76938.00 4 974602.00
You surely have to tweak it a bit including your t2.unique_id column (that I don't understand from your queries) but this is a practical way to work around ASE's missing CTE feature.
BTW: it's either an INNER JOIN (only the corresponding records from both tables) or a LEFT (OUTER) JOIN (all from the left, only the corresponding records from the right table) but a LEFT INNER JOIN makes no sense.

SQL query to bring back all orders only if all order details are in a list

I have an orders table that contains a lot of order specific info that is irrelevant to the question. However, I then have an orderDetails table that has a foreign key (orders.id == orderDetails.orderId). In this orderDetails, a customer can select to order lots of flavors of a product, each flavor gets a new entry in this table linked back to the main order.
What I want to do is select all the orders where ALL the flavors are present in the order. So, if an order has apple, peach and orange and I query for apple and peach, it wouldn't return that order because orange wasn't in my query.
I have tried subqueries and so on, but I feel like the only way to solve it is with looping each order and looking at the details, but that is horribly inefficient. Any thoughts?
SELECT *
FROM orders
WHERE id IN (SELECT orderId
FROM orderdetails
WHERE flavor IN('apple', 'peach', 'orange'))
AND isInvoiced = 1
AND isShipped = 0
AND isOnHold = 0
So, if I don't have any peach in stock, I want to see orders that do not contain any peach:
SELECT *
FROM orders
WHERE id IN (SELECT orderId
FROM orderdetails
WHERE flavor IN ('apple', 'orange'))
AND isInvoiced = 1
AND isShipped = 0
AND isOnHold = 0
The problem with the existing query here is that it returns everything because it just says, sure, you asked for apple... sure you asked for orange and this order contains those so I will return it. I need it to be ALL or nothing.
In the real database, the flavors are ID's, I just simplified it for this example.
Database tables were requested... I'll go ahead and list them as they really exist.
orders
-------
id
isInvoiced
isShipped
isOnHold
orderDetails
------------
id
orderId
flavorId
One more edit, this is my original failed attempt:
select * from orders WHERE id in
(
select orderId from orderdetails where flavorId in
(
'616a6d8e-be2e-4740-820b-1cad2a3d89b5',
'5d02f25b-f717-4079-97af-8aa444fe26b1',
'3504be8b-bebe-4b69-a22f-724d90003f99',
'c0a5a036-6dbe-417d-afcf-644f5520f2a8',
'29bfdea5-f270-44f0-9f48-245992af8401',
'29e53a21-4fdc-40e7-8bd9-733058a48097',
'60a90505-b9f5-4a60-8444-a35c2477d4a5',
'c9b93e89-98b0-4765-aedf-3a5f9d182c77',
'651ea709-a885-4f12-ad53-3290e8f0b18f',
'c5962375-d4d5-4ec7-82c0-0293475e6204',
'7faeffc0-fa88-4904-a6a9-7201949b23fd',
'24979b0d-7200-4a7d-9271-d26912d1b16d',
'5efeb81a-7642-4484-b8fc-62544bc8bff7'
)
)
and isInvoiced = 1 and isShipped = 0 and isOnHold = 0
That list of ID's would change based on what flavors are actually in stock.
basically you can just GROUP BY flavor with condition HAVING COUNT(*) = 3. So orders with those 3 flavor will be listed
select *
from orders o
where exists
(
select x.flavor
from orderdetails x
where x.orderId = o.id
and x.flavor in ('apple', 'peach', 'orange')
group by x.flavor
having count(*) = 3
)
and isInvoiced = 1
and isShipped = 0
and isOnHold = 0
You can use count function to make sure all flavors are represented.
select o.*
from orders o
inner join
(
select orderId, count(*) as flavorCount
from orderdetails
where flavor in ('apple', 'peach', 'orange')
group by orderId
) as t1
on o.orderId = t1.orderId
and isInvoiced = 1
and isShipped = 0
and isOnHold = 0
and t1.falvourCount = 3;
It would be simpler if you have a list of out-of-stock and in-stock flavors.
So if for example 'peach' is out of stock , and 'apple' and 'orange' are in stock, the following query will produce Orders that have only 'apple' OR 'orange' :
SELECT * FROM orders
WHERE
id IN (SELECT id FROM orderdetails WHERE flavor IN ('apple','orange') ) -- in stock
AND
id NOT IN (SELECT id FROM orderdetails WHERE flavor IN ('peach') ) --out of stock
What do you think ?
The question completely changed via this comment "I realize now I left out one very important part in that each order may have 1 or any number of flavors, not all have to be present."
The following query meets the original requirements: "What I want to do is select all the orders where ALL the flavors are present in the order" & "I need it to be ALL or nothing."
An order might have more than one item referring to a flavor ("apple pie", and "apple cake" for example), so I recommend you use 3 case expressions in a having clause to guard against this, whilst still achieving your objective:
select o.*
from orders as o
inner join (
select orderId
from orderdetails
group by orderId
having sum(case when flavor = 'apple' then 1 else 0 end) > 0
and sum(case when flavor = 'peach' then 1 else 0 end) > 0
and sum(case when flavor = 'orange' then 1 else 0 end) > 0
) as od on o.id = od.orderid
where o.isInvoiced = 1
and o.isShipped = 0
and o.isOnHold = 0
Note that the use of an inner join limits the orders listed to only those that refer to all 3 flavors.
This query is demonstrated here: http://rextester.com/AKMM54555

T-SQL query to show all the past steps, active and future steps

I have 3 tables in SQL Server:
map_table: (workflow map path)
stepId step_name
----------------
1 A
2 B
3 C
4 D
5 E
history_table:
stepId timestamp author
----------------------------
1 9:00am John
2 9:20am Mary
current_stageTable:
Id currentStageId waitingFor
------------------------------------
12345 3 Kat
I would like to write a query to show the map with the workflow status. Like this result here:
step name time author
----------------------------
1 A 9:00am John
2 B 9:20am Mary
3 C waiting Kat
4 D
5 E
I tried left join
select
m.stepId, m.step_name, h.timestamp, h.author
from
map_table m
left join
history_table h on m.stepId = h.stepId
I thought it will list all the records from the map table, since I am using left join, but somehow it only shows 3 records which is from history table..
So I changed to
select
m.stepId, m.step_name, h.timestamp, h.author
from
map_table m
left join
history_table h on m.stepId = h.stepId
union
select
m.stepId, m.step_name, '' as timestamp, '' as author
from
map_table m
where
m.stageId not in (select stageId from history_table)
order by
m.stepId
Then it list the result almost as I expected, but how do I add the 3rd table in to show the current active stage?
Thank you very much for all your help!! Much appreciated.
Looks like it's what you asked:
with map_table as (
select * from (values (1,'A')
,(2,'B')
,(3,'C')
,(4,'D')
,(5,'E')) t(stepId, step_name)
)
, history_table as (
select * from (values
(1,'9:00am','John')
,(2,'9:20am','Mary')) t(stepId, timestamp, author)
)
, current_stapeTable as (
select * from (values (2345, 3, 'Kat')) t(Id, currentStageId, waitingFor)
)
select
m.stepId, m.step_name
, time = coalesce(h.timestamp, case when c.waitingFor is not null then 'waiting' end)
, author = coalesce(h.author, c.waitingFor)
from
map_table m
left join history_table h on m.stepId = h.stepId
left join current_stapeTable c on m.stepId = c.currentStageId
I think a union fits well with the data and avoids the coalescing the values on multiple joins.
with timeline as (
select stepId, "timestamp" as ts, author from history_table
union all
select currentStageId, 'waiting', waitingFor from current_stageTable
)
select step_id, step_name, "timestamp", author
from
map_table as m left outer join timeline as t
on t.stepId = m.stepId

How to combine fields from 2 columns to create a "matrix"?

I have a logging table in my application that only logs changed data, and leaves the other columns NULL. What I'm wanting to do now is create a view that takes 2 of those columns (Type and Status),
and create a resultset that returns the Type and Status on the entry of that log row, assuming that either one or both columns could be null.
For example, with this data:
Type Status AddDt
A 1 7/8/2013
NULL 2 7/7/2013
NULL 3 7/6/2013
NULL NULL 7/5/2013
B NULL 7/4/2013
C NULL 7/3/2013
C 4 7/2/2013
produce the resultset:
Type Status AddDt
A 1 7/8/2013
A 2 7/7/2013
A 3 7/6/2013
A 3 7/5/2013
B 3 7/4/2013
C 3 7/3/2013
C 4 7/2/2013
From there I'm going to figure out the first time in these results the Type and Status meet certain conditions, such as a Type of B and Status 3 (7/4/2013) and ultimately use that date in a calculation, so performance is a huge issue with this.
Here's what I was thinking so far, but it doesn't get me where I need to be:
SELECT
Type.TypeDesc
, Status.StatusDesc
, *
FROM
jw50_Item c
OUTER APPLY (SELECT TOP 10000 * FROM jw50_ItemLog csh WHERE csh.ItemID = c.ItemID AND csh.StatusCode = 'OPN' ORDER BY csh.AddDt DESC) [Status]
OUTER APPLY (SELECT TOP 10000 * FROM jw50_ItemLog cth WHERE cth.ItemID = c.ItemID AND cth.ItemTypeCode IN ('F','FG','NG','PF','SXA','AB') ORDER BY cth.AddDt DESC) Type
WHERE
c.ItemID = #ItemID
So with the help provided below, I was able to get where I needed. Here is my final solution:
SELECT
OrderID
, CustomerNum
, OrderTitle
, ItemTypeDesc
, ItemTypeCode
, StatusCode
, OrdertatusDesc
FROM
jw50_Order c1
OUTER APPLY (SELECT TOP 1 [DateTime] FROM
(SELECT c.ItemTypeCode, c.OrderStatusCode, c.OrderStatusDt as [DateTime] FROM jw50_Order c WHERE c.OrderID = c1.OrderID
UNION
select (select top 1 c2.ItemTypeCode
from jw50_OrderLog c2
where c2.UpdatedDt >= c.UpdatedDt and c2.ItemTypeCode is not null and c2.OrderID = c.OrderID
order by UpdatedDt DESC
) as type,
(select top 1 c2.StatusCode
from jw50_OrderLog c2
where c2.UpdatedDt >= c.UpdatedDt and c2.StatusCode is not null and c2.OrderID = c.OrderID
order by UpdatedDt DESC
) as status,
UpdatedDt as [DateTime]
from jw50_OrderLog c
where c.OrderID = c1.OrderID AND (c.StatusCode IS NOT NULL OR c.ItemTypeCode IS NOT NULL)
) t
WHERE t.ItemTypeCode IN ('F','FG','NG','PF','SXA','AB') AND t.StatusCode IN ('OPEN')
order by [DateTime]) quart
WHERE quart.DateTime <= #FiscalPeriod2 AND c1.StatusCode = 'OPEN'
Order By c1.OrderID
The union is to bring in the current data in addition to the log table data to create the resultset, since the current data maybe what meets the conditions required. Thanks again for the help guys.
Here is an approach that uses correlated subqueries:
select (select top 1 c2.type
from jw50_Item c2
where c2.AddDt >= c.AddDt and c2.type is not null
order by AddDt
) as type,
(select top 1 c2.status
from jw50_Item c2
where c2.AddDt >= c.AddDt and c2.status is not null
order by AddDt
) as status,
(select AddDt
from jw50_Item c
If you have indexes on jw50_item(AddDt, type) and jw50_item(AddDt, status), then the performance should be pretty good.
I suppose you want to "generate a history": for those dates that has some data missing, the next available data should be set.
Something similar should work:
Select i.AddDt, t.Type, s.Status
from Items i
join Items t on (t.addDt =
(select min(t1.addDt)
from Items t1
where t1.addDt >= i.addDt
and t1.Type is not null))
join Items s on (s.addDt =
(select min(s1.addDt)
from Items s1
where s1.addDt >= i.addDt
and s1.status is not null))
Actually I'm joining the base table to 2 secondary tables and the join condition is that we match the smallest row where the respective column in the secondary table is not null (and of course smaller than the current date).
I'm not absolutely sure that it will work, since I don't have an SQL Server in front of me but give it a try :)

Writing a parent to child group query in SQL Server?

Is there a more efficient way to write this query without subqueries?
Table Question_Group:
Question_Group_ID int,
Question_Group_Name nvarchar,
Question_Group_Indent int, // 1=parent,2=child,3=grandchild
Question_Group_Order int,
Question_Parent_ID int
My query:
SELECT
parent.Question_Group_Name, parent.Question_Group_Order,
L2Child.Question_Group_Name, L2Child.Question_Group_Order
FROM
(SELECT
Question_Group_ID, Question_Group_Name, Question_Group_Indent,
Question_Group_Order, Question_Parent_ID
FROM
Question_Groups
WHERE
Question_Group_Indent = 1) parent
LEFT JOIN
(SELECT
Question_Group_ID, Question_Group_Name, Question_Group_Indent,
Question_Group_Order, Question_Parent_ID
FROM
Question_Groups
WHERE
Question_Group_Indent = 2) L2Child ON parent.Question_Group_ID = L2Child.Question_Parent_ID
ORDER BY
parent.question_group_Order
Results:
Pre-Site 1 NULL NULL
Agency Information 2 Contacts 1
Agency Information 2 Contracting Services 2
Agency Information 2 Start-Up Agency 3
Agency Information 2 Hiring 4
Agency Information 2 Budgeted and Actual Sworn Force 5
Agency Information 2 CP Questions 6
Hiring Grants 3 Per Hiring Grant Questions 1
Non-Hiring Grants 4 Per Non-Hiring Grant Questions 1
I'm not sure if it is more efficient but you can write it with CTEs to make it more readable.
WITH parent AS (SELECT Question_Group_ID, Question_Group_Name, Question_Group_Indent,
Question_Group_Order, Question_Parent_ID
FROM Question_Groups
WHERE Question_Group_Indent=1),
L2Child AS (SELECT Question_Group_ID, Question_Group_Name, Question_Group_Indent,
Question_Group_Order, Question_Parent_ID
FROM Question_Groups
WHERE Question_Group_Indent=2)
SELECT parent.Question_Group_Name, parent.Question_Group_Order,
L2Child.Question_Group_Name, L2Child.Question_Group_Order
FROM parent
LEFT JOIN L2Child
ON parent.Question_Group_ID = L2Child.Question_Parent_ID
ORDER BY parent.question_group_Order
The other option is to re-write it this way
SELECT parent.Question_Group_Name, parent.Question_Group_Order,
L2Child.Question_Group_Name, L2Child.Question_Group_Order
FROM Question_Groups AS parent
LEFT JOIN Question_Groups AS L2Child
ON parent.Question_Group_ID = L2Child.Question_Parent_ID
AND L2Child.Question_Group_Indent=2
WHERE parent.Question_Group_Indent=1
ORDER BY parent.question_group_Order
Read this to see why I put the "left" table condition in the WHERE clause and the "right" table condition in the ON clause.

Resources