Oracle SQL: Return a set without duplicates - oracle

Using the following query is working for me except that I need to only return one result for each TPHONE.ID.
What I'm trying to accomplish is to check two tables for three date fields (two in the TPHONE table and one in the TPHONEREQUEST table and return the TPHONE.ID of any place where I find dates within a particular range. However, if more than one entry in either table has one or more of the dates within the date range I'd still only want to return the TPHONE.ID once.
SELECT
TPHONE.ID,
TPHONE.LOCATIONID,
TPHONE.DLASTCHANGED,
TPHONE.D02,
TPHONEREQUEST.D03
FROM
TPHONE, TPHONEREQUEST
WHERE
TPHONE.ID = TPHONEREQUEST.DEVICEID
AND TPHONE.ID IN
(
SELECT
TPHONE.ID
FROM
TPHONE
WHERE
TPHONE.DLASTCHANGED >= '7/1/2019' AND TPHONE.DLASTCHANGED < '10/1/2019'
OR TPHONE.D02 >= '7/1/2019' AND TPHONE.D02 < '10/1/2019'
OR TPHONEREQUEST.D03 >= '7/1/2019' AND TPHONEREQUEST.D03 < '10/1/2019'
)
ORDER BY
TPHONE.LOCATIONID, TPHONE.ID

You can use aggregation and filter with a HAVING clause:
SELECT p.ID,
FROM
TPHONE p
INNER JOIN TPHONEREQUEST t ON p.ID = r.DEVICEID
GROUP BY p.ID
HAVING
MAX(
CASE WHEN
(
p.DLASTCHANGED >= TO_DATE('07/01/2019', 'DD/MM/YYYY')
AND p.DLASTCHANGED < TO_DATE('10/1/2019', 'DD/MM/YYYY')
) OR (
p.D02 >= TO_DATE('07/01/2019', 'DD/MM/YYYY')
AND p.D02 < TO_DATE('10/1/2019', 'DD/MM/YYYY')
) OR (
r.D03 >= TO_DATE('07/01/2019', 'DD/MM/YYYY')
AND r.D03 < TO_DATE('10/1/2019', 'DD/MM/YYYY')
)
THEN 1
END
) = 1
Notes:
this returns only TPHONE.ID, since this seems to be what you are looking for; if you want more columns, then you can add them to the SELECT clause and to the GROUP BY clause (beware that this might change the grouping condition though, if these columns are not functionnaly dependant on TPHONE.ID)
I used table aliases to shorten the query
I used TO_DATE() to generate proper dates instead of relying on the default format of the database (which may change across databses and sessions). There is an assumption here that the format of your dates is DD/MM/YYYY (it could also be MM/DD/YYYY)

You could use ROW_NUMBER to enumerate the rows with the same ID and then select only the first instance:
SELECT TPHONE.ID,
TPHONE.LOCATIONID,
TPHONE.DLASTCHANGED,
TPHONE.D02,
TPHONEREQUEST.D03
FROM (
SELECT TPHONE.ID,
TPHONE.LOCATIONID,
TPHONE.DLASTCHANGED,
TPHONE.D02,
TPHONEREQUEST.D03,
ROW_NUMBER() over (PARTITION BY TPHONE.ID ORDER BY TPHONE.ID) as RN
FROM TPHONE,
TPHONEREQUEST
WHERE TPHONE.ID = TPHONEREQUEST.DEVICEID
AND TPHONE.ID IN
(
SELECT TPHONE.ID
FROM TPHONE
WHERE TPHONE.DLASTCHANGED >= '7/1/2019' AND TPHONE.DLASTCHANGED < '10/1/2019'
OR TPHONE.D02 >= '7/1/2019' AND TPHONE.D02 < '10/1/2019'
OR TPHONEREQUEST.D03 >= '7/1/2019' AND TPHONEREQUEST.D03 < '10/1/2019'
)
ORDER BY TPHONE.LOCATIONID, TPHONE.ID
)
WHERE RN = 1

Related

Oracle AVG COUNT

I have this query:
SELECT TRUNC(date_added,'MM'), count(*)
FROM payments_log l, product p
WHERE l.amount > 0
AND l.product_id = p.product_id
AND p.subproduct_id = 238
AND TRUNC(l.date_added) BETWEEN TO_DATE('01012020','MMDDYYYY') AND TO_DATE('01012021','MMDDYYYY')
AND l.return_code = 1
GROUP BY TRUNC(date_added,'MM')
ORDER BY TRUNC(date_added,'MM');
In addition to the count, per month, I want a column that is the average each month, of the total......not sure how to do this in the same query.
To get:
I want a column that is the average each month, of the total
You appear to want to use the AVG analytic function over the entire range:
SELECT month,
cnt,
AVG( cnt ) OVER () AS avg_cnt
FROM (
SELECT TRUNC(date_added,'MM') AS month,
COUNT(*) AS cnt
FROM payments_log l
INNER JOIN product p
ON ( l.product_id = p.product_id )
WHERE l.amount > 0
AND p.subproduct_id = 238
AND l.date_added >= DATE '2020-01-01'
AND l.date_added < DATE '2021-01-01'
AND l.return_code = 1
GROUP BY TRUNC(date_added,'MM')
)
ORDER BY month
You also should use ANSI joins rather than the (confusing) legacy comma joins and can filter on the date_added column without needing the TRUNC function (which, if you do use it, would prevent Oracle from using an index on the date_added column and would require a function-based index on TRUNC( date_added )).
(Note: BETWEEN is inclusive so that you will include 2021-01-01 in your range rather than just those dates in 2020; I am assuming that you do not want this date but if you do then you can set the upper bound to l.date_added < DATE '2021-01-02'.)
If you want the number of counts per month as a fraction of the total number of counts (this is not an average) then, again, you want to use an analytic function:
SELECT month,
cnt,
cnt / SUM( cnt ) OVER () AS fraction_of_total_cnt
FROM (
SELECT TRUNC(date_added,'MM') AS month,
COUNT(*) AS cnt
FROM payments_log l
INNER JOIN product p
ON ( l.product_id = p.product_id )
WHERE l.amount > 0
AND p.subproduct_id = 238
AND l.date_added >= DATE '2020-01-01'
AND l.date_added < DATE '2021-01-01'
AND l.return_code = 1
GROUP BY TRUNC(date_added,'MM')
)
ORDER BY month

How to avoid Internal_Function by TO_CHAR IN Oracle

I'm using Oracle 12C and I have the following code:
SELECT *
FROM
(
SELECT
*
FROM
t.tablea
WHERE
name = 'FIS'
) A
LEFT JOIN (
SELECT
*
FROM
t.tableb
WHERE
enabled = 1
) B ON b.id = a.id
AND TO_CHAR(b.createdate, 'dd-mm-yy hh24:mi') = TO_CHAR(a.createdate, 'dd-mm-yy hh24:mi')
Both a and b createdate are timestamp datatype.
Optimizer return an internal_function at TO_CHAR(b.createdate, 'dd-mm-yy hh24:mi') = TO_CHAR(a.createdate, 'dd-mm-yy hh24:mi') in Execution Plan
If I compare like this: 'AND b.createdate = a.createdate', It will lost 1000 rows that look like this '11-JUN-18 04.48.34.269928000 PM'. And If I change 269928000 to 269000000 It will work
Now, I don't want to using to_char to avoid internal_function(must create Function-based-Index)
Anyone can help me?
If I compare like this: AND b.createdate = a.createdate, It will lost 1000 rows that look like this 11-JUN-18 04.48.34.269928000 PM. And If I change 269928000 to 269000000 It will work
Your values appear to have a fractional seconds component and would have the TIMESTAMP data type. If so, you can use TRUNC( timestamp_value, 'MI' ) to truncate to the nearest minute.
SELECT *
FROM t.tablea a
LEFT OUTER JOIN t.tableb b
ON ( a.createdate >= TRUNC( b.createdate, 'MI' )
AND a.createdate < TRUNC( b.createdate, 'MI' ) + INTERVAL '1' MINUTE
AND a.id = b.id
AND b.enabled = 1
)
WHERE a.name = 'FIS'
This will remove the need to apply a function to one of the two tables (a.createdate in this case but you could swap them).
I don't even see the need for the subqueries:
SELECT a.*, b.*
FROM t.tablea a
LEFT JOIN t.tableb b
ON a.id = b.id AND
TRUNC(b.createdate, 'MI') = TRUNC(a.createdate, 'MI') AND
b.enabled = 1
WHERE
a.name = 'FIS'

Datepart function in oracle

I have some sample records in Oracle 12
Date_Time Item
10/1/2012 12:05:00 AM 3
12/3/2012 06:00:00 AM 2
11/8/2012 14:05:05 PM 10
12/9/2012 16:00:59 PM 5
I like to aggregate the Item field based on military time or in three different times: 00:00:00AM to 05:59:00AM, 06:00:00AM to 15:59:00PM, and 16:00:00PM to 23:59:00PM. I was able to use the Datepart function in SQL to do this. I was wondering what function in Oracle 12 that allows me to count the Item between these three different times.
My desired output would be:
Date_Time Count
00:00:00AM to 05:59:00AM = 3
06:00:00AM to 15:59:00PM = 12
16:00:00PM to 23:59:00PM = 5
In oracle, date datatype contains date+time ,so you just need just use group by
SELECT Date_Time, COUNT(*) item FROM YOUR_TABLES
GROUP BY Date_Time;
NEW Answer:
SELECT TO_CHAR(DATE_TIME,'HH24') time, count(*) FROM YOUR_TABLE
WHERE TO_CHAR(DATE_TIME,'HH24') >= '00'
AND TO_CHAR(DATE_TIME,'HH24') < '06'
GROUP BY TO_CHAR(DATE_TIME,'HH24')
UNION ALL
SELECT TO_CHAR(DATE_TIME,'HH24') time, count(*) FROM YOUR_TABLE
WHERE TO_CHAR(DATE_TIME,'HH24') >= '06'
AND TO_CHAR(DATE_TIME,'HH24') < '16'
GROUP BY TO_CHAR(DATE_TIME,'HH24')
UNION ALL
SELECT TO_CHAR(DATE_TIME,'HH24') time, count(*) FROM YOUR_TABLE
WHERE TO_CHAR(DATE_TIME,'HH24') >= '16'
AND TO_CHAR(DATE_TIME,'HH24') < '00'
GROUP BY TO_CHAR(DATE_TIME,'HH24');
and if your table is huge :
first :partition it
second: create local functional index on TO_CHAR(DATE_TIME,'HH24:MI:SS')
Assuming that date_time column is datatype DATE, we can use the TO_CHAR function to extract a two character representation... in the range 00 to 23.
(The selected answer demonstrates this approach to extracting the "hour" from an Oracle DATE.)
Assuming that we want every non-null time value to fall into one of three time ranges... that is, if we don't want any of time values to be omitted because of a crack/gap in between the ranges, and we don't want any overlap in the ranges...
We can use a simple "less than" tests in a CASE expression.
Consider a time close to a boundary: '05:59:33'. That's after 05:59:00 but before 06:00:00. If we want that included in the first range, we can just test for hour < '06'.
If was grouping the rows into three ranges, and I wanted a total of the item column, I'd do something like this:
SELECT CASE
WHEN TO_CHAR( t.date_time ,'HH24') < '06' THEN '00:00:00 to 05:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '16' THEN '06:00:00 to 15:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '24' THEN '16:00:00 to 23:59:59'
END AS time_range
, SUM(t.item)
FROM mytable t
GROUP
BY CASE
WHEN TO_CHAR( t.date_time ,'HH24') < '06' THEN '00:00:00 to 05:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '16' THEN '06:00:00 to 15:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '24' THEN '16:00:00 to 23:59:59'
END
and add an ORDER BY clause if I want the results returned in a particular order.
If the table contains any NULL values of date_time, the query above will also return a fourth time_range with a NULL value.
Here is how you would get the desired result in three columns (rather than three rows), which makes more sense for most applications. You can change this easily to get the result in rows instead.
Note that if dt is any date in Oracle, dt - trunc(dt) is the number of days (a fraction with value less than 1) since midnight.
select sum(case when dt-trunc(dt) < 6/24 then item else 0 end) as morning,
sum(case when dt-trunc(dt) >= 6/24
and dt-trunc(dt) < 16/24 then item else 0 end) as daytime,
sum(case when dt-trunc(dt) >= 16/24 then item else 0 end) as evening
from your_table
;

oracle olap query execution is taking too long

I have these following tables:
1) date_table_dim
2) clock_table_dim
3) onlinegpspoint : which contains our main information for olap reports
And also there is a sql query like this:
SELECT
date_table_dim.day_id day_id,
clock_table_dim.hour_id hour_id
FROM onlinegpspoint olgps
INNER JOIN date_table_dim
ON (
olgps.occurance_time >= to_date('2014-03-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss')
AND olgps.occurance_time >= date_table_dim.day_id
AND olgps.occurance_time < date_table_dim.day_id + 1
)
INNER JOIN clock_table_dim
ON ( clock_table_dim.hour_id <= TO_NUMBER(TO_CHAR(occurance_time, 'HH24'))
AND clock_table_dim.hour_id > TO_NUMBER(TO_CHAR((occurance_time - 1/24), 'HH24') ))
GROUP BY
date_table_dim.day_id,
clock_table_dim.hour_id ;
My problem is that this query is taking too long to execute.
What actions can be taken to improve the performance of this query execution?
EDIT
There is an index for occurance_time on onlinegpspoint. By this query I want to get some Olap information for 1 hour period of time. (This query is a kind of summary of my fact_table query.)
You can try following query.
SQLFiddle
with t as (select d.day_id, o.occurance_time ot
from onlinegpspoint o
join date_table_dim d on ( o.occurance_time >= date '2014-03-01'
and d.day_id <= o.occurance_time and o.occurance_time < d.day_id + 1) )
select day_id, c.hour_id
from t join clock_table_dim c on (
c.hour_id <= to_char(t.ot, 'HH24') and to_char((t.ot - 1/24), 'HH24') < c.hour_id )
group by day_id, c.hour_id order by day_id, c.hour_id;
In your original query you compare to_char(occurance_time, 'HH24') with hour_id, here index probably does not work.
So idea is to firstly filter data to interesting period and then work only with these filtered data.
There is one more query worth trying, which gave me promising results:
select distinct trunc(occurance_time) day_id, to_char(occurance_time, 'hh24')+0 hour_id
from onlinegpspoint o join (
select to_date(to_char(day_id, 'yyyy-mm-dd ')||' '
||lpad(hour_id, 2, 0), 'yyyy-mm-dd hh24') dt
from date_table_dim, clock_table_dim) d
on (o.occurance_time >= date '2014-03-01'
and d.dt-1/24 <= o.occurance_time and o.occurance_time < d.dt)

How to exclude holidays between two dates?

I have two dates and I have to find out the number of Sundays and holidays fall between those two dates. Can I do this using BETWEEN? If so, how?
SELECT date1, date2, trunc(deposit_date - transaction_date) TOTAL
FROM Table_Name FULL OUTER JOIN Holidays ON date2 = hdate
WHERE hdate IN (date1, date2)
Using this I can definitely check whether there is a holiday on either of the two days, i.e. date1 or date2 but what I am not able to find out that whether there lies a holiday or a Sunday between these two dates. Help!
The solution you've posted is horribly inefficient; you can do all of this in a single SQL statement:
Firstly generate all possible dates between the two you have:
select trunc(:min_date) + level - 1
from dual
connect by level <= trunc(:min_date) - trunc(:max_date)
Then use your HOLIDAY table to restrict to what you want:
with all_dates as (
select trunc(:min_date) + level - 1 as the_date
from dual
connect by level <= trunc(:min_date) - trunc(:max_date)
)
select count(*)
from all_dates a
left outer join holiday b
on a.the_date = b.hdate
where b.hdate is null
and to_char(a.the_date, 'DY') <> 'SUN'
If you want to check if hdate is between the two dates you can query using
where hdate between date1 and date2
If you want to check if hdate is on the same day as date1 or date two you can query like this
where trunc(hdate) in (trunc(date1) ,trunc(date2))
The trunc function removed the time.
You should create a table with the holidays and maintain it on your own.
CREATE TABLE holidays
(
holiday VARCHAR2(100)
, d_date DATE
);
INSERT INTO holidays VALUES ('National Developer Day', DATE'2013-06-01');
SELECT *
FROM holidays;
-- National Developer Day 2013-06-01 00:00:00
The rest is just a matter of a SQL statment
Scenario 1: EXISTS
SELECT COUNT
(
CASE
WHEN TRIM(TO_CHAR(d.start_date_level, 'DAY')) = 'SUNDAY'
OR CASE
WHEN EXISTS (SELECT 1 FROM holidays h WHERE d.start_date_level = h.d_date)
THEN 1
ELSE NULL
END = 1
THEN 1
ELSE NULL
END
) AS holiday_check
FROM
(
SELECT start_date + (LEVEL - 1) AS start_date_level
FROM
(
SELECT start_date, end_date, end_date - start_date AS diff_date
FROM
(
SELECT TRUNC(ADD_MONTHS(SYSDATE, -2)) AS start_date
, TRUNC(SYSDATE) AS end_date
FROM DUAL
)
)
CONNECT BY
LEVEL <= (diff_date + 1)
) d
Scenario 2: LEFT JOIN
SELECT COUNT
(
CASE
WHEN TRIM(TO_CHAR(d.start_date_level, 'DAY')) = 'SUNDAY'
OR h.d_date IS NOT NULL
THEN 1
ELSE NULL
END
) AS holiday_check
FROM
(
SELECT start_date + (LEVEL - 1) AS start_date_level
FROM
(
SELECT start_date, end_date, end_date - start_date AS diff_date
FROM
(
SELECT TRUNC(ADD_MONTHS(SYSDATE, -2)) AS start_date
, TRUNC(SYSDATE) AS end_date
FROM DUAL
)
)
CONNECT BY
LEVEL <= (diff_date + 1)
) d
LEFT JOIN holidays h
ON d.start_date_level = h.d_date
9 Sundays + 1 "National Developer Day" = 10
CREATE OR REPLACE FUNCTION workdays (dt1 DATE, dt2 DATE) RETURN NUMBER IS
weekday_count NUMBER := 0;
date1 DATE := dt1;
date2 DATE := dt2;
cur_dt date;
holiday_count number;
begin
if date1 = date2 then
return 0;
end if;
cur_dt := transaction_date;
while cur_dt <= date2 loop
if cur_dt = date2 then
null;
else
SELECT count(*) INTO holiday_count
FROM holiday
WHERE hdate = cur_dt;
IF holiday_count = 0 THEN
IF to_char(cur_dt,'DY') NOT IN ('SUN') THEN
weekday_count := weekday_count + 1;
END IF;
END IF;
END IF;
cur_dt := cur_dt +1;
END LOOP;
RETURN weekday_count;
END;
And then I queried my database and got the right results. Do post if you have an optimal solution for this.
Here is an even better and efficient solution to the problem,
SELECT A.ID,
COUNT(A.ID) AS COUNTED
FROM tableA A
LEFT JOIN TableB B
ON A.tableB_id=B.id
LEFT JOIN holiday C
ON TRUNC(C.hdate) BETWEEN (TRUNC(a.date1) +1) AND TRUNC(B.date2)
WHERE c.hdate IS NOT NULL
GROUP BY A.ID;
where TableA contains date1 and tableB contains date2. Holiday contains the list of holidays and Sundays. And this query excludes 'date1' from the count.
RESULT LOGIC
trunc(date2) - trunc(date1) = x
x - result of the query
Make a table T$HOLIDAYS with your holidays (HDATE column). These dates will be excluded from calculation of working days within given period (sdate is start date and edate end date of period). Here is the function that calculates working days within given period excluding holidays, saturdays and sundays:
CREATE OR REPLACE FUNCTION WorkingDays(sdate IN DATE,edate IN DATE) RETURN NUMBER IS
days NUMBER;
BEGIN
WITH dates AS (SELECT sdate+LEVEL-1 AS d FROM DUAL CONNECT BY LEVEL<=edate-sdate+1)
SELECT COUNT(*) INTO days
FROM dates
WHERE d NOT IN (SELECT hdate FROM t$holidays) --exclude holidays
AND TO_CHAR(d,'D') NOT IN (6,7); --exclude saturdays + sundays
RETURN days;
END WorkingDays;
/
select sum(qq) from (
select case when to_number(to_char((trunc(sysdate-10) + level - 1),'D'))<=5 then 1 else 0 end as qq
from dual
connect by level <= trunc(sysdate) - trunc(sysdate-10))

Resources