SQL performance on ROWCOUNT check - better way? - sql

I have a MS SQL 2005 stored procedure that I pass an ID to from a web page.
The ID is used to read pre-set search criteria from a database of widget category landing pages into a set of variables.
The variables are then used in a SELECT to search a database of widget items. Some variables can be overridden by a visitor's page choices, such as maximum price.
I'd like to check if the SELECT returns records, and, if not, default to the landing page's normal criteria to make sure the visitor always sees some items.
I have tried using ##ROWCOUNT to check the first SELECT but, as the SELECT is fairly involved (I've taken out a lot of fields in the example below), the performance hit of running it twice is unacceptably long. The first SELECT on its own takes around 1 second, whereas checking for ##ROWCOUNT = 0 and running the SELECT again takes around 4 seconds.
Is there a better way to accomplish this check and return of records if the first SELECT returns none?
I also want to return only one recordset, not two, at the end of the stored procedure.
Having researched this, I have found people using UNION ALL on two selects, but I don't think it helps me in this case. I also want to know which SELECT was used by passing through a field with contents True or False, so I can flag up on the website whether no records have been found.
Thanks for your help.
CREATE PROCEDURE dbo.NW_LANDING_GET_Widgets
/* options from the web page */
#WidgetID int,
#PageNumber int,
#WidgetsPerPage int,
#Sort VARCHAR(1),
#MinPrice int,
#MaxPrice int,
#Override_WidgetType varchar(20),
#Override_WidgetInfo1 int
AS
SET NOCOUNT ON
BEGIN
/* ---------- Declare variables for criteria to be gathered from WidgetLanding table ------------ */
DECLARE #WidgetCategory1 varchar(50)
DECLARE #WidgetCategory2 varchar(50)
DECLARE #WidgetType varchar(30)
DECLARE #WidgetInfo1 int
/* ---------- Read default criteria into variables from WidgetLanding table ----- */
SELECT
#WidgetCategory1=Criteria_WidgetCategory1,
#WidgetCategory2=Criteria_WidgetCategory2,
#WidgetType=Criteria_WidgetType
FROM dbo.WidgetLanding
WHERE pk_WidgetID = #WidgetID
/* -------- Set PageNumber variable for SELECT of Widgets ----------- */
SET #PageNumber=(#PageNumber-1)*#WidgetsPerPage
/* Set Minimum and Maximum Prices - if Null, set highest and lowest number possible */
DECLARE #Min int
DECLARE #Max int
SET #Min = ISNULL(#MinPrice,0)
SET #Max = ISNULL(#MaxPrice,999999999)
/* -------- Override variables if visitor has changed the search criteria from default ------------------- */
IF #Override_WidgetType is not null
BEGIN
SET #WidgetType=#Override_WidgetType
END
IF #Override_WidgetInfo1 is not null
BEGIN
SET #WidgetInfo1=#Override_WidgetInfo1
END
/* ------------------- Retrieve widget records based on variables ------ */
SELECT TOP(#WidgetsPerPage) * FROM (SELECT RowID=ROW_NUMBER()
OVER (ORDER BY
CASE WHEN dbo.Widgets.Featured_WidgetLandingID = #WidgetID then 1 else 0 end DESC,
CASE WHEN #Sort = 'D' THEN dbo.Widgets.Price END DESC, /* Price Descending */
CASE WHEN #Sort = 'U' THEN dbo.Widgets.Price END ASC, /* Price Ascending */
CASE WHEN #Sort = 'P' THEN dbo.Widgets.viewed END DESC, /* Popular */
CASE WHEN #Sort = 'L' THEN dbo.Widgets.Date END DESC), /* Latest */
Count(dbo.Widgets.WidgetID) OVER() As TotalRecords,
dbo.Widgets.Price,
dbo.Widgets.WidgetID,
dbo.Widgets.WidgetCategory1,
dbo.Widgets.WidgetCategory2,
dbo.Widgets.WidgetType,
dbo.Widgets.WidgetType2,
dbo.Widgets.WidgetInfo1
FROM dbo.Widgets
WHERE
(WidgetCategory1 = #WidgetCategory1 OR #WidgetCategory1 is null) AND
(WidgetCategory2 = #WidgetCategory2 OR #WidgetCategory2 is null) AND
(Price >= #Min AND Price <= #Max) AND
(WidgetInfo1 >= #WidgetInfo1 OR #WidgetInfo1 is null)
) TAB WHERE TAB.RowId > CAST(#PageNumber AS INT)
/*
-----------------------------
THIS IS WHERE I WANT TO CHECK IF RECORDS ARE RETURNED - IF NOT DO ANOTHER SELECT BUT WITHOUT OVERRIDING VARIABLES SO RECORDS WILL ALWAYS BE RETURNED
-----------------------------
*/
END
SET NOCOUNT OFF

Simpliest way is code like this:
SELECT TOP(#WidgetsPerPage) *
INTO #Res
FROM (SELECT RowID=ROW_NUMBER()
OVER (ORDER BY
CASE WHEN dbo.Widgets.Featured_WidgetLandingID = #WidgetID then 1 else 0 end DESC,
CASE WHEN #Sort = 'D' THEN dbo.Widgets.Price END DESC, /* Price Descending */
CASE WHEN #Sort = 'U' THEN dbo.Widgets.Price END ASC, /* Price Ascending */
CASE WHEN #Sort = 'P' THEN dbo.Widgets.viewed END DESC, /* Popular */
CASE WHEN #Sort = 'L' THEN dbo.Widgets.Date END DESC), /* Latest */
Count(dbo.Widgets.WidgetID) OVER() As TotalRecords,
dbo.Widgets.Price,
dbo.Widgets.WidgetID,
dbo.Widgets.WidgetCategory1,
dbo.Widgets.WidgetCategory2,
dbo.Widgets.WidgetType,
dbo.Widgets.WidgetType2,
dbo.Widgets.WidgetInfo1
FROM dbo.Widgets
WHERE
(WidgetCategory1 = #WidgetCategory1 OR #WidgetCategory1 is null) AND
(WidgetCategory2 = #WidgetCategory2 OR #WidgetCategory2 is null) AND
(Price >= #Min AND Price <= #Max) AND
(WidgetInfo1 >= #WidgetInfo1 OR #WidgetInfo1 is null)
) TAB WHERE TAB.RowId > CAST(#PageNumber AS INT)
IF (##ROWCOUNT>0)
BEGIN
INSERT #Res (......)
SELECT
END
SELECT * FROM #Res
But anyway your query should be rewritten.

Related

How to set total number of rows before an OFFSET occurs in stored procedure

I've created a stored procedure that filters and paginates for a DataTable.
Problem: I need to set an OUTPUT variable for #TotalRecords found before an OFFSET occurs, otherwise it sets #TotalRecord to #RecordPerPage.
I've messed around with CTE's and also simply trying this:
SELECT *, #TotalRecord = COUNT(1)
FROM dbo
But that doesn't work either.
Here is my stored procedure, with most of the stuff pulled out:
ALTER PROCEDURE [dbo].[SearchErrorReports]
#FundNumber varchar(50) = null,
#ProfitSelected bit = 0,
#SortColumnName varchar(30) = null,
#SortDirection varchar(10) = null,
#StartIndex int = 0,
#RecordPerPage int = null,
#TotalRecord INT = 0 OUTPUT --NEED TO SET THIS BEFORE OFFSET!
AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM
(SELECT *
FROM dbo.View
WHERE (#ProfitSelected = 1 AND Profit = 1)) AS ERP
WHERE
((#FundNumber IS NULL OR #FundNumber = '')
OR (ERP.FundNumber LIKE '%' + #FundNumber + '%'))
ORDER BY
CASE
WHEN #SortColumnName = 'FundNumber' AND #SortDirection = 'asc'
THEN ERP.FundNumber
END ASC,
CASE
WHEN #SortColumnName = 'FundNumber' AND #SortDirection = 'desc'
THEN ERP.FundNumber
END DESC
OFFSET #StartIndex ROWS
FETCH NEXT #RecordPerPage ROWS ONLY
Thank you in advance!
You could try something like this:
create a CTE that gets the data you want to return
include a COUNT(*) OVER() in there to get the total count of rows
return just a subset (based on your OFFSET .. FETCH NEXT) from the CTE
So your code would look something along those lines:
-- CTE definition - call it whatever you like
WITH BaseData AS
(
SELECT
-- select all the relevant columns you need
p.ProductID,
p.ProductName,
-- using COUNT(*) OVER() returns the total count over all rows
TotalCount = COUNT(*) OVER()
FROM
dbo.Products p
)
-- now select from the CTE - using OFFSET/FETCH NEXT, get only those rows you
-- want - but the "TotalCount" column still contains the total count - before
-- the OFFSET/FETCH
SELECT *
FROM BaseData
ORDER BY ProductID
OFFSET 20 ROWS FETCH NEXT 15 ROWS ONLY
As a habit, I prefer non-null entries before possible null. I did not reference those in my response below, and limited a working example to just the two inputs you are most concerned with.
I believe there could be some more clean ways to apply your local variables to filter the query results without having to perform an offset. You could return to a temp table or a permanent usage table that cleans itself up and use IDs that aren't returned as a way to set pages. Smoother, with less fuss.
However, I understand that isn't always feasible, and I become frustrated myself with those attempting to solve your use case for you without attempting to answer the question. Quite often there are multiple ways to tackle any issue. Your job is to decide which one is best in your scenario. Our job is to help you figure out the script.
With that said, here's a potential solution using dynamic SQL.
I'm a huge believer in dynamic SQL, and use it extensively for user based table control and ease of ETL mapping control.
use TestCatalog;
set nocount on;
--Builds a temp table, just for test purposes
drop table if exists ##TestOffset;
create table ##TestOffset
(
Id int identity(1,1)
, RandomNumber decimal (10,7)
);
--Inserts 1000 random numbers between 0 and 100
while (select count(*) from ##TestOffset) < 1000
begin
insert into ##TestOffset
(RandomNumber)
values
(RAND()*100)
end;
set nocount off;
go
create procedure dbo.TestOffsetProc
#StartIndex int = null --I'll reference this like a page number below
, #RecordsPerPage int = null
as
begin
declare #MaxRows int = 30; --your front end will probably manage this, but don't trust it. I personally would store this on a table against each display so it can also be returned dynamically with less manual intrusion to this procedure.
declare #FirstRow int;
--Quick entry to ensure your record count returned doesn't excede max allowed.
if #RecordsPerPage is null or #RecordsPerPage > #MaxRows
begin
set #RecordsPerPage = #MaxRows
end;
--Same here, making sure not to return NULL to your dynamic statement. If null is returned from any variable, the entire statement will become null.
if #StartIndex is null
begin
set #StartIndex = 0
end;
set #FirstRow = #StartIndex * #RecordsPerPage
declare #Sql nvarchar(2000) = 'select
tos.*
from ##TestOffset as tos
order by tos.RandomNumber desc
offset ' + convert(nvarchar,#FirstRow) + ' rows
fetch next ' + convert(nvarchar,#RecordsPerPage) + ' rows only'
exec (#Sql);
end
go
exec dbo.TestOffsetProc;
drop table ##TestOffset;
drop procedure dbo.TestOffsetProc;

Loop and update without cursor

I have a table where item balances are stored.
CREATE TABLE itembalance (
ItemID VARCHAR(15),
RemainingQty INT,
Cost Money,
Id INT
)
I need to make sure that whenever an item is being sent out, the proper balances are deducted from the itembalance table. I do it this way:
DECLARE crsr CURSOR LOCAL FAST_FORWARD FOR
SELECT
itembalance.Cost,
itembalance.RemainingQty
itembalance.Id
FROM dbo.itembalance
WHERE itembalance.ItemID = #v_item_to_be_updated AND RemainingQty > 0
OPEN crsr
FETCH crsr
INTO
#cost,
#qty,
#id
WHILE ##FETCH_STATUS = 0
BEGIN
IF #qty >= #qty_to_be_deducted
BEGIN
UPDATE itembalance SET RemainingQty = RemainingQty - #qty_to_be_deducted WHERE Id = #id
/*do something with cost*/ BREAK
END
ELSE
BEGIN
UPDATE itembalance SET RemainingQty = 0 WHERE Id = #id
/*do something with cost*/ SET #qty_to_be_deducted = #qty_to_be_deducted - #qty
END
FETCH crsr
INTO
#cost,
#qty,
#id
END
CLOSE crsr
DEALLOCATE crsr
The table may contain same item code but with different cost. This code is okay for few items being updated at a time but whenever a lot of items/quantities are being sent out, the process becomes really slow. Is there a way to optimize this code? I am guessing the cursor is making it slow so I want to explore a different code for this process.
This looks like you just need a simple CASE expression:
UPDATE dbo.itembalance
SET Qty = CASE WHEN Qty >= #qty_to_be_deducted THEN Qty - #qty_to_be_deducted ELSE 0 END
WHERE ItemID = #v_item_to_be_updated
--What is the difference between Qty and RemainingQty?
--Why are you checking one and updating the other?
AND RemainingQty > 0;
You code is not very clear as to how and why the mechanism is required and works.
However assuming that you must have multiple records with an outstanding balance, and that you must consider multiple records sequentially as part of this mechanism, then you have two options to solve that within SQL (handling in client code is another option):
1) Use a cursor as you have done
2) Use a temp table or table variable and iterate over it - pretty similar to a cursor but might be faster - you'd have to try and see e.g.
declare #TableVariable table (Cost money, RemainingQty int, Id int, OrderBy int, Done bit default(0))
declare #Id int, #Cost money, #RemainingQty int
insert into #TableVariable (Cost, RemainingQty, Id, OrderBy)
SELECT
itembalance.Cost
, itembalance.RemainingQty
, itembalance.Id
, 1 /* Some order by condition */
FROM dbo.itembalance
WHERE itembalance.ItemID = #v_item_to_be_updated AND RemainingQty > 0
while exists (select 1 from #TableVariable where Done = 0) begin
select top 1 #Id = id, #Cost = Cost, #RemainingQty
from #TableVariable
where Done = 0
order by OrderBy
-- Do stuff here
update #TableVariable set Done = 1 where id = #Id
end
However the code you have shown doesn't appear that it should be slow - so it may be that you are lacking the appropriate indexes, and that a single ItemId update is locking too many rows in the ItemBalance table which is then affecting other ItemId updates.

how to apply isnull to variable?

I have table called sample with columns:
Id, Name, Dept, active
Query:
select Id
from Sample
where Dept = #Dept and active = 1
I want to fetch id from sample table name by passing deptment name whose is active. There can come situation where where I get 2 records. Two dept might be active. That's why I am taking top 1. Some time might not come any record.
That's why I used like this in stored procedure:
declare #TempId int
set top 1 #TempId = Id
from Sample
where Dept = #Dept and active = 1
if(#TempId is null)
begin
#TempId = 0
end
Can I use isnull in the above select instead of after which is suitable for both my conditions?
No. First it must be select, not set.
And if select returns no rows, #TempId will not be changed. See this simple example
declare #TempId int = 0;
select #TempId = null where 1=2;
select #TempId;
I would write following code:
DECLARE #TempId int =
COALESCE((SELECT TOP 1 Id FROM [Sample] WHERE Dept = #dept AND Active=1), 0)
If no rows are returned, NULL coalescing function is used.
At the time of selecting record, check for the NULL value, and select the record which is NOT NULL.
declare #TempId int
select top 1 #TempId = Id from Sample where Dept = #Dept and active = 1 and Id is not null

Temp table has only one row inserted

Hi I have an SP in which i create a temporary table to store some values.
ALTER PROCEDURE [dbo].[test]
#id int,
#funds_limit money
AS
BEGIN
SET NOCOUNT ON;
DECLARE #threshold money;
CREATE TABLE #ConfigurationTemp
(id int,
name varchar(100) not null,
type varchar(100),
value varchar(100))
INSERT #ConfigurationTemp EXEC get_config #id, 'testType', null
select #threshold = value
from #ConfigurationTemp
where id=#id and name='testLimit'
print #threshold
IF (#funds_limit IS NOT NULL) AND (#threshold < #funds_limit)
BEGIN
DROP TABLE #ConfigurationTemp;
RETURN 1000;
END
select #threshold = value
from #ConfigurationTemp
where id=#id and name='testLimit1'
print #threshold
IF (#funds_limit IS NOT NULL) AND (#threshold < #funds_limit)
BEGIN
DROP TABLE #ConfigurationTemp;
RETURN 1001;
END
END
RETURN 0;
END
The temporary table have multiple rows.
eg:
1, fund_limit, testType, 10
2, fund_min_limit, testType, 20
I need to first validate the value for fund_limit (10) with the user input value (which will be an input parameter to the SP). If the validation fails, i return with an error code. If not, I go for the next check. i.e., fund_min_limit. I do the same with it and return a different error code. If no validation fails, i will return 0 which is considered to be a success.
In this case, I am getting same value for threshold always. i.e., the value of first row... 10.
How can I get the different threshold value from the temp table with respect to the name?
When you assign scalar variable with select - it may be not assigned (unchanged - may keep value from previous assignment) if this select returned zero rows. To ensure your variable changed it's value rewrite it as set expression.
So if you misspelled second threshold name you may be "getting" same #threshold value because second statement does not assign anything to your variable i.e. variable contains value from prior assignment (select). You may test it with additional variable for second threshold - it will be always NULL if i guessed the issue reason.
Also you are applying same #id filter which is a scalar variable. But your rows have different ids. So there is no chances right now to get any other threshold's value than for #id given.
set #threshold = (select t.value
from #ConfigurationTemp t
where t.name='testLimit')
print #threshold
IF #threshold < #funds_limit
RETURN 1000;
set #threshold = (select t.value
from #ConfigurationTemp t
where t.name='testLimit 2')
print #threshold
IF #threshold < #funds_limit
RETURN 1001;
If will succeed only when both arguments are NOT NULL.
One more approach:
declare
#threshold_a int,
#threshold_b int,
#threshold_c int
;with test as
(
select 'a' as name, 25 as value
union all
select 'b', 3
union all
select 'c', 100
union all
select 'd', -1
)
select
#threshold_a = case when t.name = 'a' then t.value else #threshold_a end,
#threshold_b = case when t.name = 'b' then t.value else #threshold_b end,
#threshold_c = case when t.name = 'c' then t.value else #threshold_c end
from test t
select
#threshold_a as [a],
#threshold_b as [b],
#threshold_c as [c]
GO
single select, several variables.
You have RETURN in your IF statment.
... RETURN 1000
and
... RETURN 1001
After insert a row the procedure end.
Maybe you want to assign a result to a variable
#return_Value = ''
#return_Value = #return_Value + '1000, '
....
#return_Value = #return_Value + '1001, '
RETURN #return_Value

Sql Optimization on advertising system

I am currently developing on an advertising system, which have been running just fine for a while now, apart from recently when our views per day have shot up from about 7k to 328k. Our server cannot take the pressure on this anymore - and knowing that I am not the best SQL guy around (hey, I can make it work, but not always in the best way) I am asking here for some optimization guidelines. I hope that some of you will be able to give rough ideas on how to improve this - I don't specifically need code, just to see the light :).
As it is at the moment, when an advert is supposed to be shown a PHP script is called, which in return calls a stored procedure. This stored procedure does several checks, it tests up against our customer database to see if the person showing the advert (given by a primary key id) is an actual customer under the given locale (our system is running on several languages which are all run as separate sites). Next up is all the advert details fetched out (image location as an url, height and width of the advert) - and lest step calls a separate stored procedure to test if the advert is allowed to be shown (is the campaign expired by either date or number of adverts allowed to show?) and if the customer has access to it (we got 2 access systems running, a blacklist and a whitelist one) and lastly what type of campaign we're running, is the view unique and so forth.
The code consists of a couple of stored procedures that I will post in here.
--- procedure called from PHP
CREATE PROCEDURE [dbo].[ExecView]
(
#publisherId bigint,
#advertId bigint,
#localeId int,
#ip varchar(15),
#ipIsUnique bit,
#success bit OUTPUT,
#campaignId bigint OUTPUT,
#advert varchar(500) OUTPUT,
#advertWidth int OUTPUT,
#advertHeight int OUTPUT
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #unique bit
DECLARE #approved bit
DECLARE #publisherEarning money
DECLARE #advertiserCost money
DECLARE #originalStatus smallint
DECLARE #advertUrl varchar(500)
DECLARE #return int
SELECT #success = 1, #advert = NULL, #advertHeight = NULL, #advertWidth = NULL
--- Must be valid publisher, ie exist and actually be a publisher
IF dbo.IsValidPublisher(#publisherId, #localeId) = 0
BEGIN
SELECT #success = 0
RETURN 0
END
--- Must be a valid advert
EXEC #return = FetchAdvertDetails #advertId, #localeId, #advert OUTPUT, #advertUrl OUTPUT, #advertWidth OUTPUT, #advertHeight OUTPUT
IF #return = 0
BEGIN
SELECT #success = 0
RETURN 0
END
EXEC CanAddStatToAdvert 2, #advertId, #publisherId, #ip, #ipIsUnique, #success OUTPUT, #unique OUTPUT, #approved OUTPUT, #publisherEarning OUTPUT, #advertiserCost OUTPUT, #originalStatus OUTPUT, #campaignId OUTPUT
IF #success = 1
BEGIN
INSERT INTO dbo.Stat (AdvertId, [Date], Ip, [Type], PublisherEarning, AdvertiserCost, [Unique], Approved, PublisherCustomerId, OriginalStatus)
VALUES (#advertId, GETDATE(), #ip, 2, #publisherEarning, #advertiserCost, #unique, #approved, #publisherId, #originalStatus)
END
END
--- IsValidPublisher
CREATE FUNCTION [dbo].[IsValidPublisher]
(
#publisherId bigint,
#localeId int
)
RETURNS bit
AS
BEGIN
DECLARE #customerType smallint
DECLARE #result bit
SET #customerType = (SELECT [Type] FROM dbo.Customer
WHERE CustomerId = #publisherId AND Deleted = 0 AND IsApproved = 1 AND IsBlocked = 0 AND LocaleId = #localeId)
IF #customerType = 2
SET #result = 1
ELSE
SET #result = 0
RETURN #result
END
-- Fetch advert details
CREATE PROCEDURE [dbo].[FetchAdvertDetails]
(
#advertId bigint,
#localeId int,
#advert varchar(500) OUTPUT,
#advertUrl varchar(500) OUTPUT,
#advertWidth int OUTPUT,
#advertHeight int OUTPUT
)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SELECT #advert = T1.Advert, #advertUrl = T1.TargetUrl, #advertWidth = T1.Width, #advertHeight = T1.Height FROM Advert as T1
INNER JOIN Campaign AS T2 ON T1.CampaignId = T2.Id
WHERE T1.Id = #advertId AND T2.LocaleId = #localeId AND T2.Deleted = 0 AND T2.[Status] <> 1
IF #advert IS NULL
RETURN 0
ELSE
RETURN 1
END
--- CanAddStatToAdvert
CREATE PROCEDURE [dbo].[CanAddStatToAdvert]
#type smallint, --- Type of stat to add
#advertId bigint,
#publisherId bigint,
#ip varchar(15),
#ipIsUnique bit,
#success bit OUTPUT,
#unique bit OUTPUT,
#approved bit OUTPUT,
#publisherEarning money OUTPUT,
#advertiserCost money OUTPUT,
#originalStatus smallint OUTPUT,
#campaignId bigint OUTPUT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #campaignLimit int
DECLARE #campaignStatus smallint
DECLARE #advertsLeft int
DECLARE #campaignType smallint
DECLARE #campaignModeration smallint
DECLARE #count int
SELECT #originalStatus = 0
SELECT #success = 1
SELECT #approved = 1
SELECT #unique = 1
SELECT #campaignId = CampaignId FROM dbo.Advert
WHERE Id = #advertId
IF #campaignId IS NULL
BEGIN
SELECT #success = 0
RETURN
END
SELECT #campaignLimit = Limit, #campaignStatus = [Status], #campaignType = [Type], #publisherEarning = PublisherEarning, #advertiserCost = AdvertiserCost, #campaignModeration = ModerationType FROM dbo.Campaign
WHERE Id = #campaignId
IF (#type <> 0 AND #type <> 2 AND #type <> #campaignType) OR ((#campaignType = 0 OR #campaignType = 2) AND (#type = 1)) -- if not a click or view type, then type must match the campaign (ie, only able to do leads on lead campaigns, no isales or etc), click and view campaigns however can do leads too
BEGIN
SELECT #success = 0
RETURN
END
-- Take advantage of the fact that the variable only gets touched if there is a record,
-- which is supposed to override the existing one, if there is one
SELECT #publisherEarning = Earning FROM dbo.MapCampaignPublisherEarning
WHERE CanpaignId = #campaignId AND PublisherId = #publisherId
IF #campaignStatus = 1
BEGIN
SELECT #success = 0
RETURN
END
IF NOT #campaignLimit IS NULL
BEGIN
SELECT #advertsLeft = AdvertsLeft FROM dbo.Campaign WHERE Id = #campaignId
IF #advertsLeft < 1
BEGIN
SELECT #success = 0
RETURN
END
END
IF #campaignModeration = 0 -- blacklist
BEGIN
SELECT #count = COUNT([Status]) FROM dbo.MapCampaignModeration WHERE CampaignId = #campaignId AND PublisherId = #publisherId AND [Status] = 3
IF #count > 0
BEGIN
SELECT #success = 0
RETURN
END
END
ELSE -- whitelist
BEGIN
SELECT #count = COUNT([Status]) FROM dbo.MapCampaignModeration WHERE CampaignId = #campaignId AND PublisherId = #publisherId AND [Status] = 2
IF #count < 1
BEGIN
SELECT #success = 0
RETURN
END
END
IF #ipIsUnique = 1
BEGIN
SELECT #unique = 1
END
ELSE
BEGIN
IF (SELECT COUNT(T1.Id) FROM dbo.Stat AS T1
INNER JOIN dbo.IQ_Advert AS T2
ON T1.AdvertId = T2.Id
WHERE T2.CampaignId = #campaignId
AND T1.[Type] = #type
AND T1.[Unique] = 1
AND T1.PublisherCustomerId = #publisherId
AND T1.Ip = #ip
AND DATEADD(SECOND, 86400, T1.[Date]) > GETDATE()
) = 0
SELECT #unique = 1
ELSE
BEGIN
SELECT #unique = 0, #originalStatus = 1 -- not unique, and set status to be ip conflict
END
END
IF #unique = 0 AND #type <> 0 AND #type <> 2
BEGIN
SELECT #unique = 1, #approved = 0
END
IF #originalStatus = 0
SELECT #originalStatus = 5
IF #approved = 0 OR #type <> #campaignType
BEGIN
SELECT #publisherEarning = 0, #advertiserCost = 0
END
END
I am thinking this needs more than just a couple of indexes thrown in to help it, but rather a total rethinking of how to handle it. I have been heard that running this as a batch would help, but I am not sure how to get this implemented, and really not sure if i can implement it in a such way where I keep all these nice checks before the actual insert or if I have to give up on some of this.
Anyhow, all help would be appreciated, if you need any of the table layouts, let me know :).
Thanks for taking the time to look at it :)
Make sure to reference tables with the ownership prefix. So instead of:
INNER JOIN Campaign AS T2 ON T1.CampaignId = T2.Id
Use
INNER JOIN dbo.Campaign AS T2 ON T1.CampaignId = T2.Id
That will allow the database to cache the execution plan.
Another possibility is to disable database locking, which has data integrity risks, but can significantly increase performance:
INNER JOIN dbo.Campaign AS T2 (nolock) ON T1.CampaignId = T2.Id
Run a sample query in SQL Analyzer with "Show Execution Plan" turned on. This might give you a hint as to the slowest part of the query.
it seems like FetchAdvertDetails hit the same tables as the start of CanAddStatToAdvert (Advert and Campaign). If possible, I'd try to eliminate FetchAdvertDetails and roll its logic into CanAddStatToAdvert, so you don't have the hit Advert and Campaign the extra times.
Get rid of most of the SQL.
This stored procedure does several
checks, it tests up against our
customer database to see if the person
showing the advert (given by a primary
key id) is an actual customer under
the given locale (our system is
running on several languages which are
all run as separate sites). Next up is
all the advert details fetched out
(image location as an url, height and
width of the advert) - and lest step
calls a separate stored procedure to
test if the advert is allowed to be
shown (is the campaign expired by
either date or number of adverts
allowed to show?) and if the customer
has access to it (we got 2 access
systems running, a blacklist and a
whitelist one) and lastly what type of
campaign we're running, is the view
unique and so forth.
Most of this should not be done in the database for every request. In particular:
Customer and local can be stored in memory. Expire them after 5 minutes or so, but do not ask for this info on every repetitive request.
Advert details can also be stored. Every advert will have a "key" to identify it (Number)?. Ust a dictionary / hashtable in memory.
Eliminate as many SQL Parts as you can. Dumping repetitive work on the SQL Database is a typical mistake.