Oracle 11 query group/count and split result by hour - oracle

I have a simple query on Oracle v11 to group and count records, nothing special:
select ADDR, count(ADDR) from DBTB group by ADDR;
The rable have also the TIMESTAMP column, what I'm trying to do is group and count unique ADDR by hour.
e.g. on table:
TIMESTAMP ADDR
19-OCT-2021 17:15:00 12345
19-OCT-2021 17:20:00 12345
19-OCT-2021 17:25:00 12345
19-OCT-2021 17:27:00 67890
19-OCT-2021 18:10:00 55555
19-OCT-2021 18:20:00 55555
19-OCT-2021 18:30:00 66666
19-OCT-2021 18:43:00 77777
The output should be:
HOUR COUNT
17 2
18 3
Could someone help me to do a query to count and group the same ADDR split by hour?
Thank you
Lucas

Use TO_CHAR() to get the hour of each timestamp:
SELECT TO_CHAR("TIMESTAMP", 'HH24') HOUR,
COUNT(DISTINCT "ADDR") COUNT
FROM DBTB
GROUP BY TO_CHAR("TIMESTAMP", 'HH24');
Probably you also need to group by the date:
SELECT TRUNC("TIMESTAMP") DAY,
TO_CHAR("TIMESTAMP", 'HH24') HOUR,
COUNT(DISTINCT "ADDR") COUNT
FROM DBTB
GROUP BY TRUNC("TIMESTAMP"), TO_CHAR("TIMESTAMP", 'HH24');
Or filter for a specific date:
SELECT TO_CHAR("TIMESTAMP", 'HH24') HOUR,
COUNT(DISTINCT "ADDR") COUNT
FROM DBTB
WHERE TRUNC("TIMESTAMP") = TO_DATE('19-Oct-2021', 'DD-MON-YYYY')
GROUP BY TO_CHAR("TIMESTAMP", 'HH24');
See the demo.

Related

In SQL Developer, how can I simulate nested for loops to extract a large volume of counts?

I am using Oracle SQL Developer and I need to pull counts from a very large database.
I need to count both the number of visits and the number of unique visitors at each of 5 sites (and in total) for each of 10 quarters (and in total) - resulting in 120 counts. Each row in this database represents one visit, and the visitors each have a unique visitor_ID.
Currently, I have one line for each count, but I need to make modifications and I don't want to do it in such an inefficient way this time.
select
sum(case when visit_date between '01-JAN-2019 00:00:00' and '31-MAR-2019 00:00:00' and site=site1 then 1 else 0 end) as 19Q1_visits_site1,
count(distinct case when visit_date between '01-JAN-2019 00:00:00' and '31-MAR-2019 00:00:00' and site=site1 then visitor_id) as 19Q1_unique_site1,
[...]
from visitdata
where [additional qualifiers];
If possible, I would like to create something along the lines of:
19Q1 = visit_date between '01-JAN-2019 00:00:00' and '31-MAR-2019 00:00:00'
19Q2 = visit_date between '01-APR-2019 00:00:00' and '30-JUN-2019 00:00:00'
[...]
allQ = visit_date between '01-JAN-2019 00:00:00' and '30-SEP-2021 00:00:00'
S1 = site in (site1)
[...]
allS = site in (site1, site2, site3, site4, site5)
sites = [S1, S2, S3, S4, S5, allS]
quarters = [19Q1, 19Q2, ..., allQ]
for s in sites:
for q in quarters:
select
sum(case when q and s then 1 else 0 end) as (str(q) + 'visits' + str(s)),
count(distinct case when q and s then visitor_id) as (str(q) + 'unique' + str(s))
from visitdata
where [additional qualifiers];
I know SQL doesn't do for loops. Any advice would be fantastic so I don't have to create another embarrassing script with almost 200 lines. Thanks!
Very briefly, use a built-in or user-defined function that takes a date and returns the quarter to which that date belongs. (SQL Server supports "quarter" as a datepart, but you can probably write this yourself if Oracle doesn't support it. You could also add visit_quarter to your table as a materialized computed column and even index it if you use this a lot.) Then you can write a single grouping query along the lines of
SELECT
site,
quarter(visit_date) as Q,
COUNT(visitor_id) as numvisits,
COUNT(DISTINCT visitor_id) AS numDistinctVisitors
FROM T
WHERE <additional conditions>
GROUP BY site, quarter(visit_date)
ORDER BY site, Q
For finding quarters you can use TO_CHAR() on dates, and for changing the start date and end date of a quarter, you can use ADD_MONTHS() eg
Table
create table randomdates( date_ )
as
select sysdate - dbms_random.value( 1, 600 )
from dual
connect by level <= 50 ;
Query
select date_
, to_char( date_, 'YYYY-Q' ) quarter
from randomdates
;
-- result
DATE_ QUARTER
09/23/2021 2021-3
09/24/2020 2020-3
03/23/2020 2020-1
03/29/2021 2021-1
11/29/2020 2020-4
03/05/2021 2021-1
04/08/2021 2021-2
...
GROUP BY should also be possible - as #Steve Kass suggested.
select to_char( date_, 'YYYY-Q' ), count(*)
from randomdates
group by to_char( date_, 'YYYY-Q' )
order by 1 desc
;
TO_CHAR(DATE_,'YYYY-Q') COUNT(*)
2021-4 6
2021-3 13
2021-2 22
2021-1 13
2020-4 20
2020-3 12
2020-2 10
2020-1 4
...
From your comment:
I'm using a modified fiscal year anyway. Do you know how I can define
quarters when the visit_date is stored as yyyymmdd?
If you need different start/end days for the, ADD_MONTHS() could help eg the modified_quarters in the following query start a month later than the "standard" quarters. Regarding the dates: in Oracle, they contain the century, the year within the century, the month, the day of the month, the hour, the minute and the second (7 bytes). You can just use TO_CHAR() and pick up whichever component (of the date) you need using the "Format Model" eg 'Q' in the example below.
-- Query executed in APEX.
-- Column date_ : no formatting (compare the output to the same query in the dbfiddle).
select date_
, to_char( date_, 'YYYY-Q' ) quarter
, to_char( date_, 'YY' ) || 'Q' || to_char( date_, 'Q' ) quarter_
, to_char( add_months( date_, 1 ), 'YYYY-Q' ) modified_quarter
from randomdates
;
DATE_ QUARTER QUARTER_ MODIFIED_QUARTER
09/23/2021 2021-3 21Q3 2021-4
09/24/2020 2020-3 20Q3 2020-4
03/23/2020 2020-1 20Q1 2020-2
03/29/2021 2021-1 21Q1 2021-2
11/29/2020 2020-4 20Q4 2020-4
03/05/2021 2021-1 21Q1 2021-2
For calculating subtotals and total (counts) per site, you could use GROUP BY ROLLUP() eg
Table & data
-- Caution: dates in this table are not the same as in the randomdates table.
create table sitesanddates( site_, date_ )
as
select
trunc( dbms_random.value( 1, 6 ) )
, sysdate - dbms_random.value( 1, 600 )
from dual
connect by level <= 50 ;
-- group by site and quarter
select site_, to_char( date_, 'YYYY-Q' ), count(*)
from sitesanddates
group by site_, to_char( date_, 'YYYY-Q' )
order by 1, 2
;
SITE_ TO_CHAR(DATE_,'YYYY-Q') COUNT(*)
1 2020-1 1
1 2020-4 3
1 2021-1 1
1 2021-2 1
1 2021-3 2
2 2020-1 1
2 2020-2 1
GROUP BY ROLLUP
select site_, to_char( date_, 'YYYY-Q' ) q_, count(*)
from sitesanddates
group by rollup( site_, to_char( date_, 'YYYY-Q' ) )
order by 1, 2
;
SITE_ Q_ COUNT(*)
1 2020-1 1
1 2020-4 3
1 2021-1 1
1 2021-2 1
1 2021-3 2
1 - 8 -- <- subtotal for site 1
2 2020-1 1
2 2020-2 1
...
5 2020-4 2
5 2021-1 2
5 2021-2 1
5 2021-4 1
5 - 10 -- <- subtotal for site 5
- - 50 -- <- grand total
link to dbfiddle

Separating Overlapping Date Ranges in Oracle

I have data with overlapping data ranges. Example below
Customer_ID
FAC_NUM
Start_Date
End_Date
New_Monies
12345
ABC1234
26/NOV/2014
26/MAY/2015
100000
12345
ABC1234
12/DEC/2014
12/JUN/2015
200000
12345
ABC1234
15/JUN/2015
15/DEC/2015
500000
12345
ABC1234
20/DEC/2015
20/JUN/2016
600000
I want to convert this table into data with non overlapping ranges such that for each overlapping period, the New_Monies column is summed together and shown as a new row. For the example above, I want the output to be as follows
Customer_ID
FAC_NUM
Start_Date
End_Date
New_Monies
12345
ABC1234
26/NOV/2014
11/DEC/2014
100000
12345
ABC1234
12/DEC/2014
26/MAY/2015
300000
12345
ABC1234
27/MAY/2015
12/JUN/2015
200000
12345
ABC1234
15/JUN/2015
15/DEC/2015
500000
12345
ABC1234
20/DEC/2015
20/JUN/2016
600000
Row 2 above being the overlapping period of 12 Dec 2014 to 26 May 2015 showing the total New_Monies as 300000 (100000+200000)
What would be the best way to do this in Oracle?
Thanks in advance for your support.
Regards,
Ani
with
prep (customer_id, fac_num, dt, amount) as (
select t.customer_id, t.fac_num,
case h.col when 's' then t.start_date else t.end_date + 1 end as dt,
case h.col when 's' then t.new_monies else - t.new_monies end as amount
from sample_data t
cross join
(select 's' as col from dual union all select 'e' from dual) h
)
, cumul_sums (customer_id, fac_num, dt, amount) as (
select distinct
customer_id, fac_num, dt,
sum(amount) over (partition by customer_id, fac_num order by dt)
from prep
)
, with_intervals (customer_id, fac_num, start_date, end_date, amount) as (
select customer_id, fac_num, dt,
lead(dt) over (partition by customer_id, fac_num order by dt) - 1,
amount
from cumul_sums
)
select customer_id, fac_num, start_date, end_date, amount
from with_intervals
where end_date is not null
order by customer_id, fac_num, start_date
;
The prep subquery unpivots the inputs, while at the same time changing the "end date" to the "start date" of the following interval and assigning a positive amount to the "start date" and the negative of the same amount to the following "start date". cumul_sums computes the cumulative sums; note that if two or more intervals begin on the same date (so the same date from prep appears multiple times for a customer and fac_num), the analytic sum will include the amounts from ALL the rows up to that date - the default windowing clause is range between...... After the cumulative sums are computed, this subquery also de-duplicates the output rows (to handle precisely that complication, of multiple intervals starting on the same date). with_intervals recovers the "start date" - "end date" intervals, and the final step simply removes the last interval ("to infinity") which would have an "amount" of zero.
EDIT This solution answers the OP's original question. After posting the solution, the OP changed the question. The solution can be changed easily to address the new formulation. I'm not going to chase shadows though; the solution will remain as is.
Here is an way to do this.
with all_data
as (select Customer_ID,FAC_NUM,start_date as dt,new_monies as calc_monies
from t
union all
select Customer_ID,FAC_NUM,end_date as dt,new_monies*-1 as calc_monies
from t
)
select x.customer_id
,x.fac_num
,x.start_date
,case when row_number() over(order by end_date desc)=1 then
x.end_date + 1
else x.end_date
end as new_end_date
from (
select t.customer_id
,t.fac_num
,t.dt as start_date
,lead(dt) over(order by dt)-1 as end_date
,sum(calc_monies) over(order by dt) as new_monies
from all_data t
)x
where end_date is not null
order by 3
db fiddle link
https://dbfiddle.uk/?rdbms=oracle_11.2&fiddle=856c9ac0954e45429994f4ac45699e6f
+-------------+---------+------------+--------------+------------+
| CUSTOMER_ID | FAC_NUM | START_DATE | NEW_END_DATE | NEW_MONIES |
+-------------+---------+------------+--------------+------------+
| 12345 | ABC1234 | 26-NOV-14 | 11-DEC-14 | 100000 |
| 12345 | ABC1234 | 12-DEC-14 | 25-MAY-15 | 300000 |
| 12345 | ABC1234 | 26-MAY-15 | 12-JUN-15 | 200000 |
+-------------+---------+------------+--------------+------------+

Group by hour using Oracle SQL

I have a varchar5 column with times from
00:00
00:01
00:02
00:03
... all the way to
23:59
How would I count how many minutes are in an hour? To get the result
00 60
01 60
02 60
and so on...
SQL:
select 24_HOUR_CLOCK
From time table
Group by ...
Order by 24_HOUR_CLOCK ASC
Means to count records
I think you can use substr to extract first two characters from the time string and group on that.
select substr(col, 1, 2) hour, count(*) minutes
from your_table
group by substr(col, 1, 2)
order by hour
or find substr inside a subquery as #Mathguy suggested:
select hour,
count(*) minutes
from (
select substr(col, 1, 2) hour
from your_table
)
group by hour
order by hour

Find only particular days between two dates

I have an Oracle table with data like below:
1. ID DATE
2. 12 02/11/2013
3. 12 02/12/2013
4. 13 02/11/2013
5. 13 02/12/2013
6. 13 02/13/2013
7. 13 02/14/2013
8. 14 02/11/2013
9. 14 02/12/2013
10. 14 02/13/2013
I need to find only those ID who has only Monday, Tuesday and Wednesday dates, so here only ID = 14 should be returned. I am using Oracle and dates are in format MM/DD/YYYY.
Please advice.
Regards,
Nitin
If date column is DATE datatype, then you can
select id
from your_table
group by id
having sum(case
when to_char(date_col,'fmday')
in ('monday','tuesday','wednesday') then 1
else 99
end) = 3;
EDIT: Corected the above code at the igr's observation
But this is ok only if you don't have a day twice for the same id.
If the column is varchar2 then the condition becomes to_char(to_date(your_col,'mm/dd/yyyy'),'fmday') in ...
A more robust code would be:
select id
from(
select id, date_col
from your_table
group by id, date_col
)
group by id
having sum(case
when to_char(date_col,'fmday', 'NLS_DATE_LANGUAGE=ENGLISH')
in ('monday','tuesday','wednesday') then 1
else 99
end) = 3;
select id
from (
select
id,
sum (case when to_char(dt, 'D', 'nls_territory=AMERICA') between 1 and 3 then 1 else -1 end) AS cnt
from t
group by id
)
where cnt=3
NOTE: I assumed (id,dt) is unique - no two lines with same id and date.
do something like
SELECT * FROM your_table t
where to_char(t.DATE, 'DY') in ('whatever_day_abbreviation_day_you_use');
alternatively if you prefer you could use day numbers like:
SELECT * FROM your_table t
where to_number(to_char(d.ts, 'D')) in (1,2,3);
if you'd like to avoid ID repetition add DISTINCTION
SELECT DISTINCT ID FROM your_table t
where to_number(to_char(d.ts, 'D')) in (1,2,3);

How to find the difference between two timestamp column

I have table name Record which has the following columns, Empid in number column, dat in timestamp
Which has the following values
empid dat
====== ====
101 4/9/2012 9:48:54 AM
101 4/9/2012 9:36:28 AM
101 4/9/2012 6:16:28 PM
101 4/10/2012 9:33:48 AM
101 4/10/2012 12:36:28 PM
101 4/10/2012 8:36:12 PM
101 4/11/2012 9:36:28 AM
101 4/11/2012 4:36:22 PM
Here I need to display the following columns,
empid,min(dat) as start,max(dat) as end and difference(max(dat)-min(dat) for each day
Here 3 different days are exists so It should return 3 records with the above mentioned columns.
Please give some ways to get this.
Simply subtract them: max(dat) - min(dat)
SELECT empid,
min(dat) as strt,
max(dat) as end,
max(dat) - min(dat) as diff
FROM the_table
GROUP BY empid;
If you want to group by the day instead of the empid, use this one:
select trunc(dat),
min(dat) as strt,
max(dat) as end,
max(dat) - min(dat) as diff
from the_table
group by trunc(dat)
Date arithmetic is pretty straightforward in Oracle: the difference betwwen two dates is returned as the number of days. Values of less than a day are returned as *decimals". That is, 75 minutes is 1.25 hours not 1.15. If you want it as hours and minutes you need to work with an interval.
The inner query calculates the difference between the minimum and maximum data for each employee for each day, and converts it to a DAY interval. The outer query extracts the HOUR and MINUTES from that interval.
select empid
, tday
, sdt
, edt
, extract(hour from diff) diff_hours
, extract (minute from diff) diff_minutes
from (
select empid
, trunc(dat) tday
, min(dat) sdt
, max(dat) edt
, numtodsinterval(max(dat) - min(dat), 'DAY') diff
from t42
group by empid, trunc(dat)
)

Resources