ORACLE - Problem understanding NOT EXISTS operator - sql

I ran into a simple bit of code, but I've been wracking my brains ever since.
It's either counter-intuitive, or I'm missing something, either simple or fundamental. No idea.
WITH T1 AS
(
SELECT TO_DATE ('14/11/19', 'DD/MM/YY') as SomeDate FROM DUAL
UNION
SELECT TO_DATE ('28/11/19', 'DD/MM/YY') as SomeDate FROM DUAL
),
T2 AS
(
SELECT TO_DATE ('14/11/19', 'DD/MM/YY') as SomeDate FROM DUAL
UNION
SELECT TO_DATE ('28/11/19', 'DD/MM/YY') as SomeDate FROM DUAL
)
SELECT * FROM T1 WHERE T1.SomeDate >= TO_DATE ('05/11/19','DD/MM/YY')
AND NOT EXISTS
(SELECT 1 FROM T2
WHERE T2.SomeDate >= TO_DATE ('05/11/19','DD/MM/YY')
AND T2.Somedate < T1.Somedate
);
Excluding all other conditions, the basic version does something like the code above.
The subquery returns all rows where SomeDate = 14/11/19 since those are the ones that are greater than the 5th but strictly lower than the 28th.
And Not Exists should evaluate to false when rows are returned.
So by my logic, that means that excluding all rows with 14/11/19, the code above should return 28/11/19.
But it returns '14/11/2019'.
Could someone please explain what I'm missing?
Thank you very much.

I would write the query like this so I can read it more easily:
WITH T1 AS (
SELECT DATE '2019-11-14' as SomeDate FROM DUAL UNION ALL
SELECT DATE '2019-11-28' as SomeDate FROM DUAL
),
T2 AS (
SELECT DATE '2019-11-14' as SomeDate FROM DUAL UNION ALL
SELECT DATE '2019-11-28' as SomeDate FROM DUAL
)
SELECT *
FROM T1
WHERE T1.SomeDate >= DATE '2019-11-05' AND
NOT EXISTS (SELECT 1
FROM T2
WHERE T2.SomeDate >= DATE '2019-11-05' AND
T2.Somedate < T1.Somedate
);
I'm not sure where your confusion is. There are two possibilities for T1.SomeDate, 2019-11-14 and 2019-11-18.
Only the earlier date (2019-11-14) has no rows in T2 that are less than the date. Hence, NOT EXISTS matches this date.
I suspect that you are confused by the inequality in the subquery.

Related

how can I use subquery in CASE THEN?

I tried to use subquery in CASE THEN like below.(I wrote down the code in ORACLE)
WITH SAMPLE AS (
SELECT 1 AS VALUE_,
'20191230' AS DATE_
FROM DUAL
UNION
SELECT 2 AS VALUE_,
'20201230' AS DATE_
FROM DUAL)
SELECT VALUE_,
DATE_,
CASE
WHEN TO_CHAR(ADD_MONTHS(DATE, -12), 'YYYYMMDD') IN (SELECT DATE_ FROM SAMPLE)
THEN (SELECT VALUE_ FROM SAMPLE WHERE DATE_ = TO_CHAR(ADD_MONTHS(DATE, -12),
'YYYYMMDD'))
ELSE NULL
END AS ONE_YEAR_AGO_VALUE_
FROM SAMPLE;
I expected to get a new column(ONE_YEAR_AGO_VALUE_) that has NULL in first row and 1 in second row.
However, result was different then I expected.
The result had a 'ONE_YEAR_AGO_VALUE_' that had NULL in first row and NULL in second row.
I want to make second row have 1.
If you let me know the wrong thing in my code, I really appreciate that.
Thank you.
When I fix your query to have correct use of dates, then it produces one row with NULL and one with 1:
WITH SAMPLE AS (
SELECT 1 AS VALUE_, DATE '2019-12-30' AS DATE_
FROM DUAL
UNION ALL
SELECT 2 AS VALUE_, DATE '2020-12-30' AS DATE_
FROM DUAL
)
SELECT S.VALUE_, S.DATE_,
(CASE WHEN ADD_MONTHS(S.DATE_, -12) IN (SELECT S2.DATE_ FROM SAMPLE S2)
THEN (SELECT S2.VALUE_ FROM SAMPLE S2 WHERE S2.DATE_ = ADD_MONTHS(S.DATE_, -12))
END) AS ONE_YEAR_AGO_VALUE_
FROM SAMPLE S;
Your code is also overly complicated. You don't need a CASE expression, which you can use directly for the value. Here is a db<>fiddle.

Other way to rewrite/improve this query

Is there other way to rewrite/improve this query, trying to make it with less typo and if possible improve performance:
Select
(Select Sum(value) from table1
where code = 'B2'
and date between DATE '2017-01-01'
and DATE '2017-03-31')
+
(Select Sum(value) from table2
where code = 'B2'
and date between DATE '2017-04-01'
and DATE '2017-04-30')
I also tried with union all but this still is not what I need:
Select Sum(value)
from (Select code, value from table1
Where date between DATE '2017-01-01'
and DATE '2017-03-31')
union all
(Select code, value from table1
Where date between DATE '2017-04-01'
and DATE '2017-04-30')
where code = 'B2'
Thanks
Your first query is fine . . . assuming you have a from dual at the end.
For performance, you want indexes on table1(code, date, value) and table2(code, date, value). Note that the order of the columns in the indexes is important.
If, with typo you mean that you have the criteria code = 'B2' twice in your query, you can move it to your from clause. Anyway, be aware that a subquery can return NULL. Use NVL (or COALESCE) to deal with this.
select
nvl((select sum(value) from table1
where code = x.code and date between date '2017-01-01' and date '2017-03-31'), 0)
+
nvl((select sum(value) from table2
where code = x.code and date between date '2017-04-01' and date '2017-04-30'), 0)
from (select 'B2' as code from dual) x;

How to convert partial dates in Oracle SQL

I have 2 date columns in 2 diff tables that I need to compare, both varchar2 type. Both columns have partial and full dates based on the data.
T1:
ID Partial_date1
1 19-DEC-2016
2 06-MAY-2015
3 2016
4
5 AUG-2016
6 16-NOV-2015 00:00
7 01-JAN-2016
T2:
ID Partial_date2
1 09-JAN-2016
2 2016
3 SEP-2015
4
5 23-MAR-2016 00:00
6 15-MAY-2015
7
I want to search for all records that have full dates (as it is not possible to convert partial dates), to select only the records with full dates, I have used length >10. Here is the SQL I wrote but does not seem to be working.
select t1.id from t1, t2
where t1.id =t2.id
and length(t1.partial_date1)>10
and length(t2.partial_date2)>10
and to_date(t1.partial_date1,'DD-MON-YYYY') > to_date(t2.partial_date2,'DD-MON-YYYY')
I either get an error - ORA-01830: date format picture ends before converting entire input string
or literal does not match format string.
What am I doing wrong? how do I get the right results?
It seems you consider a date complete when the string starts with DD-MMM-YYYY. You can use REGEXP_LIKE to find such rows:
where regexp_like(partial_date, '^[[:digit:]]{2}-[[:upper:]]{3}-[[:digit:]]{4}')
(You may want to adjust the pattern according to your needs, e.g. replace [[:upper:]] with [[:alpha:]].)
In order to convert a date containing a textual month you should use TO_DATE with a language parameter:
to_date(partial_date, 'DD-MON-YYYY', 'NLS_DATE_LANGUAGE=AMERICAN')
A possible query:
select tt1.id
from
(
select
id,
to_date(substr(partial_date1, 1, 11), 'DD-MON-YYYY', 'NLS_DATE_LANGUAGE=AMERICAN')
as dt
from t1
where regexp_like(partial_date1, '^[[:digit:]]{2}-[[:upper:]]{3}-[[:digit:]]{4}')
) tt1
join
(
select
id,
to_date(substr(partial_date2, 1, 11), 'DD-MON-YYYY', 'NLS_DATE_LANGUAGE=AMERICAN')
as dt
from t2
where regexp_like(partial_date2, '^[[:digit:]]{2}-[[:upper:]]{3}-[[:digit:]]{4}')
) tt2 on tt2.id = tt1.id and tt2.dt < tt1.dt;
However, keep in mind that you are still dealing with strings. Conversion on "dates" like these would fail and crash your query:
30-FEB-2017
01-YAN-2017
99-XXX-9999
So depending on the data quality your best bet may be to write a PL/SQL function in order to catch conversion errors.
When you write condition in where clause oracle may use it as access predicate. And it function to_date(t1.partial_date1,'DD-MON-YYYY') is used to any row.
That's why you get an error.
I see two ways:
First way is use a subquery to get a shrunk dataset, substr only 10 symbols and then convert it
with t1(id,partial_date1) as
(
select 1,'19-DEC-2016' from dual union all
select 2,'06-MAY-2015' from dual union all
select 3, '2016' from dual union all
select 4,'' from dual union all
select 5,'AUG-2016' from dual union all
select 6,'16-NOV-2015 00:00' from dual union all
select 7, '01-JAN-2016' from dual
), t2(id,partial_date2) as
(
select 1,'09-JAN-2016' from dual union all
select 2,'2016' from dual union all
select 3,'SEP-2015' from dual union all
select 4,'' from dual union all
select 5,'23-MAR-2016 00:00' from dual union all
select 6,'15-MAY-2015' from dual union all
select 7,'' from dual
)
select *
from
(select
t1.id,
partial_date1,
partial_date2
from
t1, t2
where
t1.id =t2.id
and length(t1.partial_date1) > 10
and length(t2.partial_date2) > 10
and rownum > 0)
where
to_date(substr(partial_date1,1,10),'DD-MON-YYYY') > to_date(substr(partial_date2,1,10),'DD-MON-YYYY');
/
The second way is to explicitly convert any format to properly one
with t1(id,partial_date1) as (
select 1,'19-DEC-2016' from dual union all
select 2,'06-MAY-2015' from dual union all
select 3, '2016' from dual union all
select 4,'' from dual union all
select 5,'AUG-2016' from dual union all
select 6,'16-NOV-2015 00:00' from dual union all
select 7, '01-JAN-2016' from dual)
,t2(id,partial_date2) as (
select 1,'09-JAN-2016' from dual union all
select 2,'2016' from dual union all
select 3,'SEP-2015' from dual union all
select 4,'' from dual union all
select 5,'23-MAR-2016 00:00' from dual union all
select 6,'15-MAY-2015' from dual union all
select 7,'' from dual)
select * from(
select t1.id,
case
when regexp_like(t1.partial_date1,'\d{1,2}-\w{3}-\d{4} \d{1,2}:\d{2}') then to_date(t1.partial_date1,'dd-MON-yyyy HH24:MI')
when regexp_like(t1.partial_date1,'\d{1,2}-\w{3}-\d{4}') then to_date(t1.partial_date1,'dd-MON-yyyy')
when regexp_like(t1.partial_date1,'\w{3}-\d{4}') then to_date(t1.partial_date1,'MON-yyyy')
when regexp_like(t1.partial_date1,'\d{4}') then to_date(t1.partial_date1,'yyyy')
end as pd1,
case
when regexp_like(t2.partial_date2,'\d{1,2}-\w{3}-\d{4} \d{1,2}:\d{2}') then to_date(t2.partial_date2,'dd-MON-yyyy HH24:MI')
when regexp_like(t2.partial_date2,'\d{1,2}-\w{3}-\d{4}') then to_date(t2.partial_date2,'dd-MON-yyyy')
when regexp_like(t2.partial_date2,'\w{3}-\d{4}') then to_date(t2.partial_date2,'MON-yyyy')
when regexp_like(t2.partial_date2,'\d{4}') then to_date(t2.partial_date2,'yyyy')
end as pd2
from t1,t2
where t1.id = t2.id)
where pd1 > pd2
/
case
when PurchaseDate = '0' then NULL
when right(PurchaseDate, 4) = '0000' then convert(date, left(PurchaseDate,4) + '1231', 112)
when RIGHT(PurchaseDate, 2) = '00' then DATEADD(day,-1,DATEADD(month,cast(left(RIGHT(Purchasedate,4),2) AS INT),DATEADD(year,cast(LEFT(Purchasedate, 4) AS int)-1900,0)))
else convert(date, ltrim(rtrim(cast(PurchaseDate as varchar(50)))), 112)
end
Apologize. This is from my phone. This does a few things. It assumes different formats plus it counters for missing days and months.
Hope this helps.

Lowest continuous date without break

I have a table and each record has a date. We can assume that a date range is contiguous if there's not a 3 month break. How can I find the start of the most recent contiguous date range?
For example, imagine if I had this data:
1990-5-1
1990-6-4
1990-10-28
1990-11-14
1990-12-19
1991-1-20
1991-4-30
1991-5-13
I'd like for it to return 1991-4-30 because it's the start of the most recent contiguous range of dates.
I think this does what you're looking for. Using my own table and column names as test data. This is on Oracle.
select * from (
select * from sm_ss_tickets t1 where exists (
select * from sm_ss_tickets t2 where t2.created_date between t1.created_date and t1.created_date+90 and t1.rowid <> t2.rowid
) order by created_date asc
) where rownum = 1;
Maybe something like the following would work:
WITH d1 AS (
SELECT date'1990-05-01' AS dt FROM dual
UNION ALL
SELECT date'1990-06-04' AS dt FROM dual
UNION ALL
SELECT date'1990-10-28' AS dt FROM dual
UNION ALL
SELECT date'1990-11-14' AS dt FROM dual
UNION ALL
SELECT date'1990-12-19' AS dt FROM dual
UNION ALL
SELECT date'1991-01-20' AS dt FROM dual
UNION ALL
SELECT date'1991-04-30' AS dt FROM dual
UNION ALL
SELECT date'1991-05-13' AS dt FROM dual
)
SELECT MAX(dt) FROM (
SELECT dt, LAG(dt) OVER ( ORDER BY dt ) AS prev_dt, LEAD(dt) OVER ( ORDER BY dt ) AS next_dt
FROM d1
) WHERE ( dt > ADD_MONTHS(prev_dt, 3) OR prev_dt IS NULL )
AND dt > ADD_MONTHS(next_dt, -3)
In the above, a date can only be the start of a contiguous sequence if there is no prior date within 3 months (either it is more than three months ago or it doesn't exist at all) and there is also a subsequent date within 3 months.
You can use LAG and LEAD. Find the query below. I think it works fine.
tmp_year is the table I have created. tdate is the column.
The records in the table are
28-JAN-15
27-JAN-15
26-JAN-15
25-JAN-15
12-JUL-14
11-JUL-14
10-JUL-14
09-JUL-14
24-DEC-13
23-DEC-13
22-DEC-13
21-DEC-13
15-SEP-13
07-JUN-13
27-FEB-13
19-NOV-12
11-AUG-12
Please find the query which returns 25th Jan 2015.
select max(d.tdate) from (
select c.tdate,c.next_date,c.date_diff,lag(date_diff) over( order by tdate) prev_diff from (
select b.tdate ,b.next_date,(next_date-tdate) date_diff from
(select a.tdate,lead(a.tdate) over(order by a.tdate) next_date from tmp_year a ) b ) c) d where d.date_diff<90 and d.prev_diff>=90;

get date range between dates

I have following table tbl in database and I have dynamic joining date 1-1-2012 and I want this date is between (Fall and spring) or (spring and summer) or (summer and fall).I want query in which i passed only joining date which return semestertime and joining date in Oracle.
Semestertime joiningDate
Fall 10-13-2011
Spring 2-1-2012
Summer 6-11-2012
Fall 10-1-2015
If I understand your question correctly:
SELECT *
FROM your_table
WHERE joiningDate between to_date (your_lower_limit_date_here, 'mm-dd-yyyy')
AND to_date (your_upper_limit_date_here, 'mm-dd-yyyy`);
What about something like that:
select 'BEFORE' term,
t."Semestertime", to_char(t."joiningDate", 'MM-DD-YYYY')
from (
select tbl.*, rownum rn from tbl where tbl."joiningDate" < to_date('1-1-2012','MM-DD-YYYY')
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- your reference date
order by tbl."joiningDate" desc) t
where rn = 1
union all
select 'AFTER' term,
t."Semestertime", to_char(t."joiningDate", 'MM-DD-YYYY')
from (
select tbl.*, rownum rn from tbl where tbl."joiningDate" > to_date('1-1-2012','MM-DD-YYYY')
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- your reference date
order by tbl."joiningDate" asc) t
where rn = 1
This will return the "term" before and after a given date. You will probably have to adapt such query to your specific needs. But that might be a good starting point.
For example, given your business rules, you might consider using <= instead of <. You you might require to have the result displayer a column instead of rows. Bu all of this shouldn't be too had to change.
As an alternate solution using CTE and sub-queries:
with testdata as (select to_date('1-1-2012','MM-DD-YYYY') refdate from dual)
select v.what, tbl.* from tbl join
(
select 'BEFORE' what, max(t1."joiningDate") d
from tbl t1
where t1."joiningDate" < to_date('1-1-2012','MM-DD-YYYY')
union all
select 'AFTER' what, min(t1."joiningDate") d
from tbl t1
where t1."joiningDate" > to_date('1-1-2012','MM-DD-YYYY')
) v
on tbl."joiningDate" = v.d
See http://sqlfiddle.com/#!4/c7fa5/15 for a live demo comparing those solutions.