Oracle query - without temporary table - sql

I'm new to Oracle and I need to help with this query. I have table with data samples /records like:
name | datetime
-----------
A | 20140414 10:00
A | 20140414 10:30
A | 20140414 11:00
B | 20140414 11:30
B | 20140414 12:00
A | 20140414 12:30
A | 20140414 13:00
A | 20140414 13:30
And I need to "group"/get informations into this form:
name | datetime_from | datetime_to
----------------------------------
A | 20140414 10:00 | 20140414 11:00
B | 20140414 11:30 | 20140414 12:00
A | 20140414 12:30 | 20140414 13:30
I couldnt find any solution for query similar to this. Could anyone please help me?
Note: I dont want do use temporary tables.
Thanks,
Pavel

SQL> with t (name, datetime) as
2 (
3 select 'A', to_date('20140414 10:00','YYYYMMDD HH24:MI') from dual union all
4 select 'A', to_date('20140414 10:30','YYYYMMDD HH24:MI') from dual union all
5 select 'A', to_date('20140414 11:00','YYYYMMDD HH24:MI') from dual union all
6 select 'B', to_date('20140414 11:30','YYYYMMDD HH24:MI') from dual union all
7 select 'B', to_date('20140414 12:00','YYYYMMDD HH24:MI') from dual union all
8 select 'A', to_date('20140414 12:30','YYYYMMDD HH24:MI') from dual union all
9 select 'A', to_date('20140414 13:00','YYYYMMDD HH24:MI') from dual union all
10 select 'A', to_date('20140414 13:30','YYYYMMDD HH24:MI') from dual
11 )
12 select name, min(datetime) datetime_from, max(datetime) datetime_to
13 from (
14 select name, datetime,
15 datetime-(1/48)*(row_number() over(partition by name order by datetime)) dt
16 from t
17 )
18 group by name,dt
19 order by 2,1
20 /
N DATETIME_FROM DATETIME_TO
- -------------- --------------
A 20140414 10:00 20140414 11:00
B 20140414 11:30 20140414 12:00
A 20140414 12:30 20140414 13:30

You need to find periods where the values are the same. The easiest way in Oracle is to use the lag() function, some logic, and aggregation:
select name, min(datetime), max(datetime)
from (select t.*,
sum(case when name <> prevname then 1 else 0 end) over (order by datetime) as cnt
from (select t.*, lag(name) over (order by datetime) as prevname
from table t
) t
) t
group by name, cnt;
What this does is count, for a given value of datetime, the number of times that the name has switched on or before that datetime. This identifies the periods of "constancy", which are then used for aggregation.

As 9000 is suggesting you can have a query like the following:
select
a.name,
Max(a.datetime),
Min(b.datetime)
from
table a,
table b
group by
a.name
where a.name = b.name

Related

Merge several rows into one if they have a gap less than a 5 seconds

I'm trying to find a SQL query that lets me to merge some rows into one from a table that have a gap less than 5 seconds. For example, I have a table like the following:
Name | Time
==============================
John 2021-02-01 13:08:10
John 2021-02-01 13:08:12
John 2021-02-01 17:35:23
John 2021-02-07 11:16:31
Walt 2021-01-14 10:23:48
Joseph 2021-01-23 07:04:33
Walt 2021-01-14 10:23:51
Walt 2021-01-04 09:22:45
So, I want to have a result like this:
Name | Time
==============================
John 2021-02-01
John 2021-02-01
John 2021-02-07
Walt 2021-01-14
Walt 2021-01-04
Joseph 2021-01-23
For John there are two rows that have a gap less than 5 seconds, so they will merge in one row for the same day. The same happens with Walt.
Can I do this with a SQL query?
Thank you in advance.
You just need to check if the next date is within 5 seconds after the current row and if so - remove such a row. This can be achieved with LEAD analytical function.
with a as (
select 'John' as name, convert(datetime, '2021-02-01 13:08:10', 120) as dt union all
select 'John' as name, convert(datetime, '2021-02-01 13:08:12', 120) as dt union all
select 'John' as name, convert(datetime, '2021-02-01 13:08:15', 120) as dt union all
select 'John' as name, convert(datetime, '2021-02-01 17:35:23', 120) as dt union all
select 'John' as name, convert(datetime, '2021-02-07 11:16:31', 120) as dt union all
select 'Walt' as name, convert(datetime, '2021-01-14 10:23:48', 120) as dt union all
select 'Joseph' as name, convert(datetime, '2021-01-23 07:04:33', 120) as dt union all
select 'Walt' as name, convert(datetime, '2021-01-14 10:23:51', 120) as dt union all
select 'Walt' as name, convert(datetime, '2021-01-04 09:22:45', 120) as dt
)
, gap_size as (
select
name,
dt,
/*Check the difference between current row and the next row per name*/
datediff(s,
dt,
lead(dt) over(partition by name order by dt asc)
) as within_5_secs_with_next
from a
)
select
name,
cast(dt as date) as dt_date
from gap_size
where coalesce(within_5_secs_with_next, 10) >= 5
order by name, dt asc
GO
name | dt_date
:----- | :---------
John | 2021-02-01
John | 2021-02-01
John | 2021-02-07
Joseph | 2021-01-23
Walt | 2021-01-04
Walt | 2021-01-14
db<>fiddle here

How to do a query on Oracle SQL to get time intervals, grouping by specific fields

I love a good challenge, but this one has been breaking my head for too long. :)
I'm trying to build a query to get dates intervals, grouping the information by one field.
Let me try to explain it in a simple way.
We have this table:
I need to get the intervals a soldier spent on each ranking, so the end result I need to get should be something like this:
As you can see the soldier can be promoted/demoted along the time.
Any suggestion on how to build a query to do this?
THANK YOU!
From Oracle 12, you can use MATCH_RECOGNIZE:
SELECT *
FROM table_name
MATCH_RECOGNIZE (
PARTITION BY id
ORDER BY start_date, end_date
MEASURES
FIRST( name ) AS name,
FIRST( ranking ) AS ranking,
FIRST( start_date ) AS start_date,
LAST( end_Date ) AS end_Date
PATTERN ( same_rank+ )
DEFINE same_rank AS FIRST( ranking ) = ranking
)
Which, for the sample data:
CREATE TABLE table_name ( id, name, ranking, start_date, end_date ) AS
SELECT 1001, 'Jones', 'Lieutenant', DATE '2000-03-20', DATE '2002-08-15' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Lieutenant', DATE '2002-08-16', DATE '2003-03-18' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Lieutenant', DATE '2003-03-19', DATE '2004-06-01' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Lieutenant', DATE '2004-06-02', DATE '2004-10-01' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2004-10-02', DATE '2005-04-20' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2005-04-21', DATE '2007-02-20' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Major', DATE '2007-02-21', DATE '2008-10-22' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Major', DATE '2008-10-23', DATE '2010-01-26' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2010-01-27', DATE '2013-11-25' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2013-11-26', DATE '2014-05-11' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Major', DATE '2014-05-12', DATE '2016-04-22' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'General', DATE '2016-04-23', DATE '2020-10-10' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'General', DATE '2020-10-11', DATE '2020-11-30' FROM DUAL;
Outputs:
ID | NAME | RANKING | START_DATE | END_DATE
---: | :---- | :--------- | :------------------ | :------------------
1001 | Jones | Lieutenant | 2000-03-20 00:00:00 | 2004-10-01 00:00:00
1001 | Jones | Captain | 2004-10-02 00:00:00 | 2007-02-20 00:00:00
1001 | Jones | Major | 2007-02-21 00:00:00 | 2010-01-26 00:00:00
1001 | Jones | Captain | 2010-01-27 00:00:00 | 2014-05-11 00:00:00
1001 | Jones | Major | 2014-05-12 00:00:00 | 2016-04-22 00:00:00
1001 | Jones | General | 2016-04-23 00:00:00 | 2020-11-30 00:00:00
db<>fiddle here
This is a type of gaps and islands problem. You want to find groups of rows that are the same, which you can do using lag() to compare the ranking and then a cumulative sum to keep track of the changes:
select soldier_id, soldier_name, ranking,
min(start_date), max(end_date)
from (select t.*,
sum(case when prev_end_date = start_date - interval '1' day then 0 else 1 end)
(partition by soldier_id order by start_date) as island
from (select t.*,
lag(end_date) over (partition by soldier_id, ranking order by start_date) as prev_end_date
from t
) t
) t
group by soldier_id, soldier_name, ranking, island;
Note: This assumes that the soldier_name does not change over time for a given soldier. If that is something you need to deal with, then ask a new question with appropriate sample data and desired results.

How to aggregate count of rows having the same value in a sequence?

I have a query that returns data in the following sample:
SELECT timestamp, atm_id FROM TRANSACTIONS ORDER BY TIMESTAMP ASC;
Output
TIMESTAMP | ATM_ID |
--------------------
2010-01-01 | EP02 |
2010-01-01 | EP02 |
2010-01-28 | EP02 |
2010-02-07 | EP02 |
2010-02-09 | EP11 |
2010-03-19 | EP11 |
2010-03-19 | EP02 |
2010-04-03 | EP05 |
2010-04-30 | EP02 |
I know how to group by ATM_ID and put the count in-front of each
SELECT
ATM_ID,
COUNT(*) CNT
FROM
TRANSACTIONS
GROUP BY
ATM_ID;
Based on the sample data above, this will yield something like
ATM_ID | CNT
---------------
EP02 | 6
EP11 | 2
EP05 | 1
However, I am interested in grouping on a different level. If a certain ATM_ID is duplicated in consecutive rows, the number of rows having the same ATM_ID in sequence should be included in the output, even if the same ATM_ID appears later after a different ATM_ID
Desired Output
ATM_ID | CNT
---------------
EP02 | 4 --Four rows of ATM_ID EP02
EP11 | 2 --Followed by 2 rows of ATM_ID EP11
EP02 | 1 --Followed by 1 row of ATM_ID EP02
EP05 | 1 --Followed by 1 row of ATM_ID EP05
EP02 | 1 --Followed by 1 row of ATM_ID EP02
Ignore the comments on the right, these are just for clarifications, not part of the output.
Is that possible?
PS: The answer below by Syed Aladeen gives the output with the correct count, but with the wrong order. I create an SQL fiddle for convenience:
SQL Fiddle
Try this
select atm_id , count(*)
from (select TRANSACTIONS.*,
(row_number() over (order by id) -
row_number() over (partition by atm_id order by id)
) as grp
from TRANSACTIONS
) TRANSACTIONS
group by grp, atm_id order by max(id)
-- Oracle 12c+: pattern matching
with s(dt, atm_id) as (
select to_date('2010-01-01', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-01-01', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-01-28', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-02-07', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-02-09', 'yyyy-mm-dd'), 'EP11' from dual union all
select to_date('2010-03-19', 'yyyy-mm-dd'), 'EP11' from dual union all
select to_date('2010-03-19', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-04-03', 'yyyy-mm-dd'), 'EP05' from dual union all
select to_date('2010-04-30', 'yyyy-mm-dd'), 'EP02' from dual)
select *
from s
match_recognize (
order by dt
measures v.atm_id as atm_id,
count(v.atm_id) as cnt,
first(dt) as min_dt,
last (dt) as max_dt
pattern (v+)
define v as v.atm_id = first(atm_id)
);
ATM_ CNT MIN_DT MAX_DT
---- ---------- ------------------- -------------------
EP02 4 2010-01-01 00:00:00 2010-02-07 00:00:00
EP11 2 2010-02-09 00:00:00 2010-03-19 00:00:00
EP02 1 2010-03-19 00:00:00 2010-03-19 00:00:00
EP05 1 2010-04-03 00:00:00 2010-04-03 00:00:00
EP02 1 2010-04-30 00:00:00 2010-04-30 00:00:00
Elapsed: 00:00:00.01
-- Oracle 8i+: window sort + window buffer + group by [+ order by]
with s(dt, atm_id) as (
select to_date('2010-01-01', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-01-01', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-01-28', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-02-07', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-02-09', 'yyyy-mm-dd'), 'EP11' from dual union all
select to_date('2010-03-19', 'yyyy-mm-dd'), 'EP11' from dual union all
select to_date('2010-03-19', 'yyyy-mm-dd'), 'EP02' from dual union all
select to_date('2010-04-03', 'yyyy-mm-dd'), 'EP05' from dual union all
select to_date('2010-04-30', 'yyyy-mm-dd'), 'EP02' from dual)
select atm_id, count(*) cnt, min(dt) min_dt, max(dt) as max_dt
from
(select dt, atm_id, count(lg) over (order by dt) ct, lg
from
(select dt, atm_id, decode(atm_id, lag(atm_id) over (order by dt), null, 1) lg
from s
)
)
group by ct, atm_id
order by min_dt;
ATM_ CNT MIN_DT MAX_DT
---- ---------- ------------------- -------------------
EP02 4 2010-01-01 00:00:00 2010-02-07 00:00:00
EP11 1 2010-02-09 00:00:00 2010-02-09 00:00:00
EP02 1 2010-03-19 00:00:00 2010-03-19 00:00:00
EP11 1 2010-03-19 00:00:00 2010-03-19 00:00:00
EP05 1 2010-04-03 00:00:00 2010-04-03 00:00:00
EP02 1 2010-04-30 00:00:00 2010-04-30 00:00:00
6 rows selected.

SQL Server select missing Dates in result set

I have one table containing Employee Daily Attendance punchtime in space separated form.
EmployeePunch
EmpID EmpName Date Time
1 ABC 2014-12-01 10:00 18:00
1 ABC 2014-12-02 09:50 17:50
1 ABC 2014-12-04 09:30 17:30
1 ABC 2014-12-07 10:00 18:00
1 ABC 2014-12-08 09:50 17:50
1 ABC 2014-12-10 09:30 17:30
Now I want to write a query for following output
EmpID EmpName Date Time
1 ABC 2014-12-01 10:00 18:00
1 ABC 2014-12-02 09:50 17:50
1 ABC 2014-12-03 ABSENT
1 ABC 2014-12-04 09:30 17:30
1 ABC 2014-12-05 ABSENT
1 ABC 2014-12-06 ABSENT
1 ABC 2014-12-07 10:00 18:00
1 ABC 2014-12-08 09:50 17:50
1 ABC 2014-12-09 ABSENT
1 ABC 2014-12-10 09:30 17:30
First define CTE to generate missing records:
WITH dates AS (
SELECT DISTINCT EmpId, EmpName, '2014-12-01' AS Date, 'ABSENT' AS Time
FROM EmployeePunch
UNION
SELECT EmpId, EmpName, DATEADD(DAY, 1, Date), 'ABSENT'
FROM dates
WHERE Date < DATEADD(DAY, -1, DATEADD(MONTH, 1, '2014-12-01')))
SELECT * FROM dates
In the next step replace the last line with:
SELECT * FROM EmployeePunch
UNION ALL
SELECT d.* FROM dates d
LEFT JOIN EmployeePunch e
ON e.EmpId = d.EmpId AND e.Date = d.Date
WHERE e.Time IS NULL
The missing rows are the outerjoined ones.
Without CTE:
select ep1.EmpId, ep1.EmpName, a.Date, ISNULL(ep2.Time, 'ABSENT') as Time
from (
select DATEADD(day, a.a + (10 * b.a) + (100 * c.a), CAST('2014-12-01' /*begin date*/ AS DATE)) as Date
from (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as a
cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as b
cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as c
) a cross apply (select distinct EmpId, EmpName from EmployeePunch) ep1 --on a.Date = f.Date
left join EmployeePunch ep2 on ep2.Date = a.Date and ep2.EmpId = ep1.EmpId
where a.Date <= '2014-12-10' and ep1.EmpId is not null
Be aware about the maximal allowed range - 1000 days, but it can be extended if necessary

How can I select only newest entries?

I have table like this:
IST | FILEDATE | DATE | ...
1 | 2013-2014 | 27.03.2015 10:20:47 | ...
2 | 2013-2014 | 27.03.2015 10:20:47 | ...
3 | 2013-2014 | 27.03.2015 10:20:47 | ...
1 | 2013-2014 | 28.03.2015 11:20:47 | ...
2 | 2013-2014 | 28.03.2015 11:20:47 | ...
3 | 2013-2014 | 28.03.2015 11:20:47 | ...
1 | 2014-2015 | 29.03.2015 12:20:47 | ...
2 | 2014-2015 | 29.03.2015 12:20:47 | ...
3 | 2014-2015 | 29.03.2015 12:20:47 | ...
...
I need to select newest(with date value) entry of all IST, like this:
IST | FILEDATE | DATE | ...
1 | 2014-2015 | 29.03.2015 11:20:47 | ...
2 | 2014-2015 | 29.03.2015 11:20:47 | ...
3 | 2014-2015 | 29.03.2015 11:20:47 | ...
I tried order by and rownum=1, but its working for just single IST.
How can I do that? Thank you.
That's a typical scenario where analytical functions (aka windowing functions) are really helpful:
with v_data(ist, filedate, entry_date) as (
select 1, '2013-2014', to_date('27.03.2015 10:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 2, '2013-2014', to_date('27.03.2015 10:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 3, '2013-2014', to_date('27.03.2015 10:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 1, '2013-2014', to_date('28.03.2015 11:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 2, '2013-2014', to_date('28.03.2015 11:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 3, '2013-2014', to_date('28.03.2015 11:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 1, '2014-2015', to_date('29.03.2015 12:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 2, '2014-2015', to_date('29.03.2015 12:20:47','DD.MM.YYYY hh24:mi:ss') from dual union all
select 3, '2014-2015', to_date('29.03.2015 12:20:47','DD.MM.YYYY hh24:mi:ss') from dual)
select * from (
select
v1.*,
row_number() over (partition by ist order by entry_date desc) as rn
from v_data v1
)
where rn=1
This solution
computes an ordering per group using the ROW_NUMBER analytical function
removes everything but the newest entry per group with WHERE rn = 1
You can first group the result:
select ist, max(date) date
from table
group
by ist
Then you can combine that result with a select to get all matching lines:
select master.*
from table master
join
( select ist, max(date) date
from table
group
by ist
) filter
on master.ist = filter.ist
and master.date = filter.date
Use NOT EXISTS to find ist's that have no newer row in table:
select *
from tablename t1
where not exists (select 1 from tablename t2
where t2.ist = t1.ist
and t2.date > t1.date)*