I made a user-define function for business hours calculation.
This is my UDF.
CREATE FUNCTION fn_GetBusinessHour (#date datetime, #addHours int)
RETURNS datetime
AS
BEGIN
DECLARE #CalcuatedDate datetime;
DECLARE #addDayCount int, #addHourCount int, #addMinCount int;
SET #addDayCount = #addHours / 8.5;
SET #addHourCount = #addHours - (#addDayCount * 8.5);
SET #addMinCount = #addHours - (#addDayCount * 8.5) - #addHourCount;
IF(#addDayCount != 0)
SET #CalcuatedDate = DATEADD(DD, #addDayCount, #date);
SET #CalcuatedDate = DATEADD(HH, #addHourCount, #CalcuatedDate);
IF(#addMinCount != 0)
SET #CalcuatedDate = DATEADD(MM, #addMinCount, #CalcuatedDate);
RETURN #CalcuatedDate;
END
When I test using following statement,
SELECT dbo.fn_GetBusinessHour(GETDATE(), 40)
It shows proper result.
However, I use my function like this,
SELECT TicketID
, DateTimeLogged --Type: Datetime
, Priority --Type: int
, [dbo].[fn_GetBusinessHour](DateTimeLogged, Priority)
FROM TicketHeader
the result shows only NULL value.
TicketID DateTimeLogged Priority (No column name)
1 2011-07-04 11:26:19.510 30 NULL
2 2011-07-04 13:58:45.683 30 NULL
3 2011-07-05 10:09:16.923 10 NULL
4 2011-07-05 13:13:30.237 30 NULL
5 2011-07-05 16:50:34.033 20 NULL
I tried CONVERT because it worked when I give a value 40 but it also shows null values.
SELECT TicketID
, DateTimeLogged --Type: Datetime
, Priority --Type: int
, [dbo].[fn_GetBusinessHour](DateTimeLogged, CONVERT(int, Priority))
FROM TicketHeader
How can I fix this to work my UDF?
Why this thing happen?
I cannot understand what is different between Priority and 40.
Thank you in advance.
For values of priority > 8.5, this seems to work fine for me:
DECLARE #t TABLE(TicketID INT, DateTImeLogged DATETIME, Priority INT);
INSERT #t SELECT 1,'20110704 11:26:19.510',30
UNION ALL SELECT 2,'20110704 13:58:45.683',30
UNION ALL SELECT 3,'20110705 10:09:16.923',10
UNION ALL SELECT 4,'20110705 13:13:30.237',30
UNION ALL SELECT 5,'20110705 16:50:34.033',20;
SELECT TicketID
, DateTimeLogged --Type: Datetime
, Priority --Type: int
, [dbo].[fn_GetBusinessHour](DateTimeLogged, Priority)
FROM #t;
Yields:
TicketID DateTimeLogged Priority (No column name)
-------- ----------------------- -------- -----------------------
1 2011-07-04 11:26:19.510 30 2011-07-07 15:26:19.510
2 2011-07-04 13:58:45.683 30 2011-07-07 17:58:45.683
3 2011-07-05 10:09:16.923 10 2011-07-06 11:09:16.923
4 2011-07-05 13:13:30.237 30 2011-07-08 17:13:30.237
5 2011-07-05 16:50:34.033 20 2011-07-07 19:50:34.033
If I add another row with a Priority < 8.5, e.g.:
INSERT #t SELECT 6,'20110705 13:13:30.237',5;
Then this row is added to the result:
TicketID DateTimeLogged Priority (No column name)
-------- ----------------------- -------- -----------------------
6 2011-07-05 13:13:30.237 5 NULL
In other words, the function will output NULL if the function logic leaves #CalculatedDate unassigned, which will happen if #addDayCount = 0. In the function you say:
IF(#addDayCount != 0)
SET #CalcuatedDate = DATEADD(DD, #addDayCount, #date);
Since #addDayCount is an INT, try this:
DECLARE #addDayCount INT;
SET #addDayCount = 5 / 8.5;
SELECT #addDayCount;
Result:
0
So because #CalculatedDate isn't assigned a value initially, all of the following DATEADD operations are performing DATEADD(interval, number, NULL) which still yields NULL.
So perhaps you need to use a different data type for the variables in the function...
Related
I have a calendar type check I'm trying to do on SQL Server. For each month of the year, I need to check if the employee was hired or not. There can be an original hire date, a rehire date, a termination date, and the last termination date; other than the original hire date, which will always have a value, all of these date fields can be null.
Given the following data:
EmpID OrigHireDate TermDate LastTermDate RehireDate
42 2017-09-25 NULL 2019-03-26 2019-10-30
What I am trying to achieve is the following result for each month for last year (i.e. 2019) and having no luck in coming up with the right statement. Assume I already have a table containing each month's number along with the start/end date of the month that I can use to compare the date ranges.
EmpID Month EmployeeDuring
42 1 True
42 2 True
42 3 True
42 4 False
42 5 False
42 6 False
42 7 False
42 8 False
42 9 False
42 10 True
42 11 True
42 12 True
The following works. May need some minor adjustments to handle all possible combinations of EmpID, OrigHireDate, TermDate, LastTermDate, RehireDate
I apologize for posting mostly code. Will add more explanation and or comments tomorrow.
DECLARE #EmpID int, #OrigHireDate date, #TermDate date, #LastTermDate date, #RehireDate date
DECLARE #year int
SET #year = 2019
SET #EmpID = 42
SET #OrigHireDate = '2017-09-25'
SET #TermDate = NULL
SET #LastTermDate = '2019-03-26'
SET #RehireDate = '2019-10-30'
SET #OrigHireDate = DATEADD(day,-DAY(#OrigHireDate)+1, #OrigHireDate)
SET #LastTermDate = DATEADD(day,-DAY(ISNULL(#LastTermDate,GETDATE()))+1, #LastTermDate)
SET #RehireDate = DATEADD(day,-DAY(#RehireDate)+1, #RehireDate)
SET #TermDate = DATEADD(day,-DAY(ISNULL(#TermDate,GETDATE()))+1, #TermDate)
;WITH CTE_DATES_ORIGINAL([Date],[Level])
AS
(
SELECT #OrigHireDate AS [DATE],
1 AS [Level]
UNION ALL
SELECT
DATEADD(MONTH,1, [DATE] ) , [Level] + 1
FROM CTE_DATES_ORIGINAL
WHERE [DATE] < ISNULL(#LastTermDate,GETDATE())
),
CTE_DATES_REHIRE([Date],[Level])
AS
(
SELECT #RehireDate AS [DATE],
1 AS [Level]
UNION ALL
SELECT
DATEADD(MONTH,1, [DATE] ) , [Level] + 1
FROM CTE_DATES_REHIRE
WHERE [DATE] < ISNULL(#TermDate,GETDATE())
),
CTE_DATES_YEAR(m) AS
(
SELECT 1
UNION ALL
SELECT m+1
FROM CTE_DATES_YEAR
WHERE m < 12
)
SELECT #EmpID AS EmpID, m AS [Month], ISNULL(EmployeeDuring.EmployeeDuring,0) AS EmployeeDuring
FROM CTE_DATES_YEAR y
LEFT OUTER JOIN
(
SELECT
[Date], 1 AS EmployeeDuring
FROM
CTE_DATES_ORIGINAL
UNION
SELECT
[Date] , 1 AS EmployeeDuring
FROM
CTE_DATES_REHIRE
) employeeDuring
ON DATEADD(month,m-1, CAST(CAST(#year AS CHAR(4)) + '-1-1' AS DATE)) = employeeDuring.[Date]
ORDER BY m
OPTION (MAXRECURSION 5000)
Test Data Setup:
create table #Data(ID INT IDENTITY PRIMARY KEY CLUSTERED, EventDateTime datetime, Instance int)
insert into #Data(EventDateTime)
values
('2018-10-16T01:37:23.173'),
('2018-10-16T01:37:31.447'),
('2018-10-16T01:37:36.577'),
('2018-10-16T01:37:45.457'),
('2018-10-16T01:37:48.860'),
('2018-10-16T01:38:06.407'),
('2018-10-16T01:38:11.030'),
('2018-10-16T01:38:15.470'),
('2018-10-16T01:38:19.133'),
('2018-10-16T01:38:27.830')
Desired Output (which I can get):
Did this using quirky update:
declare #Instance int = 1
declare #StartDate datetime
select top 1 #StartDate = EventDateTime from #Data order by id asc
update t
set #Instance = Instance = case when ABS(datediff(MILLISECOND,t.EventDateTime, #StartDate)) < 10000 then #Instance else #Instance+1 end
, #StartDate = case when ABS(datediff(MILLISECOND,t.EventDateTime, #StartDate)) < 10000 then #StartDate else EventDateTime end
from #Data t
This is what it's doing in this specific example and it's the logic required:
Check row 1, if it’s within 10 seconds of #StartDate (Yes) then keep both the #InstanceID and #StartDate the same and set Instance to #InstanceID
Check row 2, if it’s within 10 seconds of #StartDate (still the original one, so Yes) then keep both the #InstanceID and #StartDate the same and set Instance to #InstanceID
Check row 3, if it’s within 10 seconds of #StartDate (still the original one, so No) then increment #instanceID by one and set Instance to that, and reset #StartDate to the Transactiondate of this record
Check row 4, if it’s within 10 seconds of #StartDate (now the one from row 3, so Yes) then keep both the #InstanceID and #StartDate the same and set Instance to #InstanceID
Check row 5, if it’s within 10 seconds of #StartDate (still the one from row 3, so No) then increment #instanceID by one and set Instance to that, and reset #StartDate to the Transactiondate of this record
etc
etc
The Question:
This can be done using a while loop, a cursor, or a quirky update. Is there a truly set based method for doing this (i.e none of the mentioned methods)?
Recursive CTE query can be a solution. No updates required at all.
DECLARE #OffsetMsec int = 10000;
WITH d1 (ID, EventDateTime, IsStarted, StartDate)
AS (
SELECT TOP 1 ID, EventDateTime, 1, EventDateTime FROM #Data
UNION ALL
SELECT d2.ID, d2.EventDateTime,
CASE WHEN ABS(DATEDIFF(MILLISECOND, d1.StartDate, d2.EventDateTime)) < #OffsetMsec
THEN 0 ELSE 1 END AS IsStarted,
CASE WHEN ABS(DATEDIFF(MILLISECOND, d1.StartDate, d2.EventDateTime)) < #OffsetMsec
THEN d1.StartDate ELSE d2.EventDateTime END AS StartDate
FROM #Data d2 INNER JOIN d1 ON d2.ID = d1.ID + 1
),
d2 (ID, EventDateTime, Instance)
AS (
SELECT ID, EventDateTime, IsStarted, StartDate,
(SELECT SUM(IsStarted) FROM d1 d11 WHERE d11.ID <= d1.ID) AS Instance
FROM d1
)
SELECT * FROM d2
Result
ID EventDateTime Instance
----------- ----------------------- -----------
1 2018-10-16 01:37:23.173 1
2 2018-10-16 01:37:31.447 1
3 2018-10-16 01:37:36.577 2
4 2018-10-16 01:37:45.457 2
5 2018-10-16 01:37:48.860 3
6 2018-10-16 01:38:06.407 4
7 2018-10-16 01:38:11.030 4
8 2018-10-16 01:38:15.470 4
9 2018-10-16 01:38:19.133 5
10 2018-10-16 01:38:27.830 5
I have a table of data which i am using a count statement to get the amount of records for the submission date
example
AuditId Date Crew Shift Cast ObservedBy 2ndObserver AuditType Product
16 2017-06-27 3 Day B1974, B1975 Glen Mason NULL Identification Billet
20 2017-06-29 1 Day 9879 Corey Lundy NULL Identification Billet
21 2017-06-29 4 Day T9627, T9625 Joshua Dwyer NULL ShippingPad Tee
22 2017-06-29 4 Day NULL Joshua Dwyer NULL Identification Billet
23 2017-06-29 4 Day S9874 Joshua Dwyer NULL ShippingPad Slab
24 2017-06-29 4 Day Bay 40 Joshua Dwyer NULL Identification Billet
Basically I am using the following code to get my results
SELECT YEAR([Date]) as YEAR, CAST([Date] as nvarchar(25)) AS [Date], COUNT(*) as "Audit Count"
FROM AuditResults
where AuditType = 'Identification' AND Product = 'Billet'
group by Date
this returns example
YEAR Date Audit Count
2017 2017-06-27 1
2017 2017-06-29 3
Now I want to be able to retrieve all dates even if blank
so I would like the return to be
YEAR Date Audit Count
2017 2017-06-27 1
2017 2017-06-28 0
2017 2017-06-29 3
I have the following function I am trying to use:
ALTER FUNCTION [dbo].[fnGetDatesInRange]
(
#FromDate datetime,
#ToDate datetime
)
RETURNS #DateList TABLE (Dt date)
AS
BEGIN
DECLARE #TotalDays int, #DaysCount int
SET #TotalDays = DATEDIFF(dd,#FromDate,#ToDate)
SET #DaysCount = 0
WHILE #TotalDays >= #DaysCount
BEGIN
INSERT INTO #DateList
SELECT (#ToDate - #DaysCount) AS DAT
SET #DaysCount = #DaysCount + 1
END
RETURN
END
How do I use my select statement with this function? or is there a better way?
cheers
Try this;
ALTER FUNCTION [dbo].[fnGetDatesInRange]
(
#FromDate datetime,
#ToDate datetime
)
RETURNS #YourData TABLE ([Year] int, DateText nvarchar(25),[Audit Count] int)
AS
begin
insert into #YourData
SELECT
YEAR(allDates.[Date]) as YEAR,
CAST(allDates.[Date] as nvarchar(25)) AS [Date],
COUNT(r.Product) as "Audit Count"
from
(
SELECT
[date]=convert(datetime, CONVERT(float,d.Seq))
FROM
(
select top 100000 row_number() over(partition by 1 order by A.name) as Seq
from syscolumns A, syscolumns B
)d
)allDates
left join
AuditResults r on r.[Date]=allDates.[date] and r.AuditType = 'Identification' AND r.Product = 'Billet'
where
allDates.[Date]>=#FromDate and allDates.[Date]<=#ToDate
group by
allDates.[Date]
return
end
The key is the 'allDates' section ;
SELECT
[date]=convert(datetime, CONVERT(float,d.Seq))
FROM
(
select top 100000 row_number() over(partition by 1 order by A.name) as Seq
from syscolumns A, syscolumns B
)d
This will return all dates between 1900 and 2173 (in this example). Limit that as you need but a nice option. A ton of different ways to approach this clearly
you have to create another table calendar as (Mysql)- idea is the same on all RDBMS-
CREATE TABLE `calendar` (
`dt` DATE NOT NULL,
UNIQUE INDEX `calendar_dt_unique` (`dt`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
and fill with date data.
more details
I have this piece of code that I want to use to add an unique identifier to, but I cannot seem to get it to work correctly. Here is the code along with current output and desired output.
Begin
DECLARE #StartDate DATETIME,
#EndDate DATETIME,
#MonthList as Varchar(50),
#NewLeaseID as int,
#LeaseID as int,
#PropertyID as int,
#Amount as int,
#ExpectedID as int
SELECT
#StartDate = '20100501'
,#EndDate = '20100801'
,#leaseID = 6,
#PropertyID = 12,
#Amount = 600,
#ExpectedID = (SELECT ISNULL(MAX(ExpectedPaymentID) + 1, 1) FROM Payments_ExpectedPayments)
INSERT INTO Payments_ExpectedPayments(ExpectedPaymentID, Amount, PropertyID, LeaseID, Month)
SELECT
#ExpectedID as ExpectedPaymentID,
(x.number + 1) * #Amount as Amount,
#PropertyID as PropertyID,
#leaseID as Leaseid,
DATENAME(MONTH, DATEADD(MONTH, x.number, #StartDate)) AS Month
FROM
master.dbo.spt_values x
WHERE
x.type = 'P'
AND x.number <= DATEDIFF(MONTH, #StartDate, #EndDate);
End
Output wanted:
ExpecedID PropertyID LeaseID Month Amount
1 12 13 Jan 600
2 12 13 Feb 1200
3 12 13 March 1800
4 12 13 April 2400
Output I'm currently getting:
ExpecedID PropertyID LeaseID Month Amount
1 12 13 Jan 600
1 12 13 Feb 1200
1 12 13 March 1800
1 12 13 April 2400
You have two options:
Your table design needs to be fixed. You can use an identity column for ExpectedId. Just change the CREATE TABLE from Expected int to Expected int identity(1,1).
You can use a ROW_NUMBER() on your INSERT like that: ROW_NUMBER() OVER(ORDER BY x.number) as ExpectedId
This is failing because you set the #ExpectedID once, before your select statement, hence you get all the same values.
Try substituting the #ExpectedID parameter with the following line in your SELECT statement.
(SELECT ISNULL(MAX(ExpectedPaymentID) + 1, 1) FROM Payments_ExpectedPayments) as ExpectedPaymentID
This will execute the select statement once per row, which I guess should work.
I've written a query that groups the number of rows per hour, based on a given date range.
SELECT CONVERT(VARCHAR(8),TransactionTime,101) + ' ' + CONVERT(VARCHAR(2),TransactionTime,108) as TDate,
COUNT(TransactionID) AS TotalHourlyTransactions
FROM MyTransactions WITH (NOLOCK)
WHERE TransactionTime BETWEEN CAST(#StartDate AS SMALLDATETIME) AND CAST(#EndDate AS SMALLDATETIME)
AND TerminalId = #TerminalID
GROUP BY CONVERT(VARCHAR(8),TransactionTime,101) + ' ' + CONVERT(VARCHAR(2),TransactionTime,108)
ORDER BY TDate ASC
Which displays something like this:
02/11/20 07 4
02/11/20 10 1
02/11/20 12 4
02/11/20 13 1
02/11/20 14 2
02/11/20 16 3
Giving the number of transactions and the given hour of the day.
How can I display all hours of the day - from 0 to 23, and show 0 for those which have no values?
Thanks.
UPDATE
Using the tvf below works for me for one day, however I'm not sure how to make it work for a date range.
Using the temp table of 24 hours:
-- temp table to store hours of the day
DECLARE #tmp_Hours TABLE ( WhichHour SMALLINT )
DECLARE #counter SMALLINT
SET #counter = -1
WHILE #counter < 23
BEGIN
SET #counter = #counter + 1
--print
INSERT INTO #tmp_Hours
( WhichHour )
VALUES ( #counter )
END
SELECT MIN(CONVERT(VARCHAR(10),[dbo].[TerminalTransactions].[TransactionTime],101)) AS TDate, [#tmp_Hours].[WhichHour], CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108) AS TheHour,
COUNT([dbo].[TerminalTransactions].[TransactionId]) AS TotalTransactions,
ISNULL(SUM([dbo].[TerminalTransactions].[TransactionAmount]), 0) AS TransactionSum
FROM [dbo].[TerminalTransactions] RIGHT JOIN #tmp_Hours ON [#tmp_Hours].[WhichHour] = CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108)
GROUP BY [#tmp_Hours].[WhichHour], CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108), COALESCE([dbo].[TerminalTransactions].[TransactionAmount], 0)
Gives me a result of:
TDate WhichHour TheHour TotalTransactions TransactionSum
---------- --------- ------- ----------------- ---------------------
02/16/2010 0 00 4 40.00
NULL 1 NULL 0 0.00
02/14/2010 2 02 1 10.00
NULL 3 NULL 0 0.00
02/14/2010 4 04 28 280.00
02/14/2010 5 05 11 110.00
NULL 6 NULL 0 0.00
02/11/2010 7 07 4 40.00
NULL 8 NULL 0 0.00
02/24/2010 9 09 2 20.00
So how can I get this to group properly?
The other issue is that for some days there will be no transactions, and these days also need to appear.
Thanks.
You do this by building first the 23 hours table, the doing an outer join against the transactions table. I use, for same purposes, a table valued function:
create function tvfGetDay24Hours(#date datetime)
returns table
as return (
select dateadd(hour, number, cast(floor(cast(#date as float)) as datetime)) as StartHour
, dateadd(hour, number+1, cast(floor(cast(#date as float)) as datetime)) as EndHour
from master.dbo.spt_values
where number < 24 and type = 'p');
Then I can use the TVF in queries that need to get 'per-hour' basis data, even for missing intervals in the data:
select h.StartHour, t.TotalHourlyTransactions
from tvfGetDay24Hours(#StartDate) as h
outer apply (
SELECT
COUNT(TransactionID) AS TotalHourlyTransactions
FROM MyTransactions
WHERE TransactionTime BETWEEN h.StartHour and h.EndHour
AND TerminalId = #TerminalID) as t
order by h.StartHour
Updated
Example of a TVF that returns 24hours between any arbitrary dates:
create function tvfGetAnyDayHours(#dateFrom datetime, #dateTo datetime)
returns table
as return (
select dateadd(hour, number, cast(floor(cast(#dateFrom as float)) as datetime)) as StartHour
, dateadd(hour, number+1, cast(floor(cast(#dateFrom as float)) as datetime)) as EndHour
from master.dbo.spt_values
where type = 'p'
and number < datediff(hour,#dateFrom, #dateTo) + 24);
Note that since master.dbo.spt_values contains only 2048 numbers, the function will not work between dates further apart than 2048 hours.
You have just discovered the value of the NUMBERS table. You need to create a table with a single column containing the numbers 0 to 23 in it. Then you join again this table using an OUTER join to ensure you always get 24 rows returned.
So going back to using Remus' original function, I've re-used it in a recursive call and storing the results in a temp table:
DECLARE #count INT
DECLARE #NumDays INT
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
DECLARE #CurrentDay DATE
DECLARE #tmp_Transactions TABLE
(
StartHour DATETIME,
TotalHourlyTransactions INT
)
SET #StartDate = '2000/02/10'
SET #EndDate = '2010/02/13'
SET #count = 0
SET #NumDays = DateDiff(Day, #StartDate, #EndDate)
WHILE #count < #NumDays
BEGIN
SET #CurrentDay = DateAdd(Day, #count, #StartDate)
INSERT INTO #tmp_Transactions (StartHour, TotalHourlyTransactions)
SELECT h.StartHour ,
t.TotalHourlyTransactions
FROM tvfGetDay24Hours(#CurrentDay) AS h
OUTER APPLY ( SELECT COUNT(TransactionID) AS TotalHourlyTransactions
FROM [dbo].[TerminalTransactions]
WHERE TransactionTime BETWEEN h.StartHour AND h.EndHour
AND TerminalId = 4
) AS t
ORDER BY h.StartHour
SET #count = #Count + 1
END
SELECT *
FROM #tmp_Transactions
group by datepart('hour', thetime). to show those hours with no values you'd have to left join a table of times against the grouping (coalesce(transaction.amount, 0))
I've run into a version of this problem before. The suggestion that worked the best was to setup a table (temporary, or not) with the hours of the day, then do an outer join to that table and group by datepart('h', timeOfRecord).
I don't remember why, but probably due to lack of flexibility because of the need for the other table, I ended up using a method where I group by whatever datepart I want and order by the datetime, then loop through and fill any spaces that are skipped with a 0. This approach worked well for me because I'm not reliant on the database to do all my work for me, and it's also MUCH easier to write an automated test for it.
Step 1, Create #table or a CTE to generate a hours days table. Outer loop for days and inner loop hours 0-23. This should be 3 columns Date, Days, Hours.
Step 2, Write your main query to also have days and hours columns and alias it so you can join it. CTE's have to be above this main query and pivots should be inside CTE's for it to work naturally.
Step 3, Do a select from step 1 table and Left join this Main Query table
ON A.[DATE] = B.[DATE]
AND A.[HOUR] = B.[HOUR]
You can also create a order by if your date columns like
ORDER BY substring(CONVERT(VARCHAR(15), A.[DATE], 105),4,2)
Guidlines
This will then give you all data for hours and days and including zeros for hours with no matches to do that use isnull([col1],0) as [col1].
You can now graph facts against days and hours.