WHERE clause match or null - sql

I have searched and searched and tested and tested but I can't seem to find the answer.
Here's the setup. I have 4 parameters coming in that can either:
Match a column in the table
Parameter and table column are null
There's no match for the parameter but the associated table column has a null
I've tried:
PCN = ( CASE WHEN PCN = #group THEN #PCN
WHEN PCN IS NULL THEN NULL
END )
Handles, 2. Doesn't Handle, 3. Doesn't handle
( PCN= #PCN
or
PCN is null )
Doesn't handle, 2. Doesn't handle 3. doesn't handle
(IsNull(PCN,'none') = IsNull(#PCN,'none'))
Handles, 2. Handles, 3. Doesn't handle
Since the last one is the only one that's handling scenario 3, here's more details on that one.
DECLARE #bin varchar(6) = '123456'
, #PCN varchar(50) = 'abc'
, #Group varchar(50) = '0.0'
, #NDC = '01234567891'
select * from dbo.Insurance
WHERE (IsNull([Group],'none') = IsNull(#group,'none'))
and (IsNull(PCN,'none') = IsNull(#PCN,'none'))
and (IsNull(BIN,'none') = IsNull(#bin,'none'))
and (IsNull(NDC,'none') = IsNull(#NDC,'none') )
order by [Group] desc, PCN desc, BIN desc, NDC;
This query returns no results. My expectation is to return the row where Bin, Group and NDC match and PCN is NULL. Any insight you can provide would be much appreciated!
UPDATE:
RowId
BIN
PCN
Group
NDC
1
123456
abc
0.0
01234567891
2
123456
NULL
0.0
01234567891
Scenario 1 - PCN column = PCN variable , rowID 1 should be returned
Scenario 2 - PCN column & PCN variable are null, rowID 2 should be returned
Scenario 3 - PCN variable = 123, rowID 2 should be returned

Solution 1:
((Group IS NULL AND #group IS NULL) OR (Group IS NOT NULL AND #group IS NOT NULL AND Group = #group ))
and ...
Solution 2:
(Group= #group) OR (ISNULL(Group, #group) IS NULL)
and ...

you can have 3 OR's per parameter
select *
from dbo.Insurance
where ([Group] is null or #Group is null or [Group] = #Group)
and (PCN is null or #PCN is null or PCN = #PCN)
and (BIN is null or #bin is null or BIN = #bin)
and (NDC is null or #NDC is null or NDC = #NDC)
order by [Group] desc
, PCN desc
, BIN desc
, NDC;

WHERE
(colA = #a OR colA IS NULL)
The values match: accepts based on colA = #a
Both are NULL: accepts based on colA IS NULL
colA is NULL: also accepts based on colA IS NULL
But we also need to make sure no other cases pass:
There are values in both and they do not match: will not be accepted based on either half.
colA is not NULL and #a is NULL: also does not pass

I discussed the business requirements with our team again. Slight adjustment and just in case anyone runs into something similar in the future, here were are the final requirements and solve:
Parameter and column are the same
Parameter and column are both null
Column has a wildcard (*)
DECLARE #bin varchar(6) = '123456'
, #PCN varchar(50) = 'abc'
, #Group varchar(50) = '0.0'
, #NDC = '01234567891'
select * from dbo.Insurance
WHERE ((IsNull([Group],'none') = IsNull(#group,'none')) OR [Group] = '*')
and
((IsNull(PCN,'none') = IsNull(#PCN,'none')) OR PCN = '*')
and
((IsNull(BIN,'none') = IsNull(#bin,'none')) OR BIN = '*')
and
((IsNull(NDC9,'none') = IsNull(#NDC9,'none') ) OR NDC9 = '*')
order by [Group] desc, PCN desc, BIN desc, NDC;
Thank you everyone for your comments!

Related

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.

Using ISNULL in where Clause doesn't return records with NULL field

Table:
ID AppType AppSubType Factor
1 SC CD 1.0000000000
2 SC CD 2.0000000000
3 SC NULL 3.0000000000
4 SC NULL 4.0000000000
Query:
declare #ast varchar(10)
set #ast = null
select *
from tbl
where AppType = 'SC' and AppSubType = ISNULL(#ast, AppSubType)
Result:
ID AppType AppSubType Factor
1 SC CD 1.0000000000
2 SC CD 2.0000000000
Question:
Shouldn't this query return all 4 records and not just the first 2?
Abviously #ast is null and Isnull would exchange null with other value, so you shouldn't expect #ast to be not null. If your AppSubType is null , so the result become null but AppSubType=null doesn't mean because AppSubType is null is true. Because null is not a value so it cant work with equal.
for your expected result this code will work.
declare #ast varchar(10)
set #ast = null
select *
from tbl
where AppType = 'SC' and (AppSubType = ISNULL(#ast, AppSubType) Or AppSubType is null)
You can write a case condition in where clause as:
declare #ast varchar(10)
set #ast = null
select *
from tbl
where AppType = 'SC' and 1=
case when isnull(#ast ,'') = '' and isnull(AppSubType ,'') = '' then 1
when AppSubType = ISNULL(#ast, AppSubType) then 1
else 0
end
Please understand the behavior of ISNULL explained in below blog.
the very first expression in the isnull function is a column value or the expression of some result.
ISNULL Explored in Detail
The code looks like a search functionality. If the value is not given then replace them with whatever is there in database and if given pull only those records which are matched.

Why do I get sub query returning more than one row?

I am using Advantage Database Server.
I have one table Areas and I want to find out all the child nodes of parent area.
The table and column names are:
Areas
(
AreaID INTEGER
, Name NVARCHAR(50)
, Code NVARCHAR(50)
)
The stored procedure is:
CREATE PROCEDURE GetAreaLocations
(
AreaID INTEGER
, AreaOutID INTEGER OUTPUT
, AreaName NVARCHAR(100) OUTPUT
, AreaCode NVARCHAR(50) OUTPUT
, WithParent NVARCHAR(100) OUTPUT
, DepthSpace NVARCHAR(50) OUTPUT
, Depth INTEGER OUTPUT
, ParentID INTEGER OUTPUT
)
BEGIN
DECLARE
AID INTEGER
, depthid INTEGER
, tempdepth INTEGER
, depthspaceid NVARCHAR(50)
;
AID = (SELECT AreaID FROM __input);
depthid = 1;
depthspaceid = '';
INSERT INTO
__output
SELECT TOP 50
A.AreaID
, A.Name
, A.Code
, (SELECT Name + '->' + A.Name FROM Areas WHERE AreaID = A.ParentID)
, depthspaceid
, depthid
, AID
FROM
Areas A
WHERE
A.ParentID = AID
ORDER BY
A.AreaID ASC
;
IF (SELECT COUNT(AreaOutID) FROM __output) > 0 THEN
SELECT TOP 1
AID = AreaOutID
, depthid = Depth
FROM
__output
WHERE
ParentID = AID
ORDER BY
AreaOutID ASC
;
WHILE depthid > 0 DO
WHILE AID > 0 DO
INSERT INTO
__output
SELECT
AreaID
, Name
, Code
, Name
, (SELECT CASE WHEN WithParent IS NULL THEN '' ELSE WithParent + '->' + Name END FROM __output WHERE AreaOutID=AID)
, depthspaceid
, depthid + 1
, AID
FROM
Areas
WHERE
ParentID = AID
;
AID = ISNULL((SELECT TOP 1 AreaOutID FROM __output WHERE Depth=depthid and AreaOutID > AID ORDER BY AreaOutID ASC),0);
END WHILE;
tempdepth = depthid;
AID = 0;
depthid = 0;
SELECT TOP 1
depthid = Depth
, AID = AreaOutID
FROM
__output
WHERE
depthid > tempdepth
ORDER BY
depthid ASC
, AreaOutID ASC
;
WND WHILE;
END IF;
END;
I'm getting an error about a subquery returning more than one row.
What's causing it and how to fix it?
If you use a subquery as an expression you have to make sure that it doesn't return more than one row.
This can be done in the subquery by:
SELECT TOP 1
SELECT DISTINCT (Won't work for all cases)
GROUP BY (Won't work for all cases)
SELECT COUNT(*)
Careful constructed WHERE conditions and Constraints on the database
You can also turn the subquery into a JOIN when it is used as part of another query, but this will lead to more rows in the main query if not done correctly.
In other databases there are also EXISTS and NOT EXISTS or ANY clauses to solve this problem.
Without seeing the data and seeing that all other subqueries have TOP 1 included, I think that the following subquery must be returning >1 row:
(SELECT CASE WHEN WithParent IS NULL THEN '' ELSE WithParent + '->' + Name END FROM __output WHERE AreaOutID=AID)

IF ELSE condition in SQL select

I want to do a if-else condition statement in SQL Server but am not sure how.
Inside the stored procedure I have the following parameters:
#MarketId nvarchar (10),
#RegionId nvarchar (10)
And the following statement:
select * from Interaction I
where
(#MarketId = 0 ) OR (I.MarketId = (SELECT Id FROM Market WHERE ExternalId = #MarketId))
What I want to do is to check the value of #MarketId
if #MarketId = 0
then I want the where condition for I.MarketId to get its Ids from elsewhere like
(SELECT ID FROM Market WHERE ExternalId = #RegionId)
otherwise, if its 1, then I just want to leave it as is and get the Id from #MarketId instead of #RegionId..
How should I go about this?
Thanks!
This should work:
SELECT *
FROM Interaction I
WHERE ( #MarketID = 0
AND EXISTS (SELECT 1 FROM Market
WHERE ExternalId = #RegionId AND Id = I.MarketID)
OR I.MarketID = #MarketID

Trying to merge rows into one row with certain conditions

Given 2 or more rows that are selected to merge, one of them is identified as being the template row. The other rows should merge their data into any null value columns that the template has.
Example data:
Id Name Address City State Active Email Date
1 Acme1 NULL NULL NULL NULL blah#yada.com 3/1/2011
2 Acme1 1234 Abc Rd Springfield OR 0 blah#gmail.com 1/12/2012
3 Acme2 NULL NULL NULL 1 blah#yahoo.com 4/19/2012
Say that a user has chosen row with Id 1 as the template row, and rows with Ids 2 and 3 are to be merged into row 1 and then deleted. Any null value columns in row Id 1 should be filled with (if one exists) the most recent (see Date column) non-null value, and non-null values already present in row Id 1 are to be left as is. The result of this query on the above data should be exactly this:
Id Name Address City State Active Email Date
1 Acme1 1234 Abc Road Springfield OR 1 blah#yada.com 3/1/2011
Notice that the Active value is 1, and not 0 because row Id 3 had the most recent date.
P.S. Also, is there any way possible to do this without explicitly defining/knowing beforehand what all the column names are? The actual table I'm working with has a ton of columns, with new ones being added all the time. Is there a way to look up all the column names in the table, and then use that subquery or temptable to do the job?
You might do it by ordering rows first by template flag, then by date desc. Template row should always be the last one. Each row is assigned a number in that order. Using max() we are finding fist occupied cell (in descending order of numbers). Then we select columns from rows matching those maximums.
; with rows as (
select test.*,
-- Template row must be last - how do you decide which one is template row?
-- In this case template row is the one with id = 1
row_number() over (order by case when id = 1 then 1 else 0 end,
date) rn
from test
-- Your list of rows to merge goes here
-- where id in ( ... )
),
-- Finding first occupied row per column
positions as (
select
max (case when Name is not null then rn else 0 end) NamePosition,
max (case when Address is not null then rn else 0 end) AddressPosition,
max (case when City is not null then rn else 0 end) CityPosition,
max (case when State is not null then rn else 0 end) StatePosition,
max (case when Active is not null then rn else 0 end) ActivePosition,
max (case when Email is not null then rn else 0 end) EmailPosition,
max (case when Date is not null then rn else 0 end) DatePosition
from rows
)
-- Finally join this columns in one row
select
(select Name from rows cross join Positions where rn = NamePosition) name,
(select Address from rows cross join Positions where rn = AddressPosition) Address,
(select City from rows cross join Positions where rn = CityPosition) City,
(select State from rows cross join Positions where rn = StatePosition) State,
(select Active from rows cross join Positions where rn = ActivePosition) Active,
(select Email from rows cross join Positions where rn = EmailPosition) Email,
(select Date from rows cross join Positions where rn = DatePosition) Date
from test
-- Any id will suffice, or even DISTINCT
where id = 1
You might check it at Sql Fiddle.
EDIT:
Cross joins in last section might actually be inner joins on rows.rn = xxxPosition. It works this way, but change to inner join would be an improvement.
It's not so complicated.
At first..
DECLARE #templateID INT = 1
..so you can remember which row is treated as template..
Now find latest NOT NULL values (exclude template row). The easiest way is to use TOP 1 subqueries for each column:
SELECT
(SELECT TOP 1 Name FROM DataTab WHERE Name IS NOT NULL AND NOT ID = #templateID ORDER BY Date DESC) AS LatestName,
(SELECT TOP 1 Address FROM DataTab WHERE Address IS NOT NULL AND NOT ID = #templateID ORDER BY Date DESC) AS AddressName
-- add more columns here
Wrap above into CTE (Common Table Expression) so you have nice input for your UDPATE..
WITH Latest_CTE (CTE_LatestName, CTE_AddressName) -- add more columns here; I like CTE prefix to distinguish source columns from target columns..
AS
-- Define the CTE query.
(
SELECT
(SELECT TOP 1 Name FROM DataTab WHERE Name IS NOT NULL AND NOT ID = #templateID ORDER BY Date DESC) AS LatestName,
(SELECT TOP 1 Address FROM DataTab WHERE Address IS NOT NULL AND NOT ID = #templateID ORDER BY Date DESC) AS AddressName
-- add more columns here
)
UPDATE
<update statement here (below)>
Now, do smart UPDATE of your template row using ISNULL - it will act as conditional update - update only if target column is null
WITH
<common expression statement here (above)>
UPDATE DataTab
SET
Name = ISNULL(Name, CTE_LatestName), -- if Name is null then set Name to CTE_LatestName else keep Name as Name
Address = ISNULL(Address, CTE_LatestAddress)
-- add more columns here..
WHERE ID = #templateID
And the last task is delete rows other then template row..
DELETE FROM DataTab WHERE NOT ID = #templateID
Clear?
For dynamic columns, you need to write a solution using dynamic SQL.
You can query sys.columns and sys.tables to get the list of columns you need, then you want to loop backwards once for each null column finding the first non-null row for that column and updating your output row for that column. Once you get to 0 in the loop you have a complete row which you can then display to the user.
I should pay attention to posting dates. In any case, here's a solution using dynamic SQL to build out an update statement. It should give you something to build from, anyway.
There's some extra code in there to validate the results along the way, but I tried to comment in a way that made that non-vital code apparent.
CREATE TABLE
dbo.Dummy
(
[ID] int ,
[Name] varchar(30),
[Address] varchar(40) null,
[City] varchar(30) NULL,
[State] varchar(2) NULL,
[Active] tinyint NULL,
[Email] varchar(30) NULL,
[Date] date NULL
);
--
INSERT dbo.Dummy
VALUES
(
1, 'Acme1', NULL, NULL, NULL, NULL, 'blah#yada.com', '3/1/2011'
)
,
(
2, 'Acme1', '1234 Abc Rd', 'Springfield', 'OR', 0, 'blah#gmail.com', '1/12/2012'
)
,
(
3, 'Acme2', NULL, NULL, NULL, 1, 'blah#yahoo.com', '4/19/2012'
);
DECLARE
#TableName nvarchar(128) = 'Dummy',
#TemplateID int = 1,
#SetStmtList nvarchar(max) = '',
#LoopCounter int = 0,
#ColumnCount int = 0,
#SQL nvarchar(max) = ''
;
--
--Create a table to hold the column names
DECLARE
#ColumnList table
(
ColumnID tinyint IDENTITY,
ColumnName nvarchar(128)
);
--
--Get the column names
INSERT #ColumnList
(
ColumnName
)
SELECT
c.name
FROM
sys.columns AS c
JOIN
sys.tables AS t
ON
t.object_id = c.object_id
WHERE
t.name = #TableName;
--
--Create loop boundaries to build out the SQL statement
SELECT
#ColumnCount = MAX( l.ColumnID ),
#LoopCounter = MIN (l.ColumnID )
FROM
#ColumnList AS l;
--
--Loop over the column names
WHILE #LoopCounter <= #ColumnCount
BEGIN
--Dynamically construct SET statements for each column except ID (See the WHERE clause)
SELECT
#SetStmtList = #SetStmtList + ',' + l.ColumnName + ' =COALESCE(' + l.ColumnName + ', (SELECT TOP 1 ' + l.ColumnName + ' FROM ' + #TableName + ' WHERE ' + l.ColumnName + ' IS NOT NULL AND ID <> ' + CAST(#TemplateID AS NVARCHAR(MAX )) + ' ORDER BY Date DESC)) '
FROM
#ColumnList AS l
WHERE
l.ColumnID = #LoopCounter
AND
l.ColumnName <> 'ID';
--
SELECT
#LoopCounter = #LoopCounter + 1;
--
END;
--TESTING - Validate the initial table values
SELECT * FROM dbo.Dummy ;
--
--Get rid of the leading common in the SetStmtList
SET #SetStmtList = SUBSTRING( #SetStmtList, 2, LEN( #SetStmtList ) - 1 );
--Build out the rest of the UPDATE statement
SET #SQL = 'UPDATE ' + #TableName + ' SET ' + #SetStmtList + ' WHERE ID = ' + CAST(#TemplateID AS NVARCHAR(MAX ))
--Then execute the update
EXEC sys.sp_executesql
#SQL;
--
--TESTING - Validate the updated table values
SELECT * FROM dbo.Dummy ;
--
--Build out the DELETE statement
SET #SQL = 'DELETE FROM ' + #TableName + ' WHERE ID <> ' + CAST(#TemplateID AS NVARCHAR(MAX ))
--Execute the DELETE
EXEC sys.sp_executesql
#SQL;
--
--TESTING - Validate the final table values
SELECT * FROM dbo.Dummy;
--
DROP TABLE dbo.Dummy;