How to get WeekDay in between two days in SQL server - sql

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.

Related

SQL Server : average count of alerts per day, not including days with no alerts

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
...

SQL, Stock movements where no zero stock records

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.

How to rearrange the value of column

I have a table (tblDates). In this table I have two column (Date,Age) . Now I want If I add new date in this table then Age column rearranged there values.
Table - tblDates
Date Age
--------------------
12/01/14 5
12/02/14 4
12/03/14 3
12/04/14 2
12/05/14 1
If I add New date i.e., 12/06/14 then I want result like this
Table - tblDates
Date Age
--------------------
12/01/14 6
12/02/14 5
12/03/14 4
12/04/14 3
12/05/14 2
12/06/14 1
I may be reading too much into your question, but if your goal is to compute the age (in days) from a given date (today?) to the date stored in your tables, then you'll be better off using the DATEDIFF function and computing the value when you query it each time.
For example:
-- Option 1: Compute when you query it each time in the query you require it
SELECT d.[Date], DATEDIFF(dd, d.[Date], CONVERT(DATE, GETDATE())) as [Age]
FROM tblDates AS d
You can also define the Age column on your table as a Computed Column if it will be used frequently enough, or wrap the table in a View to embed this computation:
-- Option 2: Compute at query time, but build the computation into the table definition
CREATE TABLE [dbo].[tblDates] (
[Date] DATE NOT NULL,
[AgeInDaysComputed] AS (DATEDIFF(dd, [Date], CONVERT(DATE, GETDATE())) )
)
GO
-- Option 3: Compute at query time, but require caller interact with a different object
-- (view) to get the computation
CREATE VIEW [dbo].[vwDates]
AS
SELECT d.[Date], DATEDIFF(dd, d.[Date], CONVERT(DATE, GETDATE())) as [AgeInDays]
FROM dbo.tblDates AS D
GO
One note regarding the GETDATE function: you need to be aware of your server timezone, as GETDATE returns the date according to your server's local timezone. As long as your server configuration and user's configurations are all in the same timezone, this should provide the correct result.
(If the age in days is what you're trying to compute, you may want to edit your question to better reflect this intent for the benefit of future readers, as it is quite different from "rearranging the value of columns")
Pull the values that you want out when you query, not when you insert data. You seem to want:
select d.*, row_number() over (order by date desc) as age
from tblDates d;
Otherwise, your insert operation will become very cumbersome, requiring changes to all the rows in the table.

using a loop in a stored procedure

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

3rd <day_of_week> of the Month - MySQL

I'm working on a recurrence application for events. I have a date range of say, January 1 2010 to December 31 2011. I want to return all of the 3rd Thursdays (arbitrary) of the each month, efficiently. I could do this pretty trivially in code, the caveat is that it must be done in a stored procedure. Ultimately I'd want something like:
CALL return_dates(event_id);
That event_id has a start_date of 1/1/2010 and end_date of 12/31/2011. Result set would be something like:
1/20/2010
2/14/2010
3/17/2010
4/16/2010
5/18/2010
etc.
I'm just curious what the most efficient method of doing this would be, considering I might end up with a very large result set in my actual usage.
One idea that comes to mind - you can create a table and store the dates you're interested in there.
Ok, I haven't tested it, but I think the most efficient way of doing it is via a tally table which is a useful thing to have in the db anyway:
IF EXISTS (SELECT * FROM sys.objects
WHERE object_id = OBJECT_ID(N'[dbo].[num_seq]') AND type in (N'U'))
DROP TABLE [dbo].[num_seq];
SELECT TOP 100000 IDENTITY(int,1,1) AS n
INTO num_seq
FROM MASTER..spt_values a, MASTER..spt_values b;
CREATE UNIQUE CLUSTERED INDEX idx_1 ON num_seq(n);
You can then use this to build up the date range between the two dates. It's fast because
it just uses the index (in fact often faster than a loop, so I'm led to believe)
create procedure getDates
#eventId int
AS
begin
declare #startdate datetime
declare #enddate datetime
--- get the start and end date, plus the start of the month with the start date in
select #startdate=startdate,
#enddate=enddate
from events where eventId=#eventId
select
#startdate+n AS date,
from
dbo.num_seq tally
where
tally.n<datediff(#monthstart, #enddate) and
Datepart(dd,#startdate+n) between 15 and 21 and
Datepart(dw, #startdate+n) = '<day>'
Aside from getting the start and end dates, the third x id each month must be between the 15th and the 21st inclusive.
The day names in that range must be unique, so we can locate it straight away.
If you wanted the second dayname, just modify the range appropriately or use a parameter to calculate it.
It constucts a date table using the startdate, and then adding days on (via the list of numbers in the tally table) until it reaches the end date.
Hope it helps!