Oracle select latest record if another column is null - sql

I have two columns for example. A is a Varchar2 and B is a date column.
Data sample:
A B
---------- -----------
10/27/15
10/28/15 Y
How do I write a sql which will check max(A) record if B is NULL, and if it is then only select it. So in this scenario no rows should return.
I hope that made sense.

Here is one method:
select s.*
from (select s.*
from (select s.*
from sample s
order by a desc
) s
where rownum = 1
) s
where s.B is null;
This is slightly simpler in Oracle 12+:
select s.*
from (select s.*
from sample s
order by a desc
fetch first 1 row only
) s
where s.B is null;
An alternative uses conditional aggregation:
select max(a)
from sample
having max(a) = max(case when b is null then a end);

Related

How to return two values from PostgreSQL subquery?

I have a problem where I need to get the last item across various tables in PostgreSQL.
The following code works and returns me the type of the latest update and when it was last updated.
The problem is, this query needs to be used as a subquery, so I want to select both the type and the last updated value from this query and PostgreSQL does not seem to like this... (Subquery must return only one column)
Any suggestions?
SELECT last.type, last.max FROM (
SELECT MAX(a.updated_at), 'a' AS type FROM table_a a WHERE a.ref = 5 UNION
SELECT MAX(b.updated_at), 'b' AS type FROM table_b b WHERE b.ref = 5
) AS last ORDER BY max LIMIT 1
Query is used like this inside of a CTE;
WITH sql_query as (
SELECT id, name, address, (...other columns),
last.type, last.max FROM (
SELECT MAX(a.updated_at), 'a' AS type FROM table_a a WHERE a.ref = 5 UNION
SELECT MAX(b.updated_at), 'b' AS type FROM table_b b WHERE b.ref = 5
) AS last ORDER BY max LIMIT 1
FROM table_c
WHERE table_c.fk_id = 1
)
The inherent problem is that SQL (all SQL not just Postgres) requires that a subquery used within a select clause can only return a single value. If you think about that restriction for a while it does makes sense. The select clause is returning rows and a certain number of columns, each row.column location is a single position within a grid. You can bend that rule a bit by putting concatenations into a single position (or a single "complex type" like a JSON value) but it remains a single position in that grid regardless.
Here however you do want 2 separate columns AND you need to return both columns from the same row, so instead of LIMIT 1 I suggest using ROW_NUMBER() instead to facilitate this:
WITH LastVals as (
SELECT type
, max_date
, row_number() over(order by max_date DESC) as rn
FROM (
SELECT MAX(a.updated_at) AS max_date, 'a' AS type FROM table_a a WHERE a.ref = 5
UNION ALL
SELECT MAX(b.updated_at) AS max_date, 'b' AS type FROM table_b b WHERE b.ref = 5
)
)
, sql_query as (
SELECT id
, name, address, (...other columns)
, (select type from lastVals where rn = 1) as last_type
, (select max_date from lastVals where rn = 1) as last_date
FROM table_c
WHERE table_c.fk_id = 1
)
----
By the way in your subquery you should use UNION ALL with type being a constant like 'a' or 'b' then even if MAX(a.updated_at) was identical for 2 or more tables, the rows would still be unique because of the difference in type. UNION will attempt to remove duplicate rows but here it just isn't going to help, so avoid that wasted effort by using UNION ALL.
----
For another way to skin this cat, consider using a LEFT JOIN instead
SELECT id
, name, address, (...other columns)
, lastVals.type
, LastVals.last_date
FROM table_c
WHERE table_c.fk_id = 1
LEFT JOIN (
SELECT type
, last_date
, row_number() over(order by last_date DESC) as rn
FROM (
SELECT MAX(a.updated_at) AS last_date, 'a' AS type FROM table_a a WHERE a.ref = 5
UNION ALL
SELECT MAX(b.updated_at) AS last_date, 'b' AS type FROM table_b b WHERE b.ref = 5
)
) LastVals ON LastVals.rn = 1

LAG functions and NULLS

How can I tell the LAG function to get the last "not null" value?
For example, see my table bellow where I have a few NULL values on column B and C.
I'd like to fill the nulls with the last non-null value. I tried to do that by using the LAG function, like so:
case when B is null then lag (B) over (order by idx) else B end as B,
but that doesn't quite work when I have two or more nulls in a row (see the NULL value on column C row 3 - I'd like it to be 0.50 as the original).
Any idea how can I achieve that?
(it doesn't have to be using the LAG function, any other ideas are welcome)
A few assumptions:
The number of rows is dynamic;
The first value will always be non-null;
Once I have a NULL, is NULL all up to the end - so I want to fill it with the latest value.
Thanks
You can do it with outer apply operator:
select t.id,
t1.colA,
t2.colB,
t3.colC
from table t
outer apply(select top 1 colA from table where id <= t.id and colA is not null order by id desc) t1
outer apply(select top 1 colB from table where id <= t.id and colB is not null order by id desc) t2
outer apply(select top 1 colC from table where id <= t.id and colC is not null order by id desc) t3;
This will work, regardless of the number of nulls or null "islands". You may have values, then nulls, then again values, again nulls. It will still work.
If, however the assumption (in your question) holds:
Once I have a NULL, is NULL all up to the end - so I want to fill it with the latest value.
there is a more efficient solution. We only need to find the latest (when ordered by idx) values. Modifying the above query, removing the where id <= t.id from the subqueries:
select t.id,
colA = coalesce(t.colA, t1.colA),
colB = coalesce(t.colB, t2.colB),
colC = coalesce(t.colC, t3.colC)
from table t
outer apply (select top 1 colA from table
where colA is not null order by id desc) t1
outer apply (select top 1 colB from table
where colB is not null order by id desc) t2
outer apply (select top 1 colC from table
where colC is not null order by id desc) t3;
You could make a change to your ORDER BY, to force the NULLs to be first in your ordering, but that may be expensive...
lag(B) over (order by CASE WHEN B IS NULL THEN -1 ELSE idx END)
Or, use a sub-query to calculate the replacement value once. Possibly less expensive on larger sets, but very clunky.
- Relies on all the NULLs coming at the end
- The LAG doesn't rely on that
COALESCE(
B,
(
SELECT
sorted_not_null.B
FROM
(
SELECT
table.B,
ROW_NUMBER() OVER (ORDER BY table.idx DESC) AS row_id
FROM
table
WHERE
table.B IS NOT NULL
)
sorted_not_null
WHERE
sorted_not_null.row_id = 1
)
)
(This should be faster on larger data-sets, than LAG or using OUTER APPLY with correlated sub-queries, simply because the value is calculated once. For tidiness, you could calculate and store the [last_known_value] for each column in variables, then just use COALESCE(A, #last_known_A), COALESCE(B, #last_known_B), etc)
if it is null all the way up to the end then can take a short cut
declare #b varchar(20) = (select top 1 b from table where b is not null order by id desc);
declare #c varchar(20) = (select top 1 c from table where c is not null order by id desc);
select is, isnull(b,#b) as b, insull(c,#c) as c
from table;
Select max(diff) from(
Select
Case when lag(a) over (order by b) is not null
Then (a -lag(a) over (order by b)) end as diff
From <tbl_name> where
<relevant conditions>
Order by b) k
Works fine in db visualizer.
UPDATE table
SET B = (#n := COALESCE(B , #n))
WHERE B is null;

How to exclude some (not all) record that has same values

After query table A using first query, I have these records:
pID cID code
1 1 A
1 1 B
1 1 B
1 1 B
After query table B using second query, I have one record:
pID cID code
1 1 B
1 1 B
I want table A exclude the records of table B. The result is:
pID cID code
1 1 A
1 1 A
How can I do that? Hope u could help me. thanks.
Updating...
Sorry for the example to make you confuse
If I got these record from second table:
pID cID code
1 1 B
Then the result I want is (exclude one record):
pID cID code
1 1 A
1 1 B
1 1 B
you try GROUP BY function in your Query
example :
select pID,cID,code from table group by code
using EXCEPT and row_number() to generate a unique no
;with cte1 as
(
select *, rn = row_number() over (partition by pID, cID, code order by pID, cID, code)
from query1
),
cte2 as
(
select *, rn = row_number() over (partition by pID, cID, code order by pID, cID, code)
from query2
)
select *
from cte1
except
select *
from cte2
Based on your question, which I think you want to delete the records from B which occur more than once in A:
first select all records from A which are not there in B and then union them 1 distinct records which are there in both A and B:
select * from A
except
select * from B
union all
select distinct *
from
(select a.pid, a.cid, a.code
from
A
inner join
B
on a.pid=b.pid and a.cid=b.cid and a.code=b.code)
Just use EXCEPT. How ever your desired output is wrong as 1 1 B also the same record from TableB
SELECT * FROM TABLE_A
EXCEPT
SELECT * FROM TABLE_B
Refer this Link
If your case NOt all But some then.
Simply you can use DISTINCT
As per the UPdate in Question (From what I understood)
SELECT DISTINCT * FROM TABLE_A
UNION ALL
SELECT * FROM TABLE_B

SQL: Concatenate sequential integer values

I have a column like this:
ID
--------
1
2
3
4
5
7
10
and I want to get the following resultset:
ID
--------
1-5
7
10
Is there a way to achieve this with (Oracle) SQL only?
Yes:
select (case when min(id) < max(id)
then cast(min(id) as varchar2(255)) || '-' || cast(max(id) as varchar2(255))
else cast(min(id) as varchar2(255))
end)
from (select id, id - rownum as grp
from t
order by id
) t
group by grp
order by min(id);
Here is a SQL Fiddle demonstrating it.
The idea behind the query is that subtracting rownum from a sequence of numbers results in a constant. You can use the constant for grouping.
self joins are necessary... I think this will work
Select a.id, b.id
From table a -- to get beginning of each range
Join table b -- to get end of each range
On b.id >= a.Id -- guarantees that b is after a
And Not exists (Select * From table m -- this guarantees all values between
where id Between a.id+1 and b.id
And Not exists(Select * From table
Where id = m.id-1))
And Not exists (Select * From table -- this guarantees that table a is start
Where id = a.id -1)
And Not exists (Select * From table -- this guarantees that table b is end
Where id = b.id + 1)

How to get Original Rows filtered by a HAVING Condition?

What is the method in T-SQL to select the orginal values limited by a HAVING attribute. For example, if I have
A|B
10|1
11|2
10|3
How would I get all the values of B (Not An Average or some other summary stat), Grouped by A, having a Count (Occurrences of A) greater than or equal two 2?
Actually, you have several options to choose from
1. You could make a subquery out of your original having statement and join it back to your table
SELECT *
FROM YourTable yt
INNER JOIN (
SELECT A
FROM YourTable
GROUP BY
A
HAVING COUNT(*) >= 2
) cnt ON cnt.A = yt.A
2. another equivalent solution would be to use a WITH clause
;WITH cnt AS (
SELECT A
FROM YourTable
GROUP BY
A
HAVING COUNT(*) >= 2
)
SELECT *
FROM YourTable yt
INNER JOIN cnt ON cnt.A = yt.A
3. or you could use an IN statement
SELECT *
FROM YourTable yt
WHERE A IN (SELECT A FROM YourTable GROUP BY A HAVING COUNT(*) >= 2)
A self join will work:
select B
from table
join(
select A
from table
group by 1
having count(1)>1
)s
using(A);
You can use window function (no joins, only one table scan):
select * from (
select *, cnt=count(*) over(partiton by A) from table
) as a
where cnt >= 2