Subquery in ISNULL,IIF,CASE statements - sql

Subquery that is give as a parameter in ISNULL or IIF or CASE gets executed irrespective of the condition
To explain what I mean, consider the below example. When I run the below query and look in the execution plan, I find that even if the variable #Id not NULL, the second condition gets executed always.
Can anyone explain Why does the subquery gets executed in Query 1,2,3?
--Create a Temp table
IF OBJECT_ID('tempdb..#TempTable') IS NOT NULL DROP TABLE #TempTable
CREATE TABLE #TempTable
(
ID INT
)
--Insert some data
INSERT INTO #TempTable VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)
DECLARE #Id INT = 1
--Query 1: ISNULL
SET #Id= ISNULL(#Id, (SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1))
SELECT #Id AS ID
--Query 2: IIF
SET #Id= IIF(#Id IS NULL,(SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1),#Id)
SET #Id= IIF(#Id IS NOT NULL,#Id,(SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1))
SELECT #Id AS ID
--Query 3: CASE
SET #Id= CASE WHEN #Id IS NULL THEN (SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1) ELSE #Id END
SET #Id= CASE WHEN #Id IS NOT NULL THEN #Id ELSE (SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1) END
SELECT #Id AS ID
---Query 4: IF
IF #Id IS NULL
BEGIN
SET #Id = (SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1)
SELECT #Id AS ID
END
ELSE
BEGIN
SELECT #Id AS ID
END

If you want the subquery to execute only when the null condition is met, use an IF instead of ISNULL
if #Id is null
set #id = (SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1)
instead of
SET #Id= ISNULL(#Id, (SELECT TOP 1 ID FROM #TempTable TT WHERE TT.ID = 1))

Related

Select data where condition matches and if none, then select all?

select * from (values
('dept1','user1'),
('dept2','user2'),
('dept3','user3'),
('dept4','user4')
)table1([department],[user])
where [user] = #id
scenario1:
#id = 'user1'
dept1
scenario2:
#id = 'user5'
dept1
dept2
dept3
dept4
this is what it looks like from a noobish query
declare #id varchar(12) = 'user1'
declare #var int = (select count(*) from table1 where [user] = #id)
select * from table1 where [user] = #id or #var = 0
DECLARE #id VARCHAR(5) = 'user1';
--DECLARE #id VARCHAR(5) = 'user5';
WITH UsersAndDepartments
AS ( SELECT *
FROM ( VALUES ( 'dept1', 'user1'), ( 'dept2', 'user2'),
( 'dept3', 'user3'), ( 'dept4', 'user4') ) x ( [department], [user] )
)
SELECT *
FROM UsersAndDepartments ud1
WHERE ud1.[user] =
CASE
WHEN EXISTS ( SELECT 1 FROM UsersAndDepartments ud2 WHERE ud2.[user] = #id ) THEN #id
ELSE ud1.[user]
END
The above just checks on user column if any row exists for an id, else matches on all.
declare #tab table (id int , value varchar(10))
declare #id int = 4
insert into #tab
select 1,'Ajay'
union all
select 2,'Ajay1'
union all
select 3,'Ajay2'
union all
select 4,'Ajay3'
union all
select 5,'Ajay4'
select * from #tab
where id = case when exists (select * from #tab where id = #id) then #id else id end
I would do this with a simple OR, not a CASE expression in the WHERE.
In general, you want to avoid CASE expressions in the WHERE clause for several reasons:
The logic can almost be written concisely using basic boolean operations.
Adding additional constructs (in addition to AND, OR, and NOT) just makes the logic harder for people to follow.
It pretty much kills any optimization paths.
I would suggest:
with table1 as
select v.*
from (values ('dept1', 'user1'),
('dept2', 'user2'),
('dept3', 'user3'),
('dept4', 'user4')
) v([department], [user])
)
select t1.*
from table1 t1
where t1.[user] = #id or
not exists (select 1 from table1 t1 where t1.user = #id);
You may have to do a check first something like this
Declare #RowCount int
Select #RowCount = (select count(*) from [Table] Where [Column] = 'xxx')
If #RowCount > 0
begin
Select 1 -- put code here if records
end
else
begin
Select 2 -- put code here if no records
end
you can try this:
DECLARE #id varchar(12) = 'user1'
IF EXISTS(SELECT COUNT(*) FROM table1 WHERE [user] = #id)
BEGIN
SELECT * FROM table1 WHERE [user] = #id
END
ELSE
BEGIN
SELECT * FROM table1
END
you can also read more about "EXISTS" syntax on:
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/exists-transact-sql?view=sql-server-2017
You can have a slightly better execution plan if you separate the "if exist" logic from the actual query:
DECLARE #id varchar(10) = 'user5'
DECLARE #table TABLE ([department] varchar(10), [user] varchar(10))
insert into #table values
('dept1','user1'),
('dept2','user2'),
('dept3','user3'),
('dept4','user4')
DECLARE #exists BIT =
(SELECT 1 FROM #table WHERE [user] = #id)
SELECT department FROM #table
WHERE [user] = CASE #exists WHEN 1 THEN #id ELSE [user] END

Using ORDER BY command in an UPDATE SQL

I am looking at updating an item number column from a number of rows in a table (which match a particular Product ID & type) but I want to order the rows by numbers in a 'Seqnumber' column then the item number column start at 1 and count each row sequentially by 1.
Using the code below I have been able to select the product id and type I want and update the item number column with sequentially increasing values, however I don't know how I can order the required rows?
DECLARE #id nvarchar(6)
SET #id = 0
UPDATE Table1
SET #id = Table1.ItemNumber = #id + 1
WHERE Product = '5486' AND Table1.Type = 'C'
I know you can't use the ORDER BY command within the UPDATE command without using it as a sub-query. But I'm not sure how I should include this in the above code?
I think I need to incorporate the code below but not sure what the SELECT statement should be and when I try I can't get it to work?
DECALRE #id nvarchar(6)
SET #id = 0
UPDATE Table1
SET #id = TABLE1.ItemNumber = #id + 1
FROM (SELECT TOP (??)* FROM Table1
WHERE Product = '5486' AND Table1.Type ='C'
ORDER BY Table1.SeqNumber ASC
You should be able to build a CTE with the values you want from the table and just update the CTE
declare #Table1 table (Product varchar(4), [Type] varchar(1), SeqNumber int, ItemNumber int)
INSERT INTO #Table1 VALUES
('5486', 'C', 3, 0),
('5486', 'C', 2, 0);
with cte as (
select *,
ROW_NUMBER() OVER (ORDER BY SeqNumber) rn
from #Table1
where Product = '5486' and Type ='C'
)
Update cte
set ItemNumber = rn
This should work:
SET #id = 0;
UPDATE Table1
SET #id = Table1.ItemNumber = (#id := #id + 1)
WHERE Product = 5486 and Table1.Type = 'C'
ORDER BY Table1.seqnumber;
You cannot use ORDER BY with a JOIN, so you need to initialize the variable before the update.
UPDATE sales_data
SET ID = ID + 1
ORDER BY ID DESC;

Update of column values on condition

I wanted to update the column values on satisfying condition. I wanted the values to 1,2,3,4,5,...
I tried this query but its behaving in a strange way. I'm getting the values starting from 2 i.e 2,3,4,5,..
declare #id int;
set #id = 0;
UPDATE table
SET id = #id + 1, #id = #id + 1
WHERE col4 = 100
AND col5 = 500
AND col3 = 2
It seems like you want to enumerate records that meet the criteria specified in the WHERE clause. I would suggest using a CTE with ROW_NUMBER:
;WITH ToUpdate AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY col3) AS rn
FROM mytable
WHERE col4=100 AND col5=500 AND col3=2
)
UPDATE ToUpdate
SET id = rn
Try this -
Declare #id int;
Set #id=1;
UPDATE table
SET #id = id = #id + 1
WHERE
(col4=100 AND col5=500 AND col3=2)

Subquery returned more than one value error

I am using the following query, but it is throwing an error. It is working fine for some scenarios, depending on the id, but not for all.
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression.
Set #NoOfRows = (Select COUNT(*) from #TempT)
While #i <= #NoOfRows
Begin
SET #Voucher_No=( select V_No from #TempT where RowID=#i)
SET #Voucher_Type_No=( select voucher_type_no from #TempT where RowID=#i)
SET #Voucher_Id=( select Voucher_Id from #TempT where RowID=#i)
set #strPartyName=''
set #strPartyName = (Select Party_Name from Cheque_Book where Voucher_No= #Voucher_No and Voucher_Type_No= #Voucher_Type_No and Company_No= #Comp_No and Bank_Account_No= #dbc_Account )
if NULLIF(#strPartyName,'') IS NULL
begin
set #strPartyName =(Select a.account_name from Voucher v inner join account a on v.Account_No = a.account_No where v.Voucher_Id= #Voucher_Id)
Update #TempT Set Party_Name =#strPartyName Where RowID =#i
set #i=#i+1
end
End
Select * from #TempT
Rather than setting variable using sub-query, set variable as shown below.
SELECT #NoOfRows = COUNT(*) FROM #TempT
WHILE #i <= #NoOfRows
BEGIN
SELECT #Voucher_No = V_No FROM #TempT WHERE RowID=#i
SELECT #Voucher_Type_No = voucher_type_no FROM #TempT WHERE RowID=#i
SELECT #Voucher_Id = Voucher_Id FROM #TempT where RowID=#i
SET #strPartyName=''
SELECT #strPartyName = Party_Name FROM Cheque_Book WHERE Voucher_No = #Voucher_No AND Voucher_Type_No = #Voucher_Type_No AND Company_No= #Comp_No AND Bank_Account_No= #dbc_Account
IF NULLIF(#strPartyName,'') IS NULL
BEGIN
SELECT #strPartyName = a.account_name FROM Voucher v INNER JOIN account a ON v.Account_No = a.account_No WHERE v.Voucher_Id= #Voucher_Id
UPDATE #TempT SET Party_Name = #strPartyName WHERE RowID = #i
SET #i = #i + 1
END
END
SELECT * FROM #TempT
Its show that your sub-query return more than one value.
You have to modify your sub-query with TOP keyword or give filter (where condition and order by ) more which return only one value.
While #i <= #NoOfRows
Begin
SET #Voucher_No=( select top 1 V_No from #TempT where RowID=#i)
SET #Voucher_Type_No=( select top 1 voucher_type_no from #TempT where RowID=#i)
SET #Voucher_Id=( select top1 Voucher_Id from #TempT where RowID=#i)
......
END
To resolve the problem, you just only run the actual select query in while loop which gives the result of more than 2 value like
While #i <= #NoOfRows
Begin
--SET #Voucher_No=(
select V_No from #TempT where RowID=#i --)
--SET #Voucher_Type_No=(
select voucher_type_no from #TempT where RowID=#i --)
--SET #Voucher_Id=(
select Voucher_Id from #TempT where RowID=#i --)
......
END
UPDATED
This is sample data to understand what happened with you and how to resolve it.
declare #department table (deptid int, name varchar(50))
declare #emp table (id int, name varchar(50), deptid int)
insert into #department values (1,'d1'), (2,'d2'),(3,'d3'),(4,'d4')
insert into #emp values(1,'ajay',1), (2,'ajay1',1),(3,'ajay3' ,2),(4,'ajay4' ,3),(5,'ajay5' ,4)
select * from #department where deptid = 1
/* suppose your sub-query like */
--1. this give result
select * from #department where deptid = (select deptid from #emp where id = 1)
--2. this give result, but may be get wrong result as it take top 1.
select * from #department where deptid = (select top 1 deptid from #emp )
--3. this give error same you have
select * from #department where deptid = (select deptid from #emp)
--to resolve the above error, just uncomment below sub-query.
--select deptid from #emp
I see answers here for the issue where the following queries fail because their subqueries return multiple values:
SET #Voucher_No = (SELECT [V_No] FROM #TempT WHERE [RowID] = #i);
SET #Voucher_Type_No = (SELECT [voucher_type_no] FROM #TempT WHERE [RowID] = #i);
SET #Voucher_Id = (SELECT [Voucher_Id] FROM #TempT WHERE [RowID] = #i);
There are mainly two proposed solutions in those answers:
SET #Variable = (SELECT TOP (1) [Field] FROM [Table] WHERE ...);, or
SELECT #Variable = [Field] FROM [Table] WHERE ...);.
To my surprise, the second approach is considered to be the preferred approach. There are a few important things to consider, however.
First: if the SELECT-query returns multiple results, the value of #Variable will become the last retrieved value from the query's result set. This has to be regarded when an ORDER BY clause is specified.
So the query below will return 5 instead of 1:
DECLARE #TempTable TABLE ([ID] INT);
INSERT INTO #TempTable VALUES (1), (3), (2), (5), (4);
DECLARE #Variable INT = 42;
SELECT #Variable = [ID] FROM #TempTable ORDER BY [ID];
SELECT #Variable AS [#Variable];
Second: if #Variable already contains a value and the SELECT query does not produce any results (0 rows), the value of #Variable remains unchanged!
So the query below will produce result 42 instead of NULL:
DECLARE #TempTable TABLE ([ID] INT);
INSERT INTO #TempTable VALUES (1), (3), (2), (5), (4);
DECLARE #Variable INT = 42;
SELECT #Variable = [ID] FROM #TempTable WHERE [ID] = 6;
SELECT #Variable AS [#Variable];
For all these reasons, my personal preference lies with the SET #Variable = (SELECT TOP (1) ...) approach.

More efficient double coalesce join alternative

I have a procedure with a (slightly more complex) version of the below:
CREATE PROC sp_Find_ID (
#Match1 varchar(10),
#Match2 varchar(10)
) AS
DECLARE #ID int
SELECT #ID = ID
FROM Table1
WHERE Match1 = #Match1
AND Coalesce(Match2,#Match2,'') = Coalesce(#Match2,Match2,'')
SELECT #ID ID
Essentially Match1 is a mandatory match, but Match2 is both optional on the input to the procedure, and on the table being searched. The 2nd match succeeds where the input and/or the table Match2 values are null, or where they're both the same (not null) value.
My question is: Is there a more efficient (or even more readable) way of doing this?
I've used this method a few times, and I feel slightly sullied each time (subjective dirtiness admittedly).
Is there a more efficient (or even more readable) way of doing this?
The example you provided, using COALESCE/etc is non-sargable. You need to separate things so only what needs to be present in the query is run:
DECLARE #ID int
IF #Match2 IS NOT NULL
BEGIN
SELECT #ID = t.id
FROM TABLE1 t
WHERE t.match1 = #Match1
AND (t.match2 = #Match2 OR t.match2 IS NULL)
END
ELSE
BEGIN
SELECT #ID = t.id
FROM TABLE1 t
WHERE t.match1 = #Match1
END
SELECT #ID ID
If you want this to occur in a single SQL statement, dynamic SQL is the only real alternative. I highly recommend reading The curse and blessing of dynamic SQL before reading further:
DECLARE #SQL NVARCHAR(MAX)
SET #SQL = N' SELECT #ID = t.id
FROM TABLE1 t
WHERE t.match1 = #Match1 '
SET #SQL = #SQL + CASE
WHEN #Match2 IS NOT NULL THEN
' AND (t.match2 = #Match2 OR t.match2 IS NULL) '
ELSE
' '
END
BEGIN
EXEC sp_executesql #SQL,
N'#ID INT OUTPUT, #Match1 VARCHAR(10), #Match2 VARCHAR(10)',
#ID, #Match1, #Match2
END
Avoiding OR and ISNULL etc
The EXCEPT bit returns no rows if either side IS NULL
Match2 <> #Match2 means exclude non-NULL non-matching
Something like this
DROP TABLE dbo.Table1
CREATE TABLE dbo.Table1 (ID int NOT NULL, Match1 int NOT NULL, Match2 int NULL)
INSERT dbo.Table1 VALUES (1, 55, 99), (2, 55, NULL)
DECLARE #Match1 int = 55, #Match2 int
SELECT ID
FROM
(
SELECT ID FROM Table1 WHERE Match1 = #Match1
EXCEPT -- #Match2 = NULL, match both rows (99, NULL)
SELECT ID FROM Table1 WHERE Match2 <> #Match2
) foo
SET #Match2 = -1
SELECT ID
FROM
(
SELECT ID FROM Table1 WHERE Match1 = #Match1
EXCEPT -- #Match2 = -1, match ID = 2 only where Match2 IS NULL
SELECT ID FROM Table1 WHERE Match2 <> #Match2
) foo
Don't know if this is any more preferable.
SELECT #ID = ID
FROM Table1
WHERE Match1 = #Match1
AND ((Match2 = #Match2) OR Coalesce(Match2,#Match2) IS NULL)
I would have thought this should do it - providing that the #Match2 value will be NULL if it is optional.
CREATE PROC sp_Find_ID (
#Match1 varchar(10),
#Match2 varchar(10)
) AS
DECLARE #ID int
SELECT #ID = ID
FROM Table1
WHERE Match1 = #Match1
AND Match2 = IsNull(#Match2, Match2)
SELECT #ID ID
Seems simple to me? I must be missing something.. you dont need the Coalesce
SELECT #ID = ID
FROM Table1
WHERE Match1 = #Match1
AND (
(Match2 is null and #Match2 is null)
or
#Match2=Match2
)
SELECT #ID ID