SQL Query to calculate median and group by - sql

I have following table.
DECLARE #TBL_RESULT Table
(
ID varchar(10),
CreateDate DateTime,
PEOPLE_CODE_ID varchar(10),
CONVERSION_DATE DateTime,
CAMPUS varchar(20),
DAYS_TOOK int
);
This table has records from January 01,2013 to date of all the leads that were received and converted.
I initially needed to find the Median time it took to convert leads that arrived in last 10 weeks and group them by Campus I was able to do that Using the SQL Query below
WITH CTE_RESULT
AS ( SELECT *
FROM #TBL_RESULT
WHERE CreateDate > DATEADD(WEEK, -10, GETDATE())
)
SELECT Campus ,
AVG(DAYS_TOOK) AS MedianTime
FROM ( SELECT CAMPUS ,
Days_Took ,
ROW_NUMBER() OVER ( PARTITION BY Campus ORDER BY Days_Took ASC ) AS AgeRank ,
COUNT(*) OVER ( PARTITION BY CAMPUS ) AS CampusCount
FROM CTE_RESULT
) x
WHERE x.AgeRank IN ( x.CampusCount / 2 + 1, ( x.CampusCount + 1 ) / 2 )
GROUP BY x.Campus
I now need to plot this trend on a graph i.e. find records the previous 10 weeks buckets and plot the median on a line chart - where each line is one campus. (Grouped by campus)
Is cursor my only option? where I will find the leads of first 10 week starting from Jan 01, do the above SQL query to get median, push it to a temp table and then find the next 10 weeks and so on.
Or is there anything better i can do?

Without trying to optimise your query, if you need to produce the same result across multiple 10-WEEK periods, you can expand your current (10 week ago to today) ranges to as many ranges as required, threading a PeriodEndDate throughout the query as shown below.
SQL Fiddle
MS SQL Server 2012 Schema Setup:
Query 1:
DECLARE #TBL_RESULT Table
(
ID varchar(10),
CreateDate DateTime,
PEOPLE_CODE_ID varchar(10),
CONVERSION_DATE DateTime,
CAMPUS varchar(20),
DAYS_TOOK int
);
-- fill the table with some dummy data from 2013-01-01
INSERT #TBL_RESULT (CreateDate, Campus, Days_Took)
SELECT DATEADD(D, A.Number, '20130101'), 'Campus' + Right(B.Number, 10),
ABS(CAST(NEWID() AS binary(6)) % 130) + 1
FROM master..spt_values A
JOIN master..spt_values B on B.type='P' and B.number < 50 -- 50 campuses
WHERE A.type='P'
AND DATEADD(D, A.Number, '20130101') <= GetDate();
-- This first CTE is used to create the required number of 10-week periods
WITH N(NUMBER) AS (
SELECT 0
union all
select number+1 from N
where Number <= DATEDIFF(WEEK, '20130101', GETDATE())
),
-- and from below here it's your query with the PeriodEndDate threaded through
CTE_RESULT AS (
SELECT DATEADD(WEEK, -Number, GETDATE()) PeriodEndDate,
T.*
FROM #TBL_RESULT T
CROSS JOIN N
-- you see the range built up dynamically here
WHERE CreateDate > DATEADD(WEEK, -Number-10, GETDATE())
AND CreateDate < DATEADD(WEEK, -Number, GETDATE()) +1
)
SELECT PeriodEndDate, Campus ,
AVG(DAYS_TOOK) AS MedianTime
FROM (
SELECT PeriodEndDate, CAMPUS ,
Days_Took ,
ROW_NUMBER() OVER ( PARTITION BY PeriodEndDate, Campus ORDER BY Days_Took ASC ) AS AgeRank ,
COUNT(*) OVER ( PARTITION BY PeriodEndDate, CAMPUS ) AS CampusCount
FROM CTE_RESULT
) x
WHERE x.AgeRank IN ( x.CampusCount / 2 + 1, ( x.CampusCount + 1 ) / 2 )
GROUP BY x.PeriodEndDate, x.Campus
ORDER BY x.PeriodEndDate, x.Campus;

It seems that you solved the hard part of the problem.
To get what you want, you need to introduce a grouping variable. In this case, I measure the number of weeks in the past and divide by 10 (SQL Server does integer division so this produces an integer).
You just then judiciously use this in the partition by and group by statements:
WITH CTE_RESULT AS (
SELECT t.*,
DATEDIFF(week, CreateDate, GETDATE()) / 10 as groupnum
FROM #TBL_RESULT t
)
SELECT Campus, groupnum, MIN(CreateDate), MAX(CreateDate),
AVG(DAYS_TOOK) AS MedianTime
FROM (SELECT t.*,
ROW_NUMBER() OVER (PARTITION BY groupnum, Campus ORDER BY Days_Took ASC ) AS AgeRank ,
COUNT(*) OVER (PARTITION BY groupnum, CAMPUS) AS CampusCount
FROM CTE_RESULT t
) x
WHERE x.AgeRank IN ( x.CampusCount / 2 + 1, ( x.CampusCount + 1 ) / 2 )
GROUP BY x.Campus, groupnum
I haven't tested this, so it might have a syntax error or two.

Related

SELECT DateTime not in SQL

I have the following table:
oDateTime pvalue
2017-06-01 00:00:00 70
2017-06-01 01:00:00 65
2017-06-01 02:00:00 90
ff.
2017-08-01 08:00:00 98
The oDateTime field is an hourly data which is impossible to have a duplicate value.
My question is, how can I know if the oDateTime data is correct? I meant, I need to make sure the data is not jump? It should be always 'hourly' base.
Am I missing the date? Am I missing the time?
Please advice. Thank you.
Based on this answer, you can get the missing times form your table MyLogTable it like this:
DECLARE #StartDate DATETIME = '20170601', #EndDate DATETIME = '20170801'
SELECT DATEADD(hour, nbr - 1, #StartDate)
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY c.object_id ) AS Nbr
FROM sys.columns c
) nbrs
WHERE nbr - 1 <= DATEDIFF(hour, #StartDate, #EndDate) AND
NOT EXISTS (SELECT 1 FROM MyLogTable WHERE DATEADD(hour, nbr - 1, #StartDate)= oDateTime )
If you need to check longer period, you can just add CROSS JOIN like this
FROM sys.columns c
CROSS JOIN sys.columns c1
It enables you to check much more than cca thousand records (rowcount of sys.columns table) in one query.
Since your table is not having any unique id number, use a row_number() to get the row number in the cte , then perform an self inner join with the row id and next id ,take the difference of oDateTime accordingly, this will show exactly which row do not have time difference of one hour
;with cte(oDateTime,pValue,Rid)
As
(
select *,row_number() over(order by oDateTime) from [YourTableName] t1
)
select *,datediff(HH,c1.oDateTime,c2.oDateTime) as HourDiff from cte c1
inner join cte c2
on c1.Rid=c2.Rid-1 where datediff(HH,c1.oDateTime,c2.oDateTime) >1
You could use DENSE_RANK() for numbering the hours in a day from 1 to 24. Then all you have to do is to check whether the max rank is 24 or not for a day. if there is at least one entry for each hour, then dense ranking will have max value of 24.
Use the following query to find the date when you have a oDateTime missing.
SELECT [date]
FROM
(
SELECT *
, CAST(oDateTime AS DATE) AS [date]
, DENSE_RANK() OVER(PARTITION BY CAST(oDateTime AS DATE) ORDER BY DATEPART(HOUR, oDateTime)) AS rank_num
FROM Test
) AS t
GROUP BY [date]
HAVING(MAX(rank_num) != 24);
If you need validation for each row of oDateTime, you could do self join based on rank and get the missing hour for each oDateTime.
Perhaps you are looking for this? This will return dates having count < 24 - which indicates a "jump"
;WITH datecount
AS ( SELECT CAST(oDateTime AS DATE) AS [date] ,
COUNT(CAST(oDateTime AS DATE)) AS [count]
FROM #temp
GROUP BY ( CAST(oDateTime AS DATE) )
)
SELECT *
FROM datecount
WHERE [count] < 24;
EDIT: Since you changed the requirement from "How to know if there is missing" to "What is the missing", here's an updated query.
DECLARE #calendar AS TABLE ( oDateTime DATETIME )
DECLARE #min DATETIME = (SELECT MIN([oDateTime]) FROM #yourTable)
DECLARE #max DATETIME = (SELECT MAX([oDateTime]) FROM #yourTable)
WHILE ( #min <= #max )
BEGIN
INSERT INTO #calendar
VALUES ( #min );
SET #min = DATEADD(hh, 1, #min);
END;
SELECT t1.[oDateTime]
FROM #calendar t1
LEFT JOIN #yourTable t2 ON t1.[oDateTime] = t2.[oDateTime]
GROUP BY t1.[oDateTime]
HAVING COUNT(t2.[oDateTime]) = 0;
I first created a hourly calendar based on your MAX and MIN Datetime, then compared your actual table to the calendar to find out if there is a "jump".

How To Select Records in a Status Between Timestamps? T-SQL

I have a T-SQL Quotes table and need to be able to count how many quotes were in an open status during past months.
The dates I have to work with are an 'Add_Date' timestamp and an 'Update_Date' timestamp. Once a quote is put into a 'Closed_Status' of '1' it can no longer be updated. Therefore, the 'Update_Date' effectively becomes the Closed_Status timestamp.
I'm stuck because I can't figure out how to select all open quotes that were open in a particular month.
Here's a few example records:
Quote_No Add_Date Update_Date Open_Status Closed_Status
001 01-01-2016 NULL 1 0
002 01-01-2016 3-1-2016 0 1
003 01-01-2016 4-1-2016 0 1
The desired result would be:
Year Month Open_Quote_Count
2016 01 3
2016 02 3
2016 03 2
2016 04 1
I've hit a mental wall on this one, I've tried to do some case when filtering but I just can't seem to figure this puzzle out. Ideally I wouldn't be hard-coding in dates because this spans years and I don't want to maintain this once written.
Thank you in advance for your help.
You are doing this by month. So, three options come to mind:
A list of all months using left join.
A recursive CTE.
A number table.
Let me show the last:
with n as (
select row_number() over (order by (select null)) - 1 as n
from master..spt_values
)
select format(dateadd(month, n.n, q.add_date), 'yyyy-MM') as yyyymm,
count(*) as Open_Quote_Count
from quotes q join
n
on (closed_status = 1 and dateadd(month, n.n, q.add_date) <= q.update_date) or
(closed_status = 0 and dateadd(month, n.n, q.add_date) <= getdate())
group by format(dateadd(month, n.n, q.add_date), 'yyyy-MM')
order by yyyymm;
This does assume that each month has at least one open record. That seems reasonable for this purpose.
You can use datepart to extract parts of a date, so something like:
select datepart(year, add_date) as 'year',
datepart(month, date_date) as 'month',
count(1)
from theTable
where open_status = 1
group by datepart(year, add_date), datepart(month, date_date)
Note: this counts for the starting month and primarily to show the use of datepart.
Updated as misunderstood the initial request.
Consider following test data:
DECLARE #test TABLE
(
Quote_No VARCHAR(3),
Add_Date DATE,
Update_Date DATE,
Open_Status INT,
Closed_Status INT
)
INSERT INTO #test (Quote_No, Add_Date, Update_Date, Open_Status, Closed_Status)
VALUES ('001', '20160101', NULL, 1, 0)
, ('002', '20160101', '20160301', 0, 1)
, ('003', '20160101', '20160401', 0, 1)
Here is a recursive solution, that doesn't rely on system tables BUT also performs poorer. As we are talking about months and year combinations, the number of recursions will not get overhand.
;WITH YearMonths AS
(
SELECT YEAR(MIN(Add_Date)) AS [Year]
, MONTH(MIN(Add_Date)) AS [Month]
, MIN(Add_Date) AS YMDate
FROM #test
UNION ALL
SELECT YEAR(DATEADD(MONTH,1,YMDate))
, MONTH(DATEADD(MONTH,1,YMDate))
, DATEADD(MONTH,1,YMDate)
FROM YearMonths
WHERE YMDate <= SYSDATETIME()
)
SELECT [Year]
, [Month]
, COUNT(*) AS Open_Quote_Count
FROM YearMonths ym
INNER JOIN #test t
ON (
[Year] * 100 + [Month] <= CAST(FORMAT(t.Update_Date, 'yyyyMM') AS INT)
AND t.Closed_Status = 1
)
OR (
[Year] * 100 + [Month] <= CAST(FORMAT(SYSDATETIME(), 'yyyyMM') AS INT)
AND t.Closed_Status = 0
)
GROUP BY [Year], [Month]
ORDER BY [Year], [Month]
Statement is longer, also more readable and lists all year/month combinations to date.
Take a look at Date and Time Data Types and Functions for SQL-Server 2008+
and Recursive Queries Using Common Table Expressions

Query to get results for every hour of the day even data if not present

I am trying to get the query for getting the count of users every hour of day in the table. If the data for that hour is not present, I want to record the hour with count of zero. Also users should be counted only for their first entry. Subsequent entries should be ignored.
Table:
userId creationDate
1 2014-10-08 14:33:20.763
2 2014-10-09 04:24:14.283
3 2014-10-10 18:34:26.260
Desired output:
Date UserCount
2014-10-08 00:00:00.000 1
2014-10-08 01:00:00.000 1
2014-10-08 02:00:00.000 1
2014-10-08 03:00:00.000 0
2014-10-08 04:00:00.000 1
....
.....
2014-10-10 23:00:00.000 1
2014-10-10 00:00:00.000 0
My attempt:
SELECT
CAST(creationDate as date) AS ForDate,
DATEPART(hour, date) AS OnHour,
COUNT(distinct userId) AS Totals
FROM
Table
WHERE
primaryKey = 123
GROUP BY
CAST(creationDate as date), DATEPART(hour, createDate)
This only gives me per hour for the record that is present. Not the data for the missing hours. I think there is a way by using a cross join to get 0 data even for the missing hours.
Something like this, I came across, but not able to construct a proper query with it.
cross join (select
ROW_NUMBER() over (order by (select NULL)) as seqnum
from
INFORMATION_SCHEMA.COLUMNS) hours
where hours.seqnum >= 24
Once again, I am not a SQL expert, but trying hard to construct this result set.
One more attempt :
with dh as (
select DATEADD(hour, seqnum - 1, thedatehour ) as DateHour
from (select distinct cast(cast(createDate as DATE) as datetime) as thedatehour
from Table a
) a
cross join
(select ROW_NUMBER() over (order by (select NULL)) as seqnum
from INFORMATION_SCHEMA.COLUMNS
) hours
where hours.seqnum (less than)= 24
)
select dh.DateHour, COUNT(distinct c.userId)
from dh cross join Table c
--on dh.DateHour = c.createDate
group by dh.DateHour
order by 1
You need to build up a table of possible hours, and then join this to your actual records.
The best way to build up a table of possible hours is to use a recursive common table expression.
Here's how:
-- Example data
DECLARE #users TABLE(UserID INT, creationDate DATETIME)
INSERT #users
( UserID, creationDate )
VALUES ( 1, '2014-10-08 14:33:20.763'),
( 2, '2014-10-09 04:24:14.283'),
( 3, '2014-10-10 18:34:26.260')
;WITH u1st AS ( -- determine the FIRST time the user appears
SELECT UserID, MIN(creationDate) AS creationDate
FROM #users
GROUP BY UserID
), hrs AS ( -- recursive CTE of start hours
SELECT DISTINCT CAST(CAST(creationDate AS DATE) AS DATETIME) AS [StartHour]
FROM #users AS u
UNION ALL
SELECT DATEADD(HOUR, 1, [StartHour]) AS [StartHour] FROM hrs
WHERE DATEPART(HOUR,[StartHour]) < 23
), uGrp AS ( -- your data grouped by start hour
SELECT -- note that DATETIMEFROMPARTS is only in SQL Server 2012 and later
DATETIMEFROMPARTS(YEAR(CreationDate),MONTH(CreationDate),
DAY(creationDate),DATEPART(HOUR, creationDate),0,0,0)
AS StartHour,
COUNT(1) AS UserCount FROM u1st AS u
GROUP BY YEAR(creationDate), MONTH(creationDate), DAY(creationDate),
DATEPART(HOUR, creationDate)
)
SELECT hrs.StartHour, ISNULL(uGrp.UserCount, 0) AS UserCount
FROM hrs LEFT JOIN uGrp ON hrs.StartHour = uGrp.StartHour
ORDER BY hrs.StartHour
NB - DATETIMEFROMPARTS is only in SQL SERVER 2012 and greater. If you are using an earlier version of SQL SERVER you could have
WITH u1st AS ( -- determine the FIRST time the user appears
SELECT UserID, MIN(creationDate) AS creationDate
FROM #users
GROUP BY UserID
), hrs AS ( -- recursive CTE of start hours
SELECT DISTINCT CAST(CAST(creationDate AS DATE) AS DATETIME) AS [StartHour]
FROM #users AS u
UNION ALL
SELECT DATEADD(HOUR, 1, [StartHour]) AS [StartHour] FROM hrs
WHERE DATEPART(HOUR,[StartHour]) < 23
), uGrp AS ( -- your data grouped by start hour
SELECT -- note that DATETIMEFROMPARTS is only in SQL Server 2012 and later
CAST(CAST(YEAR(creationDate) AS CHAR(4)) + '-'
+ RIGHT('0' + CAST(MONTH(creationDate) AS CHAR(2)), 2) + '-'
+ RIGHT('0' + CAST(DAY(creationDate) AS CHAR(2)), 2) + ' '
+ RIGHT('0' + CAST(DATEPART(HOUR, creationDate) AS CHAR(2)), 2)
+ ':00:00.000'
AS DATETIME) AS StartHour,
COUNT(1) AS UserCount FROM u1st AS u
GROUP BY YEAR(creationDate), MONTH(creationDate), DAY(creationDate),
DATEPART(HOUR,creationDate)
)
SELECT hrs.StartHour, ISNULL(uGrp.UserCount, 0) AS UserCount
FROM hrs LEFT JOIN uGrp ON hrs.StartHour = uGrp.StartHour
ORDER BY hrs.StartHour
I asked a similar question on dba just this morning...https://dba.stackexchange.com/questions/86435/filling-in-date-holes-in-grouped-by-date-sql-data.
You can used my GetSequence function, or create a Numbers table. I haven't done my own testing yet to validate what was suggested in my scenario.
Try this:
BUILD SAMPLE DATA
CREATE TABLE yourTable(
userId INT,
creationDate DATETIME
)
INSERT INTO yourTable VALUES (1, '2014-10-08 14:33:20.763'), (2, '2014-10-09 04:24:14.283'),(3, '2014-10-10 18:34:26.260');
SOLUTION
WITH tally(N) AS(
SELECT TOP(23) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM sys.columns
)
,hourly(creationDate) AS(
SELECT DATEADD(HOUR, t.N, d.creationDate)
FROM tally t
CROSS JOIN(
SELECT DISTINCT DATEADD(DD, DATEDIFF(DD, 0, creationDate), 0) AS creationDate FROM yourTable
) d
)
SELECT
h.creationDate,
userCount = ISNULL(t.userCount, 0)
FROM hourly h
LEFT JOIN(
SELECT
creationDate = DATEADD(HOUR, DATEPART(HOUR, creationDate) ,DATEADD(DD, DATEDIFF(DD, 0, creationDate), 0)),
userCount = COUNT(*)
FROM yourTable
GROUP BY DATEADD(DD, DATEDIFF(DD, 0, creationDate), 0), DATEPART(HOUR, creationDate)
)t
ON t.creationDate = h.creationDate
CLEANUP
DROP TABLE yourTable
Create a temporary table (let's say #CreationDateHours) containing create date and hours from 0 to 23.
Declare #date as date
SELECT MAX(CAST(creationDate as date)) AS ForDate, 0 as OnHour into #CreationDateHours
FROM Table
WHERE
primaryKey = 123
Select #date=ForDate from #CreationDateHours
Declare #i int
Set #i=1
While #i<24
begin
insert into #CreationDateHours
select #date as ForDate, #i as OnHour
set #i+=1
end
Now, Run this query to get the desired results
select t1.ForDate, t1.OnHour, isnull(t2.Totals,0) AS Totals
from
#CreationDateHours t1 left join (SELECT
CAST(creationDate as date) AS ForDate,
DATEPART(hour, date) AS OnHour,
COUNT(distinct userId) AS Totals
FROM
Table
WHERE
primaryKey = 123
GROUP BY
CAST(creationDate as date), DATEPART(hour, createDate)) as t2
on t1.ForDate= t2.ForDate and t1.OnHour=t2.OnHour
select count, strftime('%H', creationDate) as hour from table group by hour;
OUTUPUT:
count hour
n1 01
n2 02
n3 03
... ...
n24 24
Tested on SQLite3
https://www.sqlite.org/lang_datefunc.html
you can see all the formats
(like %d for every day of month)
if you want the whole date
strftime('%Y-%m-%d %H, creationDate)

Create a weekCount column in SQL Server 2012

I have this data:
id worked_date
-----------------
1 2013-09-25
2 2013-09-26
3 2013-10-01
4 2013-10-04
5 2013-10-07
I want to add a column called weekCount. The based date is 2013-09-25. So all the data with worked_date from 2013-09-25 to 2013-10-01 will have weekCount as 1 and from 2013-10-02 to 2013-10-8 will have weekCount as 2 and so on. How can that be done?
Thanks.
Here's one way using DATEDIFF:
select id,
worked_date,
1 + (datediff(day, '2013-09-25', worked_date) / 7) weekCount
from yourtable
SQL Fiddle Demo
Perhaps an approach like this will solve your problem.
I compute an in-memory table that contains the week's boundaries along with a monotonically increasing number (BuildWeeks). I then compare my worked_date values to my date boundaries. Based on your comment to #sgeddes, you need the reverse week number so I then use a DENSE_RANK function to calculate the ReverseWeekNumber.
WITH BOT(StartDate) AS
(
SELECT CAST('2013-09-25' AS date)
)
, BuildWeeks (WeekNumber, StartOfWeek, EndOfWeek) AS
(
SELECT
N.number AS WeekNumber
, DateAdd(week, N.number -1, B.StartDate) AS StartOfWeek
, DateAdd(d, -1, DateAdd(week, N.number, B.StartDate)) AS EndOfWeek
FROM
dbo.Numbers AS N
CROSS APPLY
BOT AS B
)
SELECT
M.*
, BW.*
, DENSE_RANK() OVER (ORDER BY BW.WeekNumber DESC) AS ReverseWeekNumber
FROM
dbo.MyTable M
INNER JOIN
BuildWeeks AS BW
ON M.worked_date BETWEEN BW.StartOfWeek ANd BW.EndOfWeek
;
SQLFiddle
If you are looking for a Fiscal Week number, I would use a function that would calculate the week:
CREATE FUNCTION FiscalWeek(#FiscalStartDate datetime, #EvalDate datetime)
RETURNS INT
AS
BEGIN
DECLARE #weekNumber INT = (DATEDIFF(DAY, #FiscalStartDate, #EvalDate) / 7) + 1
RETURN (#weekNumber % 52)
END
GO
If you used a fiscal starting date of '2013-09-25' and an evaluation date of '2014-09-25' you would get a week number of 1.
Using a function gives you a little more flexibility to do whatever you need.
Perhaps not the most elegant way but this works for me to get the top rank number:
WITH CTE AS (
SELECT employee_id, DENSE_RANK() OVER (ORDER BY DATEDIFF(DAY, ''20130925'', worked_date )/7 DESC) AS weekRank
FROM Timesheet
)
SELECT TOP (1) weekRank
FROM CTE
WHERE employee_id=#employee_id
ORDER BY weekRank DESC
This is how I can create weekRank column and pass a parameter dynamically:
WITH rank_cte AS (
SELECT timesheet_id,employee_id, date_worked,
dateadd(week, datediff(day,'20000105',worked_date) / 7, '20000105') AS WeekStart,
dateadd(week, datediff(day,'20000105',worked_date) / 7, '20000105')+6 AS WeekEnd,
DENSE_RANK() OVER (ORDER BY 1 + DATEDIFF(DAY, '20130925', worked_date )/7 DESC) AS weekRank
FROM Timesheet
)
SELECT timesheet_id, worked_date, WeekStart, WeekEnd, weekRank
FROM rank_cte rc
WHERE employee_id=#employee_id
AND weekRank=#weekRank
ORDER BY worked_date DESC
Thanks

tsql month problem

I have a table like:
id month cost
------------------
1 Jan 200
1 Mar 204
1 May 200
1 Dec 201
I need an output like( order by month including the other months of a year-displaying all 12 months):
to month cost
------------------
1 Jan 200
NULL Feb NULL
1 Mar 204
....
....
....
1 Dec 201
any idea or solution how to do this in TSQL?
Thanks!
edit:: month is extracted from a datetime value.
in real world i'll have to show previous 12 months from last month in a DESC order! any suggestion for that?
Try building a reference table of months, and JOINing on it. It's the quickest way to do this with months in varchar datatype.
declare #foo table (id int, [mon] varchar(100), cost int)
declare #mon table (mon varchar(100), orderWeight int)
INSERT INTO #mon (mon, orderWeight)
VALUES ('Jan',1), ('Feb',2),('Mar',3),('Apr',4),('May',5),('Jun',6),('Jul',7),
('Aug',8),('Sep',9),('Oct',10),('Nov',11),('Dec',12)
INSERT INTO #foo(id, [mon], cost)
VALUES ( 1 ,'Jan' , 200),
( 1 ,'Mar', 204),
( 1 ,'May' , 200),
( 1 ,'Dec' , 201)
select f.id,
m.[mon] ,
f.cost
from #mon as m
left join #foo as f on m.mon = f.mon
order by m.orderWeight
Results:
Your ordering will now be guaranteed with the order by orderWeight.
Sample table
create table mytable(id int, dt datetime, cost money)
insert mytable values
(1,GETDATE()-10,200),
(1,GETDATE()-40,204),
(1,GETDATE()-100,200),
(1,GETDATE()-200,201);
The query, using SQL Server 2008 specific syntax, and sorted properly
select
t.id [to],
CONVERT(char(3),dateadd(month,-M.N,L.PVT),7) [Month],
sum(t.cost) totalCost
from (select PVT=dateadd(month,datediff(month,0,getdate())-1,0)) L
cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11)) M(N)
left join mytable t
on t.dt >= dateadd(month,-M.N,L.PVT)
and t.dt < dateadd(month,-M.N+1,L.PVT)
group by t.id, right(CONVERT(char(9),dt,6),6), M.N, L.PVT
order by M.N
What it does:
right(CONVERT(char(9),dt,6),6) converts a date into the format 'DD MMM YY', we only need the MMM YY part
In the SELECT, we further extract only the 3-char month from it, using LEFT( , 3)
The subquery L has a single record and column, PVT, which is the first date of the last month
The number series 0-11 is used to create the month values for the last 12 months, using the formula dateadd(month,-M.N,L.PVT)
The range t.dt >= .. and t.dt < .. finds data for a single month
How about this?
The result contains month and year, but you can strip it as you want.
;with months
as
(
select dateadd(month, -1, dateadd(day, datediff(day, 0, getdate()), 0)) as m
union all
select dateadd(month, -1, m)
from months
where m > dateadd(month, -12, getdate())
)
-- Testdata
,yourTable(id,somedate,cost)
as
(
select 1, '2011-01-03', 200
union all
select 1, '2011-03-06', 204
union all
select 1, '2010-05-09', 200
union all
select 1, '2010-05-19', 201
union all
select 1, '2010-12-02', 201
)
-- end testdata
select yt.id
,datename(month,coalesce(yt.somedate, m.m)) as [month]
,datename(year,coalesce(yt.somedate, m.m)) as [year]
--,yt.cost
,sum(yt.cost) as cost
from months m
left join yourTable yt
on datepart(year, yt.someDate) = DATEPART(year, m.m)
and datepart(month, yt.someDate) = DATEPART(month, m.m)
group by
yt.id
,datename(month,coalesce(yt.somedate, m.m))
,datename(year,coalesce(yt.somedate, m.m))
,m.m
order by m.m desc
Edit: Altered solution to support sum.
Remove the group by-section and alter the comment of cost, to get the old solution.