SQL Server - aggregate if only one distinct value + nulls without ansi warnings - sql

Suppose I have a data like this
first_name last_name city
John Bon Jovi null
John Lennon null
John Deer null
And I want to create aggregating query which will return json which looks like this
{ "first_name": "John", "city": null }
Essentially, the query should check if there's only one distinct value within each column and if it is, put this value to json. All non-null columns are relatively easy to get with a query like this:
select
case when count(distinct first_name) = 1 then max(first_name) end as first_name,
case when count(distinct last_name) = 1 then max(last_name) end as last_name,
case when count(distinct city) = 1 then max(city) end as city
from ...
for json path, without_array_wrapper
or
select
case when max(first_name) = min(first_name) then max(first_name) end as first_name,
case when max(last_name) = min(last_name) then max(last_name) end as last_name,
case when max(city) = min(city) then max(city) end as city
from ...
for json path, without_array_wrapper
The result of the queries above is json like this {"first_name":"John"}. But then there are problems with nulls. Problem (1) - queries above do not take nulls into account, so if I have data like this
first_name last_name city
----------------------------------
John Lennon null
John Lennon null
John null null
Then last name is also included in the resulting json
{ "first_name": "John", "last_name": "Lennon" }
Ok, that's understandable (cause ...Null value is eliminated by an aggregate...) and I can solve it with a query like this:
select
case when count(distinct first_name) = 1 and count(first_name) = count(*) then max(first_name) end as first_name,
case when count(distinct last_name) = 1 and count(last_name) = count(*) then max(last_name) end as last_name,
case when count(distinct city) = 1 and count(city) = count(*) then max(city) end as city
from ...
for json path, without_array_wrapper
But there are other problems with nulls I can't really solve neatly for now. Problem (2) - I want to have also "city":null in my json. Of course I can do something like this
...
case when count(city) = 0 then 'null' end as city
...
and then replace string null with real nulls, but it's not very neat. Another annoying thing is (3) - I'd really like to get rid of warnings
Warning: Null value is eliminated by an aggregate or other SET operation.
without turning ANSI_WARNINGS off. For now I can only think about using some placeholders with isnull which doesn't look like a clean solution
...
case when count(distinct isnull(city, 'null')) = 1 then max(city) end as city
...
So, any ideas on how to elegantly solve problems (2) and (3)? see examples in db<>fiddle.

Ok, so nobody posted any answers so far, I have thought of one way doing it. It's not perfect, but it seems to work.
So the idea is to use #var = #var + 1 trick inside of select. But it should be a bit more complicated:
declare
#first_name varchar(4), #first_name_state tinyint = 0,
#last_name varchar(4), #last_name_state tinyint = 0,
#city varchar(4), #city_state tinyint = 0,
#country varchar(10), #country_state tinyint = 0,
#result nvarchar(max) = '{}';
select
#first_name_state =
case
when #first_name_state = 0 then 1
when #first_name_state = 1 and #first_name = t.first_name then 1
when #first_name_state = 1 and #first_name is null and t.first_name is null then 1
else 2
end,
#first_name = t.first_name,
#last_name_state =
case
when #last_name_state = 0 then 1
when #last_name_state = 1 and #last_name = t.last_name then 1
when #last_name_state = 1 and #last_name is null and t.last_name is null then 1
else 2
end,
#last_name = t.last_name,
#city_state =
case
when #city_state = 0 then 1
when #city_state = 1 and #city = t.city then 1
when #city_state = 1 and #city is null and t.city is null then 1
else 2
end,
#city = t.city,
#country_state =
case
when #country_state = 0 then 1
when #country_state = 1 and #country = t.country then 1
when #country_state = 1 and #country is null and t.country is null then 1
else 2
end,
#country = t.country
from Table1 as t;
if #first_name_state = 1
set #result = json_modify(json_modify(#result,'$.first_name','null'),'strict $.first_name',#first_name);
if #last_name_state = 1
set #result = json_modify(json_modify(#result,'$.last_name','null'),'strict $.last_name',#last_name);
if #city_state = 1
set #result = json_modify(json_modify(#result,'$.city','null'),'strict $.city',#city);
if #country_state = 1
set #result = json_modify(json_modify(#result,'$.country','null'),'strict $.country',#country);
select #result;
----------------------------------
{"first_name":"John","city":null}
see db<>fiddle with examples.
Please note that, according to Microsoft docs you shouldn't use this variable aggregation assignment trick cause some of the statements can be called more than once.
Don't use a variable in a SELECT statement to concatenate values (that
is, to compute aggregate values). Unexpected query results may occur.
Because, all expressions in the SELECT list (including assignments)
aren't necessarily run exactly once for each output row.
I hope in this case it should work fine cause it's not exactly an aggregation, and it's ok if these statements will be called more than once per row.
Still, you can find some useful links in this answer.

Related

WHERE clause match or null

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!

SQL Server - Using CASE statement

I have a SELECT statement with a WHERE clause that I want to dynamically change depending if a parameter is supplied or not.
I can't seem to understand how to use CASE statement in a WHERE clause but this is how I want it to look like using an IF statement.
DECLARE #Gender NVARCHAR(100) = NULL --this is an INPUT parameter and may or may not be NULL
DECLARE #Status NVARCHAR(100) = NULL --this is an INPUT parameter and may or may not be NULL
SELECT Name
FROM Person
WHERE
-- first WHERE clause
IF #Gender IS NULL
BEGIN
Gender IS NULL
END
ELSE
BEGIN
Gender = #Gender
END
AND
-- second WHERE clause
IF #Status IS NULL
BEGIN
Status IS NULL
END
ELSE
BEGIN
Status LIKE '%' + #Status + '%'
END
Is it possible to transform this code into a CASE statement?
I think you want:
select p.name
from person p
where ( (#gender is null and gender is null) or gender = #gender) and
( (#status is null and status is null) or status = #status);
Note that this does "null-matching". Often, people want to use NULL to select all records, not just the NULL ones. If that is what you intend, then:
select p.name
from person p
where ( #gender is null or gender = #gender) and
( #status is null or status = #status);
In either situation, case is not needed in the where. As a general rule, don't use case in where -- unless you really need it to control the order of evaluation of expressions.
You can do this:
SELECT Name
FROM Person
WHERE Gender = COALESCE(#gender, Gender)
AND (#Status is null or Status like '%' + #status + '%')
DECLARE #Gender NVARCHAR(100) = NULL --this is an INPUT parameter and may or may not be NULL
DECLARE #Status NVARCHAR(100) = NULL --this is an INPUT parameter and may or may not be NULL
SELECT Name
FROM Person
WHERE CASE WHEN #Gender IS NULL THEN 1
WHEN #Gender = ISNULL(Gender, '') THEN 1
ELSE 0
END = 1
AND CASE WHEN #Status IS NULL THEN 1
WHEN ISNULL(Status, '') LIKE '%' + #Status + '%' THEN 1
ELSE 0
END = 1

SQL Query with optional aggregate and group possible?

I need to run a huge query with the option to sum up one column. I'm wondering if it would be possible to do something like the following:
declare #sumIt bit
set #sumIt = 1
select ID, Name, CASE WHEN #sumIt=1 THEN sum(Time) ELSE Time END [timeCol]
from Visits
where ID = 123
Group by ID, Name, CASE WHEN #sumIt=1 THEN '' ELSE Time END
Right now I get an error:
Msg 8120, Level 16, State 1, Line 4
Column 'Visits.Time' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
I think you can do the following:
declare #sumIt bit
set #sumIt = 1
select ID, Name,
(CASE WHEN #sumIt=1 THEN sum(Time) ELSE min(Time) END) [timeCol]
from Visits
where ID = 123
Group by ID, Name, (CASE WHEN #sumIt=1 THEN '' ELSE Time END)
You can apply an aggregation function to the group by variables. This isn't commonly done, but it solves your problem.
I haven't tested it, but this should work fine:
select ID, Name, sum([Time]) [timeCol]
from Visits
where ID = 123
AND #sumint = 1
Group by ID, Name
UNION ALL
select ID, Name, [Time] [timeCol]
from Visits
where ID = 123
AND #sumint = 0
Group by ID, Name, [Time]
I guess you could take the dynamic sql approach. Not crazy about it myself, i'd just use the IF
declare #sumIt bit, #sql VARCHAR(1000)
set #sumIt = 0
SET #sql = 'select ID, Name, ' + CASE WHEN #sumIt=1 THEN 'sum(Time)' ELSE 'Time' END + ' [timeCol]
from Visits
where ID = 123
Group by ID, Name' + CASE WHEN #sumIt=1 THEN '' ELSE ',Time' END
EXEC #sql

SQL and ALL operator

Looking for an elegant way to workaround this...
DECLARE #ZIP INT
SET #ZIP = 55555
IF #ZIP = ALL(SELECT ZIP FROM PEOPLE WHERE PERSONTYPE = 1)
PRINT 'All people of type 1 have the same zip!'
ELSE
PRINT 'Not All people of type 1 have the same zip!'
The issue is that, if (SELECT ZIP FROM PEOPLE WHERE PERSONTYPE = 1) returns no records, then the above IF evaluates to true. I'm looking for a way to make this evaluate to false when there are no records returned by the ALL's subquery.
My current solution:
DECLARE #ZIP INT
SET #ZIP = 55555
DECLARE #ALLZIPS TABLE (INT ZIP)
INSERT INTO #ALLZIPS
SELECT ZIP FROM PEOPLE WHERE PERSONTYPE = 1
IF EXISTS(SELECT TOP 1 * FROM #ALLZIPS) AND (#ZIP = ALL (SELECT ZIP FROM #ALLZIPS))
PRINT 'All people of type 1 have the same zip!'
ELSE
PRINT 'Not All people of type 1 have the same zip!'
Use:
IF EXISTS(SELECT NULL
FROM PEOPLE p
WHERE p.persontype = 1
HAVING MIN(p.zip) = #Zip
AND MAX(p.zip) = #Zip)
PRINT 'All people of type 1 have the same zip!'
ELSE
PRINT 'Not All people of type 1 have the same zip!'
Jumping in:
IF (SELECT SUM(CASE WHEN ZIP = #ZIP THEN 0 ELSE 1 END)
FROM PEOPLE WHERE PERSONTYPE = 1) = 0
PRINT 'All people of type 1 have the same zip!'
ELSE
PRINT 'Not All people of type 1 have the same zip!'
Consider using EXISTS as well.
IF #ZIP = ALL(SELECT ZIP FROM PEOPLE WHERE PERSONTYPE = 1)
AND EXISTS(SELECT 1 FROM PEOPLE WHERE PERSONTYPE = 1)

SQL Switch/Case in 'where' clause

I tried searching around, but I couldn't find anything that would help me out.
I'm trying to do this in SQL:
declare #locationType varchar(50);
declare #locationID int;
SELECT column1, column2
FROM viewWhatever
WHERE
CASE #locationType
WHEN 'location' THEN account_location = #locationID
WHEN 'area' THEN xxx_location_area = #locationID
WHEN 'division' THEN xxx_location_division = #locationID
I know that I shouldn't have to put '= #locationID' at the end of each one, but I can't get the syntax even close to being correct. SQL keeps complaining about my '=' on the first WHEN line...
How can I do this?
declare #locationType varchar(50);
declare #locationID int;
SELECT column1, column2
FROM viewWhatever
WHERE
#locationID =
CASE #locationType
WHEN 'location' THEN account_location
WHEN 'area' THEN xxx_location_area
WHEN 'division' THEN xxx_location_division
END
without a case statement...
SELECT column1, column2
FROM viewWhatever
WHERE
(#locationType = 'location' AND account_location = #locationID)
OR
(#locationType = 'area' AND xxx_location_area = #locationID)
OR
(#locationType = 'division' AND xxx_location_division = #locationID)
Here you go.
SELECT
column1,
column2
FROM
viewWhatever
WHERE
CASE
WHEN #locationType = 'location' AND account_location = #locationID THEN 1
WHEN #locationType = 'area' AND xxx_location_area = #locationID THEN 1
WHEN #locationType = 'division' AND xxx_location_division = #locationID THEN 1
ELSE 0
END = 1
I'd say this is an indicator of a flawed table structure. Perhaps the different location types should be separated in different tables, enabling you to do much richer querying and also avoid having superfluous columns around.
If you're unable to change the structure, something like the below might work:
SELECT
*
FROM
Test
WHERE
Account_Location = (
CASE LocationType
WHEN 'location' THEN #locationID
ELSE Account_Location
END
)
AND
Account_Location_Area = (
CASE LocationType
WHEN 'area' THEN #locationID
ELSE Account_Location_Area
END
)
And so forth... We can't change the structure of the query on the fly, but we can override it by making the predicates equal themselves out.
EDIT: The above suggestions are of course much better, just ignore mine.
The problem with this is that when the SQL engine goes to evaluate the expression, it checks the FROM portion to pull the proper tables, and then the WHERE portion to provide some base criteria, so it cannot properly evaluate a dynamic condition on which column to check against.
You can use a WHERE clause when you're checking the WHERE criteria in the predicate, such as
WHERE account_location = CASE #locationType
WHEN 'business' THEN 45
WHEN 'area' THEN 52
END
so in your particular case, you're going to need put the query into a stored procedure or create three separate queries.
OR operator can be alternative of case when in where condition
ALTER PROCEDURE [dbo].[RPT_340bClinicDrugInventorySummary]
-- Add the parameters for the stored procedure here
#ClinicId BIGINT = 0,
#selecttype int,
#selectedValue varchar (50)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SELECT
drugstock_drugname.n_cur_bal,drugname.cdrugname,clinic.cclinicname
FROM drugstock_drugname
INNER JOIN drugname ON drugstock_drugname.drugnameid_FK = drugname.drugnameid_PK
INNER JOIN drugstock_drugndc ON drugname.drugnameid_PK = drugstock_drugndc.drugnameid_FK
INNER JOIN drugndc ON drugstock_drugndc.drugndcid_FK = drugndc.drugid_PK
LEFT JOIN clinic ON drugstock_drugname.clinicid_FK = clinic.clinicid_PK
WHERE (#ClinicId = 0 AND 1 = 1)
OR (#ClinicId != 0 AND drugstock_drugname.clinicid_FK = #ClinicId)
-- Alternative Case When You can use OR
AND ((#selecttype = 1 AND 1 = 1)
OR (#selecttype = 2 AND drugname.drugnameid_PK = #selectedValue)
OR (#selecttype = 3 AND drugndc.drugid_PK = #selectedValue)
OR (#selecttype = 4 AND drugname.cdrugclass = 'C2')
OR (#selecttype = 5 AND LEFT(drugname.cdrugclass, 1) = 'C'))
ORDER BY clinic.cclinicname, drugname.cdrugname
END
Please try this query.
Answer To above post:
select #msgID, account_id
from viewMailAccountsHeirachy
where
CASE #smartLocationType
WHEN 'store' THEN account_location
WHEN 'area' THEN xxx_location_area
WHEN 'division' THEN xxx_location_division
WHEN 'company' THEN xxx_location_company
END = #smartLocation
Try this:
WHERE (
#smartLocationType IS NULL
OR account_location = (
CASE
WHEN #smartLocationType IS NOT NULL
THEN #smartLocationType
ELSE account_location
END
)
)
CREATE PROCEDURE [dbo].[Temp_Proc_Select_City]
#StateId INT
AS
BEGIN
SELECT * FROM tbl_City
WHERE
#StateID = CASE WHEN ISNULL(#StateId,0) = 0 THEN 0 ELSE StateId END ORDER BY CityName
END
Try this query, it's very easy and useful: Its ready to execute!
USE tempdb
GO
IF NOT OBJECT_ID('Tempdb..Contacts') IS NULL
DROP TABLE Contacts
CREATE TABLE Contacts(ID INT, FirstName VARCHAR(100), LastName VARCHAR(100))
INSERT INTO Contacts (ID, FirstName, LastName)
SELECT 1, 'Omid', 'Karami'
UNION ALL
SELECT 2, 'Alen', 'Fars'
UNION ALL
SELECT 3, 'Sharon', 'b'
UNION ALL
SELECT 4, 'Poja', 'Kar'
UNION ALL
SELECT 5, 'Ryan', 'Lasr'
GO
DECLARE #FirstName VARCHAR(100)
SET #FirstName = 'Omid'
DECLARE #LastName VARCHAR(100)
SET #LastName = ''
SELECT FirstName, LastName
FROM Contacts
WHERE
FirstName = CASE
WHEN LEN(#FirstName) > 0 THEN #FirstName
ELSE FirstName
END
AND
LastName = CASE
WHEN LEN(#LastName) > 0 THEN #LastName
ELSE LastName
END
GO
In general you can manage case of different where conditions in this way
SELECT *
FROM viewWhatever
WHERE 1=(CASE <case column or variable>
WHEN '<value1>' THEN IIF(<where condition 1>,1,0)
WHEN '<value2>' THEN IIF(<where condition 2>,1,0)
ELSE IIF(<else condition>,1,0)
END)
Case Statement in SQL Server Example
Syntax
CASE [ expression ]
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
...
WHEN condition_n THEN result_n
ELSE result
END
Example
SELECT contact_id,
CASE website_id
WHEN 1 THEN 'TechOnTheNet.com'
WHEN 2 THEN 'CheckYourMath.com'
ELSE 'BigActivities.com'
END
FROM contacts;
OR
SELECT contact_id,
CASE
WHEN website_id = 1 THEN 'TechOnTheNet.com'
WHEN website_id = 2 THEN 'CheckYourMath.com'
ELSE 'BigActivities.com'
END
FROM contacts;
This worked for me.
CREATE TABLE PER_CAL ( CAL_YEAR INT, CAL_PER INT )
INSERT INTO PER_CAL( CAL_YEAR, CAL_PER ) VALUES ( 20,1 ), ( 20,2 ), ( 20,3 ), ( 20,4 ), ( 20,5 ), ( 20,6 ), ( 20,7 ), ( 20,8 ), ( 20,9 ), ( 20,10 ), ( 20,11 ), ( 20,12 ),
( 99,1 ), ( 99,2 ), ( 99,3 ), ( 99,4 ), ( 99,5 ), ( 99,6 ), ( 99,7 ), ( 99,8 ), ( 99,9 ), ( 99,10 ), ( 99,11 ), ( 99,12 )
The 4 digit century is determined by the rule, if the year is 50 or more, the century is 1900, otherwise 2000.
Given two 6 digit periods that mark the start and end period, like a quarter, return the rows that fall in that range.
-- 1st quarter of 2020
SELECT * FROM PER_CAL WHERE (( CASE WHEN CAL_YEAR > 50 THEN 1900 ELSE 2000 END + CAL_YEAR ) * 100 + CAL_PER ) BETWEEN 202001 AND 202003
-- 4th quarter of 1999
SELECT * FROM PER_CAL WHERE (( CASE WHEN CAL_YEAR > 50 THEN 1900 ELSE 2000 END + CAL_YEAR ) * 100 + CAL_PER ) BETWEEN 199910 AND 199912
Try this query. Its very easy to understand:
CREATE TABLE PersonsDetail(FirstName nvarchar(20), LastName nvarchar(20), GenderID int);
GO
INSERT INTO PersonsDetail VALUES(N'Gourav', N'Bhatia', 2),
(N'Ramesh', N'Kumar', 1),
(N'Ram', N'Lal', 2),
(N'Sunil', N'Kumar', 3),
(N'Sunny', N'Sehgal', 1),
(N'Malkeet', N'Shaoul', 3),
(N'Jassy', N'Sohal', 2);
GO
SELECT FirstName, LastName, Gender =
CASE GenderID
WHEN 1 THEN 'Male'
WHEN 2 THEN 'Female'
ELSE 'Unknown'
END
FROM PersonsDetail