Say I had a Bus Garage application that contained a datatable that represented wether buses where either in the garage, out of the garage or in the shop for maintenance. It looks like this:
+-----------------------+-----+-----+
|Date |BusId|State|
+-----------------------+-----+-----+
|2013-09-12 15:02:41,844|1 |IN |
+-----------------------+-----+-----+
|2013-09-12 15:02:41,844|2 |IN |
+-----------------------+-----+-----+
|2013-09-12 15:02:41,844|3 |OUT |
+-----------------------+-----+-----+
|2013-09-12 15:02:41,844|4 |OUT |
+-----------------------+-----+-----+
|2013-09-12 15:02:41,844|5 |OUT |
+-----------------------+-----+-----+
|2013-09-13 15:02:41,844|1 |OUT |
+-----------------------+-----+-----+
|2013-09-14 15:02:41,844|1 |IN |
+-----------------------+-----+-----+
|2013-09-15 15:02:41,844|1 |OUT |
+-----------------------+-----+-----+
|2013-09-15 15:02:41,844|2 |OUT |
+-----------------------+-----+-----+
Now i want to make a nice day-by-day (or hour by hour etc) dataset giving me an overview of how many buses where in the garage an how many that where out of it.
+-------------------+-----+------------+
|Date |State|Count(buses)|
+-------------------+-----+------------+
|2013-09-12 16:00:00|IN |2 |
+-------------------+-----+------------+
|2013-09-12 16:00:00|OUT |3 |
+-------------------+-----+------------+
|2013-09-13 16:00:00|IN |1 |
+-------------------+-----+------------+
|2013-09-13 16:00:00|OUT |4 |
+-------------------+-----+------------+
|2013-09-14 16:00:00|IN |2 |
+-------------------+-----+------------+
|2013-09-14 16:00:00|OUT |3 |
+-------------------+-----+------------+
|2013-09-15 16:00:00|IN |0 |
+-------------------+-----+------------+
|2013-09-15 16:00:00|OUT |5 |
+-------------------+-----+------------+
How (not necessary explained in code) would i go about to do this just using TSQL?
I have one reqirement, and that is that i can not use variable declarations in my statement since i will have this as a View.
I asked a very similar question, but i felt that that one got too verbouse and not as general as this one.
Do you really want multiple records per day/hour just to display the different states? I would make them columns. You can use a CTE and the OVER clause to count per day/hour group:
WITH CTE AS
(
SELECT [Date] = DATEADD(day, DATEDIFF(day, 0, [Date]),0),
[BusId], [State],
[IN] = SUM(CASE WHEN State='IN' THEN 1 END) OVER (PARTITION BY DATEADD(day, DATEDIFF(day, 0, [Date]),0)),
[Out] = SUM(CASE WHEN State='Out' THEN 1 END) OVER (PARTITION BY DATEADD(day, DATEDIFF(day, 0, [Date]),0)),
[DayNum] = ROW_NUMBER() OVER (PARTITION BY DATEADD(day, DATEDIFF(day, 0, [Date]),0)
ORDER BY [Date], [BusID], [State])
FROM dbo.Garage g
)
SELECT [Date], [BusId], [State], [IN], [OUT]
FROM CTE
WHERE [DayNum] = 1
Demo
Result:
DATE BUSID STATE IN OUT
September, 12 2013 00:00:00+0000 1 IN 2 3
September, 13 2013 00:00:00+0000 1 OUT (null) 1
September, 14 2013 00:00:00+0000 1 IN 1 (null)
September, 15 2013 00:00:00+0000 1 OUT (null) 2
This works even in SQL-Server 2005. If you want to group by hour instead of day you have to change DATEADD(day, DATEDIFF(day, 0, [Date]),0) to DATEADD(hour, DATEDIFF(hour, 0, [Date]),0) everywhere.
try this....
DECLARE #businfo AS TABLE([date] datetime,busid int,[state] varchar(5))
INSERT INTO #businfo VALUES('2013-09-12 15:02:41',1,'IN')
INSERT INTO #businfo VALUES('2013-09-12 15:02:41',2,'IN')
INSERT INTO #businfo VALUES('2013-09-12 15:02:41',3,'OUT')
INSERT INTO #businfo VALUES('2013-09-12 15:02:41',4,'OUT')
INSERT INTO #businfo VALUES('2013-09-12 15:02:41',5,'OUT')
INSERT INTO #businfo VALUES('2013-09-13 15:02:41',1,'OUT')
INSERT INTO #businfo VALUES('2013-09-14 15:02:41',1,'IN')
INSERT INTO #businfo VALUES('2013-09-15 15:02:41',1,'OUT')
INSERT INTO #businfo VALUES('2013-09-15 15:02:41',2,'OUT')
select [date],[state],COUNT(busid) as [count(buses)] from #businfo
group by [date],[state]
order by [date]
Related
I'm trying to calculate how many days the stock for an item has been sitting at a site.
There are two tables: Stock table shows the items and stock currently on hand and Receipts table show the dates when the site has received stock and quantity.
I want to do a left outer join to see all the items in the Stock table and only the rows from the Receipts table with the date where there is still stock left from.
Stock
| Item |Current Stock| Value |
|-------|-------------|-------|
|Blade |8 |$40 |
|Table |15 |$100 |
|Screen |3 |$30 |
Receipts
| Item |Receipt Date| Quantity|
|-------|------------|---------|
|Blade |1/3/2020 | 20 |
|Blade |12/10/2021 | 10 |
|Blade |1/5/2022 | 5 |
|Table |3/4/2020 | 10 |
|Table |5/1/2021 | 7 |
|Table |7/10/2021 | 5 |
|Table |8/1/2021 | 5 |
Dates are in mm/dd/yyyy format. Assuming the current date here is 2/1/2022.
Desired Results
| Item |Current Stock| Value |Receipt Date|Age in Days|
|-------|-------------|-------|------------|-----------|
|Blade |8 |$40 |12/10/2021 |53 |
|Table |15 |$100 |5/1/2021 |276 |
|Screen |3 |$30 | | |
Logic:
| Item |Receipt Date | Quantity|Running Sum|Running Sum-Current Stock|
|-------|--------------|---------|-----------|-------------------------|
|Blade |1/3/2020 | 20 |35 |27 |
|Blade |**12/10/2021**| 10 |15 |7 |
|Blade |1/5/2022 | 5 |5 |0 |
For example:
Currently there are 8 units of Blades in stock. The lastest receipt (on 1/5/2022) was 5 units. So there are still 3 units remaining from the 12/10/2021 receipt date. I want to see the first receipt date where the (Running Sum-Current Stock) is greater than 0. This is based on FIFO (First In First Out)
Thanks in advance.
You didn't mention the name of DBMS. My answer is for SQL Server. For other DBMS you need to change datediff() function.
Schema and insert statements:
create table Stock(Item varchar(50), Current_Stock int, Value int);
insert into Stock values('Blade' ,8 ,40);
insert into Stock values('Table' ,15 ,100);
insert into Stock values('Screen' ,3 ,30);
create table Receipts(Item varchar(50), Receipt_Date date, Quantity int);
insert into Receipts values('Blade','1/3/2020', 20);
insert into Receipts values('Blade','12/10/2021', 10);
insert into Receipts values('Blade','1/5/2022', 5);
insert into Receipts values('Table','3/4/2020', 10);
insert into Receipts values('Table','5/1/2021', 7);
insert into Receipts values('Table','7/10/2021', 5);
insert into Receipts values('Table','8/1/2021', 5);
Query:
with Recepts_with_runningtotal_qty as
(
select *, sum(Quantity)over(partition by Item order by Receipt_Date desc) running_total_qty from Receipts
),
current_stock as
(
select *, (select max(Receipt_Date) from Recepts_with_runningtotal_qty r where r.running_total_qty>s.current_stock and s.Item=r.Item)Receipt_Date
from Stock s
)
select *,datediff(day, Receipt_Date,'2/1/2022')Age_in_Days from current_stock
Output:
Item
Current_Stock
Value
Receipt_Date
Age_in_Days
Blade
8
40
2021-12-10
53
Table
15
100
2021-05-01
276
Screen
3
30
null
null
db<>fiddle here
For Oracle you can use below query:
with Recepts_with_runningtotal_qty as
(
select Item , Receipt_Date, Quantity, sum(Quantity)over(partition by Item order by Receipt_Date desc) running_total_qty from Receipts
),
current_stock as
(
select Item , Current_Stock, Value, (select max(Receipt_Date) from Recepts_with_runningtotal_qty r where r.running_total_qty>s.current_stock and s.Item=r.Item)Receipt_Date
from Stock s
)
select Item, Current_Stock, Value, Receipt_Date,(to_date('1 Feb 2022','DD MM YY')-Receipt_Date)Age_in_Days from current_stock
Output:
ITEM
CURRENT_STOCK
VALUE
RECEIPT_DATE
AGE_IN_DAYS
Blade
8
40
10-DEC-21
53
Table
15
100
01-MAY-21
276
Screen
3
30
null
null
Query 2: without using common table expression
select Item, Current_Stock, Value, Receipt_Date,(to_date('1 Feb 2022','DD MM YY')-Receipt_Date)Age_in_Days
from
(
select Item, Current_Stock, Value, Receipt_Date,Quantity, Running_Total_Qty,
row_number()over(partition by Item order by Receipt_Date desc)rn
from
(
Select S.Item, S.Current_Stock, S.Value, R.Receipt_date, R.Quantity,
sum(Quantity)over(partition by R.Item order by Receipt_Date desc) running_total_qty
From Stock S
Left outer join Receipts R On (S.Item = R.Item)
)
where Running_Total_Qty>= Current_Stock or Running_Total_Qty is null
)
where rn=1
Output:
ITEM
CURRENT_STOCK
VALUE
RECEIPT_DATE
AGE_IN_DAYS
Blade
8
40
10-DEC-21
53
Screen
3
30
null
null
Table
15
100
01-MAY-21
276
db<>fiddle here
You could declare variable with your current date, or use GETDATE() -
DECLARE #Today AS DATE SET #Today = GETDATE or some other date if you need.
And then, you can use DATEDIFF, like that:
SELECT DATEDIFF(day, #Today, Receipt Date) AS date_diff_days
After that just perform left outer join it should work fine. Have fun :)
first of all thank you for helping me, this is my first JR job and I don't want to screw this up.
I need to return all the records (grouped by price and money_balance) of a Debts history by month, of Property for a specific year.
What I've been trying to do is
SELECT properties.name as property
, EXTRACT(month from priority_date) as month
, SUM(debts.money_balance) as money_balance
, SUM(debts.price) as price
FROM properties
JOIN debts on properties.id = debts.property_id
WHERE properties.community_id = 15
AND properties.active = TRUE
AND EXTRACT(year from priority_date) = 2021
GROUP BY month, properties.name
This is going to give me something like
id
property
month
money_balance
price
1
A1
1
1111
3131
2
A1
7
0
1111
3
A2
7
0
1111
But I need to have even months where there are no records, and to have the money_balance and price at 0 or null, if this achievable with SQL?
Thank you so much.
Edit:
Desired output:
|id|property | month|money_balance| price|
|--| --- | ---- |-- | ---- |
|1 | A1 |1 |1111 | 3131 |
|2 |A1 |2 | 0 |0 |
|3 |A1 |3 | 0 |0 |
Till month 12, it ca be 0 or null in the months were the are no records
You can use generate_series() to generate all the months, and then bring the data in:
SELECT p.name as property, mon,
SUM(d.money_balance) as money_balance,
SUM(d.price) as price
FROM GENERATE_SERIES(1, 12, 1) gs(mon) JOIN
(properties p JOIN
debts d
ON p.id = d.property_id AND
p.community_id = 15 AND
p.properties.active = TRUE AND
EXTRACT(year from priority_date) = 2021
)
ON EXTRACT(month from priority_date) = gs.mon
GROUP BY gs.mon, p.name
I need to select the earliest date out of a group of records with the same userID. However, the date field I'm using was in a string format organized as such: yyyymmdd.
So I used the DateSerial function to convert the dates to this format: mm/dd/yyyy. That was step one. Step two (which is where I need some help) is the grouping of UserIDs by oldest date. Any help would be greatly appreciated.
Current query:
SELECT
[userID],
[company],
DateSerial(Left([DateOfSale], 4), Mid([DateOfSale], 5, 2), Right([DateOfSale], 2)) AS SaleDate
FROM mytable
Result:
|userID| company | SaleDate |
_________________________________
|1 | catworld | 01/01/2005 |
|1 | catworld | 01/03/2017 |
|2 | fishworld| 05/05/2019 |
|3 | dogworld | 02/01/2005 |
|3 | dogworld | 02/03/2017 |
Desired Result:
|userID| company | SaleDate |
_________________________________
|1 | catworld | 01/01/2005 |
|2 | fishworld| 05/05/2019 |
|3 | dogworld | 02/01/2005 |
Consider CDate which can cast date strings to actual date/time values. However because yyymmdd is not a valid date format, add hyphens between date parts for proper casting. And do so at the table level with a new column. See DDL commands to be run separately or using Access GUI (table design > new field):
ALTER TABLE main_table ADD COLUMN Saledate_Actual Date;
UPDATE main_table SET SaleDate_Actual = CDate(
LEFT([DateOfSale], 4) & '-' &
MID([DateOfSale], 5, 2) & '-' &
RIGHT([DateOfSale], 2)
);
Then join an aggregate query to the main table.
SELECT m.*
FROM main_table m
INNER JOIN
(SELECT userID, MIN(Saledate_Actual) AS min_date
FROM main_table
GROUP BY userID) AS agg
ON m.userID = agg.userID
AND m.Saledate_Actual = agg.min_date
Maybe once day MS Access will have window functions. Please upvote my request to MS Access team (no need to log in to vote)!
SELECT m.*
FROM main_table m
WHERE m.Saledate_Actual = MIN(Saledate_Actual) OVER(PARTITION BY userID)
You can filter with a correlated subquery. The good thing about the current format of your string dates (yyyymmdd) is that it can be properly sorted, so this should just work:
select
[userID],
[company],
DateSerial(Left([DateOfSale], 4), Mid([DateOfSale], 5, 2), Right([DateOfSale], 2)) AS SaleDate
from mytable t
where t.SaleDate = (
select min(t1.[SaleDate]) from mytable t1 where t1.[userID] = t.[userID]
)
In my database I have a Reservation table and it has three columns Initial Day, Last Day and the House Id.
I want to count the total days and omit those who are repeated, for example:
+-------------+------------+------------+
| | Results | |
+-------------+------------+------------+
| House Id | InitialDay | LastDay |
+-------------+------------+------------+
| 1 | 2017-09-18 | 2017-09-20 |
| 1 | 2017-09-18 | 2017-09-22 |
| 19 | 2017-09-18 | 2017-09-22 |
| 20 | 2017-09-18 | 2017-09-22 |
+-------------+------------+------------+
If you noticed the House Id with the number 1 has two rows, and each row has dates but the first row is in the interval of dates of the second row. In total the number of days should be 5 because the first shouldn't be counted as those days already exist in the second.
The reason why this is happening is that each house has two rooms, and different persons can stay in that house on the same dates.
My question is: how can I omit those cases, and only count the real days the house was occupied?
In your are using SQL Server 2012 or higher you can use LAG() to get the previous final date and adjust the initial date:
with ReservationAdjusted as (
select *,
lag(LastDay) over(partition by HouseID order by InitialDay, LastDay) as PreviousLast
from Reservation
)
select HouseId,
sum(case when PreviousLast>LastDay then 0 -- fully contained in the previous reservation
when PreviousLast>=InitialDay then datediff(day,PreviousLast,LastDay) -- overlap
else datediff(day,InitialDay,LastDay)+1 -- no overlap
end) as Days
from ReservationAdjusted
group by HouseId
The cases are:
The reservation is fully included in the previous reservation: we only need to compare end dates because the previous row is obtained ordering by InitialDay, LastDay, so the previous start date is always minor or equal than the current start date.
The current reservation overlaps with the previous: in this case we adjust the start and don't add 1 (the initial day is already counted), this case include when the previous end is equal to the current start (is a one day overlap).
There is no overlap: we just calculate the difference and add 1 to count also the initial day.
Note that we don't need extra condition for the reservation of a HouseID because by default the LAG() function returns NULL when there isn't a previous row, and comparisons with null always are false.
Sample input and output:
| HouseId | InitialDay | LastDay |
|---------|------------|------------|
| 1 | 2017-09-18 | 2017-09-20 |
| 1 | 2017-09-18 | 2017-09-22 |
| 1 | 2017-09-21 | 2017-09-22 |
| 19 | 2017-09-18 | 2017-09-27 |
| 19 | 2017-09-24 | 2017-09-26 |
| 19 | 2017-09-29 | 2017-09-30 |
| 20 | 2017-09-19 | 2017-09-22 |
| 20 | 2017-09-22 | 2017-09-26 |
| 20 | 2017-09-24 | 2017-09-27 |
| HouseId | Days |
|---------|------|
| 1 | 5 |
| 19 | 12 |
| 20 | 9 |
select house_id,min(initialDay),max(LastDay)
group by houseId
If I understood correctly!
Try out and let me know how it works out for you.
Ted.
While thinking through your question I came across the wonder that is the idea of a Calendar table. You'd use this code to create one, with whatever range of dates your want for your calendar. Code is from http://blog.jontav.com/post/9380766884/calendar-tables-are-incredibly-useful-in-sql
declare #start_dt as date = '1/1/2010';
declare #end_dt as date = '1/1/2020';
declare #dates as table (
date_id date primary key,
date_year smallint,
date_month tinyint,
date_day tinyint,
weekday_id tinyint,
weekday_nm varchar(10),
month_nm varchar(10),
day_of_year smallint,
quarter_id tinyint,
first_day_of_month date,
last_day_of_month date,
start_dts datetime,
end_dts datetime
)
while #start_dt < #end_dt
begin
insert into #dates(
date_id, date_year, date_month, date_day,
weekday_id, weekday_nm, month_nm, day_of_year, quarter_id,
first_day_of_month, last_day_of_month,
start_dts, end_dts
)
values(
#start_dt, year(#start_dt), month(#start_dt), day(#start_dt),
datepart(weekday, #start_dt), datename(weekday, #start_dt), datename(month, #start_dt), datepart(dayofyear, #start_dt), datepart(quarter, #start_dt),
dateadd(day,-(day(#start_dt)-1),#start_dt), dateadd(day,-(day(dateadd(month,1,#start_dt))),dateadd(month,1,#start_dt)),
cast(#start_dt as datetime), dateadd(second,-1,cast(dateadd(day, 1, #start_dt) as datetime))
)
set #start_dt = dateadd(day, 1, #start_dt)
end
select *
into Calendar
from #dates
Once you have a calendar table your query is as simple as:
select distinct t.House_id, c.date_id
from Reservation as r
inner join Calendar as c
on
c.date_id >= r.InitialDay
and c.date_id <= r.LastDay
Which gives you a row for each unique day each room was occupied. If you need a sum of how many days each room was occupied it becomes:
select a.House_id, count(a.House_id) as Days_occupied
from
(select distinct t.House_id, c.date_id
from so_test as t
inner join Calendar as c
on
c.date_id >= t.InitialDay
and c.date_id <= t.LastDay) as a
group by a.House_id
Create a table of all the possible dates and then join it to the Reservations table so that you have a list of all days between InitialDay and LastDay. Like this:
DECLARE #i date
DECLARE #last date
CREATE TABLE #temp (Date date)
SELECT #i = MIN(Date) FROM Reservations
SELECT #last = MAX(Date) FROM Reservations
WHILE #i <= #last
BEGIN
INSERT INTO #temp VALUES(#i)
SET #i = DATEADD(day, 1, #i)
END
SELECT HouseID, COUNT(*) FROM
(
SELECT DISTINCT HouseID, Date FROM Reservation
LEFT JOIN #temp
ON Reservation.InitialDay <= #temp.Date
AND Reservation.LastDay >= #temp.Date
) AS a
GROUP BY HouseID
DROP TABLE #temp
I have a query that produces the following table with a cumulative column (cumulate)
+--+---+--------+------+
|id|qty|cumulate|value |
+--+---+--------+------+
|1 |5 |5 |419.6 |
+--+---+--------+------+
|2 |2 |7 |167.84|
+--+---+--------+------+
|3 |1 |8 |83.92 |
+--+---+--------+------+
|4 |2 |10 |167.84|
+--+---+--------+------+
|5 |1 |11 |83.92 |
+--+---+--------+------+
|6 |5 |16 |419.6 |
+--+---+--------+------+
However I need a further attachment to the query that will only select all the rows that cumulate up to 9. In this case the first 4 rows accumulate up to 10 and the first three; 8 .
I need to extract the sum of the total value where the qty is no more and no less than 9.
The rows are in date order (date not shown) and therefor rows cannot be reordered.
How would one achieve this?
EDIT
here is my query (but the results table above is not the same output as what this query would produce):
select branch,
case
when DATEDIFF(MONTH, dateInv, getDate()) between 0 and 6 then '0-6'
when DATEDIFF(MONTH, dateInv, getDate()) between 7 and 12 then '7-12'
when DATEDIFF(MONTH, dateInv, getDate()) between 13 and 18 then '13-18'
when DATEDIFF(MONTH, dateInv, getDate()) between 19 and 24 then '19-24'
when DATEDIFF(MONTH, dateInv, getDate()) between 25 and 36 then '25-36'
when DATEDIFF(MONTH, dateInv, getDate()) > 36 then '>36'
end [period]
,sum(qty*cost) [costs]
from (
select branch,qty, dateInv, max(cost)cost, max(soh)[qoh], SUM(qty*cost)[sumqty]
, sum(qty) over (partition by product order by dateInv desc) [cumulate]
from openquery(linkedserver,
'select branch,product, soh, cost, dateInv, qty
from table
group by branch,product, soh, cost, dateInv, qty
order by dateInv DESC
')
group by branch,product,qty, dateInv
)t
where cumulate <= qoh
group by branch, dateInv
Well from the looks of it each 1 in quantity has a value of 83.92. 9 * 83.92 = 755.28
Thanks everyone for your attempts. I managed to solve this problem by using a series of nested queries with "over (partition by)", Row_number and calculations.
Lots of fun !