I have a relatively simple stored procedure that is used to group data inside a relatively nasty view I have no control over based on the supplied parameters. A simplified version of the stored procedure is:
CREATE PROCEDURE dbo.GroupList
#list VARCHAR(MAX),
#start DATETIME,
#end DATETIME,
#resolution INT
AS
BEGIN
SET NOCOUNT ON
declare #elements ListTVP;
insert into #elements select Value as elementID from udf_Split(#localList, ',');
IF #resolution = 1 -- group by month
BEGIN
SELECT *
FROM (
SELECT
Timestamp = CAST(CONVERT(VARCHAR, DATEPART(YEAR, Timestamp)) + '-' + CONVERT(VARCHAR, DATEPART(MONTH, Timestamp)) + '-01' AS DATETIME)
, Total = SUM(Cost)
FROM eLP e
INNER JOIN #elements m ON e.elementID = m.elementID
WHERE Timestamp >= #start AND Timestamp <= #end
GROUP BY
DATEPART(YEAR, Timestamp),
DATEPART(MONTH, Timestamp)
) AS t1
END
ELSE IF #resolution = 2 -- group by year
BEGIN
SELECT *
FROM (
SELECT
Timestamp = CAST(CONVERT(VARCHAR, DATEPART(YEAR, Timestamp)) + '-01-01' AS DATETIME)
, Total = SUM(Cost)
FROM eLP e
INNER JOIN #elements m ON e.elementID = m.elementID
WHERE Timestamp >= #start AND Timestamp <= #end
GROUP BY
DATEPART(YEAR, Timestamp)
) AS t1
END
END
If I run the SQL code in the stored procedure inside SSMS, the code is relatively fast. If I run the stored procedure, the code is between 10 to 100 times slower for the exact same data. Why is this happening?
Also, I've noticed that if I create local variables from the supplied parameters, performance improves dramatically:
CREATE PROCEDURE dbo.GroupListWithVariables
#list VARCHAR(MAX),
#start DATETIME,
#end DATETIME,
#resolution INT
AS
BEGIN
DECLARE #localList VARCHAR(MAX) = #list
DECLARE #localStart DATETIME = #start, #localEnd DATETIME = #end
DECLARE #localResolution INT = #resolution
Comparing the execution time of the 2 versions of the stored procedure, I have:
GroupList:
568 ms average execution time
9 seconds reported by SSMS to actually run the sproc and retrieve the results
355 kB of data received from server
GroupListWithVariables:
1040 ms average execution time
1 second reported by SSMS to actually run the sproc and retrieve the results
239 kB of data received from server
Why is there such a large difference between the 2 versions? I've noticed the same behavior in both SQL Server 2008 R2, as well as in SQL Server 2012.
You may want to read about SQL Parameter Sniffing. http://blogs.technet.com/b/mdegre/archive/2012/03/19/what-is-parameter-sniffing.aspx
This might help explain the performance difference.
Related
I have two tables, one is Period and the other is UserTarget.
Period table is:
QuarterNo StartDate EndDate
-----------------------------------------
1 2018-04-01 2018-06-30
2 2018-07-01 2018-09-30
3 2018-10-01 2018-12-31
4 2019-01-01 2019-03-31
UserTarget table is :
USERID YEAR QTR AMOUNT
---------------------------------
akshay 2019 1 200
Right now I am taking the qtr no from period table. Now I don't need take qtr no from period table. I want it from stored procedure based on year is entered in usertarget table
Existing stored procedure :
ALTER PROCEDURE [dbo].[GetQuarterlyTargetData]
#Userid VARCHAR(50)
AS
BEGIN
DECLARE #QuarterNumber VARCHAR(50)
DECLARE #SetTarget DECIMAL(10);
DECLARE #StartDate DATE
DECLARE #EndDate DATE
SELECT
#QuarterNumber = p.QuarterNo,
#SetTarget = AMOUNT
FROM
PERIOD p
LEFT OUTER JOIN
USERTARGETS s ON p.QuarterNo = s.QTR
WHERE
StartDate <= GETDATE() AND EndDate >= GETDATE()
SELECT
#StartDate = StartDate,
#EndDate = EndDate
FROM
PERIOD
WHERE
QuarterNo = #QuarterNumber
From this procedure I am getting the start date and end date for quarter but I don't want to modify in period table every time when I want to check previous years data.
I believe that the term you are looking for is fiscal year. It's where the company year is different than the calendar year.
Note that many people recommend using a lookup table instead of calculating it. Date matches can be difficult for SQL to optimize.
Here's one way to do it. Finding the fiscal year and quarter would probably be good to put in a table function.
DECLARE #userTarget TABLE (UserId VARCHAR(20), Year INT, Quarter INT, Amount INT)
INSERT INTO #userTarget
VALUES
('akshay', 2018, 4, 150)
,('akshay', 2019, 1, 200)
SELECT
s.UserId
,s.Amount
,FY.FiscalYear
,FQ.FiscalQuarter
FROM
(
SELECT
--DATEFROMPARTS(2019, 2, 23)
GETDATE()
AS reportDate
) AS ReportDate
CROSS APPLY (
SELECT
CASE WHEN MONTH(ReportDate.reportDate) < 4 THEN YEAR(ReportDate.reportDate) - 1 -- Fiscal Year begins in April
ELSE YEAR(ReportDate.reportDate)
END AS FiscalYear
) AS FY
CROSS APPLY (
SELECT
DATEDIFF(QUARTER, DATEFROMPARTS(FY.FiscalYear, 4, 1), ReportDate.reportDate) + 1 AS FiscalQuarter
) AS FQ
INNER JOIN #userTarget s
ON s.Year = FY.FiscalYear
AND s.Quarter = FQ.FiscalQuarter
Also, be careful with end dates. last_day >= GETDATE() does not include the last day. Take as an example the end of last quarter, it would calculate it as '2019-03-31 00:00' >= '2019-03-31 08:20' which is false when you want it to be true.
After thinking I come up to this solutions
ALTER PROCEDURE [dbo].[Dashboard_GetQuarterlyTargetData]
#Userid varchar(50)
AS
Begin
DECLARE #QuarterNumber varchar(50)
DECLARE #SetTarget decimal(10);
DECLARE #AchievedTarget decimal(10);
DECLARE #TargetProgress decimal(10);
DECLARE #RemainingTarget decimal(10);
DECLARE #StartDate Date
DECLARE #EndDate Date
DECLARE #Year as int
Select #QuarterNumber = QTR,#Year=YEAR,#SetTarget=AMOUNT from USERTARGETS where USERID=#Userid
if(#QuarterNumber = 1)
begin
SELECT #StartDate = DATEFROMPARTS(#year,4,1), #EndDate=DATEFROMPARTS(#year,6,30)
End
else if(#QuarterNumber = 2)
begin
SELECT #StartDate = DATEFROMPARTS(#year,7,1), #EndDate=DATEFROMPARTS(#year,9,30)
End
else if(#QuarterNumber = 3)
begin
SELECT #StartDate = DATEFROMPARTS(#year,10,1), #EndDate=DATEFROMPARTS(#year,12,31)
End
else if(#QuarterNumber = 4)
begin
SELECT #StartDate = DATEFROMPARTS(#year,1,1), #EndDate=DATEFROMPARTS(#year,3,31)
End
I have a simple select statement of a CTE where I have Declared and SET values before the WITH and I'm fetching data for a single day which would be having nearly 200,000 rows of data. If I execute that query, it is taking more time(not completed in 10 minutes). But If I remove those DECLARE, SET and hardcoded those input values in WHERE condition, the results are shown in 15 SECONDS.
This table has nearly 350 million rows of data with Proper Indexing of Primary Key columns.
What could be the possibility of this slowness?
Actual Query
DECLARE #StartTime DATETIME
DECLARE #EndTime DATETIME
DECLARE #ApplicationName VARCHAR(100)
SET #StartTime = '2018-12-10'
SET #EndTime = '2018-12-10'
SET #Applicationname = 'APPNAME'
;WITH TOTAL as (
SELECT * FROM TABLE WHERE DATETIME >= + #StartTime + '00:00:01' AND
DATETIME <= + #EndTime + '23:59:59'
AND APPLICATIONID=(SELECT APPLICATIONID FROM APPLICATION WHERE ApplicationName=#Applicationname
)
SELECT * FROM TOTAL
After Change
SELECT * FROM TABLE WHERE DATETIME >= '2018-12-10 00:00:01' AND
DATETIME <= '2018-12-10 23:59:59'
AND APPLICATIONID=(SELECT APPLICATIONID FROM APPLICATION WHERE ApplicationName='APPNAME'
Actually the DB has an SP with Long query, here I have provided the first CTE table only and the same kind of table conditions applicable to my rest of the CTE tables. If I get a clue for this slowness, I would fix the rest of my queries.
Since SQL server have to compile the variables StartTime and EndTime for each record on the TABLE for filtering, it takes longer. if you do something like the following:
DECLARE #StartTime DATETIME
DECLARE #EndTime DATETIME
DECLARE #ApplicationName VARCHAR(100)
SET #StartTime = '2018-12-10 00:00:01'
SET #EndTime = '2018-12-10 23:59:59'
SET #Applicationname = 'APPNAME'
SELECT *
FROM TABLE
WHERE DATETIME >= #StartTime AND DATETIME <= #EndTime
AND APPLICATIONID=(SELECT APPLICATIONID FROM APPLICATION WHERE
ApplicationName=#Applicationname
By doing this, you are reducing the load at the filtering time and doing pre-computing the required fixed variables.
I need to write a stored procedure that given a date and a number of working days, adds those given days to that date, and returns the new date, without counting the non-working days and the weekends. the non-working day are stored in another table.
It's my second stored procedure so I'm not quite familiar with the lexic, so, sorry in advance if you find obvious mistakes.
So far I've gotten to this:
CREATE PROCEDURE DateAdd
(#GivenDate DATE, #DaysToAdd int)
DECLARE #ReturnDate DATE,
DECLARE #Counter int,
DECLARE #NextDate DATE
AS
SET #Counter = 0
SET #ReturnDate = #GivenDate
SET #NextDate = #GivenDate
GO
WHILE (#Counter < #DaysToAdd)
#Counter + 1
IF(datepart(weekday, #FechaVariable) !=6 &&
datepart(weekday, #FechaVariable) != 7)
IF(#TODO-- call the query and check it with #NextDate)
#FechaRetorno = DateAdd(dd, 1, #FechaRetorno)
ELSE IF #NextDate = DateAdd(dd, 1, #NextDate)
EN IF
END WHILE
-- I don't know where to put this query, or how to call ir from the IF
SELECT Date
FROM non_working_days
WHERE Date = $Variable
RETURN #FechaRetorno
A Tally/Calendar table would to the trick as well, but you can to do this with an ad-hoc tally table. Also, this approach would be faster than an recursive cte.
One additonal option is that you can exclude HOLIDAYS by adding the following to the WHERE clause.
and D not in ('2017-12-25','2018-01-01')
The SQL
Declare #Date date = '2017-04-01'
Declare #Days int = 5
Select D
From (
Select D,RN=Row_Number() over (Order by D)
From (Select Top ((#Days*2)+10) D=DateAdd(DAY,-1+Row_Number() Over (Order By Number),#Date) From master..spt_values ) A
Where DateName(WEEKDAY,D) not in ('Saturday','Sunday')
) A
Where RN=#Days
Returns
2017-04-07
Here. Assuming the other table is called OtherTable and the column of dates to avoid is called DatesToAvoid, this will add one day at a time to your date, and if that date is not a weekend or in the DatesToAvoid, it will decrement #DaysToAdd. Once #DaysToAdd reaches 0, it stops.
CREATE PROCEDURE DateAddsp(#GivenDate DATE, #DaysToAdd int)
AS
BEGIN
WHILE #DaystoAdd > 0
BEGIN
SET #GivenDate = DATEADD(DAY,1,#GivenDate)
SET #DaysToAdd = CASE
WHEN #GivenDate IN (SELECT DatesToAvoid FROM OtherTable) OR DATEPART(DW,#GivenDate) IN (1,7) /* Saturday or Sunday*/
THEN #DaysToAdd
ELSE #DaysToAdd + 1
END
END
RETURN DATEADD(DW, #DaysToAdd, #GivenDate);
END
You're are overcomplicating things. The function which you need to use is dateadd: https://learn.microsoft.com/en-us/sql/t-sql/functions/dateadd-transact-sql
Try something along the lines of the following:
CREATE PROCEDURE DateAddsp(#GivenDate DATE, #DaysToAdd int)
AS
BEGIN
RETURN DATEADD(DW, #DaysToAdd, #GivenDate);
END
You cannot use dateadd as the name for the stored proc, as it is a reserved word in SQL Server - it is the name of the function which I just utilized above.
Instead of a procedure, this is an in-line table-valued function to get the result of adding working days to a date.
create function dbo.udf_add_working_days (#Date date, #Days int)
returns table with schemabinding as return (
with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, days as (
select top (1000)
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,#date))
from n as deka cross join n as hecto cross join n as kilo
order by [Date]
)
, working_days as (
select top (#Days)
[Date]
from days
where datename(weekday,[date]) not in ('Saturday','Sunday')
/* -- put your non working days table info here and uncomment this clause
and not exists (
select 1
from dbo.HolidayTable h
where days.[Date] = h.HolidayDate
)
--*/
order by [Date]
)
select top 1 [date]
from working_days
order by [Date] desc
);
go
and you would call it like so:
select [date]
from dbo.udf_add_working_days('20170401',10)
rextester demo: http://rextester.com/ITXB6884
returns:
+------------+
| date |
+------------+
| 2017-04-14 |
+------------+
Or you can call it using dates from a query using cross apply()
select ...
, x.Date as NewWorkDate
from t
dbo.udf_add_working_days (t.[Date], t.[NumberOfDays]) as x
Reference on inline table valued functions
When is a SQL function not a function? "If it’s not inline, it’s rubbish." - Rob Farley
Inline Scalar Functions - Itzik Ben-Gan
Scalar functions, inlining, and performance: An entertaining title for a boring post - Adam Machanic
TSQL User-Defined Functions: Ten Questions You Were Too Shy To Ask - Robert Sheldon
I'm making a stored procedure, the purpose is to produce a Forecast for maintenance schedule, it so that when I give it a range of dates and amount of days, it would give me a set of results displaying the different dates that fall under that criteria.
I have been trying with a while loop, but I have not been able to get more than 1 result, I only get the last possible result of the range.
ALTER PROCEDURE [dbo].[ApruebaFecha](
-- Add the parameters for the stored procedure here
#LastDate DATETIME, -- Last Scheduled Date (Range Start)
#Setting1 INT, -- Length of Period
#FechaHasta datetime, -- End of Range
#Result DATETIME OUTPUT)
AS
BEGIN
/* Today */
DECLARE
#TodaysDate DATETIME
SELECT #TodaysDate = CONVERT(DATETIME, CONVERT(VARCHAR, GETDATE(), 101), 101)
SELECT convert(datetime, CONVERT(varchar,#FechaHasta,101),101 )
-- Finding today's date after resetting the time to midnight
/* Schedule Date */
DECLARE
#ScheduleDate DATETIME
-- Find the starting schedule date. If the schedule date is in a previous
-- month, adjust to the current month
-- Adjust the days
SELECT #ScheduleDate = #LastDate
WHILE (#ScheduleDate < #TodaysDate AND #TodaysDate <#FechaHasta)
SELECT #ScheduleDate = DATEADD(day, #Setting1, #ScheduleDate)
IF (#ScheduleDate = #LastDate)
SELECT #ScheduleDate = DATEADD(day, #Setting1, #ScheduleDate)
SELECT #Result = #ScheduleDate
WHILE(#Result <#FechaHasta)
BEGIN
IF(#Result <#FechaHasta)
SELECT #Result = DATEADD(day, #Setting1, #Result)
SET IDENTITY_INSERT [00TblFecha] ON
INSERT INTO dbo.[00TblFecha](idFecha,jobno,fecha)VALUES('','',#Result)
SET IDENTITY_INSERT[00TblFecha] OFF
print #Result
end
END -- GetScheduleDate_Daily_PeriodicDay
the result that you see is from this statement at line 5:
SELECT convert(datetime, CONVERT(varchar,#FechaHasta,101),101 )
you should put a statement at the end of your stored procedure like this :
select idFecha,jobno,fecha
from dbo.[00TblFecha]
I have a recursive CTE that calculates a manager hierarchy in an organization.
This below query takes < 2 seconds to finish
WITH OrganisationChart ([Identity], [DisplayName], Title, [Level], Manager) AS
(
SELECT
[Identity], [DisplayName], Title, 0, Manager
FROM
[data].[DailyUserV1]
WHERE
[Identity] = '7276DB4F-33B0-4074-9903-D95D740A8BF3' AND Date = '2015-08-03'
UNION ALL
SELECT
emp.[Identity],
emp.[DisplayName],
emp.Title,
[Level] + 1,
emp.Manager
FROM
[data].[DailyUserV1] emp
INNER JOIN
OrganisationChart ON emp.Manager = OrganisationChart.[Identity]
WHERE
Date = '2015-08-03'
)
SELECT * FROM OrganisationChart
While this same query wrapped inside a stored procedure takes > 15 mins and then times out!
IF (OBJECT_ID('[dbo].[GetOrganizationChart]') IS NOT NULL)
DROP PROCEDURE [dbo].[GetOrganizationChart]
GO
CREATE PROCEDURE [dbo].[GetOrganizationChart]
#identity varchar(256),
#date datetime
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #userId varchar(256);
SET #userId = #identity;
DECLARE #endDate datetime;
SET #endDate = #date;
WITH OrganisationChart ([Identity], [DisplayName], Title, [Level], Manager) AS
(
SELECT
[Identity], [DisplayName], Title, 0, Manager
FROM
[data].[DailyUserV1]
WHERE
[Identity] = #userId AND Date = #endDate
UNION ALL
SELECT
emp.[Identity],
emp.[DisplayName],
emp.Title,
[Level] + 1,
emp.Manager
FROM
[data].[DailyUserV1] emp
INNER JOIN
OrganisationChart ON emp.Manager = OrganisationChart.[Identity]
WHERE
Date = #endDate
)
SELECT * FROM OrganisationChart;
END
GO
EXEC [dbo].[GetOrganizationChart] #identity = '7276DB4F-33B0-4074-9903-D95D740A8BF3', #date = '2015-08-03'
I have ruled out parameter sniffing as a likely cause by using local variables inside the stored procedure. What's going on here?
UPDATE
Here are the links to the query execution plans in case you want to take a look.
cte-without-stored-proc
cte-with-stored-proc
The same problem I've faced earlier and there was parameter sniffing. To overcome this issue I used temporary table instead of CTE and the SP start running smoothly.