How to insert missing rows in a table? - sql

I have a table with the following columns (checkdate datetime, duration int). I have this stored procedure which groups the data per hour.
CREATE PROC [dbo].[last_hours]
#hours int
AS
BEGIN
SELECT
CAST(checkdate as date) AS ForDate,
DATEPART(hour, checkdate ) AS OnHour,
AVG(duration) AS Duration
FROM
pings
WHERE
DATEDIFF(hour, checkdate , GETDATE()) <= #hours
GROUP BY
CAST(checkdate as date),
DATEPART(hour, checkdate )
END
The proc works fine. However I have situations where data for certain hours is not available. In this case I need to add an extra "empty" row for every missing hour before returning the result to the client.
Is it possible to do this in an easy way without resorting to cursors and loops?

this uses recursive cte to generate a list of checkdate and then LEFT JOIN to the table.
CREATE PROC [dbo].[last_hours]
#hours int
AS
BEGIN
WITH hours as
(
SELECT checkdate = DATEADD(hour, -#hours, GETDATE())
UNION ALL
SELECT checkdate = DATEADD(hour, 1, checkdate)
FROM hours
WHERE checkdate < GETDATE()
)
SELECT
CAST(h.checkdate as date) AS ForDate,
DATEPART(hour, h.checkdate ) AS OnHour,
AVG(p.duration) AS Duration
FROM
hours h
LEFT JOIN pings p ON CAST(h.checkdate as date) = CAST(p.checkdate as date)
AND DATEPART(hour, h.checkdate) = DATEPART(hour, p.checkdate)
GROUP BY
CAST(h.checkdate as date),
DATEPART(hour, h.checkdate )
ORDER BY ForDate, OnHour
END

Related

Group By query for date based on Custom Time Period

I'm building a WCF application for calculating total time spend between in and out time, to fetch data from database I'm using GROUP BY clause to group data by date, but I want my day to start & end at 6:00 AM so if anyone leaves at 3 in the morning, it'll be added in the current day only. I'm using the following command query
SELECT MIN([Swipedatetime]) AS [Entry]
, MAX([Swipedatetime]) AS [Exit]
, [UserID]
FROM [Database_Name].[dbo].[Table_Name]
where UserID = '100'
GROUP
BY UserID
, CAST (Swipedatetime as DATE)
ORDER
BY MIN([Swipedatetime])
Also If there is any way by which the difference between the two times can be calculated in the stored procedure only then please mention it, it'll be of great use.
How about deducting 6 hours from the Swipedatetime and grouping by that new value:
GROUP BY (Swipedatetime - INTERVAL '6 hours')
(this is postgresql, for sql-server I think you need the function dateadd(hour, -6, Swipedatetime), or something along this line)
Your solution need only simple DATEADD function:
SELECT MIN([Swipedatetime]) AS [Entry]
, MAX([Swipedatetime]) AS [Exit]
, [UserID]
FROM [dbo].[Table_Name]
WHERE UserID = '100'
GROUP
BY UserID
, CAST (DATEADD(HOUR,6,Swipedatetime) AS DATE)
ORDER
BY MIN([Swipedatetime])
To get only records between 6am and 6pm you can use this:
where datepart(hour,[Swipedatetime]) > 6
and datepart(hour,[Swipedatetime]) <=18
For the diff you can use this:
select DATEDIFF(minute, MIN([Swipedatetime]), MAX([Swipedatetime]))
So full query:
declare #StartDate datetime = dateadd(HH, 6, convert(datetime, convert(date, getdate())))
declare #EndDate datetime = dateadd(day,1,#Startdate)
SELECT MIN([Swipedatetime]) AS [Entry]
, MAX([Swipedatetime]) AS [Exit]
, DATEDIFF(minute, MIN([Swipedatetime]), MAX([Swipedatetime])) AS[Diff]
, [UserID]
FROM [Database_Name].[dbo].[Table_Name]
where UserID = '100'
and [Swipedatetime] >= #Startdate
and [Swipedatetime] < #EndDate
GROUP
BY UserID
, CAST (Swipedatetime as DATE)
ORDER
BY MIN([Swipedatetime])

SQL Server: whole weeks total in a calendar month

I want weekly totals in a month. It will not include any partial week or future weeks. Week starts from Monday to Sunday.
I have a table structure like
Date Value -- Comments
----------------------------------------------------------------------
2016-10-01 7 Ignore this because its not a whole week in a month
2016-10-05 8 Week 1
2016-10-07 5 Week 1
2016-10-11 2 Week 2
2016-10-15 1 Week 2
2016-10-17 9 Ignore this because the week is not finished yet
OUTPUT
WeekNo Total
41 13
42 3
The easier way would be to build a Tally "date" table.
you can generate it from any Tally Table like:
DECLARE #StartDate DATE = '20160101'
, #EndDate DATE = '20161231';
WITH cte AS (
SELECT DATEADD(DAY, n - 1, #StartDate) AS date
FROM tally
WHERE n - 1 <= DATEDIFF(DAY, #StartDate, #EndDate)
)
SELECT
c.date
,YEAR(c.date) AS Year
,MONTH(c.date) AS Month
,DAY(c.date) AS Month
,DATEPART(WEEK,c.date) AS Week
,CASE WHEN 7<>COUNT(c.date) OVER (PARTITION BY YEAR(c.date),MONTH(c.date),DATEPART(WEEK,c.date)) THEN 0 ELSE 1 END AS isFullWeek
FROM cte c
Then you just need to Join it to what ever query you need.
DECLARE #StartDate datetime = '2011-10-01';
DECLARE #EndDate datetime = '2016-10-31';
SELECT
CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 2, tblData.RecordDate) AS date) AS WeekStart,
CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date) AS WeekEnd,
SUM(Value) AS Total
FROM tblData
WHERE (#StartDate IS NULL
OR CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 2, tblData.RecordDate) AS date) >= CAST(#StartDate AS date))
AND (#EndDate IS NULL
OR CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date) <= CAST(#EndDate AS date))
AND CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date) < CAST(GETDATE() AS date)
GROUP BY CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 2, tblData.RecordDate) AS date),
CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date)
Create a calendar table that meets your request, like this:
create table calendarTable ([date] date, weekNro int)
go
insert into calendarTable
select dateadd(d,n,'20160101'), DATEPART(WK,dateadd(d,n,'20151231'))
from numbers where n<500
If you don't have a Numbers Table, you must create it first. like this
SET NOCOUNT ON
CREATE TABLE Numbers (n bigint PRIMARY KEY)
GO
DECLARE #numbers table(number int);
WITH numbers(number) as (
SELECT 1 AS number
UNION all
SELECT number+1 FROM numbers WHERE number<10000
)
INSERT INTO #numbers(number)
SELECT number FROM numbers OPTION(maxrecursion 10000)
INSERT INTO Numbers(n) SELECT number FROM #numbers
Then query your table joining calendar table having in mind actual date for completed week, like this:
Similar to #Kilren but translated into postgres and using generate series from https://stackoverflow.com/a/11391987/10087503 to generate the dates
DECLARE #StartDate DATE = '20160101'
, #EndDate DATE = '20161231';
WITH cte AS (
SELECT i::date AS date FROM generate_series(#StartDate,
#EndDate, '1 day'::interval) i
)
SELECT
c.date
,DATE_TRUNC('month' ,c.date) AS month_trunc
,DATE_PART('week',c.date) AS week
,CASE WHEN 7<>COUNT(c.date)
OVER (PARTITION BY DATE_TRUNC('month' ,c.date),DATE_PART('week',c.date))
THEN 0 ELSE 1 END AS is_full_week
FROM cte c
Select DATEPART(ww, date) , SUM(Case When Comments Like '%1' then Value when Comments Like '%2' then Value else Value end)
from schema.tablename
group by DATEPART(ww,date)
I'm sorry if this doesn't work, it's the only way I thought to structure it.

avg data of last week in a specific hour

I created a sql query that return a AVG data of last hour. Example: right now are 11h this return the average of data at 10h.
Now I need to improve my query, I want the average data of last hour and the average data of all days of the last week in the same hour. How can I do that ?
This is my query:
DECLARE #begin_time smalldatetime,
#end_time smalldatetime
SET #begin_time = CONVERT(VARCHAR(19), GETDATE(), 120)
SET #begin_time = DateADD(HOUR, -1, #begin_time)
SET #begin_time = convert(char(14),#begin_time,121)+'00:00'
SET #end_time = CONVERT(VARCHAR(19), GETDATE(), 120)
SET #end_time = DateADD(HOUR, -1, #end_time)
SET #end_time = convert(char(14),#end_time,121)+'59:00'
SELECT u.name,
YEAR(dpr.reading_time) AS "year",
MONTH(dpr.reading_time) AS "month",
DAY(dpr.reading_time) AS "day",
DATEPART(HOUR, dpr.reading_time) AS "hour",
AVG(CAST(CAST(dpr.value as DECIMAL(22, 5))/POWER(10, 5) as DECIMAL(20, 5))) * 1 AS "data"
FROM example.users u
INNER JOIN example.datapoint_readings dpr
ON dpr.datapoint_id = u.datapoint_id
WHERE reading_time BETWEEN #begin_time AND #end_time
GROUP BY u.name, YEAR(dpr.reading_time), MONTH(dpr.reading_time), DAY(dpr.reading_time), DATEPART(HOUR, dpr.reading_time)
ORDER BY u.name, YEAR(dpr.reading_time), MONTH(dpr.reading_time), DAY(dpr.reading_time), DATEPART(HOUR, dpr.reading_time)
If I understand correctly, you want to see the previous hour on each date for some period of time:
SELECT u.name, cast(dpr.reading_time as date) as reading_time_date,
DATEPART(HOUR, dpr.reading_time) as "hour",
AVG(CAST(CAST(dpr.value as DECIMAL(22, 5))/POWER(10, 5) as DECIMAL(20, 5))) * 1 AS "data"
FROM example.users u INNER JOIN
example.datapoint_readings dpr
ON dpr.datapoint_id = u.datapoint_id
WHERE datepart(hour, reading_time) = datepart(hour, dateadd(hour, -1, getdate())) AND
reading_time >= dateadd(day, -7, getdate())
GROUP BY u.name, cast(dpr.reading_time as date),
DATEPART(HOUR, dpr.reading_time)
ORDER BY u.name, cast(dpr.reading_time as date),
DATEPART(HOUR, dpr.reading_time)
The important difference is the WHERE clause. The first condition extracts the hour for comparison. The second limits the results only to the past week.

SQL Count with zero values

I want to create a graph for my dataset for the last 24 hours.
I found a solution that works but this is pretty bad since the table I am outer joining cotains every single row in the DB since I am using the (now deprecated) "all" parameter in the group by.
Here is the solution that currently kind of works.
First I declare the date intervals that is 24 hours back in time from now. I declare it twice so I can use it later in the procedure aswell.
Declare #StartDate datetime = dateadd(hour, -24, getdate())
Declare #StartDateProc datetime = dateadd(hour, -24, getdate())
Declare #EndDate datetime = getdate()
I populate the dates into a temp table including a special formated datetsring.
create table #tempTable
(
Date datetime,
DateString varchar(11)
)
while #StartDate <= #EndDate
begin
insert into #tempTable (Date, DateString)
values (#StartDate, convert(varchar(8), #StartDate, 5) + '-' + convert(varchar(2), #StartDate, 108));
SET #StartDate = dateadd(hour,1, #StartDate);
end
This gives me data that looks like this:
Date DateString
---------------------------------------------
2015-12-09 13:59:01.970 09-12-15-13
2015-12-09 14:59:01.970 09-12-15-14
2015-12-09 15:59:01.970 09-12-15-15
2015-12-09 16:59:01.970 09-12-15-16
So what I want is to join my dataset on the matching date string and show the date even if the matching rows is zero.
Here is the rest of the query
select
Date = c.Date,
Amount = sum(c.Amount)
from
DbTable a
outer apply
(select
Date = b.DateString,
Amount = count(*)
from
#tempTable b
where
convert(varchar(8), a.DateColumn, 5) + '-' + convert(varchar(2), a.DateColumn, 108) = b.DateString
group by all
b.DateString) c
where
a.SomeParameter = 'test' and
a.DateColumn >= #StartDateProc and
a.DateColumn <= #EndDate
group by
c.Date
drop table #tempTable
Test to show actual data:
Declare #StartDate datetime = dateadd(hour, -24, getdate())
Declare #EndDate datetime = getdate()
select
dateString = convert(varchar(8),a.DateColumn,5) + '-' + convert(varchar(2),a.DateColumn, 108),
Amount = COUNT(*)
from
DbTable a
where
a.someParameter = 'test' and
a.DateColumn>= dateadd(hour, -24, getdate()) and
a.DateColumn<= getdate()
group by
convert(varchar(8),a.DateColumn,5) + '-' + convert(varchar(2),a.DateColumn, 108)
First output rows:
dateString Amount
09-12-15-14 1
09-12-15-15 1
09-12-15-16 1
09-12-15-17 3
09-12-15-18 1
09-12-15-22 3
09-12-15-23 2
As you can see here there is no data for the times from 19.00 to 21.00. This is how I want the data to be displayed:
dateString Amount
09-12-15-14 1
09-12-15-15 1
09-12-15-16 1
09-12-15-17 3
09-12-15-18 1
09-12-15-19 0
09-12-15-20 0
09-12-15-21 0
09-12-15-22 3
09-12-15-23 2
Normally, this would be approached with left join rather than outer apply. The logic is simple: keep all rows in the first table along with any matching information from the second. This means put the dates table first:
select tt.DateString, count(t.DateColumn) as Amount
from #tempTable tt left join
DbTable t
on convert(varchar(8), t.DateColumn, 5) + '-' + convert(varchar(2), t.DateColumn, 108) = tt.DateString and
t.SomeParameter = 'test'
where tt.Date >= #StartDateProc and
tt.Date <= #EndDate
group by tt.DateString;
In addition, your comparison for the dates seems overly complex, but if it works for you, it works.
The best bet here would be to use DATETIME type itself and not to lose the opportunity to use indexes:
Declare #d datetime = GETDATE()
;WITH cte1 AS(SELECT TOP 25 -1 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) h
FROM master..spt_values),
cte2 AS(SELECT DATEADD(hh, -h, #d) AS startdate,
DATEADD(hh, -h + 1, #d) AS enddate
FROM cte1)
SELECT c.startdate, c.enddate, count(*) as amount
FROM cte2 c
LEFT JOIN DbTable a ON a.DateColumn >= c.startdate AND
a.DateColumn < c.enddate AND
a.SomeParameter = 'test'
GROUP BY c.startdate, c.enddate

How to find the total playing time per day for all the users in my sql server database

I have a table which contains following columns
userid,
game,
gameStarttime datetime,
gameEndtime datetime,
startdate datetime,
currentdate datetime
I can retrieve all the playing times but I want to count the total playing time per DAY and 0 or null if game not played on a specific day.
Take a look at DATEDIFF to do the time calculations. Your requirements are not very clear, but it should work for whatever you're looking to do.
Your end result would probably look something like this:
SELECT
userid,
game,
DATEDIFF(SS, gameStarttime, gameEndtime) AS [TotalSeconds]
FROM [source]
GROUP BY
userid,
game
In the example query above, the SS counts the seconds between the 2 dates (assuming both are not null). If you need just minutes, then MI will provide the total minutes. However, I imagine total seconds is best so that you can convert to whatever unit of measure you need accurate, such as hours that might be "1.23" or something like that.
Again, most of this is speculation based on assumptions and what you seem to be looking for. Hope that helps.
MSDN Docs for DATEDIFF: https://msdn.microsoft.com/en-us/library/ms189794.aspx
You may also look up DATEPART if you want the minutes and seconds separately.
UPDATED BASED ON FEEDBACK
The query below breaks out the hour breakdowns by day, splits time across multiple days, and shows "0" for days where no games are played. Also, for your output, I have to assume you have a separate table of users (so you can show users who have no time in your date range).
-- Define start date
DECLARE #BeginDate DATE = '4/21/2015'
-- Create sample data
DECLARE #Usage TABLE (
userid int,
game nvarchar(50),
gameStartTime datetime,
gameEndTime datetime
)
DECLARE #Users TABLE (
userid int
)
INSERT #Users VALUES (1)
INSERT #Usage VALUES
(1, 'sample', '4/25/2015 10pm', '4/26/2015 2:30am'),
(1, 'sample', '4/22/2015 4pm', '4/22/2015 4:30pm')
-- Generate list of days in range
DECLARE #DayCount INT = DATEDIFF(DD, #BeginDate, GETDATE()) + 1
;WITH CTE AS (
SELECT TOP (225) [object_id] FROM sys.all_objects
), [Days] AS (
SELECT TOP (#DayCount)
DATEADD(DD, ROW_NUMBER() OVER (ORDER BY x.[object_id]) - 1, #BeginDate) AS [Day]
FROM CTE x
CROSS JOIN CTE y
ORDER BY
[Day]
)
SELECT
[Days].[Day],
Users.userid,
SUM(COALESCE(CONVERT(MONEY, DATEDIFF(SS, CASE WHEN CONVERT(DATE, Usage.gameStartTime) < [Day] THEN [Day] ELSE Usage.gameStartTime END,
CASE WHEN CONVERT(DATE, Usage.gameEndTime) > [Day] THEN DATEADD(DD, 1, [Days].[Day]) ELSE Usage.gameEndTime END)) / 3600, 0)) AS [Hours]
FROM [Days]
CROSS JOIN #Users Users
LEFT OUTER JOIN #Usage Usage
ON Usage.userid = Users.userid
AND [Days].[Day] BETWEEN CONVERT(DATE, Usage.gameStartTime) AND CONVERT(DATE, Usage.gameEndTime)
GROUP BY
[Days].[Day],
Users.userid
The query above yields the output below for the sample data:
Day userid Hours
---------- ----------- ---------------------
2015-04-21 1 0.00
2015-04-22 1 0.50
2015-04-23 1 0.00
2015-04-24 1 0.00
2015-04-25 1 2.00
2015-04-26 1 2.50
2015-04-27 1 0.00
I've edited my sql on sql fiddle and I think this might get you what you asked for. to me it looks a little more simple then the answer you've accepted.
DECLARE #FromDate datetime, #ToDate datetime
SELECT #Fromdate = MIN(StartDate), #ToDate = MAX(currentDate)
FROM Games
-- This recursive CTE will get you all dates
-- between the first StartDate and the last CurrentDate on your table
;WITH AllDates AS(
SELECT #Fromdate As TheDate
UNION ALL
SELECT TheDate + 1
FROM AllDates
WHERE TheDate + 1 <= #ToDate
)
SELECT UserId,
TheDate,
COALESCE(
SUM(
-- When the game starts and ends in the same date
CASE WHEN DATEDIFF(DAY, GameStartTime, GameEndTime) = 0 THEN
DATEDIFF(HOUR, GameStartTime, GameEndTime)
ELSE
-- when the game starts in the current date
CASE WHEN DATEDIFF(DAY, GameStartTime, TheDate) = 0 THEN
DATEDIFF(HOUR, GameStartTime, DATEADD(Day, 1, TheDate))
ELSE -- meaning the game ends in the current date
DATEDIFF(HOUR, TheDate, GameEndTime)
END
END
),
0) As HoursPerDay
FROM (
SELECT DISTINCT UserId,
TheDate,
CASE
WHEN CAST(GameStartTime as Date) = TheDate
THEN GameStartTime
ELSE NULL
END As GameStartTime, -- return null if no game started that day
CASE
WHEN CAST(GameEndTime as Date) = TheDate
THEN GameEndTime
ELSE NULL
END As GameEndTime -- return null if no game ended that day
FROM Games CROSS APPLY AllDates -- This is where the magic happens :-)
) InnerSelect
GROUP BY UserId, TheDate
ORDER BY UserId, TheDate
OPTION (MAXRECURSION 0)
Play with it your self on sql fiddle.