compare value with 2 different columns using the IN operator - sql

I have a situation where I need to compare the value of a column with 2 columns from my settings table.
Currently I have this query which works
declare #t int = 3
select 1
where #t = (select s.RelationGDGMID from dbo.tblSettings s )
or
#t = (select s.RelationGTTID from dbo.tblSettings s )
But I wonder if I can make this without reading tblSettings 2 times, and then I tried this
declare #t int = 3
select 1
where #t in (select s.RelationGDGMID, s.RelationGTTID from dbo.tblSettings s )
and this does not compiles, it returns
Only one expression can be specified in the select list when the
subquery is not introduced with EXISTS
So how can I do this without reading tblSettings 2 times, well one solution would be using the EXISTS like the error hints me
declare #t int = 3
select 1
where exists (select 1 from dbo.tblSettings s where s.RelationGDGMID = #t or s.RelationGTTID = #t)
and yes that works, only reads tblSettings once, so I can use this.
But I still wonder if there is a way to make it work with the IN operator
After all, when I do this
declare #t int = 3
select 1
where #t in (3, 1)
that works without problems,
so why does
where #t in (select s.RelationGDGMID, s.RelationGTTID from dbo.tblSettings s )
not works, when in fact it also returns (3, 1) ?

One way to do it would be to use UNION if the columns are of the same type.
where #t in (select s1.RelationGDGMID from dbo.tblSettings s1 UNION
select s2.RelationGTTID from dbo.tblSettings s2)
The reason this works is because it is returning one value set (1 column with values). The reason where #t in (3, 1) works is because this the same, it is returning one value set (value 3 and value 1).
That said I would prefer the EXISTS over IN as this could produce a better query plan.

Related

Conditionally modify query based on parameter

I have this query (something like a case statement which I can use and fix it)
select *
from mytable
where 1=1
and (isNull(ID, 0) = 0 OR UtilityID IN (9,40))
I also want to add another statement
select *
from mytable
where 1=1
and UtilityID NOT IN (9,40)
Everything is happening in a procedure, so want to use a variable like declare #something so if that is passed as 1, use the first statement and the if 0 is passed, use the latter one.
While I appreciate the genius in Dale's answer I find this more readable:
IF #something = 0
BEGIN
select *
from mytable
where ID IS NULL OR ID = 0 OR UtilityID IN (9,40);
END
IF #something = 1
BEGIN
select *
from mytable
where UtilityID NOT IN (9,40);
END
It's procedure code, so use IF to direct the control flow. Also expanded and simplified your where clauses
I think I understand your logic, ignoring the 1=1 (which does nothing) you want to only allow id = 0 when #something = 1. This should do it:
declare #something bit = 0;
declare #mytable table (ID int, UtilityID int);
insert into #mytable (ID, UtilityID)
select 0, 1 union all
select 1, 2 union all
select 2, 9 union all
select 3, 40;
select *
from #mytable
where (
(#something = 1 and (isnull(ID, 0) = 0 or UtilityID in (9,40)))
or (#something = 0 and (UtilityID not in (9,40)))
);
A more performant approach for a larger query could be:
select *
from #mytable
where (#something = 1 and (isnull(ID, 0) = 0 or UtilityID in (9,40)))
union all
select *
from #mytable
where (#something = 0 and (UtilityID not in (9,40)));
PS: Hopefully your ID cannot ever by null - it should have a constraint on it.

Multiple Filter in the same column

I have a sql table with some values and a lot of filters
ID | Name | Filter1 | Filter2 | Filter3 | Filter4 ... and so on...
As now the filters have been set as int and I am running a query as follows to get the data required
select Name
from tblABC
where Filter1=1 and Filter2 = 7 and Filter3 = 33 ... and so on...'
My issue is that I want a filter column to hold multiple numbers. eg:- row no 3 will have numbers 8 and 13 in Filter1 cell, so that when I run a query for 8 or 13 I get the same result.
ie I want both the below queries to return the same result.
select... where Filter1=8
select... where Filter1=13
How can this be done? I tried converting the Filter columns to nvarchar and entering data as .8.13. where '.' where was used as separators. After this, running a query 'select... where Filter1 LIKE '%.8.%' is working for me.. But there are like 12 Filter columns and when such a string search is run in large volumes, wouldn't it make the query slow. What would be a more efficient way of doing this?
I am using Microsoft SQL 2014
A more efficient way, hmm. Separating tblABC from the filters would be my suggested way to go, even if it's not the most efficient way it will make up for it in maintenance (and it sure is more efficient than using like with wildcards for it).
tblABC ID Name
1 Somename
2 Othername
tblABCFilter ID AbcID Filter
1 1 8
2 1 13
3 1 33
4 2 5
How you query this data depends on your required output of course. One way is to just use the following:
SELECT tblABC.Name FROM tblABC
INNER JOIN tblABCFilter ON tblABC.ID = tblABCFilter.AbcID
WHERE tblABCFilter.Filter = 33
This will return all Name with a Filter of 33.
If you want to query for several Filters:
SELECT tblABC.Name FROM tblABC
INNER JOIN tblABCFilter ON tblABC.ID = tblABCFilter.AbcID
WHERE tblABCFilter.Filter IN (33,7)
This will return all Name with Filter in either 33 or 7.
I have created a small example fiddle.
I'm going to post a solution I use. I use a split function ( there are a lot of SQL Server split functions all over the internet)
You can take as example
CREATE FUNCTION [dbo].[SplitString]
(
#List NVARCHAR(MAX),
#Delim VARCHAR(255)
)
RETURNS TABLE
AS
RETURN ( SELECT [Value] FROM
(
SELECT
[Value] = LTRIM(RTRIM(SUBSTRING(#List, [Number],
CHARINDEX(#Delim, #List + #Delim, [Number]) - [Number])))
FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
FROM sys.all_objects) AS x
WHERE Number <= LEN(#List)
AND SUBSTRING(#Delim + #List, [Number], LEN(#Delim)) = #Delim
) AS y
);
and run your query like this
select Name
from tblABC
where Filter1 IN (
SELECT * FROM SplitString(#DatatoFilter,',') and
Filter2 (IN (
SELECT * FROM SplitString(#DatatoFilter,',') and
..so on.
If you have hunderds of thousands of records it may not perform very well. But it should work.
My personal aproch would be a stored procedure and temp tables. Create a temp table with all the values you want to use as filter
SELECT *
INTO #Filter1
FROM SplitString(#DatatoFilter,',')
SELECT *
INTO #Filter2
FROM SplitString(#DatatoFilter,',')
then the final select
SELECT * FROM yourtable
WHERE Filter1 IN (SELECT DISTINCT Part FROM #Filter1) and
Filter2 IN (SELECT DISTINCT Part FROM #Filter2)
I don't think it makes any big difference from the first query, but it is easier to read.
Another solution which you can try is to convert the columns to XML. Its better than converting the columns to VARCHAR. You can use .exist to get only the records matching your criteria. Something like this.
DECLARE #table1 TABLE
(
[ID] int, [Name] varchar(9),Filter1 XML
)
INSERT INTO #table1
([ID], [Name],Filter1)
VALUES
(1, 'Somename','<Filter>8</Filter>'),
(2, 'Othername','<Filter>8</Filter><Filter>13</Filter>'),
(3, 'Thirdname','<Filter>25</Filter>')
DECLARE #FilterValue INT = 8
SELECT Filter1.query('/Filter'),*
FROM #table1
WHERE Filter1.exist('/Filter[. = sql:variable("#FilterValue")]') = 1
EDIT
You can even use the XML column to store all 12 of your filters. So this filter xml column which store all your filters and their multiple values.
DECLARE #table1 TABLE
(
[ID] int, [Name] varchar(9),Filter XML
)
INSERT INTO #table1
([ID], [Name],Filter)
VALUES
(1, 'Somename','<Filter ID = "1"><FilterVal>8</FilterVal></Filter><Filter ID = "2"><FilterVal>3</FilterVal><FilterVal>12</FilterVal></Filter>'),
(2, 'Othername','<Filter ID = "1"><FilterVal>8</FilterVal><FilterVal>13</FilterVal></Filter><Filter ID = "2"><FilterVal>8</FilterVal><FilterVal>13</FilterVal></Filter>'),
(3, 'Thirdname','<Filter ID = "2"><FilterVal>12</FilterVal><FilterVal>25</FilterVal></Filter><Filter ID = "3"><FilterVal>33</FilterVal></Filter>')
DECLARE #Filter1Value INT = 8
DECLARE #Filter2Value INT = 12
SELECT *
FROM #table1
WHERE Filter.exist('/Filter[#ID = 1]/FilterVal[. = sql:variable("#Filter1Value")]') = 1
AND Filter.exist('/Filter[#ID = 2]/FilterVal[. = sql:variable("#Filter2Value")]') = 1

Checking condition in IF vs WHERE clause

I have a problem and I have 2 solutions for that problem. I have to write a procedure which will return rows falling into required categories. The required categories will be passed as boolean (or bit) values. The first solution is:-
DECLARE #IsRowType1Req bit;
DECLARE #IsRowType2Req bit;
DECLARE #tbl1 table (ID int, RowType varchar(50));
DECLARE #tmpTbl table (ID int, RowType varchar(50));
IF #IsRowType1Req = 1
INSERT INTO
#tmpTbl
SELECT
*
FROM
#tbl1
WHERE
RowType = 'RowType1';
IF #IsRowType2Req = 1
INSERT INTO
#tmpTbl
SELECT
*
FROM
#tbl1
WHERE
RowType = 'RowType2';
SELECT * FROM #tmpTbl;
Now, this solution uses IF clause to select only required type of rows. The other one is:-
DECLARE #IsRowType1Req bit;
DECLARE #IsRowType2Req bit;
DECLARE #tbl1 table (ID int, RowType varchar(50));
SELECT
*
FROM
#tbl1
WHERE
RowType = 'RowType1' AND
#IsRowType1Req = 1
UNION
SELECT
*
FROM
#tbl1
WHERE
RowType = 'RowType2' AND
#IsRowType2Req = 1
This solution utilizes WHERE clause to restrict unwanted type of rows. Can anybody please tell me which one will be faster and why?
With the information you have given there seems no need to use a UNIONof IF.
SELECT *
FROM #tbl1
WHERE
(RowType = 'RowType2' AND #IsRowType2Req = 1)
OR (RowType = 'RowType1' AND #IsRowType1Req = 1);
The answer would likely depend on the size of the table as well as the size of each subset returned.
The IF solution has to insert rows into another table, which doesn't come cheap. The WHERE solution, on the other hand, uses UNION which implies sorting to remove duplicate entries. If you replaced UNION with UNION ALL (which you could safely do because your two subsets would never have duplicates), the WHERE solution would become the better one of the two hands down.
However, since rows in #tbl1 cannot be more than one type, you could solve the problem differently. You could match #tbl1 against a dynamically built table consisting of 0, 1 or 2 rows depending on the values of #IsRowType1Req and #IsRowType2Req. You would build that table like this:
SELECT 'RowType1' WHERE #IsRowType1Req = 1
UNION ALL
SELECT 'RowType2' WHERE #IsRowType2Req = 1
and then join it to #tbl1:
SELECT
*
FROM
#tbl1 AS t
INNER JOIN
(
SELECT 'RowType1' WHERE #IsRowType1Req = 1
UNION ALL
SELECT 'RowType2' WHERE #IsRowType2Req = 1
) AS f (RowType)
ON
t.RowType = f.RowType
;
In a way, this might mean that WHERE still "wins", but you could rewrite the virtual table without using WHERE:
SELECT CASE #IsRowType1Req WHEN 1 THEN 'RowType1' END
UNION ALL
SELECT CASE #IsRowType2Req WHEN 1 THEN 'RowType2' END
or like this, using the VALUES constructor introduced in SQL Server 2008:
VALUES
(CASE #IsRowType1Req WHEN 1 THEN 'RowType1' END),
(CASE #IsRowType2Req WHEN 1 THEN 'RowType2' END)
That way the table would always consist of 2 rows, each row containing either a requested type or NULL. The result of the join with that table would still be the same and match your desired result.
In this case, I would go with the IF, but only because you are using the UNION clause and that means your are going to run two single queries and then combine the results.
If fact, when you use UNION, SQL Server needs to make sure the combined result set does not contain any duplicated rows, so it needs to run both queries and then compare the result set to check for coincidences.
Here you have a detailed explanation http://webbtechsolutions.com/2009/08/06/the-effects-union-in-a-sql-query/

SQL: how to get random number of rows from one table for each row in another

I have two tables where the data is not related
For each row in table A i want e.g. 3 random rows in table B
This is fairly easy using a cursor, but it is awfully slow
So how can i express this in single statement to avoid RBAR ?
To get a random number between 0 and (N-1), you can use.
abs(checksum(newid())) % N
Which means to get positive values 1-N, you use
1 + abs(checksum(newid())) % N
Note: RAND() doesn't work - it is evaluated once per query batch and you get stuck with the same value for all rows of tableA.
The query:
SELECT *
FROM tableA A
JOIN (select *, rn=row_number() over (order by newid())
from tableB) B ON B.rn <= 1 + abs(checksum(newid())) % 9
(assuming you wanted up to 9 random rows of B per A)
assuming tableB has integer surrogate key, try
Declare #maxRecs integer = 11 -- Maximum number of b records per a record
Select a.*, b.*
From tableA a Join tableB b
On b.PKColumn % (floor(Rand() * #maxRecs)) = 0
If you have a fixed number that you know in advance (such as 3), then:
select a.*, b.*
from a cross join
(select top 3 * from b) b
If you want a random number of rows from "b" for each row in "a", the problem is a bit harder in SQL Server.
Heres an example of how this could be done, code is self contained, copy and press F5 ;)
-- create two tables we can join
DECLARE #datatable TABLE(ID INT)
DECLARE #randomtable TABLE(ID INT)
-- add some dummy data
DECLARE #i INT = 1
WHILE(#i < 3) BEGIN
INSERT INTO #datatable (ID) VALUES (#i)
SET #i = #i + 1
END
SET #i = 1
WHILE(#i < 100) BEGIN
INSERT INTO #randomtable (ID) VALUES (#i)
SET #i = #i + 1
END
--The key here being the ORDER BY newid() which makes sure that
--the TOP 3 is different every time
SELECT
d.ID AS DataID
,rtable.ID RandomRow
FROM #datatable d
LEFT JOIN (SELECT TOP 3 * FROM #randomtable ORDER BY newid()) as rtable ON 1 = 1
Heres an example of the output

SQL query, store result of SELECT in local variable

I create a query with some results reused. I search a way to put the result into a variable and use it.
A simple way to see what I want something looking like this - I want this:
DECLARE #result1 ?????
SET #result1 = SELECT a,b,c FROM table1
SELECT a AS val FROM #result1
UNION
SELECT b AS val FROM #result1
UNION
SELECT c AS val FROM #result1
Not this :
SELECT a AS val FROM (SELECT a,b,c FROM table1)
UNION
SELECT b AS val FROM (SELECT a,b,c FROM table1)
UNION
SELECT c AS val FROM (SELECT a,b,c FROM table1)
It's not the result of this query that I'm concerned with, but instead:
to stop selecting the result so many times - in my sample, I reselected the table 3 times
the query of #result1 is usually so much more complex. So, with a variable, the code will be cleaner.
Maybe I want to much - or there's a type of local variable. Or using the type table and set data inside.
What do you suggest me?
Thank you
You can create table variables:
DECLARE #result1 TABLE (a INT, b INT, c INT)
INSERT INTO #result1
SELECT a, b, c
FROM table1
SELECT a AS val FROM #result1
UNION
SELECT b AS val FROM #result1
UNION
SELECT c AS val FROM #result1
This should be fine for what you need.
Here are some other approaches you can take.
1. CTE with union:
;WITH cte AS (SELECT a, b, c FROM table1)
SELECT a AS val FROM cte
UNION SELECT b AS val FROM cte
UNION SELECT c AS val FROM cte;
2. CTE with unpivot:
;WITH cte AS (SELECT a, b, c FROM table1)
SELECT DISTINCT val
FROM cte
UNPIVOT (val FOR col IN (a, b, c)) u;
Isn't this a much simpler solution, if I correctly understand the question, of course.
I want to load email addresses that are in a table called "spam" into a variable.
select email from spam
produces the following list, say:
.accountant
.bid
.buiilldanything.com
.club
.cn
.cricket
.date
.download
.eu
To load into the variable #list:
declare #list as varchar(8000)
set #list += #list (select email from spam)
#list may now be INSERTed into a table, etc.
I hope this helps.
To use it for a .csv file or in VB, spike the code:
declare #list as varchar(8000)
set #list += #list (select '"'+email+',"' from spam)
print #list
and it produces ready-made code to use elsewhere:
".accountant,"
".bid,"
".buiilldanything.com,"
".club,"
".cn,"
".cricket,"
".date,"
".download,"
".eu,"
One can be very creative.
Thanks
Nico
I came here with a similar question/problem, but I only needed a single value to be stored from the query, not an array/table of results as in the orig post. I was able to use the table method above for a single value, however I have stumbled upon an easier way to store a single value.
declare #myVal int;
set #myVal = isnull((select a from table1), 0);
Make sure to default the value in the isnull statement to a valid type for your variable, in my example the value in table1 that we're storing is an int.