I'm trying to create a stock movement file from historical stock records, see picture:
I've been able to get the green movements and the orange value (had some luck union all to a 0 stock record for max(day) + 1 and removing anything less than today). Movements are then generated with a lag function.
The difficulty is there are no 0 stock records. i.e. fifth row in the example above there is no record so unable to create the correct movement to 0 with the -2. It would create movements of -10, -4, -4, 5, -3, -1, -3
Is there any easier way to identify the gaps and create phantom 0's, I know the dates (these are a mix of weekly that will become daily)?
There are several ways to get the missing gaps in this case it looks like you have a date every week (or 7 days) and in some case there is no date. So you can build a date table with a while loop and do a join and assinging Stock Opening Balance as 0 when null in your table. Here is something similar I am using table variables to simply but using a temp table would probably be better for you and if you do you don't need 2 tables just insert the missing to your temp table.
DECLARE #TableWithGaps AS TABLE ([Date] DATE, StockOpeningBalance INT)
INSERT INTO #TableWithGaps ([Date],StockOpeningBalance)
VALUES ('2016-04-04',10)
,('2016-04-11',6)
,('2016-04-18',2)
,('2016-05-09',7)
,('2016-05-16',4)
,('2016-05-23',3)
DECLARE #Dates AS TABLE ([Date] DATE)
DECLARE #MinDate DATE = '3/28/2016'
DECLARE #MaxDate DATE = '6/20/2016'
--Could also get first record as minimum date and last record, or set #maxdate = GETDATE()
--SELECT #MinDate = DATEADD(DAY,7,MIN([Date])), #MaxDate = MAX([Date]) FROM #TableWithGaps
WHILE #MinDate < DATEADD(DAY,1,#MaxDate)
BEGIN
INSERT INTO #Dates ([Date]) VALUES (#MinDate)
SET #MinDate = DATEADD(DAY,7,#MinDate)
END
SELECT
d.[Date]
,StockOpeningBalance = ISNULL(StockOpeningBalance,0)
,LAG(ISNULL(StockOpeningBalance,0),1,0) OVER (ORDER BY d.[Date])
,Movements = ISNULL(StockOpeningBalance,0) - LAG(ISNULL(StockOpeningBalance,0),1,0) OVER (ORDER BY d.[Date])
FROM
#Dates d
LEFT JOIN #TableWithGaps s
ON d.[Date] = s.[Date]
Also note, I prefer temp tables over table variables and they temp tables will perform better with a larger data set. If you start doing a ton with dates you may want to look at a date dimension table for purposes like this. If you are using Microsoft SSAS it has a built in tool to generate one that you can google/search for pretty quickly.
Related
Please help, I have a below sample data. How to find week days "Tuesday" and count between two days.
CREATE TABLE EmpDetails1 (id INT, name VARCHAR(25),startdate datetime,enddate datetime)
INSERT INTO EmpDetails1 VALUES(1,'TEST','01/01/2016','01/10/2016');
INSERT INTO EmpDetails1 VALUES(2,'TEST','01/01/2016','01/25/2016');
id name startdate enddate
1 Test 1 1/1/16 1/10/16
2 Test 2 1/1/16 1/25/16
output:
date count
1/5/16 1
1/12/16 3
I have tried with below query but not getting correct result
SELECT name,
DATENAME(WEEKDAY, startdate) as w1,
DATENAME(WEEKDAY, enddate) as w2,
startdate,enddate, count(*) OVER(PARTITION BY startdate,enddate) AS CountOfOrders from EmpDetails1 group by startdate , enddate,name
As mentioned already, you need a calendar. You can generate one dynamically but far better to create a static one since it serves so many useful purposes. This is but one of many discussions about how to generate one.
A calendar is just a special form of a tally table and I use Itzik's discussion as a basis for that. Both of these concepts are things you need to understand in most SQL query writing environments.
Once you have a calendar, you simply join your data table to the calendar and filter as needed. I did not understand exactly what you were trying to accomplish so I simply create the set of rows for "Tuesday".
declare #EmpDetails1 table (id int, name varchar(20), startdate date, enddate date);
insert #EmpDetails1 (id, name, startdate, enddate) values
(1, 'Test 1', '20210101', '20210110'),
(2, 'Test 2', '20210116', '20210126');
select emp.*, cal.*, datename(weekday, cal.caldt) as [day of week(eng)]
from #EmpDetails1 as emp
inner join calendar as cal
on cal.caldt between emp.startdate and emp.enddate
and datename(weekday, cal.caldt) = 'Tuesday'
order by emp.id, cal.caldt
;
Fiddle here to demonstrate. I must highlight the lazy usage of * as the column list but this is just a simple demo. Production code should generally always specify the columns needed completely.
If you examine the calendar table discussion, you will see that the day of week can be easily added to the table - it will never change. This will avoid the effort to calculate it in the query.
T-SQL Code:
CREATE PROCEDURE tuesdayCount #id_number INT AS
BEGIN
DECLARE #S_Date DATETIME
DECLARE #E_Date DATETIME
SET #S_Date = (SELECT startdate FROM EmpDetails1 WHERE id = #id_number)
SET #E_Date = (SELECT enddate FROM EmpDetails1 WHERE id = #id_number)
WHILE #S_Date <= #E_Date
IF (FORMAT(#S_Date, '%a') = 'Tue')
INSERT INTO TuesdayDates VALUES(#S_Date, 1)
SET #S_Date = DATEADD(DAY,1, #S_Date)
END
EXECUTE tuesdayCount #id_number=1;
EXECUTE tuesdayCount #id_number=2;
Code Steps:
First line I created a procedure named tuesdayCount with an input parameter(id_number)
Then I declared 2 variables (S_Date and E_Date) with DATETIME data type, then set them equal to startdate and endnote column values in the first row.(Please take note of id_number sp parameter in the where clause.)
Then I defined a while loop and if_ test to ensure the date is really a Tuesday. If that's so, then I inserted the date value(S_Date) into TuesdayDates table which I created myself beforehand to put the result set there. (maybe not so logical; but I did it anyway.)
After defining the sp proc, I called my functions with id_number parameter which is in fact the row number of your data set (EmpDetails1 Table)
Not a perfect solution :) but I hope It helps somehow.
I'm a bit confused if there is a simple way to do this.
I have a field called receipt_date in my data table and I wish to add 10 working days to this (with bank holidays).
I'm not sure if there is any sort of query I could use to join onto this table from my original to calculate 10 working days from this, I've tried a few sub queries but I couldn't get it right or perhaps its not possible to do this. I didn't know if there was a way to extract the 10th rowcount after the receipt date to get the calendar date if I only include 'Y' into the WHERE?
Any help appreciated.
This is making several assumptions about your data, because we have none. One method, however, would be to create a function, I use a inline table value function here, to return the relevant row from your calendar table. Note that this assumes that the number of days must always be positive, and that if you provide a date that isn't a working day that day 0 would be the next working day. I.e. adding zero working days to 2021-09-05 would return 2021-09-06, or adding 3 would return 2021-09-09. If that isn't what you want, this should be more than enough for you to get there yourself.
CREATE FUNCTION dbo.AddWorkingDays (#Days int, #Date date)
RETURNS TABLE AS
RETURN
WITH Dates AS(
SELECT CalendarDate,
WorkingDay
FROM dbo.CalendarTable
WHERE CalendarDate >= #Date)
SELECT CalendarDate
FROM Dates
WHERE WorkingDay = 1
ORDER BY CalendarDate
OFFSET #Days ROWS FETCH NEXT 1 ROW ONLY;
GO
--Using the function
SELECT YT.DateColumn,
AWD.CalendarDate AS AddedWorkingDays
FROM dbo.YourTable YT
CROSS APPLY dbo.AddWorkingDays(10,YT.DateColumn) AWD;
I have a scenario where I have a SQL table that looks like the below screenshot. Is it possible to retrieve the start and end date of just the portion that overlaps between multiple records? I've reviewed similar questions from StackOverflow, and what I found was finding the records that has overlaps, but not the actual overlap range.
The desired result will show that the start date is 1/1/2017 and the end date is 6/30/2017 since that range is in both records.
Let's use variables to keep it simple. I assume you're using some join of two rows in your table where the ON condition determines that the dates overlap. But, since the part you're asking about is displaying the overlapping dates, we can just use variables:
DECLARE #start1 date = '20160701', #end1 date = '20170630'
,#start2 date = '20170101', #end2 date = '20171231'
SELECT CASE WHEN #start2 >= #start1 THEN #start2 ELSE #start1 END AS startdate
, CASE WHEN #end2 <= #end1 THEN #end2 ELSE #end1 END AS enddate
If you're on Azure SQL where GREATEST and LEAST are supported you can use:
SELECT GREATEST(#start1, #start2) AS startdate
, LEAST(#end1, #end2) AS enddate
The key is to understand the logic on the overlap.
if we are searching for the overlap period of many rows then it will ALWAYS start at the MAX(startDate).
the end point will be the minimum of the end dates where it is larger than our start date.
But we need to think about checking if they have an overlap period or not.
So the code should be:
declare #start datetime = (select MAX(PlanYearStart) start from #t);
if exists(select PlanYearEnd from #t where PlanYearEnd<#start)
begin
select null OverlapStart, null OverlapEnd;
return;
end;
select
#start OverlapStart,
MIN(PlanYearEnd) OverlapEnd
from #t;
I have a table that acts as a message log, with the two key tables being TIMESTAMP and TEXT. I'm working on a query that grabs all alerts (from TEXT) for the past 30 days (based on TIMESTAMP) and gives a daily average for those alerts.
Here is the query so far:
--goback 30 days start at midnight
declare #olderdate as datetime
set #olderdate = DATEADD(Day, -30, DATEDIFF(Day, 0, GetDate()))
--today at 11:59pm
declare #today as datetime
set #today = dateadd(ms, -3, (dateadd(day, +1, convert(varchar, GETDATE(), 101))))
print #today
--Grab average alerts per day over 30 days
select
avg(x.Alerts * 1.0 / 30)
from
(select count(*) as Alerts
from MESSAGE_LOG
where text like 'The process%'
and text like '%has alerted%'
and TIMESTAMP between #olderdate and #today) X
However, I want to add something that checks whether there were any alerts for a day and, if there are no alerts for that day, doesn't include it in the average. For example, if there are 90 alerts for a month but they're all in one day, I wouldn't want the average to be 3 alerts per day since that's clearly misleading.
Is there a way I can incorporate this into my query? I've searched for other solutions to this but haven't been able to get any to work.
This isn't written for your query, as I don't have any DDL or sample data, thus I'm going to provide a very simple example instead of how you would do this.
USE Sandbox;
GO
CREATE TABLE dbo.AlertMessage (ID int IDENTITY(1,1),
AlertDate date);
INSERT INTO dbo.AlertMessage (AlertDate)
VALUES('20190101'),('20190101'),('20190105'),('20190110'),('20190115'),('20190115'),('20190115');
GO
--Use a CTE to count per day:
WITH Tots AS (
SELECT AlertDate,
COUNT(ID) AS Alerts
FROM dbo.AlertMessage
GROUP BY AlertDate)
--Now the average
SELECT AVG(Alerts*1.0) AS DayAverage
FROM Tots;
GO
--Clean up
DROP TABLE dbo.AlertMessage;
You're trying to compute a double-aggregate: The average of daily totals.
Without using a CTE, you can try this as well, which is generalized a bit more to work for multiple months.
--get a list of events per day
DECLARE #Event TABLE
(
ID INT NOT NULL IDENTITY(1, 1)
,DateLocalTz DATE NOT NULL--make sure to handle time zones
,YearLocalTz AS DATEPART(YEAR, DateLocalTz) PERSISTED
,MonthLocalTz AS DATEPART(MONTH, DateLocalTz) PERSISTED
)
/*
INSERT INTO #Event(EntryDateLocalTz)
SELECT DISTINCT CONVERT(DATE, TIMESTAMP)--presumed to be in your local time zone because you did not specify
FROM dbo.MESSAGE_LOG
WHERE UPPER([TEXT]) LIKE 'THE PROCESS%' AND UPPER([TEXT]) LIKE '%HAS ALERTED%'--case insenitive
*/
INSERT INTO #Event(DateLocalTz)
VALUES ('2018-12-31'), ('2019-01-01'), ('2019-01-01'), ('2019-01-01'), ('2019-01-12'), ('2019-01-13')
--get average number of alerts per alerting day each month
-- (this will not return months with no alerts,
-- use a LEFT OUTER JOIN against a month list table if you need to include uneventful months)
SELECT
YearLocalTz
,MonthLocalTz
,AvgAlertsOfAlertingDays = AVG(CONVERT(REAL, NumDailyAlerts))
FROM
(
SELECT
YearLocalTz
,MonthLocalTz
,DateLocalTz
,NumDailyAlerts = COUNT(*)
FROM #Event
GROUP BY YearLocalTz, MonthLocalTz, DateLocalTz
) AS X
GROUP BY YearLocalTz, MonthLocalTz
ORDER BY YearLocalTz ASC, MonthLocalTz ASC
Some things to note in my code:
I use PERSISTED columns to get the month and year date parts (because I'm lazy when populating tables)
Use explicit CONVERT to escape integer math that rounds down decimals. Multiplying by 1.0 is a less-readable hack.
Use CONVERT(DATE, ...) to round down to midnight instead of converting back and forth between strings
Do case-insensitive string searching by making everything uppercase (or lowercase, your preference)
Don't subtract 3 milliseconds to get the very last moment before midnight. Change your semantics to interpret the end of a time range as exclusive, instead of dealing with the precision of your datatypes. The only difference is using explicit comparators (i.e. use < instead of <=). Also, DATETIME resolution is 1/300th of a second, not 3 milliseconds.
Avoid using built-in keywords as column names (i.e. "TEXT"). If you do, wrap them in square brackets to avoid ambiguity.
Instead of dividing by 30 to get the average, divide by the count of distinct days in your results.
select
avg(x.Alerts * 1.0 / x.dd)
from
(select count(*) as Alerts, count(distinct CAST([TIMESTAMP] AS date)) AS dd
...
i am trying to write a procedure that inserts rows into a temp table. the basis of the table is an insurance policy table listing the amount of the premium earned over the life of the policys. the original data consists of the trans_date (date sold) and the policy_start and policy_end dates. i.e. if the policy is 12 months long, we give each month 1/12 of the premium collected.
so something like
while trans_month < policy_end month
insert to tblUEPtmp
select dateadd(mm, 1, trans_date), earned_premium from tblpolicys
set trans_date = dateadd(mm, 1, trans_date)
(i know this is rubbush code but i completely baffled at the moment)
My problem is that i need to create the extra 11 rows of data and modify the transaction date to add 1 month each time until the modified transaction date = policy_end date.
i've researched using a CTE, but while loops aren't posible within a CTE..
is this something a multistatement table function could do?
Many thanks.
You can defo do this with a CTE, for example this little snippet will demonstrate how to do recursion using dates:
declare #start DATETIME = '2012-02-01'
declare #end DATETIME = '2013-02-01'
;with cte (date)
AS
(
SELECT #start
UNION ALL
SELECT DATEADD(mm,1,cte.date)
FROM cte WHERE DATEADD(mm,1,cte.date)<#end
)
select * from cte
That will generate a list of dates between #start & #end with month gaps.
You can
Use your real tables in place of the dummy dates
Perform an insert into...select ... from cte to insert your required data
If you can provide more detail about your table schema, I can probably help out with a more concrete example.
Something like this?
set #trans_date = ...
while #trans_date < #policy_end
begin
insert to tblUEPtmp
select trans_date, earned_premium
from tblpolicys
where {whatever}
set #trans_date = dateadd(mm, 1, #trans_date)
end