Sum days between differing date/times progromatically - sql-server

I have a query that works perfectly fine, by summing the values for a day.
SELECT CAST(fldDateTime AS DATE) AS DayValue, SUM(fldValue) AS Val
FROM [dbo].[Data.tblMeterData]
GROUP BY CAST(fldDateTime AS DATE)
ORDER BY DayValue
The problem I have is that I needed to have the data greater than midnight and up until midnight of the next day. To test I can do this I tested the following code, which helped me deal with the first lot of values that do not have a full day.
DECLARE #a DATETIME
DECLARE #b DATETIME
SET #a = CAST('2016-03-21 00:01:00' AS DATETIME)
SET #b = DATEADD(SECOND,-86399 ,#a)
SELECT #a AS a, #b AS b
SELECT CAST(fldDateTime AS DATE) AS DayValue, SUM(fldValue) AS Val
FROM [dbo].[Data.tblMeterData]
WHERE fldDateTime BETWEEN #b AND #a
GROUP BY CAST(fldDateTime AS DATE)
ORDER BY DayValue
The problem with this is that I do not want to loop through dates, as I have thousands of ID's that I need to process in this way.I cannot include midnight for the first date as the first data is recorded after midnight and the final daily reading is at midnight the following day e.g.
Date > '2016-03-20 00:00:00' AND <= 2016-03-21 00:00:00
How can I do what I need to do the following:
Find the first date for an ID and move up to the first midnight of the following day and repeat this for all following days.
Sum these values so that they are greater than midnight and up to midnight of the next day.

My understanding of the requirements is you are looking for the sum of a value group by meter ID and date, but for each date we also want to include the next day's values. This means each value would count in the sum for its day and the previous day.
Code:
--generate test data
declare #tblMeterData table (
[ID] [int] IDENTITY(1,1) NOT NULL,
[tblMeterData_Id] [int] NOT NULL,
[fldDateTime] [datetime] NOT NULL,
[fldValue] [decimal](18, 2) NULL,
[fldBatchId] [uniqueidentifier] NOT NULL);
insert #tblMeterData (tblMeterData_Id, fldDateTime, fldValue, fldBatchId) values
(18, '2016-12-19 23:59:59', 1.0, newid()),
(18, '2016-12-20 00:00:00', 2.0, newid()),
(18, '2016-12-20 00:30:00', 3.0, newid()),
(18, '2016-12-20 01:00:00', 4.0, newid()),
(18, '2016-12-20 01:30:00', 5.0, newid()),
(18, '2016-12-21 00:00:00', 6.0, newid()),
(18, '2016-12-21 00:30:00', 7.0, newid()),
(18, '2016-12-22 00:00:00', 8.0, newid()),
(18, '2016-12-23 00:00:00', 9.0, newid()),
(19, '2016-12-20 00:00:00', 10.0, newid());
--select * from #tblMeterData order by ID;
--main query
with cte as (
--0:00:00 reports on previous day
select *, cast(dateadd(S, -1, fldDateTime) as date) group_date
from #tblMeterData
union all
--duplicate all records to also group on previous day
select *, cast(dateadd(D, -1, dateadd(S, -1, fldDateTime)) as date) group_date
from #tblMeterData
)
select tblMeterData_Id, group_date, sum(fldValue) sum_value
from cte
group by tblMeterData_Id, group_date
order by tblMeterData_Id, group_date;
Results:
tblMeterData_Id group_date sum_value
18 2016-12-18 3.00
18 2016-12-19 21.00
18 2016-12-20 33.00
18 2016-12-21 24.00
18 2016-12-22 9.00
19 2016-12-18 10.00
19 2016-12-19 10.00

The answer is as follows:
WITH cte AS (
SELECT *, CAST(DATEADD(S, -1, fldDateTime) AS DATE) group_date
FROM [dbo].[Data.tblMeterData]
)
SELECT tblMeterData_Id, group_date, SUM(fldValue) sum_value
FROM cte
GROUP BY tblMeterData_Id, group_date
ORDER BY tblMeterData_Id, group_date;
It works perfectly.

Related

TSQL: Continuous period for whole year / each month

I try to find all Cust who have membership for at least for one day in each month during 2018.
I came up with solution checking their membership at the beginning / middle / end end of each month like in snippet below, but trying to find more intelligent solution.
I know that I can use tally table for each of 365 days to check this but probably there is more elegant solution ? I'm bit new to SQL, I think I'm missing something in GROUPing area.
In the code snippet shown below, both Cust have at least one day membership.
Desired output:
CustID
------
1
22
Code:
with data as
(
select *
from (values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')) as t (CustID, ContractID, StartDD, EndDD) ---
)
select
isdate(startDD), isdate(EndDD)
from
data
), gaps as
(
select
*,
datediff(day, lag(EndDD, 1, StartDD) over (partition by CustID order by StartDD), StartDD) as BreakDD -- negative is island
from
data
)
select
*,
datepart(month,StartDD) mmS , datepart(month,EndDD) mmE
from
gaps
-- and was active any 1+ day during each of the 12 months in 2018 ????
where
1 = 1
/* and (cast('1/1/2018' as date) between StartDD and EndDD
or cast('1/15/2018' as date) between StartDD and EndDD
or cast('1/31/2018' as date) between StartDD and EndDD)
---- etc.. for each month
and ( cast('12/1/2018' as date) between StartDD and EndDD
or cast('12/15/2018' as date) between StartDD and EndDD
or cast('12/31/2018' as date) between StartDD and EndDD
)
*/
--select CustID, max(BreakDD) Max_Days
--from gaps
--group by CustID
Try this answer.
First create a function to return all the month and year between the given dates.
Function:
--SELECT * FROM dbo.Fn_GetMonthYear('2017-12-11','2018-01-16')
ALTER FUNCTION dbo.Fn_GetMonthYear(#StartDate DATETIME,#EndDate DATETIME)
RETURNS TABLE
AS
RETURN(
SELECT DATEPART(MONTH, DATEADD(MONTH, x.number, #StartDate)) AS [Month]
,DATEPART(YEAR, DATEADD(MONTH, x.number, #StartDate)) AS [Year]
FROM master.dbo.spt_values x
WHERE x.type = 'P'
AND x.number <= DATEDIFF(MONTH, #StartDate, #EndDate)
)
Table Schema:
CREATE TABLE #t(CustID INT, ContractID INT, StartDD date, EndDD date)
INSERT INTO #t values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')
Here is the T-SQL Query for your requirement.
SELECT CustID
,COUNT(DISTINCT [Month]) NoOfMonths
FROM(
SELECT *
FROM #t t
CROSS APPLY dbo.Fn_GetMonthYear(StartDD,EndDD)
)D
WHERE [Year] = 2018
GROUP BY CustID
HAVING COUNT(DISTINCT [Month])=12
Result:
CustID NoOfMonths
1 12
22 12
find all Cust who have membership for at least for one day in each
month during 2018
I think this mean that data must be present between '2018-01-01' and '2018-12-31' for each custid.
CREATE TABLE #t(CustID INT, ContractID INT, StartDD date, EndDD date)
INSERT INTO #t values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')
declare #From Datetime='2018-01-01'
declare #To datetime='2018-12-31'
;with CTE as
(
select CustID,min(StartDD)StartDD
,max(EndDD)EndDD
from #t
group by CustID
)
select CustID,StartDD
,EndDD
from CTE
where StartDD<=#From and EndDD>=#To
This script is not tested across all sample data.
But logic is clear.So it can be corrected accordingly.
So tell for what sample data it is not working.

How to count number per month and then take average of it in same select statement

How to calculate total average per month in case like below?:
We have 9 claimID's. so Aveage would be 9/ 6 distinct months = 1.5
DECLARE #TestTable TABLE (claimid int, DateClosed datetime)
INSERT INTO #TestTable
VALUES (111, '01-01-2018'), (222, '01-03-2018'), (333, '01-12-2018'),
(444, '07-03-2018'), (555, '08-15-2018'), (666, '09-13-2018'),
(777, '04-03-2019'), (888, '05-01-2019'), (999, '07-01-2018'),
(1000, NULL), (1100, NULL), (1200, NULL)
SELECT
ClaimID,
CAST(DateClosed AS DATE) AS DateClosed,
COUNT(ClaimID) CountClaimID,
COUNT(claimid) OVER (PARTITION BY MONT(DateClosed), YEAR(DateClosed)) AS CountPerMonth
FROM
#TestTable
GROUP BY
ClaimID, DateClosed
Perhaps something like this
Example
SELECT ClaimID
,cast(DateClosed AS date) AS DateClosed
,count(ClaimID) CountClaimID
,count(claimid) OVER ( PARTITION BY Month(DateClosed), year(DateClosed)) AS CountPerMonth
,case when DateClosed is null then 0 else count(DateClosed) over () / (select 0.0+count(distinct left(cast(DateClosed as date),7)) from #TestTable) end AS TotalAverage
FROM #TestTable
GROUP BY ClaimID,DateClosed
Returns

Dynamically set the Start and End time of Day

I have this table in which I am storing TimeIn and Time Out of Employee.
When I get Total Hours any Employee have worked in certain day, it works fine date wise. But in out organization the issue is that a day is considered from 6 AM till 5:59 AM (next day).
Here is my table and sample data.
CREATE TABLE [dbo].[Attendance]
(
[Employee] [varchar](50) NULL,
[TimeIn] [datetime] NULL,
[TimeOut] [datetime] NULL
) ON [PRIMARY]
GO
INSERT [dbo].[Attendance] ([Employee], [TimeIn], [TimeOut]) VALUES (N'Lewis', CAST(N'2018-12-01 06:30:00.000' AS DateTime), CAST(N'2018-12-01 18:22:00.000' AS DateTime))
GO
INSERT [dbo].[Attendance] ([Employee], [TimeIn], [TimeOut]) VALUES (N'Lewis', CAST(N'2018-12-01 20:12:00.000' AS DateTime), CAST(N'2018-12-01 23:50:00.000' AS DateTime))
GO
INSERT [dbo].[Attendance] ([Employee], [TimeIn], [TimeOut]) VALUES (N'Lewis', CAST(N'2018-12-02 00:12:00.000' AS DateTime), CAST(N'2018-12-02 04:50:00.000' AS DateTime))
GO
INSERT [dbo].[Attendance] ([Employee], [TimeIn], [TimeOut]) VALUES (N'Lewis', CAST(N'2018-12-02 07:21:00.000' AS DateTime), CAST(N'2018-12-02 19:54:00.000' AS DateTime))
GO
Here is the query and output of the query I am executing.
SELECT Employee, CAST(COALESCE(TimeIn, TimeOut) AS DATE) DATE, DATEDIFF(HOUR, MIN(TimeIn), MAX(TimeOut)) [Hours Worked]
FROM [dbo].[Attendance]
GROUP BY Employee, CAST(COALESCE(TimeIn, TimeOut) AS DATE)
Output:
Employee DATE Hours Worked
----------------- ---------- ------------
Lewis 2018-12-01 17
Lewis 2018-12-02 19
What I want is to get the working hours calculated from 6 AM to 5:59 AM next day. So the expected output is as below:
Employee DATE Hours Worked
----------------- ---------- ------------
Lewis 2018-12-01 22:20
Lewis 2018-12-02 12:33
Hope this is possible..
You should probably have a calendar table which contains all the dates which you want to appear in your report. In the absence of that, we can just assume that all dates are covered by the time, and we can group by the time in, shifted earlier by 6 hours. The trick here is that we can shift all times backwards by 6 hours, to align everything with the usual 24 hour day. Something like this should work:
SELECT
Employee,
CONVERT(date, DATEADD(HOUR, -6, TimeIn)) AS DATE,
CONVERT(VARCHAR(10), DATEDIFF(HOUR, MIN(TimeIn), MAX(TimeOut))) + ':' +
CONVERT(VARCHAR(10), DATEDIFF(MINUTE, MIN(TimeIn), MAX(TimeOut)) % 60) AS [Hours Worked]
FROM Attendance
GROUP BY
Employee,
CONVERT(date, DATEADD(HOUR, -6, TimeIn));
Demo

FirstIn and LastOut is not working

I want to calculate total working hours of an employee for specific day. With my current query it is giving me records for all days but what I want is to get for each day.
Like I have 2 days records so it should show 2 rows but instead it is only returning one.
DECLARE #T TABLE
([EmpID] int, [TimeIn] datetime, [TimeOut] datetime);
INSERT INTO #T
([EmpID], [TimeIn], [TimeOut]) VALUES
(1, '2018-01-10 9:00:00', NULL),
(1, NULL, '2018-01-10 11:00'),
(1, '2018-01-10 11:30:00', NULL),
(1, NULL, '2018-01-10 13:00'),
(1, '2018-01-10 13:30:00', NULL),
(1, NULL, '2018-01-10 18:00'),
(1, '2018-01-11 9:00:00', NULL),
(1, NULL, '2018-01-11 11:00'),
(1, '2018-01-11 11:30:00', NULL),
(1, NULL, '2018-01-11 13:00'),
(1, '2018-01-11 13:30:00', NULL),
(1, NULL, '2018-01-11 18:00')
;
;WITH TM
AS
(
SELECT
EmpID,
MIN([TimeIn]) as StartTime,
MAX([TimeOut]) as EndTime
FROM #T
GROUP BY EmpId
)
SELECT
*,
HoursSpent = DATEDIFF(HOUR, StartTime, EndTime)
FROM TM
What I really want is that by default it should give me yesterday's report or otherwise I could pass value at run time too like I want report of 10th Jan 2018 and something like that but I guess that part is far away as I'm stuck here.
Try:
;WITH cte AS
(
SELECT empid
, MIN(timein) timein
, MAX(timeout) timeout
, cast(coalesce(timein, timeout) AS DATE) d
FROM #T
GROUP BY empid, cast(coalesce(timein, timeout) AS DATE)
)
SELECT empid
, d AS Day
, DateDiff(HOUR, TimeIn, TimeOut) [Hours Worked]
FROM cte
ORDER BY d ASC
;WITH
TM1 as
(
SELECT EmpID,
convert(date, COALESCE(TimeIn, [TimeOut])) as [Date],
TimeIn,
[TimeOut],
grp = (ROW_NUMBER() OVER (PARTITION BY EmpID ORDER BY COALESCE(TimeIn, [TimeOut])) - 1) / 2 + 1
from #T
),
TM2
AS
(
SELECT EmpID,
[Date],
DATEDIFF(HOUR, MIN([TimeIn]), MAX([TimeOut])) as HoursSpent
FROM TM1
GROUP BY EmpID, [Date], grp
)
SELECT EmpID, [Date], SUM(HoursSpent) as HoursSpent
FROM TM2
GROUP BY EmpID, [Date]
/*
RESULT :
EmpID Date HoursSpent
1 2018-01-10 9
1 2018-01-11 9
*/
if you want higher precision, use DATEDIFF(minute) and divide by 60.0
Just Add a Variable to Specify the Date
DECLARE #MyDate DATE
Assign the date to the Variable if you want to get the records for a specific date else keep it NULL.
Now, Change your Code of the Common Table Expression TM as below -- To Add the Where Condition
;WITH TM
AS
(
SELECT
EmpID,
MIN([TimeIn]) as StartTime,
MAX([TimeOut]) as EndTime
FROM #T
WHERE
CAST(ISNULL(#MyDate,GETDATE()-1) AS DATE) = CAST(ISNULL([TimeIn],[TimeOut]) AS DATE)
GROUP BY EmpId
)
SELECT
*,
HoursSpent = DATEDIFF(HOUR, StartTime, EndTime)
FROM TM
When #MyDate = '2018-01-10'
When #MyDate IS NULL

TSQL - Find the 1st or 2nd day of the week

I have a table of stock prices and need to get prices for the 1st day of each week. This SQL in the WHERE clause works well,
DATEADD(ww, DATEDIFF(ww,0,PriceDt), 0)
except when the market is closed on Monday. Labor Day is a good example. I thought using COALESCE would give me the price on Tuesday if one were unavailable for Monday, but this didn't work.
coalesce(DATEADD(ww, DATEDIFF(ww,0,PriceDt), 0), DATEADD(ww, DATEDIFF(ww,0,PriceDt), 1)).
Can someone help with this?
declare #t table (PriceDt datetime, Symbol nvarchar(10), OpenPric float, ClosePrice float)
insert #t values ('2010-08-02 00:00:0.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-08-09 00:00:00.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-08-16 00:00:00.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-08-23 00:00:00.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-08-30 00:00:00.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-09-07 00:00:00.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-09-13 00:00:00.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-09-20 00:00:00.000', 'SYM', 15.00, 15.10)
insert #t values ('2010-09-27 00:00:00.000', 'SYM', 15.00, 15.10)
select * from #t
where PriceDt = coalesce(DATEADD(ww, DATEDIFF(ww,0,PriceDt), 0), DATEADD(ww, DATEDIFF(ww,0,PriceDt), 1))
(missing 2010-09-07 00:00:00.000 in the result)
2010-08-02 00:00:00.000 SYM 15 15.1
2010-08-09 00:00:00.000 SYM 15 15.1
2010-08-16 00:00:00.000 SYM 15 15.1
2010-08-23 00:00:00.000 SYM 15 15.1
2010-08-30 00:00:00.000 SYM 15 15.1
2010-09-13 00:00:00.000 SYM 15 15.1
2010-09-20 00:00:00.000 SYM 15 15.1
2010-09-27 00:00:00.000 SYM 15 15.1
This will give you the earliest date that exists in the table for each week (assuming that your week starts on Monday):
select min(Pricedt) Pricedt
from #t
group by DATEADD(ww, DATEDIFF(ww,0,PriceDt), 0)
Now you can just join that result to your table to get the prices for whatever is the first day of the week that has data entered:
select t.Pricedt, t.Symbol, t.OpenPric, t.ClosePrice
from
(
select min(Pricedt) Pricedt
from #t
group by DATEADD(ww, DATEDIFF(ww,0,PriceDt), 0)
) d
join #t t on d.Pricedt = t.PriceDt

Resources