Showing NULL values in WHERE clause - sql

I've put together the below example to explain what I'm trying to do.
I'm trying to within the query when #FeatureID is NULL then return all the records in the#Temp table for that ClientID. If a #FeatureID contains a 1 for example then returning the 1 record and the NULL record.
Where have I gone wrong in my where clause?
CREATE TABLE #Temp
(
ClientID int,
FeatureID int
)
Insert into #Temp
(
ClientID,
FeatureID
)
SELECT
1,
1
UNION
SELECT
1,
2
UNION
SELECT
1,
3
UNION
SELECT
1,
NULL
Declare #ClientID int = 1
Declare #FeatureID int = NULL
--should return all 4 records
select * from #Temp
where ClientID = 1 and
FeatureID = IIF(#FeatureID IS NULL, FeatureID, #FeatureID)
Set #ClientID = 1
Set #FeatureID = 1
--should return the 1,1 record and the 1,NULL record
select * from #Temp
where ClientID = 1 and
FeatureID = IIF(#FeatureID IS NULL, FeatureID, #FeatureID)
drop table #Temp

An alternative formulation that might be a little simpler:
select * from #Temp
where ClientID = 1 and
(ISNULL(#FeatureID, FeatureID) = FeatureID or FeatureID is NULL)
If #FeatureID is null, then FeatureID = FeatureID, which is always true. Otherwise, it will check for #FeatureID = FeatureID.
It will always return the rows where FeatureID is null.

You're effectively trying to compare NULL = NULL in your where clause which doesn't work. NULL does not compare equal to another NULL.
For your first query what you need to do is only compare the feature id column when #FeatureID is not null. This can be accomplished by testing the variable and using an OR condition.
--should return all 4 records
select * from #Temp
where ClientID = 1 and
(#FeatureID IS NULL OR FeatureID = #FeatureID)
In the second query you need to compare the feature ID column to both #FeatureID and NULL to get both rows.
--should return the 1,1 record and the 1,NULL record
select * from #Temp
where ClientID = 1 and
(#FeatureID IS NOT NULL AND (FeatureID IS NULL OR FeatureID=#FeatureID))
To handle both cases in a single query, use two conditions joined by OR that branched based on whether the variable is null or not.
select * from #Temp
where ClientID = 1 and
(
#FeatureID IS NULL
OR (#FeatureID IS NOT NULL AND (FeatureID IS NULL OR FeatureID=#FeatureID))
)

If you want to combine them then this should work:
select * from #Temp
where ClientID = 1 and
(#FeatureID is null
or
(#FeatureID is not null
and (FeatureID is null or FeatureID=#FeatureID)))
This will select all the records when #FeatureID = null and return 2 results {(1, null), (1, 1)} when #FeatureID = 1

Related

SQL Return default values if no rows returned

I've got a SQL script which returns a result set, the requirement is to return a default result set with default values if the below script does not yield any results. It should have the same column names as the below script
-- Return final results
SELECT
p.worked [AccountsWorked],
p.rcmade [RPC's],
p.obtained [PTPCount],
p.amount [PTPValue],
[PreviousDayPTPValue]
FROM
#tab_performance p JOIN
dbo.user usr ON
(p.usr_code = usr.usr_code) JOIN
dbo.team tme ON
(tme.tme_id = usr.tme_id)
AND p.usr_code = #usr_code
I need to return a default result set if no rows are returned. So all the columns should be returned with NULL's or any default value.
I have tried conditional select statements without any luck, I have also tried the ##ROWCOUNT
You can add a union all select to your existing query with default values like this:
<your existing query>
union all
select null accounts_worked, null right_contacts_made, null ppts_obtained .....
where not exists(
select *
from #tab_performance p JOIN
dbo.TR_USR_User usr ON
(p.usr_code = usr.usr_code) JOIN
dbo.TR_TME_Team tme ON
(tme.tme_id = usr.tme_id)
AND p.usr_code = #usr_code
)
The where clause could be further simplified, if your inner joins don't filter out any rows from #tab_performance:
<your existing query>
union all
select null accounts_worked, null right_contacts_made, null ppts_obtained .....
where not exists(
select *
from #tab_performance
where usr_code = #usr_code
)
I would do it with WITH and UNION ALL
Drop table if exists #test
create table #test (
Column1 int null,
Column2 varchar(50) null
);
--INSERT INTO [#test] (Column1,Column2)
--VALUES
-- (1, 'test'),
-- (2,'test2'),
-- (3,'test3');
WITH qry AS (
select Column1, Column2 from #test
)
select * from qry
UNION ALL
select NULL as Colum1, null as Column2 where (select COUNT(*) from qry) = 0

SQL - "NOT IN" in WHERE clause using INNER JOIN not working

I need to filter a table based in a sub table data.
I'll exemplify with a hypnotic data to be easier to explain:
Master table: Cars
Sub table: Attributes (like Color, car type, accessories)
These attributes have an id (idOption) and the selected value (idList)
So, in an example, I need to filter all the cars with the color (idOption = 10) yellow (idList = 45). I can't filter this directly because the search need to consider the other option's results (which include the types, accessories.
When I use NOT IN for just one table, it works. But when I use merging the 2 tables with INNER JOIN, it does not work.
So in summary, I need to filter the 3 idOption (when is not NULL) with a given value, and this needs to reflect in the main table, grouped by product.
Table Cars:
idProduct | Description
1 Product A
2 Product B
3 Product C
Table Attributes:
idRow idProduct idOption idList
---------------------------------------
1 1 10 45
2 2 10 46
3 3 10 47
4 1 11 10
5 2 11 98
6 1 14 56
7 3 16 28
8 2 20 55
This is the stored procedure that I created which is not working:
ALTER PROCEDURE [dbo].[SP_GET_TestSearch]
(#Param1 BIGINT = NULL,
#PValue1 BIGINT = NULL,
#Param2 BIGINT = NULL,
#PValue2 BIGINT = NULL,
#Param3 BIGINT = NULL,
#PValue3 BIGINT = NULL)
AS
SET NOCOUNT ON;
SELECT
Cars.idProduct,
Cars.[Description]
FROM
Cars
INNER JOIN
Attributes ON Cars.idProduct = Attributes.idProduct
WHERE
((#Param1 IS NULL OR (idOption NOT IN (#Param1)))
AND
(#Param2 IS NULL OR (idOption NOT IN (#Param2)))
AND
(#Param3 IS NULL OR (idOption NOT IN (#Param3))))
OR
(idOption = ISNULL(#Param1, NULL)
AND idList = ISNULL(#PValue1, NULL))
OR
(idOption = ISNULL(#Param2, NULL)
AND idList = ISNULL(#PValue2, NULL))
OR
(idOption = ISNULL(#Param3, NULL)
AND idList = ISNULL(#PValue3, NULL))
GROUP BY
Cars.idProduct, Cars.[Description]
The following code demonstrates how to implement the logic of excluding vehicles from query results if they have any "bad" property values. The rejection is handled by ... where not exists ... which is used to check each car against the "bad" property values.
Rather than using an assortment of (hopefully) paired parameters to pass the undesirable properties, the values are passed in a table. The stored procedure to implement this ought to use a table-valued parameter (TVP) to pass the table.
-- Sample data.
declare #Cars as Table ( CarId Int Identity, Description VarChar(16) );
insert into #Cars ( Description ) values
( 'Esplanade' ), ( 'Tankigator' ), ( 'Land Yacht' );
select * from #Cars;
declare #Properties as Table ( PropertyId Int Identity, Description VarChar(16) );
insert into #Properties ( Description ) values
( 'Turbochargers' ), ( 'Superchargers' ), ( 'Hyperchargers' ), ( 'Color' ), ( 'Spare Tires' );
select * from #Properties;
declare #CarProperties as Table ( CarId Int, PropertyId Int, PropertyValue Int );
insert into #CarProperties ( CarId, PropertyId, PropertyValue ) values
( 1, 1, 1 ), ( 1, 4, 24 ), ( 1, 4, 42 ), -- Two tone!
( 2, 2, 1 ), ( 2, 4, 7 ),
( 3, 1, 2 ), ( 3, 4, 0 ), ( 3, 5, 6 );
select C.CarId, C.Description as CarDescription,
P.PropertyId, P.Description as PropertyDescription,
CP.PropertyValue
from #Cars as C inner join
#CarProperties as CP on CP.CarId = C.CarId inner join
#Properties as P on P.PropertyId = CP.PropertyId
order by C.CarId, P.PropertyId;
-- Test data: Avoid vehicles that have _any_ of these property values.
-- This should be passed to the stored procedure as a table-value parameter (TVP).
declare #BadProperties as Table ( PropertyId Int, PropertyValue Int );
insert into #BadProperties ( PropertyId, PropertyValue ) values
( 2, 1 ), ( 2, 2 ), ( 2, 4 ),
( 4, 62 ), ( 4, 666 );
select BP.PropertyId, BP.PropertyValue, P.Description
from #BadProperties as BP inner join
#Properties as P on P.PropertyId = BP.PropertyId;
-- Query the data.
select C.CarId, C.Description as CarDescription
from #Cars as C
where not exists (
select 42
from #CarProperties as CP inner join
#BadProperties as BP on BP.PropertyId = CP.PropertyId and BP.PropertyValue = CP.PropertyValue
where CP.CarId = C.CarId )
order by C.CarId;
A few things here.
Firstly, this kind of catch all procedure is a bit of an anti pattern for all sorts of reasons, see here for a full explanation:- https://sqlinthewild.co.za/index.php/2018/03/13/revisiting-catch-all-queries/
Secondly, you need to be very careful of using NOT IN with nullable values in a list: http://www.sqlbadpractices.com/using-not-in-operator-with-null-values/
I've added the DDL for the tables:-
IF OBJECT_ID('Attributes') IS NOT NULL
DROP TABLE Attributes;
IF OBJECT_ID('Cars') IS NOT NULL
DROP TABLE Cars;
IF OBJECT_ID('SP_GET_TestSearch') IS NOT NULL
DROP PROCEDURE SP_GET_TestSearch
CREATE TABLE Cars
(idProduct INT PRIMARY KEY
, Description VARCHAR(20) NOT NULL);
CREATE TABLE Attributes
(idRow INT PRIMARY KEY
, idProduct INT NOT NULL FOREIGN KEY REFERENCES dbo.Cars(idProduct)
, idOption INT NOT NULL
, idList INT NOT NULL);
INSERT INTO dbo.Cars
VALUES
(1, 'Product A')
,(2 , 'Product B')
,(3, 'Product C');
INSERT INTO dbo.Attributes
(
idRow,
idProduct,
idOption,
idList
)
VALUES (1,1,10,45)
,(2,2,10,46)
,(3,3,10,47)
,(4,1,11,10)
,(5,2,11,98)
,(6,1,14,56)
,(7,3,16,28)
,(8,2,20,55);
GO
The issue with your query, is that the first part of the block is always evaluated to TRUE for any idOption that you don't specify:-
((#Param1 IS NULL OR (idOption NOT IN (#Param1)))
AND
(#Param2 IS NULL OR (idOption NOT IN (#Param2)))
AND
(#Param3 IS NULL OR (idOption NOT IN (#Param3))))
To explain; if I pass in the following:-
DECLARE #Param1 BIGINT
, #Param2 BIGINT
, #Param3 BIGINT
, #PValue1 BIGINT
, #PValue2 BIGINT
, #PValue3 BIGINT;
SET #Param1 = 11
SET #Pvalue1 = 42
SET #Param2 = 11
SET #Pvalue2 = 10
SET #Param3 = 14
SET #PValue3= 56
EXEC dbo.SP_GET_TestSearch #Param1, #PValue1, #Param2, #PValue2, #Param3, #PValue3
Then you effectively have WHERE idOption NOT IN (11,14) as the evaluation for the first part of the clause, so all other rows are returned.
I suspect you really want the WHERE clause to be:-
WHERE
(#Param1 IS NULL AND #Param2 IS NULL AND #Param3 IS NULL)
OR
(idOption = #Param1
AND idList = #PValue1)
OR
(idOption = #Param2
AND idList = #PValue2)
OR
(idOption = #Param3
AND idList = #PValue3)

SQL: Upsert and get the old and the new values

I have the following table Items:
Id MemberId MemberGuid ExpiryYear Hash
---------------------------------------------------------------------------
1 1 Guid1 2017 Hash1
2 1 Guid2 2018 Hash2
3 2 Guid3 2020 Hash3
4 2 Guid4 2017 Hash1
I need to copy the items from a member to another (not just to update MemberId, to insert a new record). The rule is: if I want to migrate all the items from a member to another, I will have to check that that item does not exists in the new member.
For example, if I want to move the items from member 1 to member 2, I will move only item with id 2, because I already have an item at member 2 with the same hash and with the same expiry year (this are the columns that I need to check before inserting the new items).
How to write a query that migrates only the non-existing items from a member to another and get the old id and the new id of the records? Somehow with an upsert?
You can as the below:
-- MOCK DATA
DECLARE #Tbl TABLE
(
Id INT IDENTITY NOT NULL PRIMARY KEY,
MemberId INT,
MemberGuid CHAR(5),
ExpiryYear CHAR(4),
Hash CHAR(5)
)
INSERT INTO #Tbl
VALUES
(1, 'Guid1', '2017', 'Hash1'),
(1, 'Guid2', '2018', 'Hash1'),
(2, 'Guid3', '2020', 'Hash3'),
(2, 'Guid4', '2017', 'Hash1')
-- MOCK DATA
-- Parameters
DECLARE #FromParam INT = 1
DECLARE #ToParam INT = 2
DECLARE #TmpTable TABLE (NewDataId INT, OldDataId INT)
MERGE #Tbl AS T
USING
(
SELECT * FROM #Tbl
WHERE MemberId = #FromParam
) AS F
ON T.Hash = F.Hash AND
T.ExpiryYear = F.ExpiryYear AND
T.MemberId = #ToParam
WHEN NOT MATCHED THEN
INSERT ( MemberId, MemberGuid, ExpiryYear, Hash)
VALUES ( #ToParam, F.MemberGuid, F.ExpiryYear, F.Hash)
OUTPUT inserted.Id, F.Id INTO #TmpTable;
SELECT * FROM #TmpTable
Step 1:
Get in cursor all the data of member 1
Step 2:
While moving through cursor.
Begin
select hash, expirydate from items where memberid=2 and hash=member1.hash and expirydate=member1.expirydate
Step 3
If above brings any result, do not insert.
else insert.
Hope this helps
Note: this is not actual code. I am providing you just steps based on which you can write sql.
Actually you just need an insert. When ExpiryYear and Hash matched you don't wanna do anything. You just wanna insert from source to target where those columns doesn't match. You can do that with Merge or Insert.
CREATE TABLE YourTable
(
Oldid INT,
OldMemberId INT,
Id INT,
MemberId INT,
MemberGuid CHAR(5),
ExpiryYear CHAR(4),
Hash CHAR(5)
)
INSERT INTO YourTable VALUES
(null, null, 1, 1, 'Guid1', '2017', 'Hash1'),
(null, null, 2, 1, 'Guid2', '2018', 'Hash2'),
(null, null, 3, 2, 'Guid3', '2020', 'Hash3'),
(null, null, 4, 2, 'Guid4', '2017', 'Hash1')
DECLARE #SourceMemberID AS INT = 1
DECLARE #TargetMemberID AS INT = 2
MERGE [YourTable] AS t
USING
(
SELECT * FROM [YourTable]
WHERE MemberId = #SourceMemberID
) AS s
ON t.Hash = s.Hash AND t.ExpiryYear = s.ExpiryYear AND t.MemberId = #TargetMemberID
WHEN NOT MATCHED THEN
INSERT(Oldid, OldMemberId, Id, MemberId, MemberGuid, ExpiryYear, Hash) VALUES (s.Id, s.MemberId, (SELECT MAX(Id) + 1 FROM [YourTable]), #TargetMemberID, s.MemberGuid, s.ExpiryYear, s.Hash);
SELECT * FROM YourTable
DROP TABLE YourTable
/* Output:
Oldid OldMemberId Id MemberId MemberGuid ExpiryYear Hash
-----------------------------------------------------------------
NULL NULL 1 1 Guid1 2017 Hash1
NULL NULL 2 1 Guid2 2018 Hash2
NULL NULL 3 2 Guid3 2020 Hash3
NULL NULL 4 2 Guid4 2017 Hash1
2 1 5 2 Guid2 2018 Hash2
If you just want to select then do as following
SELECT null AS OldID, null AS OldMemberID, Id, MemberId, MemberGuid, ExpiryYear, Hash FROM YourTable
UNION ALL
SELECT A.Id AS OldID, A.MemberId AS OldMemberID, (SELECT MAX(Id) + 1 FROM YourTable) AS Id, #TargetMemberID AS MemberId, A.MemberGuid, A.ExpiryYear, A.Hash
FROM YourTable A
LEFT JOIN
(
SELECT * FROM YourTable WHERE MemberId = #TargetMemberID
) B ON A.ExpiryYear = B.ExpiryYear AND A.Hash = B.Hash
WHERE A.MemberId = #SourceMemberID AND B.Id IS NULL

How to return only numbers from query where column is nvarchar

I have a simple query that is returning records where "column2" > 0
Here is the data in the database
Column1 Column2
1 123456789
2 123456781
3 13-151-1513
4 alsdjf
5
6 000000000
Her is the query
select column1, replace(a.Payroll_id,'-','')
from table1
where isnumeric(column2) = 1
I'd like to return the following:
Column1 Column2
1 123456789
2 123456781
3 131511513
This mean, I won't select any records when the column is blank (or null), will not return a row if it's not an integer, and will drop out the '-', and would not show row 6 since it's all 0.
How can I do this?
I think you can use something like this :
USE tempdb
GO
CREATE TABLE #Temp
(
ID INT IDENTITY
,VALUE VARCHAR(30)
)
INSERT INTO #Temp (VALUE) VALUES ('1213213'), ('1213213'), ('121-32-13'), ('ASDFASF2123')
GO
WITH CteData
AS
(
SELECT REPLACE(VALUE,'-','') as Valor FROM #Temp
)
SELECT * FROM CteData WHERE (ISNUMERIC(Valor) = 1 AND valor not like '%[0-0]%')
DROP TABLE #Temp
then you can apply validations for empty, NULL,0 etc
If you are using SQL2012 or above you can also use TRY_PARSE that is more selective in its parsing. This function will return NULL if a record can't be converted. You could use it like this:
CREATE TABLE #temp
(
ID INT IDENTITY ,
VALUE VARCHAR(30)
)
INSERT INTO #temp
( VALUE )
VALUES ( '1213213' ),
( '1213213' ),
( '121-32-13' ),
( 'ASDFASF2123' ),
( '0000000' )
SELECT ParsedValue
FROM #temp
CROSS APPLY ( SELECT TRY_PARSE(
Value AS INT ) AS ParsedValue
) details
WHERE ParsedValue IS NOT NULL
AND ParsedValue>0

SQL Many to Many relationship

I'm having difficulties writing a SQL query. This is the structure of 3 tables, table Race_ClassificationType is many-to-many table.
Table Race
----------------------------
RaceID
Name
Table Race_ClassificationType
----------------------------
Race_ClassificationTypeID
RaceID
RaceClassificationID
Table RaceClassificationType
----------------------------
RaceClassificationTypeID
Name
What I'm trying to do is get the races with certain classifications. The results are returned by a store procedure that has a table-value parameter which holds the desired classifications:
CREATE TYPE [dbo].[RaceClassificationTypeTable]
AS TABLE
(
RaceClassificationTypeID INT NULL
);
GO
CREATE PROCEDURE USP_GetRaceList
(#RaceClassificationTypeTable AS [RaceClassificationTypeTable] READONLY,
#RaceTypeID INT = NULL,
#IsCompleted BIT = NULL,
#MinDateTime DATETIME = NULL,
#MaxDateTime DATETIME = NULL,
#MaxRaces INT = NULL)
WITH RECOMPILE
AS
BEGIN
SET NOCOUNT ON;
SELECT DISTINCT
R.[RaceID]
,R.[RaceTypeID]
,R.[Name]
,R.[Abbreviation]
,R.[DateTime]
,R.[IsCompleted]
FROM [Race] R,[Race_ClassificationType] R_CT, [RaceClassificationType] RCT
WHERE (R.[RaceTypeID] = #RaceTypeID OR #RaceTypeID IS NULL)
AND (R.[IsCompleted] = #IsCompleted OR #IsCompleted IS NULL)
AND (R.[DateTime] >= #MinDateTime OR #MinDateTime IS NULL)
AND (R.[DateTime] <= #MaxDateTime OR #MaxDateTime IS NULL)
AND (R.RaceID = R_CT.RaceID)
AND (R_CT.RaceClassificationTypeID = RCT.RaceClassificationTypeID)
AND (RCT.RaceClassificationTypeID IN (SELECT DISTINCT T.RaceClassificationTypeID FROM #RaceClassificationTypeTable T))
ORDER BY [DateTime] DESC
OFFSET 0 ROWS FETCH NEXT #MaxRaces ROWS ONLY
END
GO
As it is this stored procedure doesnt work correctly because it returns all races that have at least one classification type ID in the table-value parameter of classification type IDs (because of the IN clause). I want that the store procedure returns only races that have all the classifications supplied in the table-valued parameter.
Example:
RaceClassificationTypeID RaceID
3 92728
3 92729
8 92729
29 92729
12 92729
2 92729
3 92730
8 92730
8 92731
1 92731
RaceClassificationTypeIDs in RaceClassificationTypeTable parameter: 3 and 8
OUTPUT: all the races with RaceClassificationID 3 and 8 and optionally any other (2, 29, 12)
That means only races 92729 and 92730 should be returned, as it is all the races in the example are returned.
I've set up two tables, one stores your result set and the other represents the values in the table valued parameter of your stored procedure. See below.
CREATE TABLE ABC
(
RCTID INT,
RID INT
)
INSERT INTO ABC VALUES (3,92728)
INSERT INTO ABC VALUES (3,92729)
INSERT INTO ABC VALUES (8,92729)
INSERT INTO ABC VALUES (29,92729)
INSERT INTO ABC VALUES (12,92729)
INSERT INTO ABC VALUES (2,92729)
INSERT INTO ABC VALUES (3,92730)
INSERT INTO ABC VALUES (8,92730)
INSERT INTO ABC VALUES (8,92731)
INSERT INTO ABC VALUES (1,92731)
GO
CREATE TABLE TABLEVALUEPARAMETER
(
VID INT
)
INSERT INTO TABLEVALUEPARAMETER VALUES (3)
INSERT INTO TABLEVALUEPARAMETER VALUES (8)
GO
SELECT RID FROM ABC WHERE RCTID IN (SELECT VID FROM TABLEVALUEPARAMETER) GROUP BY
RID HAVING COUNT(RID) = (SELECT COUNT(VID) FROM TABLEVALUEPARAMETER)
GO
If you run this on your machine you'll notice it produces the two IDs that you're after.
Because you have a stored procedure with a lot of columns selected it would be necessary to use a CTE (Common Table Expression). This is because if you were to try to group all the columns in the current select statement you would have to group by all the columns and you would then get duplication.
If the first CTE delivers the result set and then you uses a version of the select above you should be able to produce only the IDs you want.
If you don't know CTE's let me know!
This is an example of a "set-within-sets" subquery. One way to solve this is with aggregation and a having clause. Here is how you get the RaceIds:
select RaceID
from RaceClassification rc
group by RaceID
having sum(case when RaceClassificationTypeId = 3 then 1 else 0 end) > 0 and
sum(case when RaceClassificationTypeId = 8 then 1 else 0 end) > 0;
Each condition in the having clause is counts how many rows have each type. Only races with each (because of the > 0) are kept.
You can get all the race information by using this as a subquery:
select r.*
from Races r join
(select RaceID
from RaceClassification rc
group by RaceID
having sum(case when RaceClassificationTypeId = 3 then 1 else 0 end) > 0 and
sum(case when RaceClassificationTypeId = 8 then 1 else 0 end) > 0
) rc
on r.RaceID = rc.RaceId;
Your stored procedure seems to have other conditions. These can also be added in.