select to group date and time in 5-minute intervals - sql

I wish count the rows of a table grouped by date and time intervals of 5 minutes:
for example, if the minutes portion of HH:MM falls between 00 mins and 04 min it will be counted as 00, eg. 08:04 will be counted as 08:00
if the minutes portion falls between 05 mins and 09 mins it will be counted as 05, eg. 08:06 will be counted as 08:05
Table Data
Date Time
18/01/18 08:00
18/01/18 08:01
18/01/18 08:02
18/01/18 08:03
18/01/18 08:04
18/01/18 08:05
18/01/18 08:06
18/01/18 08:08
18/01/18 08:10
19/01/18 17:01
19/01/18 17:03
19/01/18 17:04
Expected Output
DATE TIME COUNT
18/01/2018 08:00 5
18/01/2018 08:05 3
18/01/2018 08:10 1
19/01/2018 17:00 3
Table Creation
create table TAB1 (tDATE DATE,tTIME VARCHAR2(5));
Data
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:00');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:01');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:02');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:03');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:04');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:05');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:06');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:08');
insert into TAB1(tDATE,tTIME) values (to_date('18/01/2018','DD/MM/YYYY'),'08:10');
insert into TAB1(tDATE,tTIME) values (to_date('19/01/2018','DD/MM/YYYY'),'17:01');
insert into TAB1(tDATE,tTIME) values (to_date('19/01/2018','DD/MM/YYYY'),'17:03');
insert into TAB1(tDATE,tTIME) values (to_date('19/01/2018','DD/MM/YYYY'),'17:04');

Whenever I need such interval I use this generic function:
CREATE OR REPLACE FUNCTION MakeInterval(ts IN TIMESTAMP, roundInterval IN INTERVAL DAY TO SECOND) RETURN TIMESTAMP DETERMINISTIC IS
denom INTEGER;
BEGIN
IF roundInterval >= INTERVAL '1' HOUR THEN
denom := EXTRACT(HOUR FROM roundInterval);
IF MOD(24, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts) + TRUNC(EXTRACT(HOUR FROM ts) / denom) * denom * INTERVAL '1' HOUR;
ELSIF roundInterval >= INTERVAL '1' MINUTE THEN
denom := EXTRACT(MINUTE FROM roundInterval);
IF MOD(60, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts, 'hh') + TRUNC(EXTRACT(MINUTE FROM ts) / denom) * denom * INTERVAL '1' MINUTE;
ELSE
denom := EXTRACT(SECOND FROM roundInterval);
IF MOD(60, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts, 'mi') + TRUNC(EXTRACT(SECOND FROM ts) / denom) * denom * INTERVAL '1' SECOND;
END IF;
END MakeInterval;
Valid intervals are: 1,2,3,4,5,6,10,12,15,20,30,60 SECOND, 1,2,3,4,5,6,10,12,15,20,30,60 MINUTE, 1,2,3,4,6,8,12 HOUR
You store time in a separate column which is a bad design. First, make a proper DATE or TIMESTAMP value, for example: TO_DATE(TO_CHAR(tDATE,'YYYY-MM-DD')||tTIME, 'YYYY-MM-DDHH24:MI')
Then you could use it like this
SELECT
MakeInterval(TO_DATE(TO_CHAR(tDATE,'YYYY-MM-DD')||tTIME, 'YYYY-MM-DDHH24:MI'), INTERVAL '5' MINUTE), ...
Of course, if you don't like to use a separate function you can put all in one line:
TRUNC(TO_DATE(TO_CHAR(tDATE,'YYYY-MM-DD')||tTIME, 'YYYY-MM-DDHH24:MI'), 'hh') + TRUNC(EXTRACT(MINUTE FROM TO_DATE(TO_CHAR(tDATE,'YYYY-MM-DD')||tTIME, 'YYYY-MM-DDHH24:MI')) / 5) * INTERVAL '5' MINUTE;

This is a bit cumbersome in Oracle, but quite feasible with string arithmetic:
select date,
substring(time, 1, 3) || lpad(floor(cast(substring(time, -2) as number) / 12) * 12, 2, '0') as time,
count(*)
from tab1
group by date,
substring(time, 1, 3) || lpad(floor(cast(substring(time, -2) as number) / 12) * 12, 2, '0')
order by date, time;

try this,
select tdate,
SUBSTR(ttime, 1, 2)||':'||
LPAD(NVL(DECODE(SIGN(ROUND(SUBSTR(ttime, 4, 2), -1)-SUBSTR(ttime, 4, 2)),
1, ROUND(SUBSTR(ttime, 4, 2), -1)-5,
-1, ROUND(SUBSTR(ttime, 4, 2), -1)), 0),2, '0') time_,
count(1)
from tab1
group by tdate, SUBSTR(ttime, 1, 2)||':'||
LPAD(NVL(DECODE(SIGN(ROUND(SUBSTR(ttime, 4, 2), -1)-SUBSTR(ttime, 4, 2)),
1, ROUND(SUBSTR(ttime, 4, 2), -1)-5,
-1, ROUND(SUBSTR(ttime, 4, 2), -1)), 0),2, '0');

Thank you all. As to why there is a separate VARCHAR2 column for the time component, the tables were originally migrated from some legacy database that had a date type but without a time component the latter which was stored as a string. Here is my own idea which gives me exactly what I want:
select tDATE,substr(tTIME,1,3)||
case
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 0 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 5 then '00'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 5 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 10 then '05'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 10 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 15 then '10'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 15 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 20 then '15'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 20 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 25 then '20'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 25 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 30 then '25'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 30 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 35 then '30'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 35 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 40 then '35'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 40 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 45 then '40'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 45 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 50 then '45'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 50 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 55 then '50'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 55 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 60 then '55'
else '00'
end as tTIME
,count(*)
from TAB1
group by tDATE,substr(tTIME,1,3)||
case
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 0 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 5 then '00'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 5 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 10 then '05'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 10 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 15 then '10'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 15 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 20 then '15'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 20 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 25 then '20'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 25 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 30 then '25'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 30 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 35 then '30'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 35 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 40 then '35'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 40 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 45 then '40'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 45 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 50 then '45'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 50 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 55 then '50'
when to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) >= 55 and to_number(regexp_substr(substr(tTIME,4,2),'^[-]?[[:digit:]]*\.?[[:digit:]]*$')) < 60 then '55'
else '00'
end
order by 3 desc;

Related

Taking this column of dates and grouping them 1-30, 31- 59, 60 - 89, +90, both upcoming and those dates that have passes

enter image description hereI have a dates column that I am trying to group in to specific groups. Some dates have passed other dates are in the future. I want to have group them by 1-30, 31- 59, 60 - 89, +90, so there would be 8 groups total for both those that have passed and those that are on the horizon. Below is what I have wrote so far, but I feel like I am over complicating and the more tweaks I make the more incorrect it becomes. Any insight is appreciated!
CASE WHEN DATEDIFF(CURRENT_DATE(),`date`) >= 90 THEN '90 days past'
WHEN DATEDIFF(CURRENT_DATE(),`date`) >= 60 THEN '60 days past'
WHEN DATEDIFF(CURRENT_DATE(),`date`) >= 30 THEN '30 days past'
WHEN DATEDIFF(`date`,CURRENT_DATE()) >= 90 THEN '90 days future'
WHEN DATEDIFF(`date`,CURRENT_DATE()) >= 60 THEN '60 days future'
WHEN DATEDIFF(`date`,CURRENT_DATE()) >= 30 THEN 'Next 30 days'
ELSE 'Not Late'
END
I think it's okay.
I would just use the same datediff for each.
create table test (`date` date);
insert into test values
( date_add(current_date, interval -90 day) ),
( date_add(current_date, interval -89 day) ),
( date_add(current_date, interval -60 day) ),
( date_add(current_date, interval -59 day) ),
( date_add(current_date, interval -30 day) ),
( date_add(current_date, interval -29 day) ),
( date_add(current_date, interval 29 day) ),
( date_add(current_date, interval 30 day) ),
( date_add(current_date, interval 59 day) ),
( date_add(current_date, interval 60 day) ),
( date_add(current_date, interval 89 day) ),
( date_add(current_date, interval 90 day) )
select `date`,
CASE
WHEN DATEDIFF(`date`, CURRENT_DATE) <= -90 THEN '90 days past'
WHEN DATEDIFF(`date`, CURRENT_DATE) <= -60 THEN '60 days past'
WHEN DATEDIFF(`date`, CURRENT_DATE) <= -30 THEN '30 days past'
WHEN DATEDIFF(`date`, CURRENT_DATE) >= 90 THEN '90 days future'
WHEN DATEDIFF(`date`, CURRENT_DATE) >= 60 THEN '60 days future'
WHEN DATEDIFF(`date`, CURRENT_DATE) >= 30 THEN 'Next 30 days'
ELSE 'Not Late'
END as status
from test
date | status
:--------- | :-------------
2021-10-12 | 90 days past
2021-10-13 | 60 days past
2021-11-11 | 60 days past
2021-11-12 | 30 days past
2021-12-11 | 30 days past
2021-12-12 | Not Late
2022-02-08 | Not Late
2022-02-09 | Next 30 days
2022-03-10 | Next 30 days
2022-03-11 | 60 days future
2022-04-09 | 60 days future
2022-04-10 | 90 days future
db<>fiddle here

Database date depending on SYSDATE with a condition

I would like to select and return a number of rows depending on 'today's' date.
Something like...
SELECT * FROM my_table
WHERE
//some conditions AND
my_DATE BETWEEN trunc (sysdate, 'mm')/*current month*/ AND SYSDATE
However I would like to return:
Only rows for the last two months (excluding this month) if today's date 'dd' is less than 15
Return rows for the last two months (including this month) if today's date is >= 15
I am thinking of a case. Something like
WHERE
(CASE
when trunc (sysdate, 'dd') < 15 THEN
TO_CHAR(my_DATE, 'MMYYY') BETWEEN TO_CHAR((add_months(sysdate,-3)) AND TO_CHAR((add_months(sysdate,-1))
Any pointers are highly appreciated as I get acquainted with this arena. Thank you
I would do all the computation on sysdate, to help the optimizer user indexes:
where datecol >= add_months(trunc(sysdate, 'MON'),
(case when extract(day from sysdate) < 15 then -2 else -1 end)
) and
datecol < add_months(trunc(sysdate, 'MON'),
(case when extract(day from sysdate) < 15 then 0 else 1 end)
)
EDIT:
Based on the comment:
where datecol >= add_months(trunc(sysdate, 'MON'), -2) and
datecol < add_months(trunc(sysdate, 'MON'),
(case when extract(day from sysdate) < 15 then 0 else 1 end)
)
If this represents sample data:
SQL> alter session set nls_date_format = 'dd.mm.yyyy';
Session altered.
SQL> select * from test order by id;
ID DATUM
---------- ----------
1 20.12.2020
2 07.01.2021
3 15.02.2021
4 25.02.2021
5 10.03.2021
then see whether the following code makes sense. The whole date '&&par_sysdate' shuld be replaced by sysdate in real life; for testing purposes, I used a parameter as sysdate returns today's date.
First example: par_sysdate = 10.03.2021 (day is less than 15):
SQL> select t.id, t.datum
2 from test t
3 where t.datum >=
4 trunc(add_months(date '&&par_sysdate',
5 -case when to_number(to_char(date '&&par_sysdate', 'dd')) < 15 then 2
6 else 1
7 end
8 ), 'mm')
9 and t.datum < case when to_number(to_char(date '&&par_sysdate', 'dd')) < 15 then
10 trunc(date '&&par_sysdate', 'mm')
11 else date '&&par_sysdate'
12 end;
Enter value for par_sysdate: 2021-03-10
ID DATUM
---------- ----------
2 07.01.2021
3 15.02.2021
4 25.02.2021
Second example: using today's date (20.03.2021) where day is greater than 15:
SQL> undefine par_sysdate
SQL> /
Enter value for par_sysdate: 2021-03-20
ID DATUM
---------- ----------
3 15.02.2021
4 25.02.2021
5 10.03.2021
SQL>
Or, as I said, using sysdate:
SQL> select t.id, t.datum
2 from test t
3 where t.datum >=
4 trunc(add_months(sysdate,
5 -case when to_number(to_char(sysdate, 'dd')) < 15 then 2
6 else 1
7 end
8 ), 'mm')
9 and t.datum < case when to_number(to_char(sysdate, 'dd')) < 15 then
10 trunc(sysdate, 'mm')
11 else sysdate
12 end;
ID DATUM
---------- ----------
3 15.02.2021
4 25.02.2021
5 10.03.2021
SQL>
Your requirements are not wholly clear, but I think you want something like this:
select t.*
from mytable t
where (to_number(to_char(sysdate, 'dd')) < 15
and t.dt >= add_months(trunc(sysdate, 'mm'),-3)
and t.dt < trunc(sysdate, 'mm')
)
or (to_number(to_char(sysdate, 'dd')) >= 15
and t.dt >= add_months(trunc(sysdate, 'mm'),-2)
and t.dt <= last_day(sysdate)
)
I have put a demo version of this code on db<>fiddle with an affordance for changing the date of today instead of using sysdate.
the previous months should be FULL not previous months based on today's date.
To get full months, truncate the date using the 'mm' mask, which returns the first of the month.

How to extract the time from a timestamp without timezone column?

I want to calculate the number of orders per each time interval for each day.
The format of the date is timestamp without timezone. I can't seem to extract only the time. I use this query for each day, but is there a way to have the time intervals for each day in the month in one table?
CASE WHEN date_created_utc >= timestamp '2020-09-01 08:00:00' AND date_created_utc <= timestamp '2020-09-01 11:00:00' THEN 'Q1'
WHEN date_created_utc >= timestamp '2020-09-01 11:00:01' AND date_created_utc <= timestamp '2020-09-01 14:00:00' THEN 'Q2'
WHEN date_created_utc >= timestamp '2020-09-01 14:00:01' AND date_created_utc <= timestamp '2020-09-01 16:00:00' THEN 'Q3'
WHEN date_created_utc >= timestamp '2020-09-01 16:00:01' AND date_created_utc <= timestamp '2020-09-01 20:00:00' THEN 'Q4'
WHEN date_created_utc >= timestamp '2020-09-01 20:00:01' AND date_created_utc <= timestamp '2020-09-01 23:59:00' THEN 'Q5'
END AS interval,
COUNT(id) as cnt
FROM order_processing
GROUP BY 1;
The desired output table:
Day Q1 Q2 Q3 Q4 Q5
1 28 57 50 65 27
2 23 50 60 90 66
3 58 60 80 70 67
You can convert to a time and then use comparisons. For aggregation:
COUNT(*) FILTER (WHERE date_created_utc::time >= '08:00:00' and date_created_utc::time < '11:00:00') as cnt_1
You just need the hour part to implement the logic: you can use extract():
select
date_created_utc::date day,
count(*) filter(where extract(hour from date_created_utc) between 8 and 10) q1,
count(*) filter(where extract(hour from date_created_utc) between 11 and 14) q2,
...
from order_processing
group by date_created_utc::date

Determine number of seconds in one year

What SQL expression would be able to calculate the number of seconds in any particular year?
For non leap years:
SELECT 365 * 24 * 60 * 60 AS secInYear
FROM dual
And in general:
SqlFiddleDemo
SELECT
24 * 60 * 60 * (
CASE
WHEN MOD(EXTRACT(YEAR FROM sysdate), 400) = 0
OR ( MOD(EXTRACT(YEAR FROM sysdate), 4) = 0 AND MOD(EXTRACT(YEAR FROM sysdate), 100) != 0) THEN 366
ELSE 365
END ) AS secInYear
FROM dual
SELECT
(ADD_MONTHS(TRUNC(SYSDATE, 'YYYY'), 12) - TRUNC(SYSDATE, 'YYYY')) * 86400 SECONDS
FROM
DUAL;
SELECT DAYOFYEAR('2015-12-31') * 24 * 60 * 60 FROM DUAL
DAYOFYEAR will return 365 for New Years Eve for regular years, and 366 for leap years.

Return zero value when no row data exists

My question has been addressed previously but I can't seem to apply any solution to my query to make it work. Would very much appreciate some guidance.
My current query below returns this data set:
| Age | Count |
0-1 day 300
2-3 days 6000
3-4 days 100
SELECT(CASE WHEN time_dtm > SYSDATE -1 THEN '0-1 day'
WHEN time_dtm > SYSDATE -2 AND time_dtm < SYSDATE -1 THEN '1-2 days'
WHEN time_dtm > SYSDATE -3 AND time_dtm < SYSDATE -2 THEN '2-3 days'
WHEN time_dtm > SYSDATE -4 AND time_dtm < SYSDATE -3 THEN '3-4 days'
WHEN time_dtm > SYSDATE -5 AND time_dtm < SYSDATE -4 THEN 'Closed'
END) AS Age,
COUNT( * ) AS "Count"
FROM table_1
WHERE id IN (1,2,3)
GROUP BY (CASE WHEN time_dtm > SYSDATE -1 THEN '0-1 day'
WHEN time_dtm > SYSDATE -2 AND time_dtm < SYSDATE -1 THEN '1-2 days'
WHEN time_dtm > SYSDATE -3 AND time_dtm < SYSDATE -2 THEN '2-3 days'
WHEN time_dtm > SYSDATE -4 AND time_dtm < SYSDATE -3 THEN '3-4 days'
WHEN time_dtm > SYSDATE -5 AND time_dtm < SYSDATE -4 THEN 'Closed'
END)
ORDER BY (CASE WHEN time_dtm > SYSDATE -1 THEN '0-1 day'
WHEN time_dtm > SYSDATE -2 AND time_dtm < SYSDATE -1 THEN '1-2 days'
WHEN time_dtm > SYSDATE -3 AND time_dtm < SYSDATE -2 THEN '2-3 days'
WHEN time_dtm > SYSDATE -4 AND time_dtm < SYSDATE -3 THEN '3-4 days'
WHEN time_dtm > SYSDATE -5 AND time_dtm < SYSDATE -4 THEN 'Closed'
END)
However, I would like it to show the zero/null rows as zeros like this:
| Age | Count |
0-1 day 300
1-2 days 0
2-3 days 6000
3-4 days 100
Closed 0
I've read all sorts from the past couple of days re: NVL, COALESCE, FULL/LEFT/RIGHT OUTER JOIN, LEFT/RIGHT JOINS, UNION ALL etc none of which had CASE statements and tried to work around it myself BUT! You have to know when to stop and ask for directions.
First off, re-write your query. Use views or common table expression to avoid repeating yourself three times for your SELECT, GROUP BY, ORDER BY clauses. Your query becomes:
WITH data AS (
SELECT(CASE WHEN time_dtm > SYSDATE -1 THEN '0-1 day'
WHEN time_dtm > SYSDATE -2 AND
time_dtm < SYSDATE -1 THEN '1-2 days'
WHEN time_dtm > SYSDATE -3 AND
time_dtm < SYSDATE -2 THEN '2-3 days'
WHEN time_dtm > SYSDATE -4 AND
time_dtm < SYSDATE -3 THEN '3-4 days'
WHEN time_dtm > SYSDATE -5 AND
time_dtm < SYSDATE -4 THEN 'Closed'
END) AS Age
FROM table_1
WHERE id IN (1,2,3)
)
SELECT Age, COUNT(*)
FROM data
GROUP BY Age
ORDER BY Age
Then, in order to be sure that any of your desired groups will be available in the result, you have lots of options.
You could use UNION ALL:
WITH data AS (
SELECT(CASE WHEN time_dtm > SYSDATE -1 THEN '0-1 day'
WHEN time_dtm > SYSDATE -2 AND
time_dtm < SYSDATE -1 THEN '1-2 days'
WHEN time_dtm > SYSDATE -3 AND
time_dtm < SYSDATE -2 THEN '2-3 days'
WHEN time_dtm > SYSDATE -4 AND
time_dtm < SYSDATE -3 THEN '3-4 days'
WHEN time_dtm > SYSDATE -5 AND
time_dtm < SYSDATE -4 THEN 'Closed'
END) AS Age
FROM table_1
WHERE id IN (1,2,3)
-- The below will add one record for every desired Age group
UNION ALL
SELECT '0-1 day' FROM DUAL UNION ALL
SELECT '1-2 days' FROM DUAL UNION ALL
SELECT '2-3 days' FROM DUAL UNION ALL
SELECT '3-4 days' FROM DUAL UNION ALL
SELECT 'Closed' FROM DUAL
)
SELECT Age, COUNT(*) - 1 -- Subtract the extra record again
FROM data
GROUP BY Age
ORDER BY Age
An entirely different solution would involve LEFT OUTER JOINs:
-- Groups is a dynamic table that contains the date ranges and their "Age" label
WITH groups AS (
SELECT SYSDATE -1 lower, SYSDATE upper, '0-1 day' Age FROM DUAL UNION ALL
SELECT SYSDATE -2 , SYSDATE -1 , '1-2 days' FROM DUAL UNION ALL
SELECT SYSDATE -3 , SYSDATE -2 , '2-3 days' FROM DUAL UNION ALL
SELECT SYSDATE -4 , SYSDATE -3 , '3-4 days' FROM DUAL UNION ALL
SELECT SYSDATE -5 , SYSDATE -4 , 'Closed' FROM DUAL
)
SELECT g.Age, NVL(SUM(t.counter), 0)
FROM groups g
-- LEFT OUTER JOINing "table_1" to "groups" will ensure that every group
-- appears at least once in the result
LEFT OUTER JOIN (
SELECT 1 counter, t.* FROM table_1 t WHERE t.id IN (1,2,3)
) t
ON t.time_dtm >= g.lower
AND t.time_dtm < g.upper
GROUP BY g.Age
ORDER BY g.Age
In the second example, you could also do without a CTE and use a nested SELECT for the groups table. It is easy to see how the second example is simpler to evolve in the future, should your requirements change.