Good index for DATE range search in MSSQL - sql-server

I have a table in Azure SQL Database that looks like this:
CREATE TABLE [dbo].[DayStatus](
[Date] [DATE] NOT NULL,
[LocationId] [INT] NOT NULL,
[TypeId] [INT] NOT NULL,
[Total] [DECIMAL](15, 5) NULL,
[Timezone] [NVARCHAR](70) NULL,
[Currency] [NVARCHAR](3) NOT NULL,
PRIMARY KEY CLUSTERED
(
[Date] ASC,
[LocationId] ASC,
[TypeId] ASC
)
) ON [PRIMARY]
GO
Question
I need to optimize the following SELECT statement to the above table:
SELECT
[Date],
[LocationId],
[TypeId],
[Total],
[Timezone],
[Currency]
FROM [dbo].[DayStatus]
WHERE
Date >= '2022-06-01' and Date <= '2023-01-17'
and Currency = 'USD'
and LocationId in (1, 2, 3, 4, 6, 10)
and TypeId in (1, 2, 3, 5)
I have considered the following indexes, but it seems I cannot see a significant performance difference.
Which one is better, and is there an even better one?
Test 1
CREATE NONCLUSTERED INDEX [IX__Test1] ON [dbo].[DayStatus]
(
[Date] ASC,
[Currency] ASC,
[LocationId] ASC,
[TypeId] ASC
)
GO
Test 2
CREATE NONCLUSTERED INDEX [IX__Test2] ON [dbo].[DayStatus]
(
[Currency] ASC,
[LocationId] ASC,
[TypeId] ASC
)
INCLUDE([Date],[Timezone],[Total])
GO
EDIT
Would it be better to have the query below?
SELECT
[Date],
[LocationId],
[TypeId],
[Total],
[Timezone],
[Currency]
FROM [dbo].[DayStatus]
WHERE
Currency = 'USD'
and LocationId in (1, 2, 3, 4, 6, 10)
and TypeId in (1, 2, 3, 5)
and Date >= '2022-06-01' and Date <= '2023-01-17'

Equality columns should go before range columns.
Potentially the best indexing strategy would be
Any ordering of Currency, LocationId, TypeId as first three columns (choose whichever ordering is most useful for other queries in your workload and don't get hung up on selectivity here) followed by Date as the fourth key column and INCLUDE (Timezone, Total)
This would allow the query to return the results by UNION ALL-ing the results of 24 distinct index seeks (as the desired results are a concatenation of the following distinct ranges which can be seeked efficiently with such an index).
Currency = USD' and LocationId = 1 and TypeId = 1 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 10 and TypeId = 1 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 2 and TypeId = 1 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 3 and TypeId = 1 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 4 and TypeId = 1 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 6 and TypeId = 1 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 1 and TypeId = 2 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 10 and TypeId = 2 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 2 and TypeId = 2 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 3 and TypeId = 2 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 4 and TypeId = 2 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 6 and TypeId = 2 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 1 and TypeId = 3 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 10 and TypeId = 3 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 2 and TypeId = 3 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 3 and TypeId = 3 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 4 and TypeId = 3 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 6 and TypeId = 3 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 1 and TypeId = 6 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 10 and TypeId = 6 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 2 and TypeId = 6 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 3 and TypeId = 6 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 4 and TypeId = 6 and Date >= '2022-06-01' and Date <= '2023-01-17'
Currency = USD' and LocationId = 6 and TypeId = 6 and Date >= '2022-06-01' and Date <= '2023-01-17'
I added Date as a fourth key column to your IX__Test2 index and the execution plan shows an index seek but digging into the properties shows SQL Server is doing the above.

Related

SQL Server - Split a date range by a given date

I have a table that stores an Id and an effective period indicating when it is active
PortfolioId StartDate EndDate
1 2018-01-01 00:00:00.000 2018-05-31 00:00:00.000
2 2017-01-01 00:00:00.000 2018-05-31 00:00:00.000
I have another table that stores a component related to the above Id and that too has an effective period. Table 2 can have more that on entry for any given entry in table 1.
PortfolioComponentId PortfolioId SplitDate
1 1 2018-02-28 00:00:00.000
2 1 2018-03-31 00:00:00.000
3 2 2017-03-31 00:00:00.000
4 2 2017-09-20 00:00:00.000
5 2 2018-01-15 00:00:00.000
I have a period where I am running the query for
i.e
StartDate : 30-JUN-2017
End date : 15-MAY-2018
I have looking for a result like the below, where data in table 1 is split based on the data from table 2
PortfolioId StartDate EndDate
1 2018-01-01 00:00:00.000 2018-02-28 00:00:00.000
1 2018-03-01 00:00:00.000 2018-03-31 00:00:00.000 - Starts from End date + 1 from the prev row
1 2018-04-01 00:00:00.000 2018-05-15 00:00:00.000
2 2017-06-30 00:00:00.000 2017-09-20 00:00:00.000 - Starts from Seach date [Portfolio component Id 3 ignored as it falls outside of search date range]
2 2017-09-21 00:00:00.000 2018-01-15 00:00:00.000
2 2018-01-16 00:00:00.000 2018-05-15 00:00:00.000 - Ends by seach end date
Data setup - In case it helps
DECLARE #SearchStartDate DATETIME = '30-JUN-2017'
DECLARE #SearchEndDate DATETIME = '15-MAY-2018'
DECLARE #Portfolio TABLE
(
PortfolioId INT PRIMARY KEY IDENTITY(1,1),
StartDate DATETIME,
EndDate DATETIME
)
INSERT INTO #Portfolio
SELECT '01-JAN-2018', '31-MAY-2018'
INSERT INTO #Portfolio
SELECT '01-JAN-2017', '31-MAY-2018'
DECLARE #PortfolioComponents TABLE
(
PortfolioComponentId INT PRIMARY KEY IDENTITY(1,1),
PortfolioId INT,
SplitDate DATETIME
)
INSERT INTO #PortfolioComponents
SELECT 1, '28-FEB-2018'
INSERT INTO #PortfolioComponents
SELECT 1, '31-MAR-2018'
INSERT INTO #PortfolioComponents
SELECT 2, '31-MAR-2017'
INSERT INTO #PortfolioComponents
SELECT 2, '20-SEP-2017'
INSERT INTO #PortfolioComponents
SELECT 2, '15-JAN-2018'
SELECT * from #Portfolio
SELECT * from #PortfolioComponents
I believe I have got a result close to what I want, though not
the most ideal approach
the best approach from a performance perspective
DECLARE #Temp TABLE (BenchmarkDate Datetime, ComponentType int, PortfolioId INT)
INSERT INTO #Temp
SELECT StartDate , 1, PortfolioId FROM #Portfolio
UNION
SELECT EndDate , 2, PortfolioId FROM #Portfolio
INSERT INTO #Temp
SELECT SplitDate , 2, PortfolioId FROM #PortfolioComponents
UNION
SELECT SplitDate + 1 , 1, PortfolioId FROM #PortfolioComponents
DECLARE #Results TABLE
(
Id INT IDENTITY(1,1),
StartDate DATETIME,
EndDate DATETIME,
PortfolioId INT
)
INSERT INTO #Results
SELECT rset1.BenchmarkDate [Startdate],
( SELECT MIN(rset2.BenchmarkDate)
FROM #Temp rset2
WHERE rset2.ComponentType = 2
AND rset2.BenchmarkDate > rset1.BenchmarkDate
AND rset1.PortfolioId = rset2.PortfolioId) [Enddate],
rset1.PortfolioId
FROM #Temp rset1
WHERE rset1.ComponentType = 1
ORDER BY rset1.PortfolioId, rset1.BenchmarkDate
SELECT
CASE WHEN (#SearchStartDate BETWEEN StartDate AND EndDate ) THEN #SearchStartDate ELSE StartDate END StartDate,
CASE WHEN (#SearchEndDate BETWEEN StartDate AND EndDate) THEN #SearchEndDate ELSE EndDate END EndDate,
PortfolioId ,
(
SELECT PortfolioComponentId
FROM #PortfolioComponents pc
WHERE
(pc.PortfolioID = r.PortfolioId AND
(DATEADD(d, 1, pc.SplitDate) = r.StartDate ))
) PortfolioComponentId
FROM #Results r
WHERE
(#SearchStartDate < StartDate AND #SearchStartDate < EndDate AND #SearchEndDate > EndDate)
OR
(#SearchEndDate BETWEEN StartDate AND EndDate)
OR
(#SearchStartDate BETWEEN StartDate AND EndDate )

skip holiday & weekly off from two date

i am using sql server 2008 R2 , i am creating application leave form. when user apply leave i.e start date & end date then between two dates , all dates should skip if they fall in holiday & weekly off
CREATE TABLE #HolidayMaster
(
[HolidayID] [int] IDENTITY(1,1) NOT NULL,
[HolidayDescription] [nvarchar](50) NULL,
[HolidayDate] [date] NULL,
)
INSERT INTO #HolidayMaster
select 'New Year', '2016-01-01'
union
select 'National Developer Day', '2016-01-05'
CREATE TABLE #ShiftMaster
(
[ShiftID] [int] IDENTITY(1,1) NOT NULL,
[Sunday] [float] NULL,
[Monday] [float] NULL,
[Tuesday] [float] NULL,
[Wednesday] [float] NULL,
[Thursday] [float] NULL,
[Friday] [float] NULL,
[Saturday] [float] NULL
)
INSERT INTO #ShiftMaster
select 0,1,1,1,1,1,0
select *,DATENAME (dw,[HolidayDate]) as [DayName] from #HolidayMaster
select * from #ShiftMaster
drop table #HolidayMaster
drop table #ShiftMaster
Declare #LeaveStartDate date = '2013-01-01'
Declare #LeaveEndDate date = '2013-01-06'
--expected out put
DName Date Desc
Friday 2016-01-01 Holiday
Saturday 2016-01-02 WeeklyOff
Sunday 2016-01-03 WeeklyOff
Monday 2016-01-04 Working
Tuesday 2016-01-05 Holiday
Wednesday 2016-01-06 Working
You need a calendar table. I have used tally table to generate dates.
Declare #LeaveStartDate date = '2016-01-01'
Declare #LeaveEndDate date = '2016-01-06'
;WITH Nbrs_3( n ) AS ( SELECT 1 UNION SELECT 0 ),
Nbrs_2( n ) AS ( SELECT 1 FROM Nbrs_3 n1 CROSS JOIN Nbrs_3 n2 ),
Nbrs_1( n ) AS ( SELECT 1 FROM Nbrs_2 n1 CROSS JOIN Nbrs_2 n2 ),
Nbrs_0( n ) AS ( SELECT 1 FROM Nbrs_1 n1 CROSS JOIN Nbrs_1 n2 ),
Nbrs ( n ) AS ( SELECT 1 FROM Nbrs_0 n1 CROSS JOIN Nbrs_0 n2 ),
calendar(dates) as
(
SELECT Dateadd(dd, n - 1, #LeaveStartDate) AS Date
FROM (SELECT Row_number()OVER (ORDER BY n)
FROM Nbrs) D ( n )
WHERE n <= Datediff(day, #LeaveStartDate, #LeaveEndDate)+ 1
)
SELECT DName =d.day_name,
Date = c.dates,
CASE
WHEN HolidayDate IS NOT NULL THEN 'Holiday'
WHEN leav_iden = 0 THEN 'WeeklyOff'
WHEN HolidayDate IS NOT NULL
AND leav_iden = 0 THEN 'Holiday/WeeklyOff'
ELSE 'Working'
END AS [Desc]
FROM calendar c
JOIN (SELECT *
FROM #ShiftMaster
CROSS apply (VALUES ([Sunday],'Sunday'),
([Monday],'Monday'),
([Tuesday],'Tuesday'),
([Wednesday],'Wednesday'),
([Thursday],'Thursday'),
([Friday],'Friday'),
([Saturday],'Saturday') ) cs(leav_iden, day_name)) d
ON Datename(WEEKDAY, c.dates) = d.day_name
LEFT JOIN #HolidayMaster h
ON h.HolidayDate = c.dates
Result :
DName Date Desc
----- ---------- -------------
Sunday 2016-01-03 WeeklyOff
Monday 2016-01-04 Working
Tuesday 2016-01-05 Holiday
Wednesday 2016-01-06 Working
Friday 2016-01-01 Holiday
Saturday 2016-01-02 WeeklyOff
try
Declare #LeaveStartDate date = '2016-01-01'
Declare #LeaveEndDate date = '2016-01-06'
;with dts(dt) as (select #LeaveStartDate
union all
select dateadd(day,1,dt) from dts where dt<#LeaveEndDate),
shifmstr as (
select shiftID,'Sunday' as dname,[sunday] tp from #ShiftMaster union all
select shiftID,'Monday' ,Monday from #ShiftMaster union all
select shiftID,'Tuesday' ,Tuesday from #ShiftMaster union all
select shiftID,'Wednesday',Wednesday from #ShiftMaster union all
select shiftID,'Thursday',Thursday from #ShiftMaster union all
select shiftID,'Friday',Friday from #ShiftMaster union all
select shiftID,'Saturday',Saturday from #ShiftMaster)
select datename(dw,dt) DName,Dt,
case when c.HolidayDate is not null then 'Holiday'
when tp=1 then 'Working' else 'WeeklyOff' end descr
from dts a join shifmstr b on
datename(dw,a.dt)=b.dname left join
#HolidayMaster c on a.dt=c.HolidayDate
First thing you need is to generate the dates based on an interval. A nice an quick way is shown here. Then, you can LEFT JOIN with your #HolidayMaster table to check if a date is a holiday or not.
The code should look like this:
Declare #LeaveStartDate date = '2016-01-01'
Declare #LeaveEndDate date = '2016-01-06'
;WITH Dates_CTE AS (
SELECT TOP (DATEDIFF(DAY, #LeaveStartDate, #LeaveEndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #LeaveStartDate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
)
SELECT DATENAME (dw,[Date]) as [DayName],
[Date],
(CASE WHEN DATEPART(dw, [Date]) IN (1, 7) THEN 'WeeklyOff'
WHEN HM.HolidayID IS NOT NULL THEN 'Holiday' -- HM.Description can be used here to actually display holiday name
ELSE 'Working' END) [Desc]
FROM Dates_CTE D
LEFT JOIN #HolidayMaster HM ON HM.HolidayDate = D.[Date]
Usually LEFT JOIN .. IS NULL check is slower than NOT EXISTS, so an an alternative is to remove LEFT JOIN and check for existence, but I find this form more readable.
Recursive CTE to generate dates between the date range, left join on HolidayMaster and left join on ShiftMaster (I changed the ShiftMaster to have day names to join on), then select the proper description depending on the values:
CREATE TABLE #HolidayMaster
(
[HolidayID] [int] IDENTITY(1,1) NOT NULL,
[HolidayDescription] [nvarchar](50) NULL,
[HolidayDate] [date] NULL,
)
INSERT INTO #HolidayMaster
select 'New Year', '2016-01-01'
union
select 'National Developer Day', '2016-01-05'
CREATE TABLE #ShiftMaster
(
[ShiftID] [int] IDENTITY(1,1) NOT NULL,
[Day] nvarchar(20) NULL,
IsHoliday bit NULL
)
INSERT INTO #ShiftMaster
values ('Sunday', 0), ('Monday', 1), ('Tuesday', 1), ('Wednesday',1), ('Thursday', 1), ('Friday', 1), ('Saturday',0)
select *,DATENAME (dw,[HolidayDate]) as [DayName] from #HolidayMaster
select * from #ShiftMaster
Declare #LeaveStartDate date = '2016-01-01'
Declare #LeaveEndDate date = '2016-01-06'
;WITH Dates AS (
SELECT
[Date] = #LeaveStartDate
UNION ALL SELECT
[Date] = DATEADD(DAY, 1, [Date])
FROM
Dates
WHERE
Date < #LeaveEndDate
) SELECT
DATENAME(dw, [Date])
, [Date]
, CASE WHEN hm.HolidayDescription IS NULL THEN
CASE WHEN sm.IsHoliday = 1 THEN 'Working' ELSE 'Weekly Off' END
ELSE 'Holiday' END AS Description
FROM
Dates
left join #HolidayMaster hm on hm.HolidayDate = [Date]
left join #ShiftMaster sm on sm.Day = DATENAME(dw, [Date])
drop table #HolidayMaster
drop table #ShiftMaster
try this,
DECLARE #LeaveStartDate DATETIME = '2016-01-01'
DECLARE #LeaveEndDate DATETIME = '2016-01-06'
;WITH CTE
AS ( SELECT #LeaveStartDate AS LeaveDate
UNION ALL
SELECT LeaveDate + 1
FROM CTE
WHERE LeaveDate < #LeaveEndDate
)
SELECT * ,DATENAME(WEEKDAY, c.LeaveDate)
FROM CTE c
LEFT JOIN #HolidayMaster h ON c.LeaveDate = h.HolidayDate
WHERE h.HolidayDate IS NULL
AND EXISTS ( SELECT 1
FROM #ShiftMaster s
WHERE (DATENAME(WEEKDAY, c.LeaveDate) = 'Sunday' AND s.Sunday = 1)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Monday' AND s.Monday = 1)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Tuesday' AND s.Tuesday = 1)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Wednesday' AND s.Wednesday = 1)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Thursday' AND s.Thursday = 1)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Friday' AND s.Friday = 1)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Saturday' AND s.Saturday = 1)
)
If you are looking to get all dates with status try this,
DECLARE #LeaveStartDate DATETIME = '2016-01-01'
DECLARE #LeaveEndDate DATETIME = '2016-01-06'
;WITH CTE
AS ( SELECT #LeaveStartDate AS LeaveDate
UNION ALL
SELECT LeaveDate + 1
FROM CTE
WHERE LeaveDate < #LeaveEndDate
)
SELECT
c.LeaveDate,
DATENAME(WEEKDAY, c.LeaveDate) AS [DayName],
CASE WHEN (DATENAME(WEEKDAY, c.LeaveDate) = 'Sunday' AND s.Sunday = 0)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Monday' AND s.Monday = 0)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Tuesday' AND s.Tuesday = 0)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Wednesday' AND s.Wednesday = 0)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Thursday' AND s.Thursday = 0)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Friday' AND s.Friday = 0)
OR (DATENAME(WEEKDAY, c.LeaveDate) = 'Saturday' AND s.Saturday = 0)
THEN 'WeeklyOff'
WHEN h.HolidayDate IS NOT NULL
THEN 'Holiday'
ELSE 'Working'
END AS [Status]
FROM CTE c
CROSS JOIN #ShiftMaster s
LEFT JOIN #HolidayMaster h ON c.LeaveDate = h.HolidayDate

sql query to find the Items with the highest difference

I have my database table ABC as shown below :
ItemId Month Year Sales
1 1 2013 333
1 2 2013 454
2 1 2013 434
and so on .
I would like to write a query to find the top 3 items that have had the highest increase in sales from last month to this month , so that I see somethinglike this in the output.
Output :
ItemId IncreaseInSales
1 +121
9 +33
6 +16
I came up to here :
select
(select Sum(Sales) from ABC where [MONTH] = 11 )
-
(select Sum(Sales) from ABC where [MONTH] = 10)
I cannot use a group by as it is giving an error . Can anyone point me how I can
proceed further ?
Assuming that you want the increase for a given month, you can also do this with an aggregation query:
select top 3 a.ItemId,
((sum(case when year = #YEAR and month = #MONTH then 1.0*sales end) /
sum(case when year = #YEAR and month = #MONTH - 1 or
year = #YEAR - 1 and #Month = 1 and month = 12
then sales end)
) - 1
) * 100 as pct_increase
from ABC a
group by a.ItemId
order by pct_increase desc;
You would put the year/month combination you care about in the variables #YEAR and #MONTH.
EDIT:
If you just want the increase, then do a difference:
select top 3 a.ItemId,
(sum(case when year = #YEAR and month = #MONTH then 1.0*sales end) -
sum(case when year = #YEAR and month = #MONTH - 1 or
year = #YEAR - 1 and #Month = 1 and month = 12
then sales
end)
) as difference
from ABC a
group by a.ItemId
order by difference desc;
Here is the SQL Fiddle that demonstrates the below query:
SELECT TOP(3) NewMonth.ItemId,
NewMonth.Month11Sales - OldMonth.Month10Sales AS IncreaseInSales
FROM
(
SELECT s1.ItemId, Sum(s1.Sales) AS Month11Sales
FROM ABC AS s1
WHERE s1.MONTH = 11
AND s1.YEAR = 2013
GROUP BY s1.ItemId
) AS NewMonth
INNER JOIN
(
SELECT s2.ItemId, Sum(s2.Sales) AS Month10Sales
FROM ABC AS s2
WHERE s2.MONTH = 10
AND s2.YEAR = 2013
GROUP BY s2.ItemId
) AS OldMonth
ON NewMonth.ItemId = OldMonth.ItemId
ORDER BY NewMonth.Month11Sales - OldMonth.Month10Sales DESC
You never mentioned if you could have more than one record for an ItemId with the same Month, so I made the query to handle it either way. Obviously you were lacking the year = 2013 in your query. Once you get past this year you will need that.
Another option could be something on these lines:
SELECT top 3 a.itemid, asales-bsales increase FROM
(
(select itemid, month, sum(sales) over(partition by itemid) asales from ABC where month=2
and year=2013) a
INNER JOIN
(select itemid, month, sum(sales) over(partition by itemid) bsales from ABC where month=1
and year=2013) b
ON a.itemid=b.itemid
)
ORDER BY increase desc
if you need to cater for months without sales then you can do a FULL JOIN and calculate increase as isnull(asales,0) - isnull(bsales,0)
You could adapt this solution based on PIVOT operator:
SET NOCOUNT ON;
DECLARE #Sales TABLE
(
ItemID INT NOT NULL,
SalesDate DATE NOT NULL,
Amount MONEY NOT NULL
);
INSERT #Sales (ItemID, SalesDate, Amount)
VALUES
(1, '2013-01-15', 333), (1, '2013-01-14', 111), (1, '2012-12-13', 100), (1, '2012-11-12', 150),
(2, '2013-01-11', 200), (2, '2012-12-10', 150), (3, '2013-01-09', 900);
-- Parameters (current year & month)
DECLARE #pYear SMALLINT = 2013,
#pMonth TINYINT = 1;
DECLARE #FirstDayOfCurrentMonth DATE = CONVERT(DATE, CONVERT(CHAR(4), #pYear) + '-' + CONVERT(CHAR(2), #pMonth) + '-01');
DECLARE #StartDate DATE = DATEADD(MONTH, -1, #FirstDayOfCurrentMonth), -- Begining of the previous month
#EndDate DATE = DATEADD(DAY, -1, DATEADD(MONTH, 1, #FirstDayOfCurrentMonth)) -- End of the current month
SELECT TOP(3) t.ItemID,
t.[2]-t.[1] AS IncreaseAmount
FROM
(
SELECT y.ItemID, y.Amount,
DENSE_RANK() OVER(ORDER BY y.FirstDayOfSalesMonth ASC) AS MonthNum -- 1=Previous Month, 2=Current Month
FROM
(
SELECT x.ItemID, x.Amount,
DATEADD(MONTH, DATEDIFF(MONTH, 0, x.SalesDate), 0) AS FirstDayOfSalesMonth
FROM #Sales x
WHERE x.SalesDate BETWEEN #StartDate AND #EndDate
) y
) z
PIVOT( SUM(z.Amount) FOR z.MonthNum IN ([1], [2]) ) t
ORDER BY IncreaseAmount DESC;
SQLFiddle demo
Your sample data seems to be incomplete, however, here is my try. I assume that you want to know the three items with the greatest sales-difference from one month to the next:
WITH Increases AS
(
SELECT a1.itemid,
a1.sales - (SELECT a2.sales
FROM dbo.abc a2
WHERE a1.itemid = a2.itemid
AND ( ( a1.year = a2.year
AND a1.month > 1
AND a1.month = a2.month + 1 )
OR ( a1.year = a2.year + 1
AND a1.month = 1
AND a2.month = 12 ) ))AS IncreaseInSales
FROM dbo.abc a1
)
SELECT TOP 3 ItemID, MAX(IncreaseInSales) AS IncreaseInSales
FROM Increases
GROUP BY ItemID
ORDER BY MAX(IncreaseInSales) DESC
Demo
SELECT
cur.[ItemId]
MAX(nxt.[Sales] - cur.[Sales]) AS [IncreaseInSales]
FROM ABC cur
INNER JOIN ABC nxt ON (
nxt.[Year] = cur.[Year] + cur.[month]/12 AND
nxt.[Month] = cur.[Month]%12 + 1
)
GROUP BY cur.[ItemId]
I'd do this this way. It should work in all the tagged versions of SQL Server:
SELECT TOP 3 [ItemId],
MAX(CASE WHEN [Month] = 2 THEN [Sales] END) -
MAX(CASE WHEN [Month] = 1 THEN [Sales] END) [Diff]
FROM t
WHERE [Month] IN (1, 2) AND [Year] = 2013
GROUP BY [ItemId]
HAVING COUNT(*) = 2
ORDER BY [Diff] DESC
Fiddle here.
The reason why I'm adding the HAVING clause is that if any item is added in only one of the months then the numbers will be all wrong. So I'm only comparing items that are only present in both months.
The reason of the WHERE clause would be to filter in advance only the needed months and improve the efficiency of the query.
An SQL Server 2012 solution could also be:
SELECT TOP 3 [ItemId], [Diff] FROM (
SELECT [ItemId],
LEAD([Sales]) OVER (PARTITION BY [ItemId] ORDER BY [Month]) - [Sales] Diff
FROM t
WHERE [Month] IN (1, 2) AND [Year] = 2013
) s
WHERE [Diff] IS NOT NULL
ORDER BY [Diff] DESC

TSQL - How to optimize the query?

I have the table with News
News
-------
NewsId
NewsText
CREATED_DATE
I need to get news starting from a specified date to a unknown date, but result should contain news for 5 days.
For example:
if I have news related to these dates: 29th, 28th, 27th, 5th, 4th, 3rd
and the starting date specified to 29th, I need to get news where created date between 29 and 4.
I don't know how to get the low date (4th) in that case without brute force:
declare #highDate date = '2011-09-20';
declare #rows int = 0;
declare #lowDate date = #highDate;
declare #i int = 0;
--Querying while rows count != 5
WHILE (#rows != 5)
BEGIN
if (#i = 60)
break;
set #i = #i + 1;
set #lowDate = (select DATEADD(day, -1, #lowDate));
set #rows = (select COUNT(*) from
(SELECT DAY(CAST(CREATED_DATE AS date)) as c1
FROM .[dbo].[NEWS]
and CREATED_DATE > #lowDate
and CREATED_DATE < #highDate
group by DAY(CAST(CREATED_DATE AS date))) as rowsCount);
END
--then return news between to known dates
SELECT *
FROM [dbo].[NEWS]
and CREATED_DATE > #lowDate
and CREATED_DATE < #highDate
order by CREATED_DATE desc
I guess in that algorithm there are too much queries against a DB and I'd like to get rid of 60-days old limitation
declare #highDate date = '2011-09-20'
select * from (
select *,
dense_rank() over (order by cast(created_date as date) desc) rnk
from News
where CREATED_DATE <= #highDate
) as t
where t.rnk <= 5
This might do the trick for you.
declare #HighDate date = '2011-11-29'
declare #LowDate date
select #LowDate = min(N3.created_date)
from (
select top(5) N2.created_date
from (
select distinct cast(N1.created_date as date) as created_date
from news as N1
where cast(N1.created_date as date) <= #HighDate
) as N2
order by 1 desc
) as N3
Or you can use dense_rank
select #LowDate = N.created_date
from (
select created_date,
dense_rank() over(order by cast(created_date as date) desc) as rn
from News
where cast(created_date as date) <= #HighDate
) as N
where N.rn = 5

SQL - 2 Counts in one query

I have 2 queries which return counts of different information in a table:
SELECT Date, COUNT(*) AS Total
FROM Table
WHERE Type = 7 AND Date >= '2010-01-01'
GROUP BY Date
HAVING COUNT(*) > 5000
ORDER BY Date
which returns the totals for all of the 'busy' dates:
Date Total
---------- -----------
2010-01-05 9466
2010-02-02 8747
2010-03-02 9010
2010-04-06 7916
2010-05-05 9342
2010-06-02 8723
2010-07-02 7829
2010-08-03 8411
2010-09-02 7687
2010-10-04 7706
2010-11-02 8567
2010-12-02 7645
and
SELECT Date, COUNT(*) AS Failures
FROM Table
WHERE Type = 7 AND ErrorCode = -2 AND Date >= '2010-01-01'
GROUP BY Date
ORDER BY Date
which returns the total failures (all of which happened on busy dates):
Date Failures
---------- -----------
2010-09-02 29
2010-10-04 16
2010-11-02 8
Is it possible to combine these into a single query to return one result?
E.g.:
Date Total Failures
---------- ----------- -----------
2010-01-05 9466
2010-02-02 8747
2010-03-02 9010
2010-04-06 7916
2010-05-05 9342
2010-06-02 8723
2010-07-02 7829
2010-08-03 8411
2010-09-02 7687 29
2010-10-04 7706 16
2010-11-02 8567 8
2010-12-02 7645
;With baseData As
(
SELECT
Date,
COUNT(*) AS Total,
COUNT(CASE WHEN ErrorCode = -2 THEN 1 END) AS Failures
FROM Table
WHERE Type = 7 AND Date >= '2010-01-01'
GROUP BY Date
)
SELECT
Date,
Total,
Failures,
CAST(Failures AS float)/Total AS Ratio
FROM baseData
WHERE Total > 5000 OR Failures > 0
ORDER BY Date
If you can refactor to the same where clause, this should be possible.
I haven't taken your HAVING(Count()) into consideration
SELECT [Date], COUNT(*) AS Total, SUM(CASE WHEN ErrorCode = -2 THEN 1 ELSE 0 END) AS Failures
FROM [Table]
WHERE [Type] = 7 AND [Date] >= '2010-01-01'
GROUP BY [Date]
ORDER BY [Date]
Edit : Here is some test data
create table [Table]
(
[ErrorCode] int,
[Type] int,
[Date] datetime
)
insert into [table]([Date], [Type], [ErrorCode] )values ('1 Jan 2010', 7, 0)
insert into [table]([Date], [Type], [ErrorCode] )values ('1 Jan 2010', 7, -2)
insert into [table]([Date], [Type], [ErrorCode] )values ('2 Jan 2010', 7, -2)
insert into [table]([Date], [Type], [ErrorCode] )values ('2 Jan 2010', 8, -2)
insert into [table]([Date], [Type], [ErrorCode] )values ('2 Jan 2010', 7, 1)
yes you should be able to do a UNION ALL between the 2 tables

Resources