Stored procedure takes more than 30 seconds to execute - sql-server

I am optimizing the stored procedure in which I get list of more than 10K user ids in one table variable as a parameter. I am setting flag #ContainsUserIds if there is any data into the table variable. In main query selecting the all the users that are present in table variable or all the users if no data found in table variable (There are more condition in where clause).
The problem is here that the statement in where clause takes more than 30 second which has OR condition. If I removed the first condition to check #ContainsUserIds = 0 then it will execute within a second. Can someone please help me to optimize this query.
This Stored procedure is called from different position so the User ids may not pass.
CREATE PROCEDURE [core].[spGetUsersByFilter]
(
#ClientId nvarchar(38) = NULL,
#UserIds [UserIdList] readonly
)
AS
BEGIN
SET NOCOUNT ON
DECLARE #ContainsUserIds BIT = 0,
SET #ContainsUserIds = CASE
WHEN EXISTS(SELECT TOP 1 1 FROM #UserIds)
THEN 1
ELSE 0
END;
SELECT DISTINCT ES.Id
FROM [db].[Users] E
JOIN [db].[UserDetails] ES ON ES.UserId = E.Id
WHERE E.[Active] = 1
AND (#ContainsUserIds = 0 OR E.UserId IN(SELECT Item FROM #UserIds))
AND ES.ClientId = #ClientId
END

SQL notoriously dislikes OR, the optimizer sometimes works around it but in most cases it's a good idea to convert things to a UNION [ALL] syntax. If there's just 1 OR that's often easy to do, if there are multiple things explode fast.
Anyway, you could thus convert your stored procedure to:
CREATE PROCEDURE [core].[spGetUsersByFilter]
(
#ClientId nvarchar(38) = NULL,
#UserIds [UserIdList] readonly
)
AS
BEGIN
SET NOCOUNT ON
DECLARE #ContainsUserIds BIT = 0,
SET #ContainsUserIds = CASE
WHEN EXISTS(SELECT * FROM #UserIds)
THEN 1
ELSE 0
END;
SELECT DISTINCT ES.Id
FROM [db].[Users] E
JOIN [db].[UserDetails] ES ON ES.UserId = E.Id
WHERE E.[Active] = 1
AND (#ContainsUserIds = 0)
AND ES.ClientId = #ClientId
UNION ALL
SELECT DISTINCT ES.Id
FROM [db].[Users] E
JOIN [db].[UserDetails] ES ON ES.UserId = E.Id
WHERE E.[Active] = 1
AND (#ContainsUserIds = 1)
AND E.UserId IN (SELECT Item FROM #UserIds))
AND ES.ClientId = #ClientId
END
That said, you might just as well split things up here and do 2 separate SELECTs. Also, if the table-variable contains more than a couple of records you may prefer to use a temp-table as the latter allows for indexing and most importantly has statistics handling which WILL result in a better execution plan. I also prefer JOIN over IN (), just make sure there are no doubled values when JOINing.
CREATE PROCEDURE [core].[spGetUsersByFilter]
(
#ClientId nvarchar(38) = NULL,
#UserIds [UserIdList] readonly
)
AS
BEGIN
SET NOCOUNT ON
SELECT DISTINCT Item
INTO #UserIds
FROM #UserIds
IF ##ROWCOUNT = 0
BEGIN
SELECT DISTINCT ES.Id
FROM [db].[Users] E
JOIN [db].[UserDetails] ES ON ES.UserId = E.Id
WHERE E.[Active] = 1
AND ES.ClientId = #ClientId;
END
ELSE
BEGIN
CREATE UNIQUE INDEX uq0_UserIds ON #UserIds ( Item ) WITH (FILLFACTOR = 100);
SELECT DISTINCT ES.Id
FROM [db].[Users] E
JOIN [db].[UserDetails] ES ON ES.UserId = E.Id
JOIN #UserIds T ON T.Item = E.UserId
WHERE E.[Active] = 1
AND ES.ClientId = #ClientId
END
END
PS: all above written in notepad and untested, some assembly may be required =)

Related

How do I add errror handling into this stored procedure?

How can I print an error message from this procedure if the employee (server) hasn't served anyone? Are try - catch blocks the only way to handle this?
I was thinking that if/else condition test followed by Print message suits my requirement.
Stored procedure:
if OBJECT_ID('customers_served', 'u') is not null
drop procedure customers_served;
go
create procedure customers_served
#employee_id int
as
set nocount on;
select
c.customer_id, c.cust_lastname,
(sum(c.cust_total_guests)) as total_guests
from
customers c
join
seating s on c.customer_id = s.customer_id
join
table_assignment ta on s.table_id = ta.table_id
join
employees e on ta.employee_id = e.employee_id
where
#employee_id = e.employee_id
group by
c.customer_id, c.cust_lastname;
/* if total_guests = 0 print message the employee has served 0 guests */
Test procedure:
exec customers_served
#employee_id = 5;
I modified your script to this.
use dbServers;
if OBJECT_ID('customers_served', 'u') is not null
drop procedure customers_served;
go
create procedure customers_served
#employee_id int
as
set nocount on;
declare #totalGuests int;
set #totalGuests = (
select(sum(c.cust_total_guests))
from customers c
join seating s on c.customer_id = s.customer_id
join table_assignment ta on s.table_id = ta.table_id
join employees e on ta.employee_id = e.employee_id
where #employee_id = e.employee_id
)
if #totalGuests = 0 OR #totalGuests IS NULL
BEGIN
print 'This server did not serve any guests'
END
else
BEGIN
select #totalGuests AS 'total_quests'
END
/* test procedure*/
exec customers_served
#employee_id = 5;
Following snippet of code might help:
declare #r int
select #r = (sum(c.cust_total_guests)) as total_guests
from customers c
join seating s on c.customer_id = s.customer_id
join table_assignment ta on s.table_id = ta.table_id
join employees e on ta.employee_id = e.employee_id
where #employee_id = e.employee_id
group by c.customer_id, c.cust_lastname;
if #r = 0
begin
-- do what ever you wish
end
else
begin
select c.customer_id, c.cust_lastname, (sum(c.cust_total_guests)) as
total_guests
from customers c
join seating s on c.customer_id = s.customer_id
join table_assignment ta on s.table_id = ta.table_id
join employees e on ta.employee_id = e.employee_id
where #employee_id = e.employee_id
group by c.customer_id, c.cust_lastname;
end
end
Rather than double querying, you can simply test ##ROWCOUNT after your query to determine if any results were returned, and print your message if ##ROWCOUNT = 0.

SQL Query produces duplicates with "Where IN" query

I have a large SQL statement which does a whole load of joins on my tables. I have converted some of my table relationships to many-to-many relationships so that it is more efficient. I have therefore decided to convert my SQL to do a WHERE IN statement (on location).
The following query is the one that currently returns the desired results:
ALTER PROCEDURE [dbo].[GetMemberListing]
#Locations nvarchar(max)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
select
Member.Id,
AspNetUsers.Salutation,
AspNetUsers.FirstName,
AspNetUsers.PhotoUrl,
AspNetUsers.LastName,
AspNetUsers.Birthday,
AspNetUsers.Gender,
Member.IDType,
Member.JoinDate,
CONCAT (AspNetUsers.FirstName, ' ', AspNetUsers.LastName) as FullName,
AspNetUsers.CountryCode,
AspNetUsers.Email,
Location.Name as LocationName,
AspNetUsers.HomePhone as HomePhone,
coalesce(Package.Name,'No Package') as PackageName,
PackageTerm = case when Package.PackageIsReoccuring = 1 then 'Recurring' when Package.PackageIsSession = 1 then 'Paid In Full' when membership.TotalPrice = 0 then 'Free' when Package.PackagePayInFull = 1 then 'Paid In Full' end,
PackageType.Name as PackageType,
MembershipId = case when membership.id IS NULL THEN '' ELSE 0 end,
coalesce(membershipstate.name, 'N/A') as MembershipState,
MembershipStartDate = case when membership.StartDate IS NULL THEN '' ELSE CONVERT(varchar(50),membership.StartDate) end,
MembershipEndDate = case when membership.EndDate IS NULL THEN '' ELSE CONVERT(varchar(50),membership.EndDate) end
from
(
select
member.id as memberid,
(
select top 1 id
from membership
where memberid = member.id
and membership.StartDate <= getdate()
order by membership.enddate desc
) as membershipid
from member
) as LatestMembership
left join membership
on latestmembership.membershipid = membership.id
join member
on latestmembership.memberid = member.id
join AspNetusers
on member.AspNetUserId = AspNetUsers.Id
join Location
on member.HomeLocationId = Location.Id
left join Package
on membership.packageid = package.Id
left join PackageType
on package.packagetypeid = packagetype.Id
left join MembershipState
on membership.membershipstateid = membershipstate.Id
Order By aspNetusers.LastName desc
END
Below is what i have tried to do; however, it is duplicating the otherwise correct results based on the number of values in the WHERE IN join.
This on member.HomeLocationId = Location.Id
becomes on member.HomeLocationId IN (SELECT Value FROM fn_Split(#Locations, ','))
As seen below:
ALTER PROCEDURE [dbo].[GetMemberListing]
#Locations nvarchar(max)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
select
Member.Id,
AspNetUsers.Salutation,
AspNetUsers.FirstName,
AspNetUsers.PhotoUrl,
AspNetUsers.LastName,
AspNetUsers.Birthday,
AspNetUsers.Gender,
Member.IDType,
Member.JoinDate,
CONCAT (AspNetUsers.FirstName, ' ', AspNetUsers.LastName) as FullName,
AspNetUsers.CountryCode,
AspNetUsers.Email,
Location.Name as LocationName,
AspNetUsers.HomePhone as HomePhone,
coalesce(Package.Name,'No Package') as PackageName,
PackageTerm = case when Package.PackageIsReoccuring = 1 then 'Recurring' when Package.PackageIsSession = 1 then 'Paid In Full' when membership.TotalPrice = 0 then 'Free' when Package.PackagePayInFull = 1 then 'Paid In Full' end,
PackageType.Name as PackageType,
MembershipId = case when membership.id IS NULL THEN '' ELSE 0 end,
coalesce(membershipstate.name, 'N/A') as MembershipState,
MembershipStartDate = case when membership.StartDate IS NULL THEN '' ELSE CONVERT(varchar(50),membership.StartDate) end,
MembershipEndDate = case when membership.EndDate IS NULL THEN '' ELSE CONVERT(varchar(50),membership.EndDate) end
from
(
select member.id as memberid,
(
select top 1 id
from membership
where memberid = member.id
and membership.StartDate <= getdate()
order by membership.enddate desc
) as membershipid
from member
) as LatestMembership
left join membership
on latestmembership.membershipid = membership.id
join member
on latestmembership.memberid = member.id
join AspNetusers
on member.AspNetUserId = AspNetUsers.Id
join Location
on member.HomeLocationId IN (SELECT Value FROM fn_Split(#Locations, ','))
left join Package
on membership.packageid = package.Id
left join PackageType
on package.packagetypeid = packagetype.Id
left join MembershipState
on membership.membershipstateid = membershipstate.Id
Order By aspNetusers.LastName desc
END
Change to using your split function in the where clause. Don't use that function in the join.
In addition you don't really have a valid join condition on the locations table go back to this
on member.HomeLocationId = Location.Id
And later
Where member.HomeLocationId IN (SELECT Value FROM fn_Split(#Locations, ','))

SSRS monthly/daily report

I have a variable in my report which holds 2 possible values: 'monthly' and 'daily'. How can I put this variable (lets call it #reportModel). I think it should be somewhere in GROUP BY clause, but don't know how should it look like.
DECLARE #reportModel VARCHAR(10)
SET #reportModel = 'monthly'
SELECT P.product, SUM(O.price * O.quantity), O.orderDate
FROM Products AS P
INNER JOIN Orders AS O ON P.ID = O.ID
And what now?
How about a stored procedure to handle this, something like.....
CREATE PROCEDURE rpt_GetData
#reportModel VARCHAR(10)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Sql NVARCHAR(MAX);
IF (#reportModel = 'daily')
BEGIN
SET #Sql = N' SELECT P.product
, SUM(O.price * O.quantity) AS Total
, O.orderDate
FROM Products AS P
INNER JOIN Orders AS O ON P.ID = O.ID
GROUP BY P.product , O.orderDate'
Exec sp_executesql #Sql
END
ELSE IF (#reportModel = 'monthly')
BEGIN
SET #Sql = N' SELECT P.product
, SUM(O.price * O.quantity) AS Total
, MONTH(O.orderDate) AS [Month]
FROM Products AS P
INNER JOIN Orders AS O ON P.ID = O.ID
GROUP BY P.product, MONTH(O.orderDate)'
Exec sp_executesql #Sql
END
END
Adding this as an answer because I mentioned it in the comments in #M.Ali's answer.
So I would suggest you change thinking slightly with one of these options.
Two reports - Simply make a report for daily and another for monthly. Now you have no worries with complex SQL etc.
Make 2 stored procedures, one with the GROUP BY daily and one monthly. Then in your SSRS dataset, create an expression for you SQL that chooses the procedure based on parameter:
=IIf(Parameters!reportModel.Value = "monthly", "GetMonthlyData", "GetDailyData")
I would put it in the Group On Expression of the table or chart rather than doing it in the query.
=IIF(Parameters!reportModel.Value = "monthly", Month(Fields!orderDate.Value), Fields!orderDate.Value)
If you'd rather do it in the query and don't want to wait for DBAs to get around to deploying a Stored Procedure (not to mention maintaining it whenever there's a change), you could use your parameter in a CASE like:
SELECT P.product, SUM(O.price * O.quantity),
CASE WHEN #reportModel = 'monthly' THEN CAST(MONTH(O.orderDate) AS VARCHAR(12))
ELSE CAST(O.orderDateAS VARCHAR(12)) END AS DT
FROM WORKFLOW_SHARED.MAIN.VW_CLAIMSOVERPAYMENT
WHERE DATECOMPLETED > '7/1/2015'
GROUP BY P.product,
CASE WHEN #reportModel = 'monthly' THEN CAST(MONTH(O.orderDate) AS VARCHAR(12))
ELSE CAST(O.orderDateAS VARCHAR(12)) END
This way you don't have to maintain two separate reports.
How about something simple like this
select
P.product
,Total = sum(O.price * O.quantity)
, O.orderDate
from
Products as P
inner join Orders as O on P.ID = O.ID
where
#reportModel = 'daily'
union all
select
P.product
,Total = sum(O.price * O.quantity)
,[Month] = MONTH(O.orderDate)
from
Products as P
inner JOIN Orders as O on P.ID = O.ID
group by
P.product
,[Month] = MONTH(O.orderDate)
where
#reportModel = 'monthly'

Creating a Stored Procedure with if statement on Inner Join

I am trying to create a stored procedure which would execute an INNER JOIN code block based on a parameter value. However, I keep getting "Incorrect syntax near '#reSourceID'."
if (#VendorID = 11)
#reSourceID = 't.reSourceID'
if (#VendorID = 5)
#reSourceID = 't.SourceID'
SELECT t.ID, fsg.SigCap, fsg.VendorId
FROM FormCap fsg
INNER JOIN FlightTrip t
ON fsg.SourceId = #reSourceID
AND fsg.VendorId = #VendorID
INNER JOIN ContractProvider cpu
ON t.Id = cpu.VendorId
WHERE (t.ID = #FinTransID)
AND (cpu.userID = #UserID)
Any ideas what would be causing the error?
Your syntax error is because you haven't really joined on FlightTrip t. You can't do what you are trying to do in compiled SQL. You would have to create a varchar variable full of your statement and then use EXEC SP_EXECUTESQL(#Statement) to run it.
declare #statement nvarchar(4000)
select #statement = '
SELECT t.ID,
fsg.SigCap,
fsg.VendorId
FROM FormCap fsg
INNER JOIN FlightTrip t
ON fsg.SourceId = '+convert(nvarchar(100),#reSourceID)+'
AND fsg.VendorId = '+convert(nvarchar(100),#VendorID)+'
INNER JOIN ContractProvider cpu
ON t.Id = cpu.VendorId
WHERE t.ID = '+convert(nvarchar(100),#FinTransID)+'
AND cpu.userID = '''+convert(nvarchar(100),#UserID)+''''
exec sp_executesql #statement
SELECT t.ID, fsg.SigCap, fsg.VendorId
FROM FormCap fsg
INNER JOIN FlightTrip t
ON fsg.SourceId = CASE #VendorID
WHEN 11 THEN t.reSourceID
WHEN 5 THEN t.SourceID
END
AND fsg.VendorId = #VendorID
INNER JOIN ContractProvider cpu
ON t.Id = cpu.VendorId
WHERE (t.ID = #FinTransID)
AND (cpu.userID = #UserID)
As Bill answered, the optimim solution is probably going to involve dynamic SQL although I'd recommend going a little farther both for performance and security.
If you parameterize your call to sp_executeSQL you'll both avoid issues with escaping quotes and potential SQL injection problems but it will allow SQL to reuse the plan since the query text won't change.
Here's what that would look like:
if (#VendorID = 11)
#reSourceID = 't.reSourceID'
if (#VendorID = 5)
#reSourceID = 't.SourceID'
DECLARE #sql NVARCHAR(MAX)
DECLARE #params NVARCHAR(500)
SET #sql ='SELECT t.ID,
fsg.SigCap,
fsg.VendorId
FROM FormCap fsg
INNER JOIN FlightTrip t
ON fsg.SourceId = '+CONVERT(NVARCHAR(MAX),#reSourceID)+'
AND fsg.VendorId = #VendorID
INNER JOIN ContractProvider cpu
ON t.Id = cpu.VendorId
WHERE t.ID = #FinTransID
AND cpu.userID = #userID'
SET #params = N'#VendorID INT, #FinTransID INT, #userID UNIQUEIDENTIFIER'
EXECUTE sp_executesql #sql, #params, #venderID = #venderID, #finTransID=#finTransID, #userID = #userID
Above is the gist of the solution, I don't fully know your types and inputs. Also, without schema, I can't test my code, but it should get your going.
For more information on sp_executesql, msnd is a great resource.

SQL how to get an equality comparator on a list of ints

I'm writing an SQL query as follows:
ALTER proc [dbo].[Invoice_GetHomePageInvoices] (
#AreaIdList varchar(max)
, #FinancialYearStartDate datetime = null
, #FinancialYearEndDate datetime = null
) as
set nocount on
select *
from Invoice i
left outer join Organisation o on i.OrganisationId = o.Id
left outer join Area a on i.AreaId = a.Id
where i.InvoiceDate BETWEEN #FinancialYearStartDate AND #FinancialYearEndDate
The #AreaIdList parameter is going to be in the format "1,2,3" etc.
I'm wanting to add a line which will only return invoices who have area id equal to any of the ids in #AreaIdList.
I know how to do a statement if it was on areaId to search on ie. where i.AreaId == areaId problem is now I have this list I got to compare for every area Id in #AreaIdList.
Can anybody tell me how you would go about this?
Unpack your ID list to a table and use where AreadID in (select ID from ...)
ALTER proc [dbo].[Invoice_GetHomePageInvoices] (
#AreaIdList varchar(max)
, #FinancialYearStartDate datetime = null
, #FinancialYearEndDate datetime = null
) as
set nocount on
set #AreaIdList = #AreaIdList+','
declare #T table(ID int primary key)
while len(#AreaIdList) > 1
begin
insert into #T(ID) values (left(#AreaIdList, charindex(',', #AreaIdList)-1))
set #AreaIdList = stuff(#AreaIdList, 1, charindex(',', #AreaIdList), '')
end
select *
from Invoice i
left outer join Organisation o on i.OrganisationId = o.Id
left outer join Area a on i.AreaId = a.Id
where i.InvoiceDate BETWEEN #FinancialYearStartDate AND #FinancialYearEndDate and
i.AreadID in (select ID from #T)

Resources