Easiest way to eliminate NULLs in SELECT DISTINCT? - sql

I am working on a query that is fairly similar the following:
CREATE TABLE #test (a char(1), b char(1))
INSERT INTO #test(a,b) VALUES
('A',NULL),
('A','B'),
('B',NULL),
('B',NULL)
SELECT DISTINCT a,b FROM #test
DROP TABLE #test
The result is, unsurprisingly,
a b
-------
A NULL
A B
B NULL
The output I would like to see in actuality is:
a b
-------
A B
B NULL
That is, if a column has a value in some records but not in others, I want to throw out the row with NULL for that column. However, if a column has a NULL value for all records, I want to preserve that NULL.
What's the simplest/most elegant way to do this in a single query?
I have a feeling that this would be simple if I weren't exhausted on a Friday afternoon.

Try this:
select distinct * from test
where b is not null or a in (
select a from test
group by a
having max(b) is null)
You can get the fiddle here.
Note if you can only have one non-null value in b, this can be simplified to:
select a, max(b) from test
group by a

Try this:
create table test(
x char(1),
y char(1)
);
insert into test(x,y) values
('a',null),
('a','b'),
('b', null),
('b', null)
Query:
with has_all_y_null as
(
select x
from test
group by x
having sum(case when y is null then 1 end) = count(x)
)
select distinct x,y from test
where
(
-- if a column has a value in some records but not in others,
x not in (select x from has_all_y_null)
-- I want to throw out the row with NULL
and y is not null
)
or
-- However, if a column has a NULL value for all records,
-- I want to preserve that NULL
(x in (select x from has_all_y_null))
order by x,y
Output:
X Y
A B
B NULL
Live test: http://sqlfiddle.com/#!3/259d6/16
EDIT
Seeing Mosty's answer, I simplified my code:
with has_all_y_null as
(
select x
from test
group by x
-- having sum(case when y is null then 1 end) = count(x)
-- should have thought of this instead of the code above. Mosty's logic is good:
having max(y) is null
)
select distinct x,y from test
where
y is not null
or
(x in (select x from has_all_y_null))
order by x,y
I just prefer CTE approach, it has a more self-documenting logic :-)
You can also put documentation on non-CTE approach, if you are conscious of doing so:
select distinct * from test
where b is not null or a in
( -- has all b null
select a from test
group by a
having max(b) is null)

;WITH CTE
AS
(
SELECT DISTINCT * FROM #test
)
SELECT a,b
FROM CTE
ORDER BY CASE WHEN b IS NULL THEN 9999 ELSE b END ;

SELECT DISTINCT t.a, t.b
FROM #test t
WHERE b IS NOT NULL
OR NOT EXISTS (SELECT 1 FROM #test u WHERE t.a = u.a AND u.b IS NOT NULL)
ORDER BY t.a, t.b

This is a really weird requirement. I wonder how you need it.
SELECT DISTINCT a, b
FROM test t
WHERE NOT ( b IS NULL
AND EXISTS
( SELECT *
FROM test ta
WHERE ta.a = t.a
AND ta.b IS NOT NULL
)
)
AND NOT ( a IS NULL
AND EXISTS
( SELECT *
FROM test tb
WHERE tb.b = t.b
AND tb.a IS NOT NULL
)
)

Well, I don't particularly like this solution, but it seems the most appropriate to me. Note that your description of what you want sounds exactly like what you get with a LEFT JOIN, so:
SELECT DISTINCT a.a, b.b
FROM #test a
LEFT JOIN #test b ON a.a = b.a
AND b.b IS NOT NULL

SELECT a,b FROM #test t where b is not null
union
SELECT a,b FROM #test t where b is null
and not exists(select 1 from #test where a=t.a and b is not null)
Result:
a b
---- ----
A B
B NULL

I'll just put here a mix of two answers that solved my issue, because my View was more complex
--IdCompe int,
--Nome varchar(30),
--IdVanBanco int,
--IdVan int
--FlagAtivo bit,
--FlagPrincipal bit
select IdCompe
, Nome
, max(IdVanBanco)
, max(IdVan)
, CAST(MAX(CAST(FlagAtivo as INT)) AS BIT) FlagAtivo
, CAST(MAX(CAST(FlagPrincipal as INT)) AS BIT) FlagPrincipal
from VwVanBanco
where IdVan = {IdVan} or IdVan is null
group by IdCompe, Nome order by IdCompe asc
Thanks to mosty mostacho and
kenwarner

Related

Consolidate information (time serie) from two tables

MS SQL Server
I have two tables with different accounts from the same customer:
Table1:
ID
ACCOUNT
FROM
TO
1
A
01.10.2019
01.12.2019
1
A
01.02.2020
09.09.9999
and table2:
ID
ACCOUNT
FROM
TO
1
B
01.12.2019
01.01.2020
As result I want a table that summarize the story of this costumer and shows when he had an active account and when he doesn't.
Result:
ID
FROM
TO
ACTIV Y/N
1
01.10.2019
01.01.2020
Y
1
02.01.2020
31.01.2020
N
1
01.02.2020
09.09.9999
Y
Can someone help me with some ideas how to proceed?
This is the typical gaps and island problem, and it's not usually easy to solve.
You can achieve your goal using this query, I will explain it a little bit.
You can test on this db<>fiddle.
First of all... I have unified your two tables into one to simplify the query.
-- ##table1
select 1 as ID, 'A' as ACCOUNT, convert(date,'2019-10-01') as F, convert(date,'2019-12-01') as T into ##table1
union all
select 1 as ID, 'A' as ACCOUNT, convert(date,'2020-02-01') as F, convert(date,'9999-09-09') as T
-- ##table2
select 1 as ID, 'B' as ACCOUNT, convert(date,'2019-12-01') as F, convert(date,'2020-01-01') as T into ##table2
-- ##table3
select * into ##table3 from ##table1 union all select * from ##table2
You can then get your gaps and island using, for example, a query like this.
It combines recursive cte to generate a calendar (cte_cal) and lag and lead operations to get the previous/next record information to build the gaps.
with
cte_cal as (
select min(F) as D from ##table3
union all
select dateadd(day,1,D) from cte_cal where d < = '2021-01-01'
),
table4 as (
select t1.ID, t1.ACCOUNT, t1.F, isnull(t2.T, t1.T) as T, lag(t2.F, 1,null) over (order by t1.F) as SUP
from ##table3 t1
left join ##table3 t2
on t1.T=t2.F
)
select
ID,
case when T = D then F else D end as "FROM",
isnull(dateadd(day,-1,lead(D,1,null) over (order by D)),'9999-09-09') as "TO",
case when case when T = D then F else D end = F then 'Y' else 'N' end as "ACTIV Y/N"
from (
select *
from cte_cal c
cross apply (
select t.*
from table4 t
where t.SUP is null
and (
c.D = t or
c.D = dateadd(day,1,t.T)
)
) t
union all
select F, * from table4 where T = '9999-09-09'
) p
order by 1
option (maxrecursion 0)
Dates like '9999-09-09' must be treated like exceptions, otherwise I would have to create a calendar until that date, so the query would take long time to resolve.

SQL query to select with Range condition in source table

Have a scenario to select the value from table where range condition is present in source table.
Like,
TableA
ID value condition
1 20 A-M
2 50 N-Z
Select value from TableA where condition = 'C%'
--want to select TableA value from TableB by passing person name starts with like,
-- Here C is item name starts with
-- Should compare with range (A-M) and return first row.
-- Condition column is varchar(3)
I have seen the solution on other way where range can be compared with input value, but here the range is present in the source table. Please help.
If I have understood what you are after correctly you can use
SELECT TOP 1 B.*
FROM TableB B
WHERE B.Name LIKE (SELECT CONCAT('[',condition,']%') FROM TableA WHERE ID =1)
ORDER BY B.Id
If I understand correctly, you should be structuring TableA as:
ID value Lower Upper
1 20 A M
2 50 N Z
Then you want:
select a.*
from tableA a
where left(#name, 1) between a.lower and a.upper;
You can get this to work with your format, by doing:
select a.*
from tableA a
where left(#name, 1) between left(a.condition) and right(a.condition);
But I don't recommend that. Better to store the condition in two columns.
I would use QUOTENAME() function as
SELECT *
FROM TableA
WHERE #Condition LIKE QUOTENAME(Condition);
This will be as
WHERE 'C' LIKE [A-M] --return True
Demo1
Demo2
Always you should try to add data and DDL for setup correctly the test scenario, here my proposed solution:
DECLARE #SourceA AS TABLE
(
ID INT,
Value INT,
Condition VARCHAR(100)
);
INSERT INTO #SourceA ( ID ,
Value ,
Condition
)
VALUES ( 1 , -- ID - int
110 , -- Value - int
'A-M' -- Condition - varchar(100)
),(2,250,'N-Z')
DECLARE #Alphabet VARCHAR(200)='A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z';
; WITH MyCTE AS
(
SELECT ID,Value,Condition, SUBSTRING(#Alphabet, PATINDEX('%'+ LEFT(Condition,1) + '%' ,#Alphabet),(LEN(#Alphabet)-PATINDEX('%'+ RIGHT(Condition,1) + '%' ,#Alphabet))+1) AS FormattedCondition
FROM #SourceA
)
SELECT * FROM MyCTE
WHERE MyCTE.FormattedCondition LIKE '%C%'

oracle sql pairing up elements from collections

i have created 2 types and a table that contains those:
create or replace type a_t as varray(5) of int
create or replace type b_t as varray(5) of int
create table test(
a a_t,
b b_t
)
insert into test Values (
a_t(1,2,3),
b_t(4,5,6)
)
what i want now is to select with a result of
a_t b_t
------|-----
1 | 4
2 | 5
3 | 6
using the table operator on 1 column result in :
select a.* from test t, table(t.a_t) a
a_t
------
1
2
3
but now i dont know how to get the second row and pair them up in the right order
Try something like This.
WITH tab_a
AS (SELECT ROWNUM rn,
a.*
FROM test t,
TABLE ( t.a ) a),
tab_b
AS (SELECT ROWNUM rn,
b.*
FROM test t,
TABLE ( t.b ) b)
SELECT a.column_value a_t,
b.column_value b_t
FROM tab_a a
FULL OUTER JOIN tab_b b
ON a.rn = b.rn;
One way is to use WITH FUNCTION (Oracle 12c):
WITH FUNCTION f1(i INT, s a_t) RETURN INT AS BEGIN RETURN s(i); END;
FUNCTION f2(i INT, s b_t) RETURN INT AS BEGIN RETURN s(i); END;
SELECT s.*
FROM test t
OUTER APPLY (SELECT f1(1, t.a) AS a_t ,f2(1, t.b) AS b_t FROM dual
UNION ALL SELECT f1(2, t.a),f2(2, t.b) FROM dual
UNION ALL SELECT f1(3, t.a),f2(3, t.b) FROM dual)s;
Output:
A_T B_T
1 4
2 5
3 6
db<>fiddle demo

Multiple Columns in an "in" statement

I am using DB 2 and i am trying to write a query which checks multiple columns against a given set of values.Like field a, field b and field c against values x,y,z,f. One way that i can think for is writing same condition 3 times with or i.e. field a in ('x','y','z','f') or field b in .... and so on . Please let me know if there is some other efficient and easy way to accomplish this. I am looking for a query like if any of the condition is true return yes else no . Please suggest !
This may or may not work on as400:
create table a (a int not null, b int not null);
insert into a (a,b) values (1,1),(1,3),(2,3),(0,23);
select a.*
from a
where a in (1,2) or b in (1,2);
A B
----------- -----------
1 1
1 3
2 3
Rewriting as a join:
select a.*
from a
join ( values (1),(2) ) b (x)
on b.x in (a.a, a.b);
A B
----------- -----------
1 1
1 3
2 3
Assuming the column data types are the same, Create a subquery joining all the columns you want to search with your IN into one column with a union
SELECT *
FROM (
SELECT
YOUR_TABLE_PRIMARY_KEY
,A AS Col
FROM YOUR_TABLE
UNION ALL
SELECT
YOUR_TABLE_PRIMARY_KEY
,B AS Col
FROM YOUR_TABLE
UNION ALL
SELECT
YOUR_TABLE_PRIMARY_KEY
,C AS Col
FROM YOUR_TABLE
) AS SQ
WHERE
SQ.Col IN ('x','y','z','f')
Make sure to include the table key so you know which row the data refers to
You can create a regular expression that describe the set of characters and use it with xquery
Assuming you're on a supported version of the OS (tested on 7.1 TR6), this should work...
with sel (val) as (values ('x'),('y'),('f'))
select * from mytbl
where flda in (select val from sel)
or fldb in (select val from sel)
or fldc in (select val from sel)
Expanding on the above since your OP asked for "condition is true return yes else no"
Assuming you've got the key to a row to check, would 'yes' or the empty set be good enough? somekey is the key for the row you want to check.
with sel (val) as (values ('x'),('y'),('f'))
select 'yes' from mytbl
where thekey = somekey
and ( flda in (select val from sel)
or fldb in (select val from sel)
or fldc in (select val from sel)
)
It's actually rather difficult to return a value when you don't have a matching row. Here's one way. Note I've switch to 1=yes, 0=no..
with sel (val) as (values ('x'),('y'),('f'))
select 1 from mytbl
where thekey = somekey
and ( flda in (select val from sel)
or fldb in (select val from sel)
or fldc in (select val from sel)
)
UNION ALL
select 0
from sysibm.sysdummy1
order by 1 desc
fetch first row only

SQL: EXCEPT Query

Here is a basic example of what I am trying to achieve:
create table #testing (
tab varchar(max), a int, b int, c int )
insert into #testing VALUES ('x',1, 2, 3)
insert into #testing VALUES ('y',1, 2, 3)
insert into #testing VALUES ('x', 4, 5, 6)
select * from #testing
Which will Produce the table:
tab a b c
-----------------------
x 1 2 3
y 1 2 3
x 4 5 6
I then want to compare rows on 'tab' based on the values of a,b,c:
select a,b,c from #testing where tab = 'x'
except
select a,b,c from #testing where tab= 'y'
Which gives me the answer I was expecting:
a b c
------------
4 5 6
However I want to also include the Tab column in my resultset, so I want somthing like this:
Select tab,a,b,c from #testing where ????
(select a,b,c from #testing where tab = 'x'
except
select a,b,c from #testing where tab= 'y')
How would I achieve this?
Use not exists:
select a.*
from #testing a
where a.tab = 'x'
and not exists (
select *
from #testing t
where t.a = a.a and t.b = a.b and t.c = a.c and t.tab = 'y'
)
And here you get SQL Fiddle demo: DEMO
Although the answer from #gzaxx does produce a correct result for this test data, the more generalized version is below, where I left 'x' and 'y' out of the statements.
select a.*
from #testing a
where not exists (
select *
from #testing t
where t.a = a.a and t.b = a.b and t.c = a.c and t.tab <> a.tab
)
Please Try it
with cte as
(
select *,rn = ROW_NUMBER() over(PARTITION by tab order by tab)from #testing
)
select tab,a,b,c from cte where rn>1