how to generate summary table with latest record - sql

Suppose I have an action table with some dummy records as below:
create table action
(
userid int
,date datetime
,action varchar(5)
)
insert into action values(1, '2017-03-24 00:00:00','1')
insert into action values(2, '2017-03-24 00:00:00','2')
insert into action values(3, '2017-03-24 00:00:00','1')
insert into action values(4, '2017-03-24 00:00:00','2')
insert into action values(4, '2017-03-24 00:01:00','1')
insert into action values(5, '2017-03-24 00:00:00','1')
insert into action values(1, '2017-03-23 00:00:00','1')
insert into action values(2, '2017-03-23 00:00:00','1')
insert into action values(3, '2017-03-23 00:00:00','2')
insert into action values(4, '2017-03-23 00:00:00','1')
insert into action values(1, '2017-03-23 00:01:00','2')
insert into action values(2, '2017-03-23 00:02:00','2')
insert into action values(1, '2017-03-23 00:03:00','1')
insert into action values(2, '2017-03-23 00:05:00','1')
I want to generate a summary table so that for each user, it shows the latest status of that day. The result would be like the following.
create table summary
(
userid int
,date datetime
,status varchar(5)
)
date userid status
2017-03-24 1 1
2017-03-24 2 2
2017-03-24 3 1
2017-03-24 4 1
2017-03-24 5 1
2017-03-23 1 1
2017-03-23 2 1
2017-03-23 3 2
2017-03-23 4 1
I tried using row_number() over (partition by userid order by date desc)
but the result wasn't as expected.
Any help please?
I'm using sql server but any database syntax are welcomed!

You need to partition by the date:
select a.*
from (select a.*,
row_number() over (partition by a.userid, cast(a.date as date)
order by a.date desc
) as seqnum
from action a
) a
where seqnum = 1

You can use this.
INSERT INTO [summary]
SELECT a.userid
,DATEADD(dd, 0, DATEDIFF(dd, 0, [date]))
, a.[action]
FROM [action] a
WHERE a.[date] IN
(
SELECT MAX([date])
FROM [action] a1 WHERE a.userid = a1.userid
GROUP BY userid, DATEADD(dd, 0, DATEDIFF(dd, 0, [date]))
)

Related

Partition the date into a weeks from a given date to the last date in the record

I wanted to count the time gap between two rows for the same id if the second is less than an hour after the first, and partition the count for the week.
Suppose given date with time is 2020-07-01 08:00
create table #Temp (
Id integer not null,
Time datetime not null
);
insert into #Temp values (1, '2020-07-01 08:00');
insert into #Temp values (1, '2020-07-01 08:01');
insert into #Temp values (1, '2020-07-01 08:06');
insert into #Temp values (1, '2020-07-01 08:30');
insert into #Temp values (1, '2020-07-08 09:35');
insert into #Temp values (1, '2020-07-15 16:10');
insert into #Temp values (1, '2020-07-15 16:20');
insert into #Temp values (1, '2020-07-17 06:40');
insert into #Temp values (1, '2020-07-17 06:41');
insert into #Temp values (2, '2020-07-01 08:30');
insert into #Temp values (2, '2020-07-01 09:26');
insert into #Temp values (2, '2020-07-01 10:25');
insert into #Temp values (2, '2020-07-09 08:30');
insert into #Temp values (2, '2020-07-09 09:26');
insert into #Temp values (2, '2020-07-09 10:25');
insert into #Temp values (3, '2020-07-21 08:30');
insert into #Temp values (3, '2020-07-21 09:26');
insert into #Temp values (3, '2020-07-21 10:25');
The week should extend up to the last date in the record. Here, the last date is
2020-07-21 10:25
Have to transform the output from this piece of code and divide the duration weekly.
select Id, sum(datediff(minute, Time, next_ts)) as duration_minutes
from (select t.*,
lead(Time) over (partition by id order by Time) as next_ts
from #Temp t
) t
where datediff(minute, Time, next_ts) < 60
group by Id;
Output:
id duration_minutes
1 41
2 230
3 115
The desired output should divide this duration on a weekly basis,
like Week 1, Week 2, Week 3, and so on.
Desired Output:
If the
start date is 2020-07-01 08:00
end date is 2020-07-21 10:25
id | Week 1 | Week 2 | Week 3
--------------------------------------
1 | 30 | 0 | 11
2 | 115 | 115 | 0
3 | 0 | 0 | 115
similarly, if the
start date is 2020-07-08 08:00
id | Week 1 | Week 2
---------------------------
1 | 11 | 0
2 | 115 | 0
3 | 0 | 115
Is this what you want?
select Id,
1 + datediff(second, '2020-07-01 06:00', time) / (24 * 60 * 60 * 7) as week_num,
sum(datediff(minute, Time, next_ts)) as duration_minutes
from (select t.*,
lead(Time) over (partition by id order by Time) as next_ts
from Temp t
) t
where datediff(minute, Time, next_ts) < 60
group by Id, datediff(second, '2020-07-01 06:00', time) / (24 * 60 * 60 * 7)
order by id, week_num;
Here is a db<>fiddle.
I am not able to understand the logic behind the week periods. Anyone, in the example below I am using the following code to set the week:
'Week ' + CAST(DENSE_RANK() OVER (ORDER BY DATEDIFF(DAY, #FirstDate, next_ts) / 7) AS VARCHAR(12))
You can adjust it to ignore the ours, be more precise or something else to match your real requirements.
Apart from that, you just need to perform a dynamic PIVOT. Here is the full working example:
DROP TABLE IF EXISTS #Temp;
create table #Temp (
Id integer not null,
Time datetime not null
);
insert into #Temp values (1, '2020-07-01 08:00');
insert into #Temp values (1, '2020-07-01 08:01');
insert into #Temp values (1, '2020-07-01 08:06');
insert into #Temp values (1, '2020-07-01 08:30');
insert into #Temp values (1, '2020-07-08 09:35');
insert into #Temp values (1, '2020-07-15 16:10');
insert into #Temp values (1, '2020-07-15 16:20');
insert into #Temp values (1, '2020-07-17 06:40');
insert into #Temp values (1, '2020-07-17 06:41');
insert into #Temp values (2, '2020-07-01 08:30');
insert into #Temp values (2, '2020-07-01 09:26');
insert into #Temp values (2, '2020-07-01 10:25');
insert into #Temp values (2, '2020-07-09 08:30');
insert into #Temp values (2, '2020-07-09 09:26');
insert into #Temp values (2, '2020-07-09 10:25');
insert into #Temp values (3, '2020-07-21 08:30');
insert into #Temp values (3, '2020-07-21 09:26');
insert into #Temp values (3, '2020-07-21 10:25');
DROP TABLE IF EXISTS #TEST
CREATE TABLE #TEST
(
[ID] INT
,[week_day] VARCHAR(12)
,[time_in_minutes] BIGINT
)
DECLARE #FirstDate DATE;
SELECT #FirstDate = MIN(Time)
FROM #Temp
INSERT INTO #TEST
select id
,'Week ' + CAST(DENSE_RANK() OVER (ORDER BY DATEDIFF(DAY, #FirstDate, next_ts) / 7) AS VARCHAR(12))
,datediff(minute, Time, next_ts)
from (select t.*,
lead(Time) over (partition by id order by Time) as next_ts
from #Temp t
) t
where datediff(minute, Time, next_ts) < 60
DECLARE #columns NVARCHAR(MAX);
SELECT #columns = STUFF
(
(
SELECT ',' + QUOTENAME([week_day])
FROM
(
SELECT DISTINCT CAST(REPLACE([week_day], 'Week ', '') AS INT)
,[week_day]
FROM #TEST
) DS ([rowID], [week_day])
ORDER BY [rowID]
FOR XML PATH(''), TYPE
).value('.', 'VARCHAR(MAX)')
,1
,1
,''
);
DECLARE #DanymicSQL NVARCHAR(MAX);
SET #DanymicSQL = N'
SELECT [ID], ' + #columns + '
FROM #TEST
PIVOT
(
SUM([time_in_minutes]) FOR [week_day] IN (' + #columns + ')
) PVT';
EXEC sp_executesql #DanymicSQL;

Return action items not in event table for all people - complex select where not exist

I feel like this should be an easy solve, but for I can't seem to solve this one.
Here are some sample tables and data.
CREATE TABLE [dbo].[people](
[PersonID] [int] NOT NULL,
[Name] [nvarchar](50) NOT NULL
) ON [PRIMARY]
INSERT INTO people (PersonID, Name) VALUES (1, 'Bob')
INSERT INTO people (PersonID, Name) VALUES (2, 'Bill')
INSERT INTO people (PersonID, Name) VALUES (3, 'Ben')
CREATE TABLE [dbo].[events](
[PersonID] [int] NOT NULL,
[ActionID] [int] NOT NULL,
[EventDate] [date] NOT NULL
) ON [PRIMARY]
-- Bob goes from sitting to running step by step
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (1, 1, getdate()-3)
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (1, 2, getdate()-2)
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (1, 3, getdate()-1)
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (1, 4, getdate())
-- Bill goes from sitting to walking, still waiting to run
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (2, 1, getdate()-2)
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (2, 2, getdate()-1)
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (2, 3, getdate())
-- Ben manages to go from sitting to running without standing or walking
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (3, 1, getdate()-3)
INSERT INTO events (PersonID, ActionID, EventDate) VALUES (3, 4, getdate())
CREATE TABLE [dbo].[actions](
[ActionID] [int] NOT NULL,
[Name] [nvarchar](50) NOT NULL
) ON [PRIMARY]
INSERT INTO actions (ActionID, Name) VALUES (1, 'Sit')
INSERT INTO actions (ActionID, Name) VALUES (2, 'Stand')
INSERT INTO actions (ActionID, Name) VALUES (3, 'Walk')
INSERT INTO actions (ActionID, Name) VALUES (4, 'Run')
What I'm trying to get is a list of all actions which do not have events for each user.
With the data above, I'd expect results like ...
|----------+----------|
| PersonID | ActionID |
|----------+----------|
| 2 | 4 |
|----------+----------|
| 3 | 2 |
|----------+----------|
| 3 | 3 |
|----------+----------|
I can do it for a specified person with something like ...
-- Get a list of actions that do not have an event for each user
SELECT
actions.ActionID,
events.PersonID
FROM
actions
LEFT OUTER JOIN events on events.ActionID = actions.ActionID
AND events.PersonID = 3
WHERE
events.PersonID IS NULL
But I'd really like to pull this out for all users without subqueries per user or cursors, etc.
The more I think about it, the less I think it can be done.
Any suggestions would be great.
Thanks. (SQL Fiddle)
Another approach can be using EXCEPT
SELECT PersonId, ActionId FROM [dbo].[people], [dbo].[actions]
EXCEPT
SELECT PersonId,ActionId FROM [dbo].[events]
Same using ANSI syntax
SELECT PersonId, ActionId FROM [dbo].[people] CROSS JOIN [dbo].[actions]
EXCEPT
SELECT PersonId,ActionId FROM [dbo].[events]
You can also do it using NOT EXISTS like following.
SELECT P.PersonId, A.ActionId FROM [dbo].[people] P CROSS JOIN [dbo].[actions] A
WHERE NOT EXISTS
(
SELECT 1 FROM [dbo].[events] E WHERE P.PersonID= E.PersonID AND A.ActionID= E.ActionID
)
Here is the solution to your problem:
SELECT p.PersonID,a.ActionID
FROM people p
CROSS JOIN actions a
LEFT JOIN Events e
ON p.PersonId = e.PersonID AND a.actionID = e.actionID
WHERE EventDate IS NULL;
Follow the link to the demo:
http://sqlfiddle.com/#!18/6b49b/19
Used Cross Join to generate all combinations of Person and Action
and then Used LEFT JOIN with Events to check which action is done by which person and selected those people and actions which didn't happen by using IS NULL.

SQL select items that make datetime range between flag toggle

Say I have a table like this one:
CREATE TABLE TESTTABLE (
ID Integer NOT NULL,
ATMOMENT Timestamp NOT NULL,
ISALARM Integer NOT NULL,
CONSTRAINT PK_TESTTABLE PRIMARY KEY (ID)
);
It has ISALARM flag that toggles between 0 and 1 at random moments ATMOMENT, like in this example dataset:
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('1', '01.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('2', '01.01.2016, 00:01:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('3', '01.01.2016, 00:02:00.000', '0');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('4', '01.01.2016, 00:02:00.000', '0');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('10', '02.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('11', '02.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('12', '02.01.2016, 00:01:00.000', '0');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('20', '03.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('21', '03.01.2016, 00:01:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('22', '03.01.2016, 00:02:00.000', '0');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('23', '03.01.2016, 00:02:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('30', '04.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('31', '04.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('32', '04.01.2016, 00:00:00.000', '0');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('33', '04.01.2016, 00:00:00.000', '0');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('40', '05.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('41', '05.01.2016, 00:00:00.000', '1');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('42', '05.01.2016, 00:00:00.000', '0');
INSERT INTO TESTTABLE (ID, ATMOMENT, ISALARM) VALUES ('43', '05.01.2016, 00:00:00.000', '0');
I need to select all alarm ranges, i.e. the ATMOMENT ranges where ISALARM is set to 1 (first time after previous range is closed) at range begin and reset back to 0 at range end. Say for clarity first reset is enough to close such range; say also that the simultaneous ISALARM set and reset are treated like the range end (while possibly as the begin).
Example dataset above is expected to produce something like this:
ALARMBEGIN | LASTALARMBEGIN | ALARMEND
-------------------------- | -------------------------- | --------
'01.01.2016, 00:00:00.000' | '01.01.2016, 00:01:00.000' | '01.01.2016, 00:02:00.000'
'02.01.2016, 00:00:00.000' | '02.01.2016, 00:00:00.000' | '02.01.2016, 00:01:00.000'
'03.01.2016, 00:00:00.000' | '03.01.2016, 00:02:00.000' | '03.01.2016, 00:02:00.000'
'04.01.2016, 00:00:00.000' | '04.01.2016, 00:00:00.000' | '04.01.2016, 00:00:00.000'
'05.01.2016, 00:00:00.000' | '05.01.2016, 00:00:00.000' | '05.01.2016, 00:00:00.000'
My own solution to this (below) looks pretty ugly and runs stunningly slow (about 1minute) even if the TESTTABLE has relatively small dataset with only ~2500 records (tested it with Firebird2.5 and Postgresql; I'm not good with DB optimization; "CREATE INDEX IDX_TESTTABLE1 ON TESTTABLE (ATMOMENT,ISALARM)" helps but not very much).
It is pretty strange for me because simple linear iteration on all TESTTABLE records (ordered by ATMOMENT) while comparing ISALARM field to one of the previous record gives me the ranges I want much faster.
Are there any elegant solution to make SQL select this faster and in cleaner way?
SELECT DISTINCT a1.ATMOMENT AS ALARMBEGIN, a2.ATMOMENT AS LASTALARMBEGIN, a3.ATMOMENT AS ALARMEND
FROM TESTTABLE a1
JOIN TESTTABLE a2 ON
(a1.ATMOMENT<a2.ATMOMENT
AND NOT EXISTS(SELECT * FROM TESTTABLE x WHERE
x.ISALARM=0 AND a1.ATMOMENT<=x.ATMOMENT AND x.ATMOMENT<a2.ATMOMENT))
OR (a1.ATMOMENT=a2.ATMOMENT)
JOIN TESTTABLE a3 ON
(a2.ATMOMENT<a3.ATMOMENT
AND NOT EXISTS(SELECT * FROM TESTTABLE x WHERE
(x.ISALARM=0 AND a2.ATMOMENT<=x.ATMOMENT AND x.ATMOMENT<a3.ATMOMENT)
OR (x.ISALARM=1 AND a2.ATMOMENT<x.ATMOMENT AND x.ATMOMENT<=a3.ATMOMENT)))
OR (a2.ATMOMENT=a3.ATMOMENT)
WHERE a1.ISALARM<>0 AND a2.ISALARM<>0 AND a3.ISALARM=0
AND (NOT EXISTS(SELECT * FROM TESTTABLE x1 WHERE
x1.ATMOMENT<a1.ATMOMENT)
OR EXISTS(SELECT * FROM TESTTABLE x1 WHERE
x1.ISALARM=0
AND x1.ATMOMENT<a1.ATMOMENT
AND NOT EXISTS(SELECT * FROM TESTTABLE x2 WHERE
x1.ATMOMENT<x2.ATMOMENT AND x2.ATMOMENT<a1.ATMOMENT)))
ORDER BY a1.ATMOMENT
Thank you.
Upd 1
Thanks to Gordon Linoff's and Jayvee's solutions (which are very good with Firebird3.0 and PostgreSQL) I've decided to rely on ordering efficiency of Firebird2.5 and contrived the "select" which is even uglier than my previous one but runs significantly faster. For those who need it done with Firebird2.5:
WITH
GROUPEDTABLE_TT (ATMOMENT, NOTISALARMRESET, ISALARMSET)
AS(
SELECT a.ATMOMENT, MIN(a.ISALARM), MAX(a.ISALARM)
FROM TESTTABLE a
GROUP BY a.ATMOMENT),
INTERVALBEGIN_TT
AS(
SELECT a1.ATMOMENT
FROM GROUPEDTABLE_TT a1
WHERE
a1.ISALARMSET<>0
AND (NOT EXISTS (SELECT * FROM GROUPEDTABLE_TT x WHERE
x.ATMOMENT<a1.ATMOMENT)
OR (SELECT FIRST 1 x.NOTISALARMRESET FROM GROUPEDTABLE_TT x WHERE
x.ATMOMENT<a1.ATMOMENT
ORDER BY x.ATMOMENT DESC)=0)),
INTERVALLAST_TT
AS(
SELECT a2.ATMOMENT FROM GROUPEDTABLE_TT a2
WHERE a2.ISALARMSET=1
AND (a2.NOTISALARMRESET=0
OR (a2.NOTISALARMRESET=1
AND (SELECT FIRST 1 x.NOTISALARMRESET FROM GROUPEDTABLE_TT x WHERE
x.ATMOMENT>a2.ATMOMENT
ORDER BY x.ATMOMENT ASC)=0
AND (SELECT FIRST 1 x.ISALARMSET FROM GROUPEDTABLE_TT x WHERE
x.ATMOMENT>a2.ATMOMENT
ORDER BY x.ATMOMENT ASC)=0))),
INTERVALEND_TT
AS(
SELECT a1.ATMOMENT
FROM GROUPEDTABLE_TT a1
WHERE
a1.NOTISALARMRESET=0
AND (a1.ISALARMSET=1
OR (a1.ISALARMSET=0
AND (SELECT FIRST 1 x.ISALARMSET FROM GROUPEDTABLE_TT x WHERE
x.ATMOMENT<a1.ATMOMENT
ORDER BY x.ATMOMENT DESC)=1
AND (SELECT FIRST 1 x.NOTISALARMRESET FROM GROUPEDTABLE_TT x WHERE
x.ATMOMENT<a1.ATMOMENT
ORDER BY x.ATMOMENT DESC)=1))),
ENCLOSEDINTERVALS_TT (BEGINMOMENT, LASTBEGINMOMENT, ENDMOMENT)
AS(
SELECT ib.ATMOMENT,
(SELECT FIRST 1 il.ATMOMENT FROM INTERVALLAST_TT il WHERE
ib.ATMOMENT<=il.ATMOMENT ORDER BY il.ATMOMENT ASC),
(SELECT FIRST 1 ie.ATMOMENT FROM INTERVALEND_TT ie WHERE
ib.ATMOMENT<=ie.ATMOMENT ORDER BY ie.ATMOMENT ASC)
FROM INTERVALBEGIN_TT ib)
SELECT * FROM ENCLOSEDINTERVALS_TT
ORDER BY BEGINMOMENT
Upd 2
...but my selects seems to show quadratic growth (or at least faster then linear) of the fetch number depending of the total record number; it's better to use procedure with single-pass linear iteration for FB2.5. Or to use FB30 with solutions below...
This has been tested in PostgreSQL, the idea is create 3 ordered common tables for beginnings, last beginnings and ends respectively and then join the 3 tables.
It can be done with less code by creating only one CTE and flagging the rows with a case statement and then a selfjoin, which you can do later but in this way the code is more self explanatory and should be fairly efficient too.
;
with beginnings
as
(
select atmoment, row_number() over(order by atmoment) rn from
(
select *, lag(atmoment,1) over(order by atmoment,isalarm desc) prevtime,
lag(isalarm,1) over(order by atmoment,isalarm desc) prevstatus
from testtable
) t
where coalesce(prevstatus,0)=0 and isalarm=1
),
ends
as
(
select atmoment, row_number() over(order by atmoment) rn from
(
select *, lead(atmoment,1) over(order by atmoment,isalarm) nexttime,
lead(isalarm,1) over(order by atmoment,isalarm) nextstatus
from testtable
) t
where coalesce(nextstatus,1)=1 and isalarm=0
),
lastbeginnings
as
(
select atmoment, row_number() over(order by atmoment) rn from
(
select *, lead(atmoment,1) over(order by atmoment,isalarm desc) nexttime,
lead(isalarm,1) over(order by atmoment,isalarm desc) nextstatus
from testtable
) t
where coalesce(nextstatus,0)=0 and isalarm=1
)
select b.atmoment ALARMBEGIN, lb.atmoment LASTALARMBEGIN, e.atmoment ALARMEND
from beginnings b
join lastbeginnings lb on lb.rn=b.rn
join ends e on e.rn=b.rn
result:
> 2016-01-01 00:00:00 | 2016-01-01 00:01:00 | 2016-01-01 00:02:00
> 2016-01-02 00:00:00 | 2016-01-02 00:00:00 | 2016-01-02 00:01:00
> 2016-01-03 00:00:00 | 2016-01-03 00:02:00 | 2016-01-03 00:02:00
> 2016-01-04 00:00:00 | 2016-01-04 00:00:00 | 2016-01-04 00:00:00
> 2016-01-05 00:00:00 | 2016-01-05 00:00:00 | 2016-01-05 00:00:00
I think you can do this in Firebird 3.0, using row_number():
select alarm, min(atmoment), max(atmoment)
from (select t.*,
row_number() over (order by atmoment) as seqnum,
row_number() over (partition by alarm order by atmoment) as seqnum_a
from testtable t
) t
group by alarm, (seqnum - seqnum_a);
It is a little hard to explain how this works. But if you run the subquery, you'll see how the difference identifies the groups you are interested in.

Select MAX dates plus ID value

Please consider the following table...
DECLARE #tmp TABLE
(
ID int,
userID int,
testID int,
someDate datetime
)
...containing the following values:
INSERT INTO #tmp (ID, userID, testID, someDate) VALUES (1, 1, 50, '2010-10-01')
INSERT INTO #tmp (ID, userID, testID, someDate) VALUES (2, 1, 50, '2010-11-01')
INSERT INTO #tmp (ID, userID, testID, someDate) VALUES (3, 1, 50, '2010-12-01')
INSERT INTO #tmp (ID, userID, testID, someDate) VALUES (4, 2, 20, '2010-10-01')
INSERT INTO #tmp (ID, userID, testID, someDate) VALUES (5, 2, 30, '2010-11-01')
INSERT INTO #tmp (ID, userID, testID, someDate) VALUES (6, 2, 20, '2012-11-01')
I need to retrieve the maximum date for each userID/testID combination of values, and also the accompanying ID value. The results should be:
ID userID testID someDate
-------------------------------
3 1 50 2010-12-01
5 2 30 2010-11-01
6 2 20 2012-11-01
When I try the following query, the result set becomes incorrect and all rows are shown. I cannot omit ID from the GROUP BY clause because it causes and error. Can anyone help please? It seems long-winded to join the table to itself to get these values.
SELECT ID, userID, testID, MAX(someDate)
FROM #tmp
GROUP BY testId,userID,ID;
http://www.sqlfiddle.com/#!6/d41d8/5219
Please try:
select * from (
select
*,
ROW_NUMBER() over (partition by userID, testID order by SomeDate desc) Rnum
From #tmp
)x where Rnum=1

How to find persons who are absent continuously for n number of days?

Database: MS SQL 2005
Table:
EmployeeNumber | EntryDate | Status
Sample Data:
200 | 3/1/2009 | P
200 | 3/2/2009 | A
200 | 3/3/2009 | A
201 | 3/1/2009 | A
201 | 3/2/2009 | P
Where P is present, A is absent.
I have tried row_number over partion. But it does not generate the sequence which I expect.
For the above data the sequence I expect is
1
1
2
1
1
SELECT EmployeeNumber, EntryDate,Status
ROW_NUMBER() OVER (
PARTITION BY EmployeeNumber, Status
ORDER BY EmployeeNumber,EntryDate ) AS 'RowNumber'
FROM [Attendance]
i'm not sure I follow what you're wanting with the 1 1 2 1 1 sequence, but simply adding an order by to your original query produces that sequence...
SELECT EmployeeNumber,
EntryDate,
Status,
ROW_NUMBER() OVER (PARTITION BY EmployeeNumber, Status ORDER BY EmployeeNumber, EntryDate) AS 'RowNumber'
FROM Attendance
ORDER BY EmployeeNumber, EntryDate
/*
EmployeeNumber EntryDate Status RowNumber
-------------- ----------------------- ------ --------------------
200 2009-03-01 00:00:00 P 1
200 2009-03-02 00:00:00 A 1
200 2009-03-03 00:00:00 A 2
201 2009-03-01 00:00:00 A 1
201 2009-03-02 00:00:00 P 1
(5 row(s) affected)
*/
You should be able to do this with a CTE in SQL 2005. Stealing Lievens data:
DECLARE #Attendance TABLE (EmployeeNumber INTEGER, EntryDate DATETIME, Status VARCHAR(1))
INSERT INTO #Attendance VALUES (200, '03/01/2009', 'P')
INSERT INTO #Attendance VALUES (200, '03/02/2009', 'A')
INSERT INTO #Attendance VALUES (200, '03/03/2009', 'A')
INSERT INTO #Attendance VALUES (200, '03/04/2009', 'A')
INSERT INTO #Attendance VALUES (200, '04/04/2009', 'A')
INSERT INTO #Attendance VALUES (200, '04/05/2009', 'A')
INSERT INTO #Attendance VALUES (201, '03/01/2009', 'A')
INSERT INTO #Attendance VALUES (201, '03/02/2009', 'A')
INSERT INTO #Attendance VALUES (201, '03/03/2009', 'P');
Then use this CTE to extract the sequence:
WITH Dates
(
EntryDate,
EmployeeNumber,
Status,
Days
)
AS
(
SELECT
a.EntryDate,
a.EmployeeNumber,
a.Status,
1
FROM
#Attendance a
WHERE
a.EntryDate = (SELECT MIN(EntryDate) FROM #Attendance)
-- RECURSIVE
UNION ALL
SELECT
a.EntryDate,
a.EmployeeNumber,
a.Status,
CASE WHEN (a.Status = Parent.Status) THEN Parent.Days + 1 ELSE 1 END
FROM
#Attendance a
INNER JOIN
Dates parent
ON
datediff(day, a.EntryDate, DateAdd(day, 1, parent.EntryDate)) = 0
AND
a.EmployeeNumber = parent.EmployeeNumber
)
SELECT * FROM Dates order by EmployeeNumber, EntryDate
Although as a final note the sequence does seem strange to me, depending on your requirements there may be a better way of aggregating the data? Never the less, this will produce the sequence you require
Does this help you?
It doesn't produce the sequence you ask (No idea how to do that) but it does give you the ammount of consecutive days someone has been absent.
DECLARE #Attendance TABLE (EmployeeNumber INTEGER, EntryDate DATETIME, Status VARCHAR(1))
INSERT INTO #Attendance VALUES (200, '03/01/2009', 'P')
INSERT INTO #Attendance VALUES (200, '03/02/2009', 'A')
INSERT INTO #Attendance VALUES (200, '03/03/2009', 'A')
INSERT INTO #Attendance VALUES (200, '03/04/2009', 'A')
INSERT INTO #Attendance VALUES (200, '04/04/2009', 'A')
INSERT INTO #Attendance VALUES (200, '04/05/2009', 'A')
INSERT INTO #Attendance VALUES (201, '03/01/2009', 'A')
INSERT INTO #Attendance VALUES (201, '03/02/2009', 'A')
INSERT INTO #Attendance VALUES (201, '03/03/2009', 'P')
SELECT a1.EmployeeNumber, [Absent] = COUNT(*) + 1
FROM #Attendance a1
INNER JOIN #Attendance a2 ON a1.EntryDate = a2.EntryDate - 1
AND a1.EmployeeNumber = a2.EmployeeNumber
AND a1.Status = a2.Status
GROUP BY a1.EmployeeNumber
You could use recursion, similar to what I have done here. It seems though that your problem is a little simpler, and since SQL Server limits recursion to 99, this might not work for people who are absent a lot. Let me think about this a few minutes.
If you have a row for every single day, go with Lieven's join.