SQL Server query without using loops - sql

I have a Payment table that looks a little like this:
Id (int identity)
CustomerId (int)
PaymentDate (SmallDateTime)
Now I want to write a query that will find those customers that have made three payments within a period of three months. Given the following data:
Id CustomerId PaymentDate (YYYY-MM-DD)
------------------------------------------
1 1 2010-01-01
2 1 2010-02-01
3 1 2010-03-01
4 1 2010-06-01
5 2 2010-04-01
6 2 2010-05-01
7 2 2010-06-01
8 2 2010-07-01
I would like to produce the following result:
CustomerId LastPaymentDateInPeriod
-------------------------------------
1 2010-03-01
2 2010-07-01
Where LastPaymentDateInPeriod is the PaymentDate with the highest value within a three-month period. If there is more than one three-month period for a given customer it would have to return the highest value from the most recent period (this is what I tried to illustrate for customer 2 in the above example). Note that three payments on three consecutive days would also meet the criteria. The payments just have to fall within a three-month period.
I know how to do this with a cursor and a lot of smaller queries but this is slow (and, I've come to understand, should only be a last resort). So do any of you SqlServer geniuses know how to do this with a query?
Thanks in advance.

This should do the job:
select
CustomerID,
max(LastPaymentDateInPeriod) as LastPaymentDateInPeriod
from
(
select
LastPaymentInPeriod.CustomerID,
LastPaymentInPeriod.PaymentDate as LastPaymentDateInPeriod
from Payment LastPaymentInPeriod
inner join Payment RelatedPayment on
LastPaymentInPeriod.CustomerID = RelatedPayment.CustomerID and
LastPaymentInPeriod.PaymentDate > RelatedPayment.PaymentDate and
datediff(m, RelatedPayment.PaymentDate, LastPaymentInPeriod.PaymentDate) < 3
group by
LastPaymentInPeriod.CustomerID,
LastPaymentInPeriod.PaymentDate
having
count(*) > 1
) as PaymentPeriods
group by
CustomerID
update: I've tested this now and it seems to work for #Martin's data
update2: If it's a requirement that Jan 31 and Apr 1 should be considered as less than 3 months apart then the DATEDIFF function call can be replaced with something like this:
create function fn_monthspan
(
#startdate datetime,
#enddate datetime
)
returns int
as
begin
return datediff(m, #startdate, #enddate) - case when datepart(d, #startdate) > datepart(d, #enddate) then 1 else 0 end
end

Bit of a rushed job as I'm off out.
declare #T TABLE
(
Id int,
CustomerId int,
PaymentDate SmallDateTime
)
insert into #T
SELECT 1, 1,'2010-01-01' UNION ALL
SELECT 2, 1,'2010-02-01' UNION ALL
SELECT 3, 1,'2010-03-01' UNION ALL
SELECT 4, 1,'2010-06-01' UNION ALL
SELECT 5, 2,'2010-04-01' UNION ALL
SELECT 6, 2,'2010-05-01' UNION ALL
SELECT 7, 2,'2010-06-01' UNION ALL
SELECT 8, 2,'2010-07-01'
;with CTE1 AS
(
SELECT Id, CustomerId, PaymentDate, ROW_NUMBER() OVER (PARTITION BY CustomerId ORDER BY PaymentDate) RN
FROM #T
), CTE2 AS
(
SELECT C1.Id, C1.CustomerId, MAX(C2.PaymentDate) AS LastPaymentDateInPeriod
FROM CTE1 C1 LEFT JOIN CTE1 C2 ON C1.CustomerId = C2.CustomerId AND C2.RN BETWEEN C1.RN AND C1.RN + 2 and C2.PaymentDate <=DATEADD(MONTH,3,C1.PaymentDate)
GROUP BY C1.Id, C1.CustomerId
HAVING COUNT(*)=3
)
SELECT CustomerId, MAX(LastPaymentDateInPeriod) LastPaymentDateInPeriod
FROM CTE2
GROUP BY CustomerId

This gives you all three payments within a 3 month span.
;
WITH CustomerPayments AS
(
SELECT 1 Id, 1 CustomerId, Convert (DateTime, '2010-01-01') PaymentDate
UNION SELECT 2, 1, '2010-02-01'
UNION SELECT 3, 1, '2010-03-01'
UNION SELECT 4, 1, '2010-06-01'
UNION SELECT 5, 2, '2010-04-01'
UNION SELECT 6, 2, '2010-05-01'
UNION SELECT 7, 2, '2010-06-01'
UNION SELECT 8, 2, '2010-07-01'
UNION SELECT 9, 3, '2010-07-01'
UNION SELECT 10, 3, '2010-07-01'
),
FirstPayment AS
(
SELECT Id, CustomerId, PaymentDate
FROM CustomerPayments
where Id IN
(
SELECT Min (Id) Id
FROM CustomerPayments
Group by CustomerId
)
),
SecondPayment AS
(
SELECT Id, CustomerId, PaymentDate
FROM CustomerPayments
where Id IN
(
SELECT Min (Id) Id
FROM CustomerPayments
WHERE ID NOT IN
(
SELECT ID
from FirstPayment
)
Group by CustomerId
)
),
ThirdPayment AS
(
SELECT Id, CustomerId, PaymentDate
FROM CustomerPayments
where Id IN
(
SELECT Min (Id) Id
FROM CustomerPayments
WHERE ID NOT IN
(
SELECT ID
from FirstPayment
UNION
SELECT ID
from SecondPayment
)
Group by CustomerId
)
)
SELECT *
FROM
FirstPayment FP
Left JOIN SecondPayment SP
ON FP.CustomerId = SP.CustomerId
Left JOIN ThirdPayment TP
ON SP.CustomerId = TP.CustomerId
WHERE 1=1
AND SP.PaymentDate IS NOT NULL
AND TP.PaymentDate IS NOT NULL
AND ABS (DATEDIFF (mm, SP.PaymentDate, TP.PaymentDate)) <3

I thought of:
select customerId,max(PaymentDate) from payment where customerId in
(select case when count(*)<3 then null else customerId end as customerId from payment
where paymentdate>dateadd(month,-3,getdate()) group by customerId)
group by customerId;

Related

Compare getdate() with two different fields

I have 2 tables:
T1 T2
id Effdate E_id DOB
-------------- ------------
1 20161212 2 1950-02-16 00:12:24
2 20130124 5 1978-01-16 10:14:30
I want to compare getdate() < Maxdate(effdate, DOB)?
I am getting datetime conversion error.
for example : getdate() < MAXDATE( 20161212 , 1950-02-16 00:12:24)
expected result should be from table T1:
id Effdate
--------------
1 20161212
If id in both tables are correspondent on id = E_id you can UNION them and GROUP BY id:
;WITH T1 AS (
SELECT 1 id,
CAST('20161212' as varchar(10)) Effdate
UNION ALL
SELECT 2,
'20130124'
), T2 AS (
SELECT 1 E_id,
CAST('1950-02-16 00:12:24' as datetime) DOB
UNION ALL
SELECT 2,
'1978-01-16 10:14:30'
)
SELECT id,
MAX(CAST(Effdate as datetime)) as MD
FROM (
SELECT *
FROM T1
UNION ALL
SELECT *
FROM T2
) t
GROUP BY id
HAVING MAX(CAST(Effdate as datetime)) >= GETDATE()
Will bring you expected result

Find consecutive working dates for each employee

In SQL 2000 I have a table that contains the following:
ID Date WorkingTime EmployeeID
For August, this table would contain 200 employees with dates of 8/1 - 8/31. I need to find out what is the MIN date of the first 5 consecutive days of working time for each employee starting at the day passed in and going backward.
For Example:
If employee 123 looked as follows and 8/10/2013 was passed in:
ID Date WorkingTime EmployeeID
1 8/1 1 123
2 8/2 0 123
3 8/3 0 123
4 8/4 1 123
5 8/5 1 123
6 8/6 1 123
7 8/7 1 123
8 8/8 1 123
9 8/9 0 123
10 8/10 1 123
The result would be 8/4. This needs to be done all at once for all of the employees in the table, so they would all have different min dates, all starting on 8/10 since that was the date that was passed into the query. This table is very large in real life and contins many dates and employees, not just in Auguest. I thought about using a cursor to go through all of this but I think that would be really slow. I was also thinking of adding all of the working times to a temp table and doing a datediff on them to find the consecutive 5 with a datediff of 1, but I wasn't quite sure how to execute that. Is there a better way I am not thinking of?
DECLARE #MyTable TABLE
(
ID INT IDENTITY PRIMARY KEY,
[Date] SMALLDATETIME NOT NULL,
WorkingTime INT NOT NULL,
EmployeeID INT NOT NULL
);
INSERT #MyTable ([Date], WorkingTime, EmployeeID)
-- First employee
SELECT '20130801', 1, 123 UNION ALL
SELECT '20130802', 0, 123 UNION ALL
SELECT '20130803', 0, 123 UNION ALL
SELECT '20130804', 1, 123 UNION ALL
SELECT '20130805', 1, 123 UNION ALL
SELECT '20130806', 1, 123 UNION ALL
SELECT '20130807', 1, 123 UNION ALL
SELECT '20130808', 1, 123 UNION ALL
SELECT '20130809', 0, 123 UNION ALL
SELECT '20130810', 1, 123 UNION ALL
-- Second employee
SELECT '20130801', 1, 126 UNION ALL
SELECT '20130802', 1, 126 UNION ALL
SELECT '20130803', 1, 126 UNION ALL
SELECT '20130804', 1, 126 UNION ALL
SELECT '20130805', 1, 126 UNION ALL
SELECT '20130806', 0, 126 UNION ALL
-- Third employee
SELECT '20130801', 0, 127 UNION ALL
SELECT '20130802', 0, 127 UNION ALL
SELECT '20130803', 1, 127 UNION ALL
SELECT '20130804', 1, 127 UNION ALL
SELECT '20130805', 0, 127 UNION ALL
SELECT '20130806', 0, 127;
--
DECLARE #Results TABLE
(
EmployeeID INT NOT NULL,
DaysDiff INT NOT NULL,
PRIMARY KEY(EmployeeID, DaysDiff), -- This is a "clustered index"/index organized table
RowNum INT IDENTITY NOT NULL,
[Date] SMALLDATETIME NOT NULL
);
INSERT #Results (EmployeeID, DaysDiff, [Date])
SELECT x.EmployeeID,
DATEDIFF(DAY, 0, x.[Date]) AS DaysDiff,
x.[Date]
FROM #MyTable x
WHERE x.WorkingTime = 1
/*
This ORDER BY clause and the clustered index (PRIMARY KEY(EmployeeID, DaysDiff))
should give a hint to SQL Server so that
RowNum IDENTITY values will be generated in this order: EmployeeID, DaysDiff
Note #1: There is not 100% guarantee that insert order will be the same as
ORDER BY x.EmployeeID, DaysDiff
and
clustered index key (EmployeeID, DaysDiff)
Note #2: This INSERT INTO table with identity column simulates the ROW_NUMBER function
which is available starting with SQL2005.
*/
ORDER BY x.EmployeeID, DaysDiff
OPTION (MAXDOP 1); -- It minimizes the risk of messing up the order of RowNum
SELECT y.EmployeeID, MAX(y.GroupStartDate) AS FirstGroupStartDate
FROM
(
SELECT x.EmployeeID, x.GroupID,
MIN(x.[Date]) AS GroupStartDate, MAX(x.[Date]) AS GroupEndDate,
DATEDIFF(DAY, MIN(x.[Date]), MAX(x.[Date]))+1 AS ContinuousDays
FROM
(
SELECT *, r.DaysDiff - r.RowNum AS GroupID
FROM #Results r
) x
GROUP BY x.EmployeeID, x.GroupID
) y
WHERE y.ContinuousDays > 4
GROUP BY y.EmployeeID;
Below query will give good start for what you want to achieve, modify the schema based on your tables.
SQL fiddle demo
#DateToPull - Date for which you want to pull data for.
#TimeSheet is your original table
#SubsetTimeSheet - table with subset of records from #TimeSheet table. Populated with records from first of the month till passed date.
Note: This query can be written more efficiently with newer version of SQL Server.
declare #DateToPull datetime
select #DateToPull = '08/10/2013'
if object_id('tempdb..#TimeSheet') is not null
drop table #TimeSheet
create table #TimeSheet
(
ID int identity(1, 1),
EmployeeID int,
[WorkDate] datetime,
WorkingTime bit
)
insert into #TimeSheet(EmployeeID, [WorkDate], WorkingTime)
select 123 , '08/01/2013', 0
union all
select 123 , '08/02/2013', 1
union all
select 123 , '08/03/2013', 0
union all
select 123 , '08/04/2013', 1
union all
select 123 , '08/05/2013', 1
union all
select 123 , '08/06/2013', 1
union all
select 123 , '08/07/2013', 1
union all
select 123 , '08/08/2013', 1
union all
select 123 , '08/09/2013', 0
union all
select 123 , '08/10/2013', 1
union all
select 123 , '08/11/2013', 1
union all
select 123 , '08/12/2013', 1
union all
select 123 , '08/13/2013', 1
union all
select 123 , '08/14/2013', 1
union all
select 123 , '08/15/2013', 0
union all
select 123 , '08/16/2013', 1
union all
select 123 , '08/17/2013', 1
union all
select 123 , '08/18/2013', 1
union all
select 123 , '08/19/2013', 1
union all
select 123 , '08/20/2013', 1
if object_id('tempdb..#SubsetTimeSheet') is not null
drop table #SubsetTimeSheet
create table #SubsetTimeSheet
(
EmployeeID int,
[WorkDate] datetime,
WorkingTime bit
)
insert into #SubsetTimeSheet(EmployeeID, [WorkDate], WorkingTime)
select EmployeeID, [WorkDate], WorkingTime
from #TimeSheet
where
datediff(dd, [WorkDate], #DateToPull) >= 0
and datediff(dd, DATEADD(dd, -(DAY(#DateToPull)-1), #DateToPull), [WorkDate]) >= 0
and WorkingTime = 1
order by
EmployeeID,
[WorkDate] desc
select A.EmployeeID, max(E.WorkDate) WorkDate
from
#SubsetTimeSheet A
inner join #SubsetTimeSheet B on datediff(dd, A.[WorkDate] - 1, B.WorkDate) = 0 and A.EmployeeID = B.EmployeeID
inner join #SubsetTimeSheet C on datediff(dd, A.[WorkDate] - 2, C.WorkDate) = 0 and A.EmployeeID = C.EmployeeID
inner join #SubsetTimeSheet D on datediff(dd, A.[WorkDate] - 3, D.WorkDate) = 0 and A.EmployeeID = D.EmployeeID
inner join #SubsetTimeSheet E on datediff(dd, A.[WorkDate] - 4, E.WorkDate) = 0 and A.EmployeeID = E.EmployeeID
group by
A.EmployeeID

Is it possible to write a sql query that is grouped based on a running total of a column?

It would be easier to explain with an example. Suppose I wanted to get at most 5 items per group.
My input would be a table looking like this:
Item Count
A 2
A 3
A 3
B 4
B 4
B 5
C 1
And my desired output would look like this:
Item Count
A 5
A>5 3
B 4
B>5 9
C 1
An alternative output that I could also work with would be
Item Count RunningTotal
A 2 2
A 3 5
A 3 8
B 4 4
B 4 8
B 5 13
C 1 1
I can use ROW_NUMBER() to get the top X records in each group, however my requirement is to get the top X items for each group, not X records. My mind is drawing a blank as to how to do this.
declare #yourTable table (item char(1), [count] int)
insert into #yourTable
select 'A', 2 union all
select 'A', 3 union all
select 'A', 3 union all
select 'B', 4 union all
select 'B', 4 union all
select 'B', 5 union all
select 'C', 1
;with cte(item, count, row) as (
select *, row_number() over ( partition by item order by item, [count])
from #yourTable
)
select t1.Item, t1.Count, sum(t2.count) as RunningTotal from cte t1
join cte t2 on t1.item = t2.item and t2.row <= t1.row
group by t1.item, t1.count, t1.row
Result:
Item Count RunningTotal
---- ----------- ------------
A 2 2
A 3 5
A 3 8
B 4 4
B 4 8
B 5 13
C 1 1
Considering the clarifications from your comment, you should be able to produce the second kid of output from your post by running this query:
select t.Item
, t.Count
, (select sum(tt.count)
from mytable tt
where t.item=tt.item and (tt.creating_user_priority < t.creating_user_priority or
( tt.creating_user_priority = t.creating_user_priority and tt.created_date < t.createdDate))
) as RunningTotal
from mytable t
declare #yourTable table (item char(1), [count] int)
insert into #yourTable
select 'A', 2 union all
select 'A', 3 union all
select 'A', 3 union all
select 'B', 4 union all
select 'B', 4 union all
select 'B', 5 union all
select 'C', 1
;with cte(item, count, row) as (
select *, row_number() over ( partition by item order by item, [count])
from #yourTable
)
select t1.row, t1.Item, t1.Count, sum(t2.count) as RunningTotal
into #RunTotal
from cte t1
join cte t2 on t1.item = t2.item and t2.row <= t1.row
group by t1.item, t1.count, t1.row
alter table #RunTotal
add GrandTotal int
update rt
set GrandTotal = gt.Total
from #RunTotal rt
left join (
select Item, sum(Count) Total
from #RunTotal rt
group by Item) gt
on rt.Item = gt.Item
select Item, max(RunningTotal)
from #RunTotal
where RunningTotal <= 5
group by Item
union
select a.Item + '>5', total - five
from (
select Item, max(GrandTotal) total
from #RunTotal
where GrandTotal > 5
group by Item
) a
left join (
select Item, max(RunningTotal) five
from #RunTotal
where RunningTotal <= 5
group by Item
) b
on a.Item = b.Item
I've updated the accepted answer and got your desired result.
SELECT Item, SUM(Count)
FROM mytable t
GROUP BY Item
HAVING SUM(Count) <=5
UNION
SELECT Item, 5
FROM mytable t
GROUP BY Item
HAVING SUM(Count) >5
UNION
SELECT t2.Item + '>5', Sum(t2.Count) - 5
FROM mytable t2
GOUP BY Item
HAVING SUM(Count) > 5
ORDER BY 1, 2
select 'A' as Name, 2 as Cnt
into #tmp
union all select 'A',3
union all select 'A',3
union all select 'B',4
union all select 'B',4
union all select 'B',5
union all select 'C',1
select Name, case when sum(cnt) > 5 then 5 else sum(cnt) end Cnt
from #tmp
group by Name
union
select Name+'>5', sum(cnt)-5 Cnt
from #tmp
group by Name
having sum(cnt) > 5
Here is what I have so far. I know it's not complete but... this should be a good starting point.
I can get your second output by using a temp table and an update pass:
DECLARE #Data TABLE
(
ID INT IDENTITY(1,1) PRIMARY KEY
,Value VARCHAR(5)
,Number INT
,Total INT
)
INSERT INTO #Data (Value, Number) VALUES ('A',2)
INSERT INTO #Data (Value, Number) VALUES ('A',3)
INSERT INTO #Data (Value, Number) VALUES ('A',3)
INSERT INTO #Data (Value, Number) VALUES ('B',4)
INSERT INTO #Data (Value, Number) VALUES ('B',4)
INSERT INTO #Data (Value, Number) VALUES ('B',5)
INSERT INTO #Data (Value, Number) VALUES ('C',1)
DECLARE
#Value VARCHAR(5)
,#Count INT
UPDATE #Data
SET
#Count = Total = CASE WHEN Value = #Value THEN Number + #Count ELSE Number END
,#Value = Value
FROM #Data AS D
SELECT
Value
,Number
,Total
FROM #Data
There may be better ways, but this should work.

SQL Monthly Summary

I have a table that contains a startdate for each item
for example:
ID - Startdate
1 - 2011-01-01
2 - 2011-02-01
3 - 2011-04-01
...
I need a query that will give me the count of each item within each month, i need a full 12 month report. I tried simply grouping by the Month(StartDate) but this doesnt give me a zero for the months with no values, in the case above, for march.
so i would like the output to be along the lines of..
Month - Count
1 20
2 14
3 0
...
Any ideas?
Thanks.
SELECT A.Month, ISNULL(B.countvalue,0) Count
FROM (SELECT 1 AS MONTH
UNION
SELECT 2
UNION
SELECT 3
UNION
SELECT 4
UNION
SELECT 5
UNION
SELECT 6
UNION
SELECT 7
UNION
SELECT 8
UNION
SELECT 9
UNION
SELECT 10
UNION
SELECT 11
UNION
SELECT 12 ) A LEFT JOIN (SELECT datepart(month,Startdate) AS Month, Count(ID) as countvalue FROM yourTable GROUP BY datepart(month,Startdate))B
ON A.month = B.month
Hope this helps
Another way to do this using SQL Server 2005+ or Oracle.
SQL Statement
;WITH q (Month) AS (
SELECT 1
UNION ALL
SELECT Month + 1
FROM q
WHERE q.Month < 12
)
SELECT q.Month
, COUNT(i.ID)
FROM q
LEFT OUTER JOIN Input i ON MONTH(i.StartDate) = q.Month
GROUP BY
q.Month
Test script
;WITH Input (ID, StartDate) AS (
SELECT 1, '2011-01-01'
UNION ALL SELECT 2, '2011-02-01'
UNION ALL SELECT 3, '2011-04-01'
)
, q (Month) AS (
SELECT 1
UNION ALL
SELECT Month + 1
FROM q
WHERE q.Month < 12
)
SELECT q.Month
, COUNT(i.ID)
FROM q
LEFT OUTER JOIN Input i ON MONTH(i.StartDate) = q.Month
GROUP BY
q.Month

Coalesce over Rows in MSSQL 2008,

I'm trying to determine the best approach here in MSSQL 2008.
Here is my sample data
TransDate Id Active
-------------------------
1/18 1pm 5 1
1/18 2pm 5 0
1/18 3pm 5 Null
1/18 4pm 5 1
1/18 5pm 5 0
1/18 6pm 5 Null
If grouped by Id and ordered by the TransDate, I want the last Non Null Value for the Active Column, and the MAX of TransDate
SELECT MAX(TransDate) AS TransDate,
Id,
--LASTNonNull(Active) AS Active
Here would be the results:
TransDate Id Active
---------------------
1/18 6pm 5 0
It would be like a Coalesce but over the rows, instead of two values/columns.
There would be many other columns that would also have this similiar method applied, so I really don't want to make a seperate join for each of the columns.
Any ideas?
I'd probably use a correlated sub query.
SELECT MAX(TransDate) AS TransDate,
Id,
(SELECT TOP (1) Active
FROM T t2
WHERE t2.Id = t1.Id
AND Active IS NOT NULL
ORDER BY TransDate DESC) AS Active
FROM T t1
GROUP BY Id
A way without
SELECT
Id,
MAX(TransDate) AS TransDate,
CAST(RIGHT(MAX(CONVERT(CHAR(23),TransDate,121) + CAST(Active AS CHAR(1))),1) AS BIT) AS Active,
/*You can probably figure out a more efficient thing to
compare than the above depending on your data. e.g.*/
CAST(MAX(DATEDIFF(SECOND,'19500101',TransDate) * CAST(10 AS BIGINT) + Active)%10 AS BIT) AS Active2
FROM T
GROUP BY Id
Or following the comments would cross apply work better for you?
WITH T (TransDate, Id, Active, SomeOtherColumn) AS
(
select GETDATE(), 5, 1, 'A' UNION ALL
select 1+GETDATE(), 5, 0, 'B' UNION ALL
select 2+GETDATE(), 5, null, 'C' UNION ALL
select 3+GETDATE(), 5, 1, 'D' UNION ALL
select 4+GETDATE(), 5, 0, 'E' UNION ALL
select 5+GETDATE(), 5, null,'F'
),
T1 AS
(
SELECT MAX(TransDate) AS TransDate,
Id
FROM T
GROUP BY Id
)
SELECT T1.TransDate,
Id,
CA.Active AS Active,
CA.SomeOtherColumn AS SomeOtherColumn
FROM T1
CROSS APPLY (SELECT TOP (1) Active, SomeOtherColumn
FROM T t2
WHERE t2.Id = T1.Id
AND Active IS NOT NULL
ORDER BY TransDate DESC) CA
This example should help, using analytical functions Max() OVER and Row_Number() OVER
create table tww( transdate datetime, id int, active bit)
insert tww select GETDATE(), 5, 1
insert tww select 1+GETDATE(), 5, 0
insert tww select 2+GETDATE(), 5, null
insert tww select 3+GETDATE(), 5, 1
insert tww select 4+GETDATE(), 5, 0
insert tww select 5+GETDATE(), 5, null
select maxDate as Transdate, id, Active
from (
select *,
max(transdate) over (partition by id) maxDate,
ROW_NUMBER() over (partition by id
order by case when active is not null then 0 else 1 end, transdate desc) rn
from tww
) x
where rn=1
Another option, quite expensive, would be doing it through XML. For educational purposes only
select
ID = n.c.value('#id', 'int'),
trandate = n.c.value('(data/transdate)[1]', 'datetime'),
active = n.c.value('(data/active)[1]', 'bit')
from
(select xml=convert(xml,
(select id [#id],
( select *
from tww t
where t.id=tww.id
order by transdate desc
for xml path('data'), type)
from tww
group by id
for xml path('node'), root('root'), elements)
)) x cross apply xml.nodes('root/node') n(c)
It works on the principle that the XML generated has each record as a child node of the ID. Null columns have been omitted, so the first column found using xpath (child/columnname) is the first non-null value similar to COALESCE.
You could use a subquery:
SELECT MAX(TransDate) AS TransDate
, Id
, (
SELECT TOP 1 t2.Active
FROM YourTable t2
WHERE t1.id = t2.id
and t2.Active is not null
ORDER BY
t2.TransDate desc
)
FROM YourTable t1
I created a temp table named #temp to test my solution, and here is what I came up with:
transdate id active
1/1/2011 12:00:00 AM 5 1
1/2/2011 12:00:00 AM 5 0
1/3/2011 12:00:00 AM 5 null
1/4/2011 12:00:00 AM 5 1
1/5/2011 12:00:00 AM 5 0
1/6/2011 12:00:00 AM 5 null
1/1/2011 12:00:00 AM 6 2
1/2/2011 12:00:00 AM 6 3
1/3/2011 12:00:00 AM 6 null
1/4/2011 12:00:00 AM 6 2
1/5/2011 12:00:00 AM 6 null
This query...
select max(a.transdate) as transdate, a.id, (
select top (1) b.active
from #temp b
where b.active is not null
and b.id = a.id
order by b.transdate desc
) as active
from #temp a
group by a.id
Returns these results.
transdate id active
1/6/2011 12:00:00 AM 5 0
1/5/2011 12:00:00 AM 6 2
Assuming a table named "test1", how about using ROW_NUMBER, OVER and PARTITION BY?
SELECT transdate, id, active FROM
(SELECT transdate, ROW_NUMBER() OVER(PARTITION BY id ORDER BY transdate desc) AS rownumber, id, active
FROM test1
WHERE active is not null) a
WHERE a.rownumber = 1