SQL WHERE ... IN clause with possibly null parameter - sql

I am having some problems with my WHERE clause (using SQL 2008) . I have to create a stored procedure that returns a list of results based on 7 parameters, some of which may be null. The ones which are problematic are #elements, #categories and #edu_id. They can be a list of ids, or they can be null. You can see in my where clause that my particular code works if the parameters are not null. I'm not sure how to code the sql if they are null. The fields are INT in the database.
I hope my question is clear enough. Here is my query below.
BEGIN
DECLARE #elements nvarchar(30)
DECLARE #jobtype_id INT
DECLARE #edu_id nvarchar(30)
DECLARE #categories nvarchar(30)
DECLARE #full_part bit
DECLARE #in_demand bit
DECLARE #lang char(2)
SET #jobtype_id = null
SET #lang = 'en'
SET #full_part = null -- full = 1, part = 0
SET #elements = '1,2,3'
SET #categories = '1,2,3'
SET #edu_id = '3,4,5'
select
jobs.name_en,
parttime.fulltime_only,
jc.cat_id category,
je.element_id elem,
jt.name_en jobtype,
jobs.edu_id minEdu,
education.name_en edu
from jobs
left join job_categories jc
on (jobs.job_id = jc.job_id)
left join job_elements je
on (jobs.job_id = je.job_id)
left join job_type jt
on (jobs.jobtype_id = jt.jobtype_id)
left join education
on (jobs.edu_id = education.edu_id)
left join
(select job_id, case when (jobs.parttime_en IS NULL OR jobs.parttime_en = '') then 1 else 0 end fulltime_only from jobs) as parttime
on jobs.job_id = parttime.job_id
where [disabled] = 0
and jobs.jobtype_id = isnull(#jobtype_id,jobs.jobtype_id)
and fulltime_only = isnull(#full_part,fulltime_only)
-- each of the following clauses should be validated to see if the parameter is null
-- if it is, the clause should not be used, or the SELECT * FROM ListToInt... should be replaced by
-- the field evaluated: ie if #elements is null, je.element_id in (je.element_id)
and je.element_id IN (SELECT * FROM ListToInt(#elements,','))
and jc.cat_id IN (SELECT * FROM ListToInt(#categories,','))
and education.edu_id IN (SELECT * FROM ListToInt(#edu_id,','))
order by case when #lang='fr' then jobs.name_fr else jobs.name_en end;
END

Something like
and (#elements IS NULL OR je.element_id IN
(SELECT * FROM ListToInt(#elements,',')))
and (#categories IS NULL OR
jc.cat_id IN (SELECT * FROM ListToInt(#categories,',')))
....
should do the trick

je.element_id IN (SELECT * FROM ListToInt(#elements,',')) OR #elements IS NULL
that way for each one

Have you tried explicitly comparing to NULL?
and (#elements is null or je.element_id IN (SELECT * FROM ListToInt(#elements,','))
And so on.

Related

Replacing Is Null Or Exist Inner Query Logic to Reduce Stored Procedure Execution Time

My stored procedure is currently using Is Null Or Exist logic combined with an inner query to filter out the records. The stored procedure is converting multiple comma-separated input values to temp tables (in the production scenario, the input record count will be much higher). And the inner query is using these temp tables for filter conditions. Due to the concern over query execution time would like to change the existing inner-query with an alternate (like left join). But need to retain the same Is Null Or Exist logic. Any suggestions?
DECLARE #SelectedOfferes varchar(1000) = 'FLT10,SPL20'
DECLARE #SelectedBrandCode varchar(1000) = '208,406'
DECLARE #CategoryCode varchar(1000) = 'GMOVN2,CELSMR,LCDTV38IN'
CREATE TABLE #SelectedOfferes
(
DiscountCode VARCHAR(20)
)
CREATE TABLE #BrandCode
(
BrandCode VARCHAR(20)
)
CREATE TABLE #CategoryCode
(
CategoryCode VARCHAR(20)
)
IF #SelectedOfferes IS NOT NULL
BEGIN
INSERT INTO #SelectedOfferes
SELECT part
FROM dbo.[FormatTextByDelimiter] (#SelectedOfferes, ',')
END
IF #SelectedBrandCode IS NOT NULL
BEGIN
INSERT INTO #BrandCode
SELECT part
FROM dbo.[FormatTextByDelimiter] (#SelectedBrandCode, ',')
END
IF #CategoryCode IS NOT NULL
BEGIN
INSERT INTO #CategoryCode
SELECT part
FROM dbo.[FormatTextByDelimiter] (#CategoryCode, ',')
END
SELECT *
FROM Products P
INNER JOIN Discount D ON P.DiscountCode = D.DiscountCode
INNER JOIN AvailableBrand AB ON P.BrandCode = AB.BrandCode
INNER JOIN Category C ON P.CategoryCode = C.CategoryCode
WHERE (#SelectedOfferes IS NULL
OR (EXISTS (SELECT 1 FROM #SelectedOfferes OFR
WHERE OFR.DiscountCode = P.DiscountCode)))
AND (#SelectedBrandCode IS NULL
OR (EXISTS (SELECT 1 FROM #BrandCode BC
WHERE BC.BrandCode = P.BrandCode)))
AND (#CategoryCode IS NULL
OR (EXISTS (SELECT 1 FROM #CategoryCode CAT
WHERE CAT.CategoryCode = P.CategoryCode)))
Dynamic SQL version
I have some questions about your string split function, is it set-based or a looping query? If it's not set-based then you should probably replace it with Jeff Moden's DelimitedSplit8K available at http://www.sqlservercentral.com/articles/Tally+Table/72993/ .
The below example should work the same as what you supplied but should be faster since it removes the ORs and the correlated subqueries from the WHERE clause. I'm not a fan of using dynamic SQL but sometimes it is the best way to get the job done. Maybe someone else can come up with a non-dynamic solution that works as well or better.
DECLARE #SelectedOfferes varchar(1000) = 'FLT10,SPL20'
DECLARE #SelectedBrandCode varchar(1000) = '208,406'
DECLARE #CategoryCode varchar(1000) = 'GMOVN2,CELSMR,LCDTV38IN'
CREATE TABLE #SelectedOfferes
(
DiscountCode VARCHAR(20)
)
CREATE TABLE #BrandCode
(
BrandCode VARCHAR(20)
)
CREATE TABLE #CategoryCode
(
CategoryCode VARCHAR(20)
)
IF #SelectedOfferes IS NOT NULL
BEGIN
INSERT INTO #SelectedOfferes
SELECT part
FROM dbo.[FormatTextByDelimiter] (#SelectedOfferes, ',')
END
IF #SelectedBrandCode IS NOT NULL
BEGIN
INSERT INTO #BrandCode
SELECT part
FROM dbo.[FormatTextByDelimiter] (#SelectedBrandCode, ',')
END
IF #CategoryCode IS NOT NULL
BEGIN
INSERT INTO #CategoryCode
SELECT part
FROM dbo.[FormatTextByDelimiter] (#CategoryCode, ',')
END
DECLARE #SQL NVarchar(4000);
SET #SQL = N'SELECT *
FROM Products P
INNER JOIN Discount D ON P.DiscountCode = D.DiscountCode
INNER JOIN AvailableBrand AB ON P.BrandCode = AB.BrandCode
INNER JOIN Category C ON P.CategoryCode = C.CategoryCode'
IF #SelectedOfferes IS NOT NULL
SET #SQL = #SQL + N'
INNER JOIN #SelectedOfferes OFR ON OFR.DiscountCode = P.DiscountCode';
IF #SelectedBrandCode IS NOT NULL
SET #SQL = #SQL + N'
INNER JOIN #BrandCode BC ON BC.BrandCode = P.BrandCode';
IF #CategoryCode IS NOT NULL
SET #SQL = #SQL + N'
INNER JOIN #CategoryCode CAT ON CAT.CategoryCode = P.CategoryCode';
EXEC sys.sp_executesql #stmt = #SQL;
This method doesn't quite do what the OP wanted but is valid in many other cases
I have some questions about your string split function, is it set-based or a looping query? If it's not set-based then you should probably replace it with Jeff Moden's DelimitedSplit8K available at http://www.sqlservercentral.com/articles/Tally+Table/72993/ .
But either way the below change to your last query should help quite a bit. The IS NULL parts aren't needed since it is a LEFT JOIN and the table will be empty if the variable it is built with is NULL, so you get the same result with less work for the engine.
SELECT *
FROM Products P
INNER JOIN Discount D ON P.DiscountCode = D.DiscountCode
INNER JOIN AvailableBrand AB ON P.BrandCode = AB.BrandCode
INNER JOIN Category C ON P.CategoryCode = C.CategoryCode
LEFT JOIN #SelectedOfferes OFR ON OFR.DiscountCode = P.DiscountCode
LEFT JOIN #BrandCode BC ON BC.BrandCode = P.BrandCode
LEFT JOIN #CategoryCode CAT ON CAT.CategoryCode = P.CategoryCode

Using multiple nested OR statements in WHERE clause makes query return incorrect results

I have a WHERE clause that has a nested OR statement, as seen here:
-- Declaration of variables
DECLARE
#PageSize INT,
#PageNumber INT,
#SearchPhraseOne VARCHAR(20),
#SearchPhraseTwo VARCHAR(20),
#FilterCategory VARCHAR(30),
#FilterStatus TINYINT,
#NeedsFollowUp TINYINT,
#NeedsTraining TINYINT,
#NeedsInitialVacc TINYINT;
SET #PageNumber = 1;
SET #PageSize = 100;
SET #SearchPhraseOne = null;
SET #SearchPhraseTwo = null;
SET #FilterCategory = 'High Exposure';
SET #FilterStatus = null;
SET #NeedsFollowUp = 1;
SET #NeedsTraining = null;
SET #NeedsInitialVacc = null;
select * from(
select
vel.fullName,
vel.EecEmpNo,
vel.EecLocation,
vel.EecDateOfLastHire,
job.JbcDesc,
vei.eiInitialBBPDate,
vei.eiVCGivenDate,
iif(jv.verTypeName is null, 'Low Risk', jv.verTypeName) as vaccineCategory,
vel.eecEmplStatus,
count(distinct vh.vhID) as vaccCount,
max(isnull(vh.vhNextDateScheduled, null)) as maxNextDateScheduled,
max(cast(vh.vhSeriesComplete as int)) as seriesComplete,
iif(vel.eecEmplStatus = 'T', null,
coalesce(iif(max(cast(vh.vhSeriesComplete as int)) = 1, null, max(isnull(vh.vhNextDateScheduled, null))), -- check if the vaccine items have a SeriesComplete of 1, otherwise use NextDateScheduled
iif(vei.eiInitialBBPDate is not null, null, vel.EecDateOfLastHire), -- check if the InitialBBPDate is not null, if it isn't use DateOfLastHire
iif(vei.eiVCGivenDate is not null, null, vel.EecDateOfLastHire), null)) as actionDate -- check if the OrientationDate is not null, if it isn't use DateOfLastHire
-- if all three of these values are null then there's no ActionDate
-- Terminated employees will not have an action date assigned even if there's a match
from dbo.vaccEmpList vel
left join dbo.vaccEmployeeInfo vei on vei.eiEmployeeNo = vel.EecEmpNo
left join dbo.vaccVaccinationHistory vh on vh.vhEmployeeNo = vel.EecEmpNo
left join dbo.vaccVaccineTypeLookup vt on vh.vhVaccinationTypeID = vt.vtlVaccineTypeID and vt.vtIsActive = 1 -- Only get active vaccination types
join dbo.U_JobCode job on vel.EecJobCode = job.JbcJobCode
left join dbo.JobVerficationXref jv on vel.EecJobCode = jv.JobCode and jv.verName = 'Vaccination Category'
group by vel.fullName, vel.EecEmpNo, job.JbcDesc, jv.verTypeName, vel.EecLocation, vel.eecEmplStatus, vei.eiInitialBBPDate, vei.eiVCGivenDate, vel.EecDateOfLastHire
) as searchResults
where (
(
#SearchPhraseOne is null
or searchResults.fullName like #SearchPhraseOne + '%'
or searchResults.EecEmpNo = #SearchPhraseOne
)
and (
#SearchPhraseTwo is null
or searchResults.fullName like #SearchPhraseTwo + '%'
or searchResults.EecEmpNo = #SearchPhraseTwo
) -- Employee Name/ID
and (
#FilterStatus is null
or (searchResults.eecEmplStatus = 'A' or searchResults.eecEmplStatus = 'L')
) -- Employee Status
and (
#FilterCategory is null
or searchResults.vaccineCategory = #FilterCategory
) -- Employee Vaccination Category
and ( -- ISSUES OCCUR HERE
(#NeedsTraining is null
or (searchResults.actionDate is not null
and (searchResults.eiInitialBBPDate is null or searchResults.eiVCGivenDate is null))
) -- Needs Training if either of these two date values are null
or (#NeedsInitialVacc is null
or (searchResults.actionDate is not null
and (searchResults.vaccCount = 0))
-- Needs Initial Vaccination if there are no vaccine records
)
or (#NeedsFollowUp is null
or (searchResults.actionDate is not null
and ((searchResults.seriesComplete is null or searchResults.seriesComplete = 0) and searchResults.maxNextDateScheduled is not null))
-- Needs a follow-up date if no series complete was detected
)
)
)
The #NeedsFollowUp, #NeedsInitialVacc, and #NeedsTraining variables are all set by the variables above. When one or more of these are set to "1", the query should return employee entries that match the criteria inside their related statements. For example, if the "NeedsFollowUp" and "NeedsTraining" values are set to "1" then the query should return employees that need a follow-up or employees that need training.
Right now, when I set all three to "1" I receive the combined results I'm looking for, but if any of them are set to null, then the query doesn't return the correct results.
EDIT: Here's a reproducible example of what I'm seeing.
I think the way the clauses are set up is causing an issue, but I'm not really sure how to fix this. How can I get the OR statements to work in the way I described above?
I was able to make the OR clauses work correcting by switching from is null to is not null in my where clauses. Using the minimal example, it would look like this:
select * from AGENTS
where (
(#NeedsName is not null and AGENTS.AGENT_NAME is null)
or
(#NeedsCountry is not null and AGENTS.COUNTRY is null)
or
(#NeedsCountry is null and #NeedsName is null)
)
Be sure to include an additional clause for when all options are NULL, so that you can return the appropriate number of rows.
Here's a working version.

Can we create a view after a script from a variable?

I would like to create a view at the end of the following request.
I know that 'create view' must be the first statement in a query batch. My problem is that for this query i must use a variable (#listOfIDRUB).
This variable is only fill correctly at the end of my little script.
I also have tried to create the view before my first declaration but it created a problem with "DECLARE".
So is it possible to create a view easily from the result of my script or i have to do something else ?
DECLARE #CounterResId int;
DECLARE #lePath varchar(255);
DECLARE #listOfIDRUB TABLE (EXTERNALREFERENCE uniqueidentifier, ID varchar(255), DOCID varchar(255) );
DECLARE #Max int;
SET #lePath = '';
SET #CounterResId = 1;
SET #Max = (SELECT COUNT(*) FROM SYNTHETIC..EXTRANET_PURGE WHERE TYPE_SUPPR = 'ResId')
WHILE (#CounterResId <= #Max )
BEGIN;
set #lePath =
(select tmp.lePath from
(
select row_number() over(order by path)as NumLigne, CONCAT(path, '%' ) as lePath from RUBRIQUE
WHERE MODELE = 'CAEEE64D-2B00-44EF-AA11-6B72ABD9FE38'
and CODE in (SELECT ID FROM SYNTHETIC..EXTRANET_PURGE where TYPE_SUPPR='ResId')
) tmp
WHERE tmp.NumLigne = #CounterResId)
INSERT INTO #listOfIDRUB(EXTERNALREFERENCE, ID, DOCID)
SELECT SEC.EXTERNALREFERENCE , SEC.ID, SEC.DOCUMENTID
FROM WEBACCESS_FRONT..SECTIONS sec
inner join rubrique rub ON rub.ID_RUBRIQUE = sec.EXTERNALREFERENCE
inner join template_tree_item tti ON tti.id_template_tree_item = rub.modele
inner join template t ON t.id_template = tti.template
WHERE t.CODE IN (SELECT TEMPLATE_CODE from SYNTHETIC..EasyFlowEngineListTemplateCode)
and rub.path like #lePath
print #CounterResId;
print #lePath;
set #CounterResId = #CounterResId + 1;
END;
select * from #listOfIDRUB;
Instead of select * from #listOfIDRUB
i wanted create view test as select * from listOfIDRUB
I have also tried create view test as (all my request)
Whenever you ask something about SQL please state your RDBMS (product and version). The answers are highly depending on this...
From your code I assume this is SQL Server.
So to your question: No, a VIEW must be "inlineable" (single-statement or "ad-hoc") statement.
You might think about a multi-statement UDF, but this is in almost all cases a bad thing (bad performance). Only go this way, if your result table will consist of rather few rows!
Without knowing your tables this is rather blind walking, but you might try this (add parameters, if you can transfer external operations (e.g. filtering) into the function):
CREATE FUNCTION dbo.MyFunction()
RETURNS #listOfIDRUB TABLE (EXTERNALREFERENCE uniqueidentifier, ID varchar(255), DOCID varchar(255) )
AS
BEGIN
DECLARE #CounterResId int;
DECLARE #lePath varchar(255);
DECLARE #Max int;
SET #lePath = '';
SET #CounterResId = 1;
SET #Max = (SELECT COUNT(*) FROM SYNTHETIC..EXTRANET_PURGE WHERE TYPE_SUPPR = 'ResId')
WHILE (#CounterResId <= #Max )
BEGIN;
set #lePath =
(select tmp.lePath from
(
select row_number() over(order by path)as NumLigne, CONCAT(path, '%' ) as lePath from RUBRIQUE
WHERE MODELE = 'CAEEE64D-2B00-44EF-AA11-6B72ABD9FE38'
and CODE in (SELECT ID FROM SYNTHETIC..EXTRANET_PURGE where TYPE_SUPPR='ResId')
) tmp
WHERE tmp.NumLigne = #CounterResId)
INSERT INTO #listOfIDRUB(EXTERNALREFERENCE, ID, DOCID)
SELECT SEC.EXTERNALREFERENCE , SEC.ID, SEC.DOCUMENTID
FROM WEBACCESS_FRONT..SECTIONS sec
inner join rubrique rub ON rub.ID_RUBRIQUE = sec.EXTERNALREFERENCE
inner join template_tree_item tti ON tti.id_template_tree_item = rub.modele
inner join template t ON t.id_template = tti.template
WHERE t.CODE IN (SELECT TEMPLATE_CODE from SYNTHETIC..EasyFlowEngineListTemplateCode)
and rub.path like #lePath
--print #CounterResId;
--print #lePath;
set #CounterResId = #CounterResId + 1;
END;
RETURN;
END
You can call it like this (very similar to a VIEW)
SELECT * FROM dbo.MyFunction();
And you might even use it in joins...
And last but not least I'm quite sure, that one could solve this without declares and a loop too...

Warning: Null value is eliminated by an aggregate or other SET operation

I'm running the following script -
declare #eventname VARCHAR(64)
declare #eventid INT
set #eventname = 'event123'
set #eventid = (select tm.trgmst_id from trgmst tm where tm.trgmst_name = #eventname)
declare #trgjobmax INT
set #trgjobmax = (select max(trgjob_id) from trgjob)
declare #jobid TABLE (jobmst_id INT, trgjob_order INT)
insert into #jobid (jobmst_id, trgjob_order)
select
jm.jobmst_id, max(tj.trgjob_order)
from
jobmst jm
left outer join
trgjob tj on tj.jobmst_id = jm.jobmst_id
inner join
workgrp wg on wg.workgrp_id = jm.jobmst_owner
where
wg.workgrp_name in ('group1', 'group2', 'group3')
and jm.jobmst_type = 2
and jm.jobmst_dirty <> 'X'
and jm.jobmst_id NOT IN (select tj.jobmst_id
from trgjob tj
where tj.trgmst_id = #eventid)
group by
jm.jobmst_id
order by
jm.jobmst_id desc
The problem is I get this message -
Warning: Null value is eliminated by an aggregate or other SET
operation.
The reason is that it is not always guaranteed to have a trgjob_order value so it displays as NULL. I then have to run the following update script in the same transaction and it seems to work fine.
update #jobid
set trgjob_order = 0
where trgjob_order IS NULL
Is there a way to write this more elegantly? The data I'm given looks correct so I'm taking the warning as just that... It's a warning but can be ignored because I'm doing an update POST to replace the NULL with 0.
Use ISNULL
select jm.jobmst_id, ISNULL(max(tj.trgjob_order), 0) from jobmst jm
Your complete script
declare #eventname VARCHAR(64)
declare #eventid INT
set #eventname = 'event123'
set #eventid = (select tm.trgmst_id from trgmst tm where tm.trgmst_name = #eventname)
declare #trgjobmax INT
set #trgjobmax = (select max(trgjob_id) from trgjob)
declare #jobid TABLE (jobmst_id INT, trgjob_order INT)
insert into #jobid (jobmst_id, trgjob_order)
select jm.jobmst_id, ISNULL(max(tj.trgjob_order), 0) from jobmst jm
left outer join trgjob tj on tj.jobmst_id = jm.jobmst_id
inner join workgrp wg on wg.workgrp_id = jm.jobmst_owner
where wg.workgrp_name in ('group1', 'group2', 'group3')
and jm.jobmst_type = 2 and jm.jobmst_dirty <> 'X' and jm.jobmst_id
NOT IN (
select tj.jobmst_id from trgjob tj
where tj.trgmst_id = #eventid
)
group by jm.jobmst_id
order by jm.jobmst_id desc
I think what you want is MAX(ISNULL(Column,0)) not ISNULL(MAX(Column),0), this will remove the null before it is aggregated.
It works fine in this case, with MAX, but careful doing this with COUNT as it would produce different results (given a null is not counted and a ISNULL'ed value is)

SQL Server stored procedure WHERE clause

Need help with WHERE clause in this stored procedure.
How to write WHERE with this parameters and if any of these param contains specific value, then I need to get all values from that column ?
Sample if #post1 contains 1 then select values from that columns that are equals to 1.
But if #post1 contains 0 than select all values from that column. And that for all other parameters.
ALTER PROCEDURE [dbo].[spStavke]
#dat1 date,
#dat2 date,
#god int,
#post1 int,
#post2 int,
#post3 int
AS
BEGIN
SET NOCOUNT ON;
SELECT
[test1]
,[test2]
,[test3]
,[test3]
FROM
[PN].[dbo].[Stavke] AS stavke
LEFT JOIN
PN.dbo.Tip AS tip ON stavke.Vrsta = tip.id
LEFT JOIN
PN.dbo.Vrsta AS vrs ON stavke.Jedinica = vrs.id
END
SELECT
[test1]
,[test2]
,[test3]
,[test3]
FROM [PN].[dbo].[Stavke] as stavke
left join PN.dbo.Tip as tip on stavke.Vrsta=tip.id
left join PN.dbo.Vrsta as vrs on stavke.Jedinica = vrs.id
WHERE (#Post1 = 0 OR (#Post1 = 1 AND 1 IN( TEST1,TEST2,TEST3))
AND (#Post2 = 0 OR (#Post2 = 1 AND 1 IN( TEST1,TEST2,TEST3))