Use WHERE + AND + CASE + IN when crafting T-SQL - sql

I have a stored procedure that has a few variables that may or may not be passed. they are a list of PKs from other tables, so FKs but formatted in as a string of CSVs.
here's what the query essentially looks like
DECLARE #SomeIds VARCHAR(MAX) = ''
CREATE TABLE #TempIds (Id INT)
IF (#SomeIds = '' OR #SomeIds = NULL) INSERT INTO #TempIds VALUES (NULL)
ELSE INSERT INTO #TempIds SELECT * FROM SplitString(#SomeIds,',') -- SplitString() is a user function
SELECT cont.varchar_LastName AS LastName
,cred.varchar_CredentialName AS CredentialName
FROM [dbo].[tbl_Contacts] AS cont
LEFT JOIN [dbo].[tbl_ContactsCredentials] AS cc ON cont.pk_int_Id = cc.fk_ContactId
LEFT JOIN [dbo].[tbl_Credentials] AS cred ON cc.fk_CredentialId = cred.pk_int_Id
So this query basically gives me a full list of contacts both with and without a credential name. I don't have a WHERE clause, so not surprised.
I get data basically like:
LastName | CredentialName
---------------------------
Stevens | Admin
Arnolds | User
Bishop | NULL
Evans | NULL
So if I add a WHERE clause like this:
WHERE cred.pk_int_Id IN (SELECT * FROM #TempIds)
I get zero results.
When I run this:
SELECT * FROM #TempIds
I get this:
Id
-----------
NULL
When I run it with "real values" in #SomeIds like '1,2' then it works fine.
I presume this is because my WHERE clause is looking in the cred table and there are no NULL values in that table, so that's why I'm not getting anything.
But I'm not sure how I fix it?
I guess I really want to do something like this:
WHERE CredentialName IN (SELECT * FROM #TempIds)
But I believe to do that, I'd have to run the first query into another temp table, then run a second query on that table.
Any help is greatly appreciated.

You can avoid a temp table
WHERE (NULLIF(#SomeIds,'') IS NULL OR cred.pk_int_Id IN (SELECT value FROM SplitString(#SomeIds,',')))
Or if your Sql Server version supports STRING_SPLIT
WHERE (NULLIF(#SomeIds,'') IS NULL OR cred.pk_int_Id IN (SELECT value FROM STRING_SPLIT(#SomeIds,',')))
And then don't initialize #SomeIds to make it get all records.

I would consider using a UNION for this. Putting an OR in a WHERE clause can make for some bad execution plans, and often makes indexes unusable.
SELECT cont.varchar_LastName AS LastName
,cred.varchar_CredentialName AS CredentialName
FROM [dbo].[tbl_Contacts] AS cont
LEFT JOIN [dbo].[tbl_ContactsCredentials] AS cc
ON cont.pk_int_Id = cc.fk_ContactId
LEFT JOIN [dbo].[tbl_Credentials] AS cred
ON cc.fk_CredentialId = cred.pk_int_Id
WHERE cred.pk_int_Id IN (SELECT value FROM STRING_SPLIT(#SomeIds,','))
UNION ALL
SELECT cont.varchar_LastName AS LastName
,cred.varchar_CredentialName AS CredentialName
FROM [dbo].[tbl_Contacts] AS cont
LEFT JOIN [dbo].[tbl_ContactsCredentials] AS cc
ON cont.pk_int_Id = cc.fk_ContactId
LEFT JOIN [dbo].[tbl_Credentials] AS cred
ON cc.fk_CredentialId = cred.pk_int_Id
WHERE NULLIF(#SomeIds,'') IS NULL
I talk about using OR in UPDATES statements here. But the same logic applies to SELECT.

Related

SQL How to get full Array in a temporary table with condition?

Well, I am really sorry because my explanation was so poor. Thank you for all the answers.
I will explain better what should be the output and what is my question.
So, first of I have an array of tagCodes like ('code0','code1','code2').
Then I have a table that contains Codes and TagTypeId.
I would like to get into a temporary table all the codes I passed in the array with their TagTypeId. So a table like:
Code
TagTypeId
903420012408181609019A18
2456
903420012408181609019A18
2135
TestCodeNull
null
So my attempt was this one:
SELECT Tags.Code AS tagCode, Tags.TagTypeId, TagTypes.Code AS tagType
INTO #TempTable
FROM Tags JOIN TagTypes ON Tags.TagTypeId = TagTypes.Id
WHERE Tags.Code IN ('903420012408181609019A18','90341808151313061101E938', 'TestCodeNull')
SELECT * FROM #TempTable;
But I dont get the codes that are not in the Tag table.
I did this an it seems to be working as intended:
CREATE TABLE #TestTable (tagCode NVARCHAR(25) NOT NULL, TagTypeId INT NULL, tagType NVARCHAR(MAX))
INSERT INTO #TestTable (tagCode) VALUES ('903420012408181609019A18'),('00007E08190D0A34E1F524D0'),('00007E08190D0B25E1F5A98B')
UPDATE #TestTable SET TagTypeId = Tags.TagTypeId, tagType = TagTypes.Code FROM #TestTable
LEFT JOIN Tags ON (#TestTable.tagCode = Tags.Code)
LEFT JOIN TagTypes ON (Tags.TagTypeId = TagTypes.Id)
SELECT * FROM #TestTable;
I think what you mean that 'TestCodeNull' does not exist in tags so you want to show null for 'TestCodeNull' in which case a join may be more appropriate. for example
SELECT S.CODE,Tags.Code AS tagCode, Tags.TagTypeId,
TagTypes.Code AS tagType
INTO #TempTable
FROM (select '903420012408181609019A18' code
union all select '90341808151313061101E938'
union all select 'TestCodeNull') s
left join Tags on tags.code = s.code
left JOIN TagTypes ON Tags.TagTypeId = TagTypes.Id
SELECT * FROM #TempTable;

Rewrite query without using temp table

I have a query that is using a temp table to insert some data then another select from to extract distinct results. That query by it self was fine but now with entity-framework it is causing all kinds of unexpected errors at the wrong time.
Is there any way I can rewrite the query not to use a temp table? When this is converted into a stored procedure and in entity framework the result set is of type int which throws an error:
Could not find an implementation of the query pattern Select not found.
Here is the query
Drop Table IF EXISTS #Temp
SELECT
a.ReceiverID,
a.AntennaID,
a.AntennaName into #Temp
FROM RFIDReceiverAntenna a
full join Station b ON (a.ReceiverID = b.ReceiverID) and (a.AntennaID = b.AntennaID)
where (a.ReceiverID is NULL or b.ReceiverID is NULL)
and (a.AntennaID IS NULL or b.antennaID is NULL)
select distinct r.ReceiverID, r.ReceiverName, r.receiverdescription
from RFIDReceiver r
inner join #Temp t on r.ReceiverID = t.ReceiverID;
No need for anything fancy, you can just replace the reference to #temp with an inner sub-query containing the query that generates #temp e.g.
select distinct r.ReceiverID, r.ReceiverName, r.receiverdescription
from RFIDReceiver r
inner join (
select
a.ReceiverID,
a.AntennaID,
a.AntennaName
from RFIDReceiverAntenna a
full join Station b ON (a.ReceiverID = b.ReceiverID) and (a.AntennaID = b.AntennaID)
where (a.ReceiverID is NULL or b.ReceiverID is NULL)
and (a.AntennaID IS NULL or b.antennaID is NULL)
) t on r.ReceiverID = t.ReceiverID;
PS: I haven't made any effort to improve the query overall like Gordon has but do consider his suggestions.
First, a full join makes no sense in the first query. You are selecting only columns from the first table, so you need that.
Second, you can use a CTE.
Third, you should be able to get rid of the SELECT DISTINCT by using an EXISTS condition.
I would suggest:
WITH ra AS (
SELECT ra.*
FROM RFIDReceiverAntenna ra
Station s
ON s.ReceiverID = ra.ReceiverID AND
s.AntennaID = ra.AntennaID)
WHERE s.ReceiverID is NULL
)
SELECT r.ReceiverID, r.ReceiverName, r.receiverdescription
FROM RFIDReceiver r
WHERE EXISTS (SELECT 1
FROM ra
WHERE r.ReceiverID = ra.ReceiverID
);
You can use CTE instead of the temp table:
WITH
CTE
AS
(
SELECT
a.ReceiverID,
a.AntennaID,
a.AntennaName
FROM
RFIDReceiverAntenna a
full join Station b
ON (a.ReceiverID = b.ReceiverID)
and (a.AntennaID = b.AntennaID)
where
(a.ReceiverID is NULL or b.ReceiverID is NULL)
and (a.AntennaID IS NULL or b.antennaID is NULL)
)
select distinct
r.ReceiverID, r.ReceiverName, r.receiverdescription
from
RFIDReceiver r
inner join CTE t on r.ReceiverID = t.ReceiverID
;
This query will return the same results as your original query with the temp table, but its performance may be quite different; not necessarily slower, it can be faster. Just something that you should be aware about.

OracleSQL: How do I add a specific AND is not null OR is not null to my query

Backstory:
I have three tables I'm working with. A directory table (directory), an general attribute table (attribute1table) and a specific attribute table (attribute2table). The general attribute tables hold attribute names (ex. Last Name) under attribute id's (attrid = 2). The specific attribute table holds specific data for these attributes (ex. Doe).
I needed to transpose rows to columns. I had tried using pivot, and max(decode) before but all options gave me the wrong string value- so I used a sub select within the select statement. This worked well- it did transpose the rows into columns but gave me a bunch of null values. See query at the bottom for steps.
Then I added in a general 'stringval IS NOT NULL' to eliminate any of the other attribute1table.attrid's (ex. 4, 5, 6). This worked.
This is the output I was getting at this point. The ? are null values.
Name DataID LastName FirstName
File10 1290 ? Jane
File10 1290 Doe ?
Then I wanted to add in a specification. Essentially to include the values where LastName is not null OR FirstName is not null. I found that someone had recommended doing this in a previous question albeit their situation was different. Eliminating specific null values in sql select
I was able to include one statement or the other but could not add in both. Instead of getting an error I just got a horrifically long run time with no foreseeable result (note that I am using software which lets you input oracle queries within the interface to query the database). It works if I run the query up until the ** (see code) but as soon as I add in the OR condition, it doesn't work anymore. I think this is because I have multiple WHERE conditions. In all cases I want the directory ID and general stringval conditions to apply but I want to have a third condition where either lastname is not null or first name is not null. I'm not sure if I'm missing something obvious- please help?
Here is my current query:
SELECT directory.name, directory.dataid,
(SELECT max(stringval) FROM attribute2table WHERE attribute1table.attrid = 2) as LastName,
(SELECT max(stringval) FROM attribute2table WHERE attribute1table.attrid = 3) as FirstName
FROM attribute2table
JOIN directory ON directory.dataid = attribute2table.id
JOIN attribute1table ON attribute1table.id = directory.dataid
WHERE directory.dataid = 1290
AND stringval IS NOT NULL
AND (SELECT max(valstr) FROM attribute1table WHERE attribute1table.attrid = 2) IS NOT NULL
**OR (SELECT max(valstr) FROM attribute1table WHERE attribute1table.attrid = 3) IS NOT NULL**
Basically I just need to get rid of the null values and want my table to look like....
Name DataID LastName FirstName
File10 1290 Doe Jane
This appears to be a parenthesization issue. If I understand the issue, you need to put the two IS NOT NULL conditions in parentheses:
SELECT directory.name,
directory.dataid,
m2.LastName,
m3.FirstName
FROM attribute2table
INNER JOIN directory
ON directory.dataid = attribute2table.id
INNER JOIN attribute1table
ON attribute1table.id = directory.dataid
LEFT OUTER JOIN (SELECT max(valstr) AS LASTNAME
FROM attribute1table
WHERE attribute1table.attrid = 2) m2
ON 1 = 1
LEFT OUTER JOIN (SELECT max(valstr) AS FIRSTNAME
FROM attribute1table
WHERE attribute1table.attrid = 3) m3
ON 1 = 1
WHERE directory.dataid = 1290 AND
stringval IS NOT NULL AND
(m2.LASTNAME IS NOT NULL OR
m3.FIRSTNAME IS NOT NULL)
I also rewrote the query using joins instead of subselects as I think it's a bit clearer.
Note also that in the M2 and M3 joins I used LEFT OUTER with a condition of 1 = 1 rather than using CROSS JOIN, because I've noticed that CROSS JOIN acts like an INNER JOIN if the query being cross-joined returns no rows - that is, it causes the entire SELECT to return no data. dbfiddle demonstrating this situation here
I'm pretty sure you just need conditional aggregation:
SELECT d.name, d.dataid,
MAX(CASE WHEN a1.attrid = 2 THEN a2.stringval END) as LastName,
MAX(CASE WHEN a1.attrid = 3 THEN a2.stringval END) as FirstName
FROM directory d JOIN
attribute2table a2
ON a2.id = d.dataid JOIN
attribute1table a1
ON a1.id = d.dataid
WHERE d.dataid = 1290
GROUP BY d.name, d.dataid

SQL query: Iterate over values in table and use them in subquery

I have a simple SQL table containing some values, for example:
id | value (table 'values')
----------
0 | 4
1 | 7
2 | 9
I want to iterate over these values, and use them in a query like so:
SELECT value[0], x1
FROM (some subquery where value[0] is used)
UNION
SELECT value[1], x2
FROM (some subquery where value[1] is used)
...
etc
In order to get a result set like this:
4 | x1
7 | x2
9 | x3
It has to be in SQL as it will actually represent a database view. Of course the real query is a lot more complicated, but I tried to simplify the question while keeping the essence as much as possible.
I think I have to select from values and join the subquery, but as the value should be used in the subquery I'm lost on how to accomplish this.
Edit: I oversimplified my question; in reality I want to have 2 rows from the subquery and not only one.
Edit 2: As suggested I'm posting the real query. I simplified it a bit to make it clearer, but it's a working query and the problem is there. Note that I have hardcoded the value '2' in this query two times. I want to replace that with values from a different table, in the example table above I would want a result set of the combined results of this query with 4, 7 and 9 as values instead of the currently hardcoded 2.
SELECT x.fantasycoach_id, SUM(round_points)
FROM (
SELECT DISTINCT fc.id AS fantasycoach_id,
ffv.formation_id AS formation_id,
fpc.round_sequence AS round_sequence,
round_points,
fpc.fantasyplayer_id
FROM fantasyworld_FantasyCoach AS fc
LEFT JOIN fantasyworld_fantasyformation AS ff ON ff.id = (
SELECT MAX(fantasyworld_fantasyformationvalidity.formation_id)
FROM fantasyworld_fantasyformationvalidity
LEFT JOIN realworld_round AS _rr ON _rr.id = round_id
LEFT JOIN fantasyworld_fantasyformation AS _ff ON _ff.id = formation_id
WHERE is_valid = TRUE
AND _ff.coach_id = fc.id
AND _rr.sequence <= 2 /* HARDCODED USE OF VALUE */
)
LEFT JOIN fantasyworld_FantasyFormationPlayer AS ffp
ON ffp.formation_id = ff.id
LEFT JOIN dbcache_fantasyplayercache AS fpc
ON ffp.player_id = fpc.fantasyplayer_id
AND fpc.round_sequence = 2 /* HARDCODED USE OF VALUE */
LEFT JOIN fantasyworld_fantasyformationvalidity AS ffv
ON ffv.formation_id = ff.id
) x
GROUP BY fantasycoach_id
Edit 3: I'm using PostgreSQL.
SQL works with tables as a whole, which basically involves set operations. There is no explicit iteration, and generally no need for any. In particular, the most straightforward implementation of what you described would be this:
SELECT value, (some subquery where value is used) AS x
FROM values
Do note, however, that a correlated subquery such as that is very hard on query performance. Depending on the details of what you're trying to do, it may well be possible to structure it around a simple join, an uncorrelated subquery, or a similar, better-performing alternative.
Update:
In view of the update to the question indicating that the subquery is expected to yield multiple rows for each value in table values, contrary to the example results, it seems a better approach would be to just rewrite the subquery as the main query. If it does not already do so (and maybe even if it does) then it would join table values as another base table.
Update 2:
Given the real query now presented, this is how the values from table values could be incorporated into it:
SELECT x.fantasycoach_id, SUM(round_points) FROM
(
SELECT DISTINCT
fc.id AS fantasycoach_id,
ffv.formation_id AS formation_id,
fpc.round_sequence AS round_sequence,
round_points,
fpc.fantasyplayer_id
FROM fantasyworld_FantasyCoach AS fc
-- one row for each combination of coach and value:
CROSS JOIN values
LEFT JOIN fantasyworld_fantasyformation AS ff
ON ff.id = (
SELECT MAX(fantasyworld_fantasyformationvalidity.formation_id)
FROM fantasyworld_fantasyformationvalidity
LEFT JOIN realworld_round AS _rr
ON _rr.id = round_id
LEFT JOIN fantasyworld_fantasyformation AS _ff
ON _ff.id = formation_id
WHERE is_valid = TRUE
AND _ff.coach_id = fc.id
-- use the value obtained from values:
AND _rr.sequence <= values.value
)
LEFT JOIN fantasyworld_FantasyFormationPlayer AS ffp
ON ffp.formation_id = ff.id
LEFT JOIN dbcache_fantasyplayercache AS fpc
ON ffp.player_id = fpc.fantasyplayer_id
-- use the value obtained from values again:
AND fpc.round_sequence = values.value
LEFT JOIN fantasyworld_fantasyformationvalidity AS ffv
ON ffv.formation_id = ff.id
) x
GROUP BY fantasycoach_id
Note in particular the CROSS JOIN which forms the cross product of two tables; this is the same thing as an INNER JOIN without any join predicate, and it can be written that way if desired.
The overall query could be at least a bit simplified, but I do not do so because it is a working example rather than an actual production query, so it is unclear what other changes would translate to the actual application.
In the example I create two tables. See how outer table have an alias you use in the inner select?
SQL Fiddle Demo
SELECT T.[value], (SELECT [property] FROM Table2 P WHERE P.[value] = T.[value])
FROM Table1 T
This is a better way for performance
SELECT T.[value], P.[property]
FROM Table1 T
INNER JOIN Table2 p
on P.[value] = T.[value];
Table 2 can be a QUERY instead of a real table
Third Option
Using a cte to calculate your values and then join back to the main table. This way you have the subquery logic separated from your final query.
WITH cte AS (
SELECT
T.[value],
T.[value] * T.[value] as property
FROM Table1 T
)
SELECT T.[value], C.[property]
FROM Table1 T
INNER JOIN cte C
on T.[value] = C.[value];
It might be helpful to extract the computation to a function that is called in the SELECT clause and is executed for each row of the result set
Here's the documentation for CREATE FUNCTION for SQL Server. It's probably similar to whatever database system you're using, and if not you can easily Google for it.
Here's an example of creating a function and using it in a query:
CREATE FUNCTION DoComputation(#parameter1 int)
RETURNS int
AS
BEGIN
-- Do some calculations here and return the function result.
-- This example returns the value of #parameter1 squared.
-- You can add additional parameters to the function definition if needed
DECLARE #Result int
SET #Result = #parameter1 * #parameter1
RETURN #Result
END
Here is an example of using the example function above in a query.
SELECT v.value, DoComputation(v.value) as ComputedValue
FROM [Values] v
ORDER BY value

SQL Join With Fallback

Given
CREATE TABLE Addresses
Id INT NOT NULL
Zip NVARCHAR(5) NULL
ZipPlus4 NVARCHAR(9) NULL
CREATE TABLE ZipLookup
Zip NVARCHAR(5) NULL
Code NVARCHAR(10) NULL
CREATE TABLE ZipPlus4Lookup
ZipPlus4 NVARCHAR(9) NULL
Code NVARCHAR(10) NULL
And data like
Addresses
1 | 92123 | 921234444
ZipLookup
92123 | Type A
ZipPlus4Lookup
921234444 | Type B
Is it possible to construct a query such that:
A given row in Addresses is outer joined to ZipPlus4Lookup if there is a match
Addresses.ZipPlus4 = ZipPlus4Lookup.ZipPlus4
Otherwise, the given row in Addresses is outer joined to ZipLookup if there is a match
Addresses.Zip = ZipLookup.Zip
Otherwise neither table is outer joined
In plain English, the Addresses table has a Zip and a ZipPlus4 column and I need to look up a code using the most precise match. If there's a match on Zip+4, use the code from that match. Otherwise, use the code from a Zip match.
I wish I had an attempted query to share, but with this one I don't know where to start.
This basic query will work:
SELECT
A.*,
Code = IsNull(Z4.Code, Z.Code)
FROM
dbo.Addresses A
LEFT JOIN dbo.ZipPlus4Lookup Z4
ON A.ZipPlus4 = Z4.ZipPlus4
LEFT JOIN dbo.ZipLookup Z
ON A.Zip = Z.Zip
AND Z4.ZipPlus4 IS NULL;
Or you could try something like this:
SELECT
A.*,
Z.Code
FROM
dbo.Addresses A
OUTER APPLY (
SELECT TOP 1 Code
FROM (
SELECT 0, Code FROM dbo.ZipPlus4Lookup Z4
WHERE A.ZipPlus4 = Z4.ZipPlus4
UNION ALL
SELECT 1, Code FROM dbo.ZipLookup Z
WHERE A.Zip = Z.Zip
) X (Seq, Code)
ORDER BY X.Seq
) Z;
They may have different performance characteristics. It's worth testing. My guess is the second query is unnecessary but it's still conceptually possible to be better.
See these in action in a SQL Fiddle.