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

孤街浪徒 提交于 2019-12-11 06:06:24

问题


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.


回答1:


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.



来源:https://stackoverflow.com/questions/58288057/sql-server-aggregate-if-only-one-distinct-value-nulls-without-ansi-warnings

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!