SQL: Getting all dates between a set of date pairs - sql

I have a table with some data and a time period i.e. start date and end date
------------------------------
| id | start_date | end_date |
|------------------------------|
| 0 | 1-1-2019 | 3-1-2019 |
|------------------------------|
| 1 | 6-1-2019 | 8-1-2019 |
|------------------------------|
I want to run a query that will return the id and all the dates that are within those time periods. for instance, the result of the query for the above table will be:
------------------
| id | date |
|------------------|
| 0 | 1-1-2019 |
|------------------|
| 0 | 2-1-2019 |
|------------------|
| 0 | 3-1-2019 |
|------------------|
| 1 | 6-1-2019 |
|------------------|
| 1 | 7-1-2019 |
|------------------|
| 1 | 8-1-2019 |
------------------
I am using Redshift therefor I need it supported in Postgres and take this into consideration
Your help will be greatly appriciated

The common way this is done is to create a calendar table with a list of dates. In fact, a calendar table can be extended to include columns like:
Day number (in year)
Week number
First day of month
Last day of month
Weekday / Weekend
Public holiday
Simply create the table in Excel, save as CSV and then COPY it into Redshift.
You could then just JOIN to the table, like:
SELECT
table.id,
calendar.date
FROM table
JOIN calendar
WHERE
calendar.date BETWEEN table.start_date AND table.end_date

This question was originally tagged Postgres.
Use generate_series():
select t.id, gs.dte
from t cross join lateral
generate_series(t.start_date, t.end_date, interval '1 day') as gs(dte);

ok, It took me a while to get there but this is what I did (though not really proud of it):
I created a query that generates a calendar for the last 6 years, cross joined it with my table and then selected the relevant dates from my calendar table.
WITH
days AS (select 0 as num UNION select 1 as num UNION select 2 UNION select 3 UNION select 4 UNION select 5 UNION select 6 UNION select 7 UNION select 8 UNION select 9 UNION select 10 UNION select 11 UNION select 12 UNION select 13 UNION select 14 UNION select 15 UNION select 16 UNION select 17 UNION select 18 UNION select 19 UNION select 20 UNION select 21 UNION select 22 UNION select 23 UNION select 24 UNION select 25 UNION select 26 UNION select 27 UNION select 28 UNION select 29 UNION select 30 UNION select 31),
month AS (select num from days where num <= 12),
years AS (select num from days where num <= 6),
rightnow AS (select CAST( TO_CHAR(GETDATE(), 'yyyy-mm-dd hh24') || ':' || trim(TO_CHAR((ROUND((DATEPART (MINUTE, GETDATE()) / 5), 1) * 5 ),'09')) AS TIMESTAMP) as start),
calendar as
(
select
DATEADD(years, -y.num, DATEADD( month, -m.num, DATEADD( days, -d.num, n.start ) ) ) AS period_date
from days d, month m, years y, rightnow n
)
select u.id, calendar.period_date
from periods u
cross join calendar
where date_part(DAY, u.finishedat) >= date_part(DAY, u.startedat) + 1 and date_part(DAY, calendar.period_date) < date_part(DAY, u.finishedat) and date_part(DAY, calendar.period_date) > date_part(DAY, u.startedat) and calendar.period_date < u.finishedat and calendar.period_date > u.startedat
This was based on the answer here: Using sql function generate_series() in redshift

Related

Oracle SQL count dates that do not exist between range of dates

I have range of dates:
| date |
| -------- |
| 1/1/2022 |
| 2/1/2022 |
| 3/1/2022 |
| 5/1/2022 |
| 6/1/2022 |
| 7/1/2022 |
| 8/1/2022 |
| 10/1/2022 |
I want to get the dates that are not included between these dates, in this case 4/1 and 9/1, I want the count of these dates, in this case 2, so I want the count of dates that do not exist between a specific range of dates, how can I achieve that?
select (max(date) - min(date) + 1) - count(distinct date)
from table_name
https://dbfiddle.uk/cSKZloYA
(max(date) - min(date) + 1) will give the total number of days in the range.
count(distinct date) will be the number of existing (different) days in the table.
The difference between these is the number of non-existing days.
Note: date is a reserved word, so if it's the actual column name, it has to be delimited as "date". (https://en.wikipedia.org/wiki/List_of_SQL_reserved_words)
You can use the LAG analytic function to find the previous date and then work out the number of days difference and if it is more than 1 then you have that many missing days:
SELECT SUM(missing_dates) AS num_missing
FROM (
SELECT GREATEST("DATE" - LAG("DATE") OVER (ORDER BY "DATE") - 1, 0)
AS missing_dates
FROM table_name
);
Which, for the sample data:
CREATE TABLE table_name ("DATE") AS
SELECT DATE '2020-01-01' FROM DUAL UNION ALL
SELECT DATE '2020-01-02' FROM DUAL UNION ALL
SELECT DATE '2020-01-03' FROM DUAL UNION ALL
SELECT DATE '2020-01-05' FROM DUAL UNION ALL
SELECT DATE '2020-01-06' FROM DUAL UNION ALL
SELECT DATE '2020-01-07' FROM DUAL UNION ALL
SELECT DATE '2020-01-08' FROM DUAL UNION ALL
SELECT DATE '2020-01-10' FROM DUAL;
Outputs:
NUM_MISSING
2
fiddle

SQL statement to return the Min and Max amount of stock per article for a given Month

I have a table from which I am trying to return the quantity per day that the article was in the system.
Example is in table Bestand the are multiple palletes of a different articles that each have a Booking In and Out date; I am try to find out the Min and Max amount of stock that was in the system per article and month.
My thinking is that if I can return the stock quantity for each day and then read out the Min and Max values.
The Timespan would be set at the time of running the SQL and the articles would be fixed.
To find out the quantity for each day I have used the following SQL:
SELECT DISTINCT
a.artbez1 AS Artikelbezeichnung,
b.artikelnr AS Artikelnummer,
SUM(CASE WHEN TO_DATE('2019-11-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS') BETWEEN b.neu_datum AND b.aender_datum THEN 1 * b.menge_ist ELSE 0 END) AS "01 Nov 2019"
FROM
artikel a, bestand b
WHERE
b.artikelnr IN ('273632002', .... (huge long list of numbers) ....)
AND b.artikelnr = a.artikelnr
GROUP BY
a.artbez1, b.artikelnr;
This returns for example:
ARTIKELBEZEICHNUNG
ARTIKELNUMMER
01 Nov 2019
SC-4400.CW
220450002
39
S-320.FK120
220502004
0
H-595.FK120
220800004
35
AC-548.FK209
220948032
0
AS-6800.CW
221355002
20
I would like return this for each day of the Month and then from that return the Min and Max Value for each Article
I have the following SQL to return the days of a given Month and was wondering if anyone had any ideas on how they could be combined (If at all possible):
SELECT to_date('01.11.2019','dd.mm.yyyy')+LEVEL-1
FROM dual
CONNECT BY LEVEL <= TO_CHAR(LAST_DAY(to_date('01.11.2019','dd.mm.yyyy')),'DD')
DATES
2019-11-01 00:00:00
2019-11-02 00:00:00
2019-11-03 00:00:00
2019-11-04 00:00:00
2019-11-05 00:00:00
2019-11-06 00:00:00
2019-11-07 00:00:00
The result i am try to get would be something like:
ARTIKELBEZEICHNUNG
ARTIKELNUMMER
Nov 19 Min
Nov 19 Max
SC-4400.CW
220450002
5
39
S-320.FK120
220502004
0
15
H-595.FK120
220800004
2
35
AC-548.FK209
220948032
0
0
AS-6800.CW
221355002
10
20
Is this at all possible in SQL?
Thanks for taking the time to read my post.
JeRi
You can use a partitioned outer join:
WITH calendar ( day ) AS (
SELECT DATE '2019-11-01'
FROM DUAL
UNION ALL
SELECT day + INTERVAL '1' DAY
FROM calendar
WHERE day < LAST_DAY( DATE '2019-11-01' )
),
daily_totals ( artbez1, Artikelnr, Day, total_menge_ist ) AS (
SELECT MAX( ab.artbez1 ),
ab.artikelnr,
c.day,
COALESCE( SUM( ab.menge_ist ), 0 )
FROM calendar c
LEFT OUTER JOIN
( SELECT a.artikelnr,
a.artbez1,
b.neu_datum,
b.aender_datum,
b.menge_ist
FROM artikel a
LEFT JOIN bestand b
ON ( a.artikelnr = b.artikelnr )
-- WHERE b.artikelnr IN ('273632002', .... (huge long list of numbers) ....)
) ab
PARTITION BY ( ab.artikelnr, ab.artbez1 )
ON ( c.day BETWEEN ab.neu_datum AND ab.aender_datum )
GROUP BY ab.artikelnr, c.day
)
SELECT MAX( artbez1 ) AS Artikelbezeichnung,
artikelnr AS Artikelnummer,
TRUNC( day, 'MM' ) AS month,
MIN( total_menge_ist ) AS min_total_menge_ist,
MAX( total_menge_ist ) AS max_total_menge_ist
FROM daily_totals
GROUP BY artikelnr, TRUNC( day, 'MM' );
Which, for the sample data:
CREATE TABLE artikel ( artikelnr, artbez1 ) AS
SELECT 220450002, 'SC-4400.CW' FROM DUAL UNION ALL
SELECT 220502004, 'S-320.FK120' FROM DUAL UNION ALL
SELECT 220800004, 'H-595.FK120' FROM DUAL UNION ALL
SELECT 220948032, 'AC-548.FK209' FROM DUAL UNION ALL
SELECT 221355002, 'AS-6800.CW' FROM DUAL;
CREATE TABLE bestand ( artikelnr, neu_datum, aender_datum, menge_ist ) AS
SELECT 220450002, DATE '2019-10-30', DATE '2019-11-01', 20 FROM DUAL UNION ALL
SELECT 220450002, DATE '2019-11-01', DATE '2019-11-05', 19 FROM DUAL UNION ALL
SELECT 220502004, DATE '2019-11-05', DATE '2019-11-03', 5 FROM DUAL UNION ALL
SELECT 220800004, DATE '2019-11-01', DATE '2019-11-15', 35 FROM DUAL UNION ALL
SELECT 221355002, DATE '2019-10-20', DATE '2019-11-05', 5 FROM DUAL UNION ALL
SELECT 221355002, DATE '2019-10-25', DATE '2019-11-10', 5 FROM DUAL UNION ALL
SELECT 221355002, DATE '2019-10-28', DATE '2019-11-13', 5 FROM DUAL UNION ALL
SELECT 221355002, DATE '2019-10-30', DATE '2019-11-15', 5 FROM DUAL UNION ALL
SELECT 221355002, DATE '2019-11-05', DATE '2019-11-20', 5 FROM DUAL;
Outputs:
ARTIKELBEZEICHNUNG | ARTIKELNUMMER | MONTH | MIN_TOTAL_MENGE_IST | MAX_TOTAL_MENGE_IST
:----------------- | ------------: | :------------------ | ------------------: | ------------------:
SC-4400.CW | 220450002 | 2019-11-01 00:00:00 | 0 | 39
S-320.FK120 | 220502004 | 2019-11-01 00:00:00 | 0 | 0
AC-548.FK209 | 220948032 | 2019-11-01 00:00:00 | 0 | 0
H-595.FK120 | 220800004 | 2019-11-01 00:00:00 | 0 | 35
AS-6800.CW | 221355002 | 2019-11-01 00:00:00 | 0 | 25
db<>fiddle here

With Oracle SQL how can I find 3 days where total sum >= 150

I have a report that needs to list activity where total is >= 150 over 3 consecutive days.
Let's say I've created a temp table foo, to summarize daily totals.
| ID | Day | Total |
| -- | ---------- | ----- |
| 01 | 2020-01-01 | 10 |
| 01 | 2020-01-02 | 50 |
| 01 | 2020-01-03 | 50 |
| 01 | 2020-01-04 | 50 |
| 01 | 2020-01-05 | 20 |
| 02 | 2020-01-01 | 10 |
| 02 | 2020-01-02 | 10 |
| 02 | 2020-01-03 | 10 |
| 02 | 2020-01-04 | 10 |
| 02 | 2020-01-05 | 10 |
How Would I write SQL to return ID 01, but not 02?
Example Result:
| ID |
| -- |
| 01 |
I suspect that you want window functions here:
select distinct id
from (
select
t.*,
sum(total) over(partition by id order by day rows between 2 preceding and current row) sum_total,
count(*) over(partition by id order by day rows between 2 preceding and current row) cnt
from mytable t
) t
where cnt = 3 and sum_total >= 150
This gives you the ids that have a total greater than the given threshold over 3 consecutive days - which is how I understood your question.
If you just want to output the rows that have 3 consecutive days with a sum >= 150, you can use an analytic function to determine the moving total across each 3 day period per id, and then find the aggregate max value of the moving total per id, returning the id where it's >= 150.
E.g.:
WITH your_table AS (SELECT 1 ID, to_date('01/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 1 ID, to_date('02/01/2020', 'dd/mm/yyyy') dy, 50 total FROM dual UNION ALL
SELECT 1 ID, to_date('03/01/2020', 'dd/mm/yyyy') dy, 50 total FROM dual UNION ALL
SELECT 1 ID, to_date('04/01/2020', 'dd/mm/yyyy') dy, 50 total FROM dual UNION ALL
SELECT 1 ID, to_date('05/01/2020', 'dd/mm/yyyy') dy, 20 total FROM dual UNION ALL
SELECT 2 ID, to_date('01/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('02/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('03/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('04/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('05/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual),
moving_sums AS (SELECT ID,
dy,
total,
SUM(total) OVER (PARTITION BY ID ORDER BY dy RANGE BETWEEN 2 PRECEDING AND CURRENT ROW) moving_sum
FROM your_table)
SELECT ID
FROM moving_sums
GROUP BY ID
HAVING MAX(moving_sum) >= 150;
ID
----------
1
You can use a HAVING Clause GROUPED BY ID to list the desired ID values
SELECT ID
FROM foo
GROUP BY ID
HAVING COUNT( distinct day )>=3 AND SUM( NVL(Total,0) ) >= 150
Demo
Use this if you are to specify dates
WITH foo( ID, Day, Total ) AS
(
SELECT '01', date'2020-01-01' , 10 FROM dual
UNION ALL SELECT '01', date'2020-01-02' , 50 FROM dual
UNION ALL SELECT '01', date'2020-01-03' , 50 FROM dual
UNION ALL SELECT '01', date'2020-01-04' , 50 FROM dual
UNION ALL SELECT '01', date'2020-01-05' , 20 FROM dual
UNION ALL SELECT '02', date'2020-01-01' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-02' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-03' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-04' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-05' , 10 FROM dual
)SELECT
ID
FROM foo
WHERE day BETWEEN TO_DATE('2020-01-01', 'YYYY-MM-DD' ) AND TO_DATE('2020-01-04', 'YYYY-MM-DD' )
GROUP BY ID HAVING SUM(Total) >= 150;
RESULT:
ID|
--|
01|
Maybe you can try something like this :
SELECT
*
FROM foo
WHERE day BETWEEN 2020-01-01 AND 2020-01-04
AND total > 150

Count running total in Oracle

I want to make a query, which shows the progress of the number of users on my webpage by week.
I use following query to run the users database and get the number, grouped by a week:
SELECT TRUNC(FAB.LICENSE_DATE, 'IW'),
COUNT(DISTINCT FAB.STATEMENT_NUMBER) AS "Number of account statements"
FROM USERS FAB
GROUP BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW');
This gives following output:
Date | Users
----------------------
2015/09/07 | 5
2015/09/14 | 4
2015/09/21 | 6
But this is actually not what I want to achieve. I want to have the following output:
Date | Users
----------------------
2015/09/07 | 5
2015/09/14 | 9 (5 + 4)
2015/09/21 | 15 (5 + 4 + 6)
How to modify the query so I get all the results?
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE USERS (
LICENSE_DATE,
LAST_UPDATED_TIME,
STATEMENT_NUMBER
) AS
SELECT DATE '2015-09-07', DATE '2015-09-07', 1 FROM DUAL
UNION ALL SELECT DATE '2015-09-08', DATE '2015-09-08', 2 FROM DUAL
UNION ALL SELECT DATE '2015-09-08', DATE '2015-09-08', 3 FROM DUAL
UNION ALL SELECT DATE '2015-09-09', DATE '2015-09-09', 4 FROM DUAL
UNION ALL SELECT DATE '2015-09-12', DATE '2015-09-12', 5 FROM DUAL
UNION ALL SELECT DATE '2015-09-14', DATE '2015-09-15', 6 FROM DUAL
UNION ALL SELECT DATE '2015-09-15', DATE '2015-09-16', 7 FROM DUAL
UNION ALL SELECT DATE '2015-09-16', DATE '2015-09-16', 8 FROM DUAL
UNION ALL SELECT DATE '2015-09-17', DATE '2015-09-18', 9 FROM DUAL
UNION ALL SELECT DATE '2015-09-21', DATE '2015-09-21', 10 FROM DUAL
UNION ALL SELECT DATE '2015-09-21', DATE '2015-09-26', 11 FROM DUAL
UNION ALL SELECT DATE '2015-09-22', DATE '2015-09-22', 12 FROM DUAL
UNION ALL SELECT DATE '2015-09-23', DATE '2015-09-25', 13 FROM DUAL
UNION ALL SELECT DATE '2015-09-24', DATE '2015-09-24', 14 FROM DUAL
UNION ALL SELECT DATE '2015-09-27', DATE '2015-09-27', 15 FROM DUAL;
Query 1:
SELECT LAST_UPDATED_WEEK,
SUM( NUM_STATEMENTS ) OVER ( ORDER BY LAST_UPDATED_WEEK ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS "Number of account statements"
FROM (
SELECT TRUNC(LAST_UPDATED_TIME, 'IW') AS LAST_UPDATED_WEEK,
COUNT(DISTINCT STATEMENT_NUMBER) AS NUM_STATEMENTS
FROM USERS
GROUP BY
TRUNC( LAST_UPDATED_TIME, 'IW')
)
Results:
| LAST_UPDATED_WEEK | Number of account statements |
|-----------------------------|------------------------------|
| September, 07 2015 00:00:00 | 5 |
| September, 14 2015 00:00:00 | 9 |
| September, 21 2015 00:00:00 | 15 |
SELECT TRUNC(FAB.LICENSE_DATE, 'IW'),
SUM(COUNT(DISTINCT FAB.STATEMENT_NUMBER)) OVER (ORDER BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW')) as "Number of account statements"
FROM USERS FAB
GROUP BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW');
You can use this code block for your problem :
select u.date
,(select sum(u1.users)
from users u1
where u1.ddate <= u.date) as users
from users u;
It gives this output :
07.09.2015 5
14.09.2015 9
21.09.2015 15
Good luck
Hello you can try this code too.
WITH t1 AS
( SELECT to_date('01/01/2015','mm/dd/yyyy') rn, 5 usrs FROM dual
UNION ALL
SELECT to_date('02/01/2015','mm/dd/yyyy') rn, 4 usrs FROM dual
UNION ALL
SELECT to_date('03/01/2015','mm/dd/yyyy') rn, 8 usrs FROM dual
UNION ALL
SELECT to_date('04/01/2015','mm/dd/yyyy') rn, 2 usrs FROM dual
)
SELECT rn,
usrs,
sum(usrs) over (order by rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) cumm_usrs
FROM t1
GROUP BY rn,
usrs;

Select records all within 10 minutes from each other

I have some data coming from a source in my Oracle database.
If a particular Office_ID has been deactivated and it has all three clients (A,B,C) for a particular day, then we have to check whether all clients have gone. If yes, then we need to check whether timeframe for all clients is within 10 Minutes.
If this repeats three times in a day for a particular office we declare the office as closed.
Here is some sample data:
+-----------+-----------+--------------+--------+
| OFFICE_ID | FAIL_TIME | ACTIVITY_DAY | CLIENT |
| 1002 | 5:39:00 | 23/01/2015 | A |
| 1002 | 17:49:00 | 23/12/2014 | A |
| 1002 | 18:41:57 | 1/5/2014 | B |
| 1002 | 10:32:00 | 1/7/2014 | A |
| 1002 | 10:34:23 | 1/7/2014 | B |
| 1002 | 10:35:03 | 1/7/2014 | C |
| 1002 | 12:08:52 | 1/7/2014 | B |
| 1002 | 12:09:00 | 1/7/2014 | A |
| 1002 | 12:26:10 | 1/7/2014 | B |
| 1002 | 13:31:32 | 1/7/2014 | B |
| 1002 | 15:24:06 | 1/7/2014 | B |
| 1002 | 15:55:06 | 1/7/2014 | C |
+-----------+-----------+--------------+--------+
The result should be like this:
1002 10:32:00 A
1002 10:34:23 B
1002 10:35:03 C
Any help would be appreciated. I am looking for a SQL query or a PL/SQL procedure.
A solution using the COUNT analytic function with a RANGE BETWEEN INTERVAL '10' MINUTE PRECEDING AND INTERVAL '10' MINUTE FOLLOWING that avoids self-joins:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE Test ( OFFICE_ID, FAIL_TIME, ACTIVITY_DAY, CLIENT ) AS
SELECT 1002, '5:39:00', '23/01/2015', 'A' FROM DUAL
UNION ALL SELECT 1002, '17:49:00', '23/12/2014', 'A' FROM DUAL
UNION ALL SELECT 1002, '18:41:57', '1/5/2014', 'B' FROM DUAL
UNION ALL SELECT 1002, '10:32:00', '1/7/2014', 'A' FROM DUAL
UNION ALL SELECT 1002, '10:34:23', '1/7/2014', 'B' FROM DUAL
UNION ALL SELECT 1002, '10:35:03', '1/7/2014', 'C' FROM DUAL
UNION ALL SELECT 1002, '12:08:52', '1/7/2014', 'B' FROM DUAL
UNION ALL SELECT 1002, '12:09:00', '1/7/2014', 'A' FROM DUAL
UNION ALL SELECT 1002, '12:26:10', '1/7/2014', 'B' FROM DUAL
UNION ALL SELECT 1002, '13:31:32', '1/7/2014', 'B' FROM DUAL
UNION ALL SELECT 1002, '15:24:06', '1/7/2014', 'B' FROM DUAL
UNION ALL SELECT 1002, '15:55:06', '1/7/2014', 'C' FROM DUAL
Query 1:
WITH Times AS (
SELECT OFFICE_ID,
TO_DATE( ACTIVITY_DAY || ' ' || FAIL_TIME, 'DD/MM/YYYY HH24/MI/SS' ) AS FAIL_DATETIME,
CLIENT
FROM Test
),
Next_Times As (
SELECT OFFICE_ID,
FAIL_DATETIME,
COUNT( CASE CLIENT WHEN 'A' THEN 1 END ) OVER ( PARTITION BY OFFICE_ID ORDER BY FAIL_DATETIME RANGE BETWEEN INTERVAL '10' MINUTE PRECEDING AND INTERVAL '10' MINUTE FOLLOWING ) AS COUNT_A,
COUNT( CASE CLIENT WHEN 'B' THEN 1 END ) OVER ( PARTITION BY OFFICE_ID ORDER BY FAIL_DATETIME RANGE BETWEEN INTERVAL '10' MINUTE PRECEDING AND INTERVAL '10' MINUTE FOLLOWING ) AS COUNT_B,
COUNT( CASE CLIENT WHEN 'C' THEN 1 END ) OVER ( PARTITION BY OFFICE_ID ORDER BY FAIL_DATETIME RANGE BETWEEN INTERVAL '10' MINUTE PRECEDING AND INTERVAL '10' MINUTE FOLLOWING ) AS COUNT_C
FROM Times
)
SELECT OFFICE_ID,
TO_CHAR( FAIL_DATETIME, 'HH24:MI:SS' ) AS FAIL_TIME,
TO_CHAR( FAIL_DATETIME, 'DD/MM/YYYY' ) AS ACTIVITY_DAY
FROM Next_Times
WHERE COUNT_A > 0
AND COUNT_B > 0
AND COUNT_C > 0
ORDER BY FAIL_DATETIME
Results:
| OFFICE_ID | FAIL_TIME | ACTIVITY_DAY |
|-----------|-----------|--------------|
| 1002 | 10:32:00 | 01/07/2014 |
| 1002 | 10:34:23 | 01/07/2014 |
| 1002 | 10:35:03 | 01/07/2014 |
To identify records you can join table to it self three times like this:
SELECT
a.*, b.*, c.*
FROM FailLog a INNER JOIN
FailLog b ON b.OFFICE_ID = A.OFFICE_ID AND
a.CLIENT = 'A' AND
b.CLIENT = 'B' AND
b.ACTIVITY_DAY = a.ACTIVITY_DAY INNER JOIN
FailLog c ON c.OFFICE_ID = A.OFFICE_ID AND
c.CLIENT = 'C' AND
c.ACTIVITY_DAY = a.ACTIVITY_DAY AND
-- need to calculate difference in min here
GREATEST (a.FAIL_TIME, b. FAIL_TIME, c. FAIL_TIME) -
LEAST (a.FAIL_TIME, b. FAIL_TIME, c. FAIL_TIME) <= 10
The output will give you one row instead of three as requested in the question, but that will be the right level for the fault data, as all three clients should have it.
The first thing we need is a way of comparing FAIL_TIME. As you haven't posted a table structure let's assume we're dealing with strings.
Oracle has some neat built-ins for casting dates and strings. If we concatenate ACTIVITY_DATE and FAIL_TIME we can convert them to a DATE data type:
to_date(ACTIVITY_DAY||' '||FAIL_TIME, 'dd/mm/yyyy hh24:mi:ss')
We can cast that to a string representing the number of seconds past midnight:
to_char(to_date(ACTIVITY_DAY||' '||FAIL_TIME, 'dd/mm/yyyy hh24:mi:ss'), 'sssss')
Then we can cast that to a number, which we can use in some arithmetic to compare with other rows; ten minutes = 600 seconds.
Next we can use the subquery factoring (the WITH clause). One of the neat features of this syntax is that we can pass the output of one subquery into another one, so we only only need to write that gnarly nested cast expression once.
with t as
( select OFFICE_ID
, ACTIVITY_DAY
, FAIL_TIME
, to_number(to_char(to_date(ACTIVITY_DAY||' '||FAIL_TIME, 'dd/mm/yyyy hh24:mi:ss'), 'sssss')) FAIL_TIME_SSSSS
, CLIENT
from faillog
)
We can use this sub-query to build other subqueries which separate the table's rows into sets for each CLIENT for use in our main query.
Finally we can use an analytic COUNT() function to track how many bunches of FAIL_TIME we have for each OFFICE and ACTIVITY_DATE combo.
count(*) over (partition by a.OFFICE_ID, a.ACTIVITY_DAY)
Putting it all together in an in-line view allows us to test for whether we can "declare the office as closed".
select * from (
with t as ( select OFFICE_ID
, ACTIVITY_DAY
, FAIL_TIME
, to_number(to_char(to_date(ACTIVITY_DAY||' '||FAIL_TIME, 'dd/mm/yyyy hh24:mi:ss'), 'sssss')) FAIL_TIME_SSSSS
, CLIENT
from faillog
)
, a as (select *
from t
where CLIENT = 'A' )
, b as (select *
from t
where CLIENT = 'B' )
, c as (select *
from t
where CLIENT = 'C' )
select a.OFFICE_ID
, a.ACTIVITY_DAY
, a.FAIL_TIME as a_fail_time
, b.FAIL_TIME as b_fail_time
, c.FAIL_TIME as a_fail_time
, count(*) over (partition by a.OFFICE_ID, a.ACTIVITY_DAY) as fail_count
from a
join b on a.OFFICE_ID = b.OFFICE_ID and a.ACTIVITY_DAY = b.ACTIVITY_DAY
join c on a.OFFICE_ID = c.OFFICE_ID and a.ACTIVITY_DAY = c.ACTIVITY_DAY
where a.FAIL_TIME_SSSSS between b.FAIL_TIME_SSSSS - 600 and b.FAIL_TIME_SSSSS + 600
and a.FAIL_TIME_SSSSS between c.FAIL_TIME_SSSSS - 600 and c.FAIL_TIME_SSSSS + 600
and b.FAIL_TIME_SSSSS between a.FAIL_TIME_SSSSS - 600 and a.FAIL_TIME_SSSSS + 600
and b.FAIL_TIME_SSSSS between c.FAIL_TIME_SSSSS - 600 and c.FAIL_TIME_SSSSS + 600
and c.FAIL_TIME_SSSSS between a.FAIL_TIME_SSSSS - 600 and a.FAIL_TIME_SSSSS + 600
and c.FAIL_TIME_SSSSS between b.FAIL_TIME_SSSSS - 600 and b.FAIL_TIME_SSSSS + 600
)
where fail_count >= 3
/
Notes
Obviously I have hard-coded the CLIENT identifier in the subqueries.
It would be possible to avoid the hard-coding, but the sample query is already complicated enough.
This query doesn't search for
triplets. Providing there is one failure for each of A, B and C
within a ten minute window it doesn't matter how many instances of
each CLIENT occur within the window. There's nothing in your
business rules to say this is wrong.
Similarly, the same instance of
one CLIENT can be matched with instances of other CLIENTs in
overlapping windows. Now this may be undesirable: double or triple
counting may inflate the FAIL_COUNT. But again, handling this will
make the final query more complicated.
The query as presented has one row for each distinct combo of A, B and C FAIL_TIME values. The result set can be pivoted if you really need a row for each CLIENT/FAIL_TIME.