I have a table with a list of Jobs that have children, grandchildren, etc. There is no limit on the level of hierarchy it goes down. The table has ID, Name, and ParentID. So for example, a Job table could look like:
ID Name Parent ID
1 Education null
2 IT null
3 Teacher 1
4 MS Teacher 3
5 7th Grade 4
6 Sys Admin 2
7 HS Teacher 3
8 12th Grade 7
9 IT Support 6
10 Developer 2
There is also a UserToJob table that is just the JobID and UserID. A person could be listed in more than one Job.
I'm looking for the most efficient way to get all people with a specified job and all decedents, so if I want to query for Education then it returns Education, Teacher, MS Teacher, 7th Grade, HS Teacher, and 12th Grade.
Right now my best attempt looks like
with
Closure AS (
select j.ID as AncestorID, j.ID as DescendantID, 0 as Depth from Jobs j
UNION ALL
select CTE.AncestorID, j.ID, CTE.Depth + 1 from Jobs j
inner join Closure CTE on j.ParentID = CTE.DescendantID
),
Job AS ( select j.UserID as ID from UserToJob j
where j.JobID in (select DescendantID from Closure where AncestorID in (1))
)
I want it to be able to work querying more than one job at a time, for example if I wanted all Education and Sys Admins then I'd change AncestorID in (1) to AncestorID in (1, 6) in the final line of my attempt.
You have your where clause in the wrong place. You want to limit the root of your recursive cte to only return the first level rows you are concerned with.
This should point you in the right direction.
declare #Jobs table
(
ID int
, Name varchar(50)
, ParentID int
)
insert #Jobs values
(1, 'Education', null)
, (2, 'IT', null)
, (3, 'Teacher', 1)
, (4, 'MS Teacher', 3)
, (5, '7th Grade', 4)
, (6, 'Sys Admin', 2)
, (7, 'HS Teacher', 3)
, (8, '12th Grade', 7)
, (9, 'IT Support', 6)
, (10, 'Developer', 2)
select *
from #Jobs;
with Closure AS
(
select j.ID as AncestorID
, j.ID as DescendantID
, 0 as Depth
from #Jobs j
where j.ID in (1, 6)
UNION ALL
select CTE.AncestorID
, j.ID
, CTE.Depth + 1
from #Jobs j
inner join Closure CTE on j.ParentID = CTE.DescendantID
)
select *
from Closure
Related
I have a table that looks like the following which was created using the following code...
SELECT Orders.ID, Orders.CHECKIN_DT_TM, Orders.CATALOG_TYPE,
Orders.ORDER_STATUS, Orders.ORDERED_DT_TM, Orders.COMPLETED_DT_TM,
Min(DateDiff("n",Orders.ORDERED_DT_TM,Orders.COMPLETED_DT_TM)) AS
Time_to_complete
FROM Orders
GROUP BY Orders.ORDER_ID, Orders.ID,
Orders.CHECKIN_DT_TM, Orders.CATALOG_TYPE, Orders.ORDERED_DT_TM,
Orders.COMPLETED_DT_TM, HAVING (((Orders.CATALOG_TYPE)="radiology");
ID Time_to_complete ... .....
1 5
1 7
1 8
2 23
2 6
3 7
4 16
4 14
I'd like to add to this code which would select the smallest Time_to_complete value per subject ID. Leaving the desired table:
ID Time_to_complete ... .....
1 5
2 6
3 7
4 14
I'm using Access and prefer to continue using Access to finish this code but I do have the option to use SQL Server if this is not possible in Access. Thanks!
I suspect you need correlated subquery :
SELECT O.*, DateDiff("n", O.ORDERED_DT_TM, O.COMPLETED_DT_TM) AS Time_to_complete
FROM Orders O
WHERE DateDiff("n", O.ORDERED_DT_TM, O.COMPLETED_DT_TM) = (SELECT Min(DateDiff("n", O1.ORDERED_DT_TM, O1.COMPLETED_DT_TM))
FROM Orders O1
WHERE O1.ORDER_ID = O.ORDER_ID AND . . .
);
EDIT : If you want unique records then you can do instead :
SELECT O.*, DateDiff("n", O.ORDERED_DT_TM, O.COMPLETED_DT_TM) AS Time_to_complete
FROM Orders O
WHERE o.pk = (SELECT TOP (1) o1.pk
FROM Orders O1
WHERE O1.ORDER_ID = O.ORDER_ID AND . . .
ORDER BY DateDiff("n", O.ORDERED_DT_TM, O.COMPLETED_DT_TM) ASC
);
pk is your identity column that specifies unique entry in Orders table, so you can change it accordingly.
Have a look at this:
DECLARE #myTable AS TABLE (ID INT, Time_to_complete INT);
INSERT INTO #myTable
VALUES (1, 5)
, (1, 7)
, (1, 8)
, (2, 23)
, (2, 6)
, (3, 7)
, (4, 16)
, (4, 14);
WITH cte AS
(SELECT *
, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Time_to_complete) AS RN
FROM #myTable)
SELECT cte.ID
, cte.Time_to_complete
FROM cte
WHERE RN = 1;
Results :
ID Time_to_complete
----------- ----------------
1 5
2 6
3 7
4 14
It uses row numbers over groups, then selects the first row for each group. You should be able to adjust your code to use this technique. If in doubt wrap your entire query in a cte first then apply the technique here.
It's worth becoming familiar with this process as it gets used in a lot of places - especially around de-duping data.
Try This
DECLARE #myTable AS TABLE (ID INT, Time_to_complete INT);
INSERT INTO #myTable
VALUES (1, 5)
, (1, 7)
, (1, 8)
, (2, 23)
, (2, 6)
, (3, 7)
, (4, 16)
, (4, 14);
SELECT O.ID, O.Time_to_complete
FROM #myTable O
WHERE o.Time_to_complete = (Select min(m.Time_to_complete) FROM #myTable m
Where o.id=m.ID
);
Result :
ID Time_to_complete
1 5
2 6
3 7
4 14
My customers own products from different product groups. Example:
Client Product group
1 All-in-one
1 Senior
2 All-in-one
2 Other
3 Senior
3 Other
4 Other
The product groups are prioritized, so that if you own products from both the all-in-one and the senior product group you would be classified over all as an All-in-one customer.
The product groups prioritized are:
All-in-one
Senior
Other
I would like a view where each customer only appear once like:
Client Product group
1 All-in-one
2 All-in-one
3 Senior
4 Other
Can this be done in a single step without a ton of views?
First one works if only those product groups remain,second one you have flexibilty to order but uses ctes
select id,min(pg) from #client
group by id
;with cte
as
(
select
id,
min(case when pg='All-in-one' then 1
when pg='senior' then 2
when pg='other' then 3
end ) pg
from
#client
group by id
)
select id,
case pg when 1 then 'All-in-one'
when 2 then 'Senior'
when 3 then 'other'
end as 'PG'
from cte
Try this (you dont need use clients_groups cte because you have this table already):
;WITH clients_groups AS (
SELECT *
FROM (VALUES
(1, 'All-in-one'),
(1, 'Senior'),
(2, 'All-in-one'),
(2, 'Other'),
(3, 'Senior'),
(3, 'Other'),
(4, 'Other')) as cg (Client, Product_group)
),
final AS (
SELECT ROW_NUMBER() OVER(PARTITION BY Client ORDER BY id Asc) AS R,g.Client, p.Name
FROM clients_groups g
LEFT JOIN (VALUES
(1, 'All-in-one'),
(2, 'Senior'),
(3, 'Other')) as p(id, name) ON g.Product_group = p.name)
SELECT Client,Name
FROM final
WHERE R = 1
Results:
Client Name
----------- ----------
1 All-in-one
2 All-in-one
3 Senior
4 Other
(4 row(s) affected)
I have a table with records Holding patrols of guards in SQL Server 2008R2.
Whenever a duty starts a new alert number is created and within this alert number there a patrols with a starting time.
Per 12 hours we can bill a flat rate when at least one patrol has been performed. When under the same alert number the 12 hour range is exceeded, a further flat rate has to be billed.
The calculation of the 12 hours starts with the time of the first patrol.
I tried with a temp table but could not solve it so far.
DECLARE #t1 TABLE (
AlertNo INT,
Starttime SMALLDATETIME,
Endtime SMALLDATETIME
)
INSERT INTO #t1 (AlertNo, Starttime, Endtime)
SELECT AlertNo,
Starttimepatrol,
DATEADD(HOUR, 12, Starttimepatrol)
FROM tblAllPatrols
WHERE PatrolNo = 1
SELECT AlertNo,
(
SELECT COUNT(*)
FROM [tblAllPatrols] a
INNER JOIN #t1 b ON b.AlertNo = a.AlertNo
WHERE a.Starttimepatrol BETWEEN b.Starttime AND b.Endtime
) AS patrols
FROM [vwAlleDatensaetze]
GROUP BY AlertNo
I know that this is not the end of the Story, but as I cannot even count the numbers of patrols I cannot find a way to solve the Problem.
It should somehow "group" the patrols over 12-hour ranges per alert number and then count how many groups exists under the same alert number.
Hope, someone of you can lead me to the result I Need.
Thanks your help
Michael
Try this, it assumes that after the first patrol the billing period is a multiple of 8 hours from this time:
SQL Fiddle
MS SQL Server 2008 Schema Setup:
Query 1:
DECLARE #Patrols TABLE
(
AlertNo INT IDENTITY PRIMARY KEY,
StartTime DateTime
)
INSERT INTO #Patrols (StartTime)
VALUES ('20160126 09:57'),
('20160126 10:21'),
('20160126 19:54'),
('20160126 23:21'),
('20160127 08:13'),
('20160127 16:43'),
('20160128 07:33')
;WITH FirstBillingPeriodCTE
AS
(
SELECT MIN(StartTime) as BillingStartTime,
DateAdd(HOUR, 12, MIN(StartTime)) As BillingEndTime,
1 As BillingPeriod
FROM #Patrols
),
Numbers
As
(
SELECT num
FROM (Values (0),(1), (2), (3), (4), (5), (6), (7), (8), (9)) AS n(Num)
),
BillingPeriodsCTE
AS
(
SELECT DATEADD(Hour, 8 * (BillingPeriod + Numbers.Num), BillingStartTime) AS BillingStartTime,
DATEADD(Hour, 8 * (BillingPeriod + Numbers.Num), BillingEndTime) AS BillingEndTime,
BillingPeriod + Numbers.Num As BillingPeriod
FROM FirstBillingPeriodCTE
CROSS JOIN Numbers
)
SELECT COUNT(DISTINCT BillingPeriod)
FROM #Patrols P
INNER JOIN BillingPeriodsCTE B
ON P.StartTime >= B.BillingStartTime AND P.StartTime < B.BillingEndTime
Results:
| |
|---|
| 4 |
Here is a query that will give each billing period, up to 65,535 billing periods, accurate to the second.
My solution uses a calculated "Tally" table, but you would be better off in the long run to create your own physical "Tally" table in your database. See What is the best way to create and populate a numbers table? for more details.
You should be able to replace #tblPatrols with your patrol table.
DECLARE #tblPatrols TABLE (alertNo int, startTime datetime);
DECLARE #hoursPerBillingPeriod int, #toHoursConversion float;
SET #hoursPerBillingPeriod = 12;
SET #toHoursConversion = 60 * 60;
INSERT INTO #tblPatrols (alertNo, startTime)
VALUES
(1, '2016-01-28 05:57')
, (1, '2016-01-28 07:23')
, (1, '2016-01-28 08:10')
, (2, '2016-01-28 09:05')
, (2, '2016-01-28 12:22')
, (2, '2016-01-28 16:06')
, (2, '2016-01-28 23:45')
, (2, '2016-01-29 00:05')
, (3, '2016-01-28 12:00')
, (3, '2016-01-28 16:06')
, (3, '2016-01-29 00:00')
, (4, '2016-01-28 12:00')
, (4, '2016-01-28 16:06')
, (4, '2016-01-28 23:59:59.997')
;
;WITH
--......................
--This section used to simulate a "Tally" table... you would be better off to Create a physical Tally table
-- see: https://stackoverflow.com/questions/1393951/what-is-the-best-way-to-create-and-populate-a-numbers-table
Pass0 as (select 1 as C union all select 1) --2 rows
, Pass1 as (select 1 as C from Pass0 as A, Pass0 as B) --4 rows
, Pass2 as (select 1 as C from Pass1 as A, Pass1 as B) --16 rows
, Pass3 as (select 1 as C from Pass2 as A, Pass2 as B) --256 rows
, Pass4 as (select 1 as C from Pass3 as A, Pass3 as B)--65536 rows
, Tally as (select row_number() over(order by C) - 1 as N from Pass4) --65536 rows
--........................
,cteNumBillings as (
SELECT fp.alertNo
, firstPatrolTime = min(fp.startTime)
, lastPatrolTime = max(fp.startTime)
, hoursBetweenStartMinMax = datediff(second, min(fp.startTime), max(fp.startTime)) / #toHoursConversion
, numberOfBillingPeriods = floor(((datediff(second, min(fp.startTime), max(fp.startTime)) / #toHoursConversion) / #hoursPerBillingPeriod) + 1)
FROM #tblPatrols fp
GROUP BY fp.alertNo
)
SELECT b.alertNo
--This is the "x" value of the expression "Billing Period x of y"
, BillingPeriodNumber = t.N + 1
, BillingPeriodPatrolCount =
(select count(*)
from #tblPatrols p
where p.alertNo = b.alertNo
and p.startTime >= dateadd(hour, 12 * t.N, b.firstPatrolTime)
and p.startTime < dateadd(hour, 12 * (t.N+1), b.firstPatrolTime)
)
, BillingStart = dateadd(hour, 12 * t.N, b.firstPatrolTime)
, BillingEnd = dateadd(second, -1, dateadd(hour, 12 * (t.N + 1), b.firstPatrolTime))
--This is the "y" value of the expression "Billing Period x of y"
, TotalBillingPeriodCount = b.numberOfBillingPeriods
FROM cteNumBillings b
INNER JOIN Tally t ON t.N >= 0 and t.N < b.numberOfBillingPeriods
ORDER BY 1,2
;
I found a solution by myself, which seems to be easier and I could not find any mistake using it.
I take the first Startime of the first patrol in a variable. Then I use datediff for die difference of the all StartTimePatrol to the startime of the first patrol and divide it by 12 hours
set #BillingPeriod=(select (datediff(hour,#StartTime,#StartTimePatrol)/12)+1)
then I put the result of each record in a temp table
insert into #t2 ( Alertno, Starttime, Billings )
values ( #Alertno, #StartTimePatrol, #BillingPeriod )
then I group the altertno and Billings and count them
select alertno, count(Billings ) from (select alertno, Billings from #t2
group by alertno, Billings ) temp group by alertno
The result looks correct for me.
Thanks for all replies.
Michael
I am facing this problem where I need to compare the most recent row with the immediate previous one based on the same criteria (it will be trader in this case).
Here is my table:
ID Trader Price
-----------------
1 abc 5
2 xyz 5.2
3 abc 5.7
4 xyz 5
5 abc 5.2
6 abc 6
Here is the script
CREATE TABLE Sale
(
ID int not null PRIMARY KEY ,
trader varchar(10) NOT NULL,
price decimal(2,1),
)
INSERT INTO Sale (ID,trader, price)
VALUES (1, 'abc', 5), (2, 'xyz', 5.2),
(3, 'abc', 5.7), (4, 'xyz', 5),
(5, 'abc', 5.2), (6, 'abc', 6);
So far I am working with this solution that is not perfect yet
select
a.trader,
(a.price - b.price ) New_price
from
sale a
join
sale b on a.trader = b.trader and a.id > b.ID
left outer join
sale c on a.trader = c.trader and a.id > c.ID and b.id < c.ID
where
c.ID is null
Above is not perfect because I want to compare only the most recent with the immediate previous on... In this sample for example
Trader abc : I will compare only id = 6 and id = 5
Trader xyz : id = 4 and id = 2
Thanks for any help!
If you are using SQL Server 2012 or later, you can use functions LEAD and LAG to join previous and next data. Unfortunately these function can only be used in SELECT or ORDER BY clause, so you will need to use subquery to get the data you need:
SELECT t.trader, t.current_price - t.previous_price as difference
FROM (
SELECT
a.trader,
a.price as current_price,
LAG(a.price) OVER(PARTITION BY a.trader ORDER BY a.ID) as previous_price,
LEAD(a.price) OVER(PARTITION BY a.trader ORDER BY a.ID) as next_price
FROM sale a
) t
WHERE t.next_price IS NULL
Here in your subquery you create additional columns for previous and next value. Then in your main query you filter only these rows where next price is NULL - that indicates this is the last row for the specific trader.
I'm going to preface this question with the disclaimer that creating what I call "complex" queries isn't in my forte. Most of the time, there is a much simpler way to accomplish what I'm trying to accomplish so if the below query isn't up to par, I apologize.
With that said, I have a table that keeps track of Vendor Invoices and Vendor Invoice Items (along with a Vendor Invoice Type and Vendor Invoice Item Type). Our bookkeeper wants a report that simply shows: Vendor | Location | Inv Number | Inv Type | Item Type | Inv Date | Rental Fee | Restock Fee | Shipping Fee | Line Item Cost | Total (Line Item + Fees)
Most of the time, one vendor invoice is one line. However, there are exceptions where a vendor invoice can have many item types, thus creating two rows. Not a big deal EXCEPT the fees (Rental, Restock, Shipping) are attached to the Vendor Invoice table. So, I first created a query that checks the temp table for Invoices that have multiple rows, takes the last row, and zero's out the fees. So that only one line item would have the fee. However, our bookkeeper doesn't like that. Instead, she'd like the fees to be "distributed" among the line items.
So, if a vendor invoice has a $25 shipping charge, has two line items, then each line item would be $12.50.
After working with the query, I got it to update the Last Row to be the adjusted amount but row 1+ would have the original amount.
I'm going to post my entire query here (again - I'm sorry that this may not be the best looking query; however, suggestions are always welcome)
DROP TABLE #tVendorInvoiceReport
DROP TABLE #tSummary
SELECT v.Name AS Vendor ,
vii.Location ,
vi.VendorInvNumber ,
vit.Descr AS InvoiceType ,
vii.VendorInvoiceItemType ,
CONVERT(VARCHAR(10), vi.VendorInvDate, 120) VendorInvDate ,
vi.RentalFee ,
vi.RestockFee ,
vi.ShippingFee ,
SUM(vii.TotalUnitCost) TotalItemCost ,
CONVERT(MONEY, 0) TotalInvoice ,
RowID = IDENTITY( INT,1,1)
INTO #tVendorInvoiceReport
FROM dbo.vVendorInvoiceItems AS vii
JOIN dbo.VendorInvoices AS vi ON vii.VendorInvID = vi.VendorInvID
JOIN dbo.Vendors AS v ON vi.VendorID = v.VendorID
JOIN dbo.VendorInvoiceTypes AS vit ON vi.VendorInvTypeID = vit.VendorInvTypeID
WHERE vi.VendorInvDate >= '2012-01-01'
AND vi.VendorInvDate <= '2012-01-31'
GROUP BY v.Name ,
vii.Location ,
vi.VendorInvNumber ,
vit.Descr ,
vii.VendorInvoiceItemType ,
CONVERT(VARCHAR(10), vi.VendorInvDate, 120) ,
vi.RentalFee ,
vi.RestockFee ,
vi.ShippingFee
ORDER BY v.Name ,
vii.Location ,
vi.VendorInvNumber ,
vit.Descr ,
vii.VendorInvoiceItemType ,
CONVERT(VARCHAR(10), vi.VendorInvDate, 120)
SELECT VendorInvNumber ,
COUNT(RowID) TotalLines ,
MAX(RowID) LastLine
INTO #tSummary
FROM #tVendorInvoiceReport
GROUP BY VendorInvNumber
WHILE ( SELECT COUNT(LastLine)
FROM #tSummary AS ts
WHERE TotalLines > 1
) > 0
BEGIN
DECLARE #LastLine INT
DECLARE #NumItems INT
SET #LastLine = ( SELECT MAX(LastLine)
FROM #tSummary AS ts
WHERE TotalLines > 1
)
SET #NumItems = ( SELECT COUNT(VendorInvNumber)
FROM #tVendorInvoiceReport
WHERE VendorInvNumber IN (
SELECT VendorInvNumber
FROM #tSummary
WHERE LastLine = #LastLine )
)
UPDATE #tVendorInvoiceReport
SET RentalFee = ( RentalFee / #NumItems ) ,
RestockFee = ( RestockFee / #NumItems ) ,
ShippingFee = ( ShippingFee / #NumItems )
WHERE RowID = #LastLine
DELETE FROM #tSummary
WHERE LastLine = #LastLine
--PRINT #NumItems
END
UPDATE #tVendorInvoiceReport
SET TotalInvoice = ( TotalItemCost + RentalFee + RestockFee + ShippingFee )
SELECT Vendor ,
Location ,
VendorInvNumber ,
InvoiceType ,
VendorInvoiceItemType ,
VendorInvDate ,
RentalFee ,
RestockFee ,
ShippingFee ,
TotalItemCost ,
TotalInvoice
FROM #tVendorInvoiceReport AS tvir
I sincerely appreciate anyone who took the time to read this and attempt to point me in the right direction.
Thank you,
Andrew
PS - I did try and remove "WHERE RowID = #LastLine" from the first Update, but that changed the Shipping Fees for the first line with two items to "0.0868" instead of 12.50 ($25/2)
If I understand correctly, you're looking for a way to split something like an invoice shipping fee over one or more invoice items.
I created some sample invoice and invoice item tables shown below and used the
over(partition) clause to split out the shipping per item.
-- sample tables
declare #Invoice table (InvoiceID int, customerID int, Date datetime, ShippingFee float)
declare #InvoiceItem table (InvoiceItemID int identity, InvoiceID int, ItemDesc varchar(50), Quantity float, ItemPrice float)
-- Example 1
insert #Invoice values(1, 800, getdate(), 20);
insert #InvoiceItem values(1, 'Widget', 1, 10.00)
insert #InvoiceItem values(1, 'Wing Nut', 5, 2.00)
insert #InvoiceItem values(1, 'Doodad', 8, 0.50)
insert #InvoiceItem values(1, 'Thingy', 3, 1.00)
-- Example 2
insert #Invoice values(2, 815, getdate(), 15);
insert #InvoiceItem values(2, 'Green Stuff', 10, 1.00)
insert #InvoiceItem values(2, 'Blue Stuff', 10, 1.60)
-- Example 3
insert #Invoice values(3, 789, getdate(), 15);
insert #InvoiceItem values(3, 'Widget', 10, 1.60)
-- query
select
n.InvoiceID,
n.InvoiceItemID,
n.ItemDesc,
n.Quantity,
n.ItemPrice,
ExtendedPrice = n.Quantity * n.ItemPrice,
Shipping = i.ShippingFee / count(n.InvoiceItemID) over(partition by n.InvoiceID)
from #InvoiceItem n
join #Invoice i on i.InvoiceID = n.InvoiceID
Output:
InvoiceID InvoiceItemID ItemDesc Quantity ItemPrice ExtendedPrice Shipping
1 1 Widget 1 10 10 5
1 2 Wing Nut 5 2 10 5
1 3 Doodad 8 0.5 4 5
1 4 Thingy 3 1 3 5
2 5 Green Stuff 10 1 10 7.5
2 6 Blue Stuff 10 1.6 16 7.5
3 7 Widget 10 1.6 16 15