Multiple columns from DUAL? - sql

I'm using Oracle 12c.
I need to generate dates for the start and end of weeks which begin on Thursday and end the following Wednesday.
An example of the output I'd like is -
I have the following SQL to generate the Start Date(s) -
SELECT startdate
FROM (SELECT next_day(date '2020-03-12' - 1, 'Thursday') + (level - 1) * 7 AS startdate
FROM dual
CONNECT BY level <=
((date'2024-03-31' - next_day(date '2020-03-12' - 1, 'Wednesday') + 7) / 7))
and this for End Dates -
(SELECT enddate
FROM (SELECT next_day(date '2020-03-12' - 1, 'Wednesday') + (level - 1) * 7 as enddate
FROM dual
CONNECT BY level <= ((date'2024-03-31' - next_day(date'2020-03-12' - 1, 'Thursday') + 7) / 7)))
Is it even possible to combine these in a single SQL query so the output of the query matches the desired format?
If so, then the addition of the week number would also be rather nice...:)

Generate the start date and then add 6 days to get the end date:
SELECT startdate,
startdate + INTERVAL '6' DAY AS enddate,
week
FROM (
SELECT NEXT_DAY(date'2020-03-12' - 1, 'Thursday')
+ ( level - 1 ) * INTERVAL '7' DAY as startdate,
LEVEL AS week
FROM DUAL
CONNECT BY
NEXT_DAY(date'2020-03-12' - 1, 'Thursday')
+ ( level - 1 ) * INTERVAL '7' DAY
+ INTERVAL '6' DAY
<= date'2024-03-31'
)
Which outputs:
STARTDATE
ENDDATE
WEEK
2020-03-12 00:00:00
2020-03-18 00:00:00
1
2020-03-19 00:00:00
2020-03-25 00:00:00
2
2020-03-26 00:00:00
2020-04-01 00:00:00
3
...
...
...
2024-03-07 00:00:00
2024-03-13 00:00:00
209
2024-03-14 00:00:00
2024-03-20 00:00:00
210
2024-03-21 00:00:00
2024-03-27 00:00:00
211
db<>fiddle here

Related

Recover all weeks of a month in Oracle SQL

I would like to build a SQL statement to automatically retrieve information about all the weeks of a month.
For example, for the month of February 2023:
Retrieve the first week (Monday to Sunday): 30/01/2023 - 05/01/2023
...
Retrieve the last week :
27/02/2023 - 05/03/2023.
Thanks in advance!
Use a row-generator to generate a list of weeks for the month:
WITH input (month) AS (
SELECT DATE '2023-02-01' FROM DUAL
),
calendar (week_start) AS (
SELECT TRUNC(TRUNC(month, 'MM'), 'IW') + 7 * (LEVEL - 1)
FROM input
CONNECT BY TRUNC(TRUNC(month, 'MM'), 'IW') + 7 * (LEVEL - 1)
< ADD_MONTHS(TRUNC(month, 'MM'), 1)
)
SELECT week_start,
week_start + INTERVAL '6 23:59:59' DAY TO SECOND AS week_end
FROM calendar
Which outputs:
WEEK_START
WEEK_END
2023-01-30 00:00:00
2023-02-05 23:59:59
2023-02-06 00:00:00
2023-02-12 23:59:59
2023-02-13 00:00:00
2023-02-19 23:59:59
2023-02-20 00:00:00
2023-02-26 23:59:59
2023-02-27 00:00:00
2023-03-05 23:59:59
fiddle

SQL query to check if there are records in the database of 6 consecutive 'Sundays'

I need to build a query to check if there are records in the database of 6 consecutive 'Sundays'
SELECT DISTINCT ST1.DATAPU, ST1.NUMCAD, TO_CHAR(ST1.DATAPU, 'DAY') AS DIA
FROM SENIOR.R066SIT ST1
WHERE ST1.DATAPU BETWEEN '01/01/22' AND '23/11/22'
AND ST1.NUMCAD = 10
AND TO_CHAR(ST1.DATAPU, 'FMDAY') = 'DOMINGO' -->which is SUNDAY in English
ORDER BY ST1.DATAPU ASC
With this query above, I get the result of the records as shown in the image below
From Oracle 12, you can use MATCH_RECOGNIZE to perform row-by-row pattern analysis:
SELECT *
FROM (
SELECT DISTINCT
TRUNC(DATAPU) AS datapu,
NUMCAD,
TO_CHAR(DATAPU,'DAY') AS DIA
FROM SENIOR.R066SIT
WHERE DATAPU BETWEEN DATE '2022-01-01' AND DATE '2022-11-23'
AND NUMCAD = 10
AND TRUNC(DATAPU) - TRUNC(DATAPU, 'IW') = 6 -- Sunday
)
MATCH_RECOGNIZE(
ORDER BY datapu
ALL ROWS PER MATCH
PATTERN (first_week consecutive_week{5,})
DEFINE
consecutive_week AS PREV(datapu) + INTERVAL '7' DAY = datapu
)
Which, for the sample data:
CREATE TABLE senior.r066sit(numcad, datapu) AS
SELECT 10, DATE '2022-01-01' + LEVEL - 1 FROM DUAL CONNECT BY LEVEL <= 5*7
UNION ALL
SELECT 10, DATE '2022-04-01' + LEVEL - 1 FROM DUAL CONNECT BY LEVEL <= 7*7
UNION ALL
SELECT 10, DATE '2022-08-01' + LEVEL - 1 FROM DUAL CONNECT BY LEVEL <= 7*7;
Outputs:
DATAPU
NUMCAD
DIA
2022-04-03 00:00:00
10
SUNDAY
2022-04-10 00:00:00
10
SUNDAY
2022-04-17 00:00:00
10
SUNDAY
2022-04-24 00:00:00
10
SUNDAY
2022-05-01 00:00:00
10
SUNDAY
2022-05-08 00:00:00
10
SUNDAY
2022-05-15 00:00:00
10
SUNDAY
2022-08-07 00:00:00
10
SUNDAY
2022-08-14 00:00:00
10
SUNDAY
2022-08-21 00:00:00
10
SUNDAY
2022-08-28 00:00:00
10
SUNDAY
2022-09-04 00:00:00
10
SUNDAY
2022-09-11 00:00:00
10
SUNDAY
2022-09-18 00:00:00
10
SUNDAY
Before Oracle 12, you can use multiple analytic functions in nested sub-queries:
SELECT datapu, numcad,
TO_CHAR(datapu, 'fmDAY') AS dia
FROM (
SELECT datapu, numcad,
COUNT(*) OVER (PARTITION BY grp) AS grp_size
FROM (
SELECT datapu, numcad,
SUM(consecutive) OVER (ORDER BY datapu) AS grp
FROM (
SELECT datapu, numcad,
CASE datapu - LAG(datapu) OVER (ORDER BY datapu)
WHEN 7
THEN 0
ELSE 1
END AS consecutive
FROM (
SELECT DISTINCT
TRUNC(DATAPU) AS datapu,
NUMCAD
FROM SENIOR.R066SIT
WHERE DATAPU BETWEEN DATE '2022-01-01' AND DATE '2022-11-23'
AND NUMCAD = 10
AND TRUNC(DATAPU) - TRUNC(DATAPU, 'IW') = 6 -- Sunday
)
)
)
)
WHERE grp_size >= 6;
fiddle

Dynamically generate last 36 month dates

How to generate list of dates dynamically for last 36 months in SQL
As of Oracle:
select add_months(trunc(sysdate), -36) + level - 1 datum
from dual
connect by level <= trunc(sysdate) - add_months(trunc(sysdate), -36) + 1
order by datum;
DATUM
----------
29.04.2017
30.04.2017
01.05.2017
02.05.2017
03.05.2017
04.05.2017
<snip>
26.04.2020
27.04.2020
28.04.2020
29.04.2020
367 rows selected.
with t (dt1, dt2) as (
select trunc(sysdate, 'month') - 1, last_day(trunc(sysdate)) + 1 - interval '36' month from dual
)
select dt2 + level - 1
from t
connect by
dt2 + level - 1 <= dt1
;
trunc(sysdate, 'month') gives you the first day of the month at 00 hours.
last_day(trunc(sysdate)) gives you the last day of the month at 00 hours.

Oracle date as fraction of month

I would like to get a table of months between two dates with a fraction of each month that the two dates cover.
For example with a start date of 15/01/2017 and end date of 01/03/2017 it would output:
01/2017 : 0.5483..
02/2017 : 1
03/2017: 0.0322..
where for January and March the calculations are 17/31 and 1/31 respectively. I currently have the query:
WITH dates_between as (SELECT ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1) date_out
FROM DUAL
CONNECT BY ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1)
<= TRUNC(TO_DATE(:givenEndDate,'dd/mm/yyyy'), 'MON')
)
select * from dates_between
This outputs each month between two dates and formats it to the start of the month. I just need another column to give me the fraction the start and end dates cover. I'm not sure of a way to do this without it getting messy.
The months_between() function "calculates the fractional portion of the result based on a 31-day month". That means that if your range starts or ends in a month that doesn't have 31 days, the fraction you get might not be quite what you expect:
select months_between(date '2017-04-02', date '2017-04-01') as calc from dual
CALC
----------
.0322580645
... which is 1/31, not 1/30. To get 0.0333... instead you'd need to calculate the number of days in each month, at least for the first and last month. This uses a recursive CTE (11gR2+) to get the months, using a couple of date ranges provided by another CTE as a demo to show the difference (you can use a hierarchical query too of course):
with ranges (id, start_date, end_date) as (
select 1, date '2017-01-15', date '2017-03-01' from dual
union all select 2, date '2017-01-31', date '2017-03-01' from dual
union all select 3, date '2017-02-28', date '2017-04-01' from dual
),
months (id, month_start, month_days, range_start, range_end) as (
select id,
trunc(start_date, 'MM'),
extract(day from last_day(start_date)),
start_date,
end_date
from ranges
union all
select id,
month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select id,
to_char(month_start, 'YYYY-MM-DD') as month_start,
month_days,
case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end as range_days,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by id, month_start;
which gets:
ID MONTH_STAR MONTH_DAYS RANGE_DAYS FRACTION
------ ---------- ---------- ---------- --------
1 2017-01-01 31 17 0.5483
1 2017-02-01 28 28 1
1 2017-03-01 31 1 0.0322
2 2017-01-01 31 1 0.0322
2 2017-02-01 28 28 1
2 2017-03-01 31 1 0.0322
3 2017-02-01 28 1 0.0357
3 2017-03-01 31 31 1
3 2017-04-01 30 1 0.0333
The first CTE ranges is just the demo data. The second, recursive, CTE months generates the start and number of days in each month, while keeping track of the original range dates too. The final query just calculates the fractions based on the number of days in the month in the range against the number of days in that month overall.
The month_days and range_days are only shown in the output so you can see what the calculation is based on, you can obviously omit those from your actual result, and format the month start date however you want.
With your original single pair of bind variables the equivalent would be:
with months (month_start, month_days, range_start, range_end) as (
select trunc(to_date(:givenstartdate, 'DD/MM/YYYY'), 'MM'),
extract(day from last_day(to_date(:givenstartdate, 'DD/MM/YYYY'))),
to_date(:givenstartdate, 'DD/MM/YYYY'),
to_date(:givenenddate, 'DD/MM/YYYY')
from dual
union all
select month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select to_char(month_start, 'MM/YYYY') as month,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by month_start;
MONTH FRACTION
------- --------
01/2017 0.5483
02/2017 1
03/2017 0.0322
Here's how I would do it (n.b. I have expanded your dates_between to work against multiple rows, purely for demonstration purposes. If you're only working with a single set of parameters, you wouldn't need to do that):
WITH params AS (SELECT 1 ID, '15/01/2017' givenstartdate, '01/03/2017' givenenddate FROM dual UNION ALL
SELECT 2 ID, '15/01/2017' givenstartdate, '23/01/2017' givenenddate FROM dual UNION ALL
SELECT 3 ID, '01/01/2017' givenstartdate, '07/04/2017' givenenddate FROM dual),
dates_between AS (SELECT ID,
to_date(givenstartdate, 'dd/mm/yyyy') givenstartdate,
to_date(givenenddate, 'dd/mm/yyyy') givenenddate,
add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) start_of_month,
last_day(add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1)) end_of_month
FROM params
CONNECT BY add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) <=
trunc(to_date(givenenddate, 'dd/mm/yyyy'), 'MON')
AND PRIOR ID = ID
AND PRIOR sys_guid() IS NOT NULL)
SELECT ID,
givenstartdate,
givenenddate,
start_of_month date_out,
end_of_month,
months_between(LEAST(givenenddate, end_of_month) + 1, GREATEST(start_of_month, givenstartdate))
FROM dates_between;
ID GIVENSTARTDATE GIVENENDDATE DATE_OUT END_OF_MONTH DIFF
1 15/01/2017 01/03/2017 01/01/2017 31/01/2017 0.54838709
1 15/01/2017 01/03/2017 01/02/2017 28/02/2017 1
1 15/01/2017 01/03/2017 01/03/2017 31/03/2017 0.03225806
2 15/01/2017 23/01/2017 01/01/2017 31/01/2017 0.29032258
3 01/01/2017 07/04/2017 01/01/2017 31/01/2017 1
3 01/01/2017 07/04/2017 01/02/2017 28/02/2017 1
3 01/01/2017 07/04/2017 01/03/2017 31/03/2017 1
3 01/01/2017 07/04/2017 01/04/2017 30/04/2017 0.22580645
N.B. You may need to add a case statement to decide whether you want to add 1 or not to the diff calculation, based on your requirements.
Try this
For first month, I have calculated remaining days / total days and for last month, I subtracted it by 1 to get days passed / total days.
DBFiddle Demo
WITH tbl AS
(SELECT date '2017-01-15' AS givenStartDate
,date '2017-03-01' AS givenEndDate
FROM dual
)
SELECT ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) AS date_out ,
CASE
WHEN
rownum - 1 = 0
THEN months_between(last_day(givenStartDate), givenStartDate)
WHEN ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) = TRUNC(givenEndDate, 'MON')
THEN 1 - (months_between(last_day(givenEndDate), givenEndDate))
ELSE 1
END AS perc
FROM tbl
CONNECT BY ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1)
<= TRUNC(givenEndDate, 'MON');
Output
+-----------+-------------------------------------------+
| DATE_OUT | PERC |
+-----------+-------------------------------------------+
| 01-JAN-17 | .5161290322580645161290322580645161290323 |
| 01-FEB-17 | 1 |
| 01-MAR-17 | .0322580645161290322580645161290322580645 |
+-----------+-------------------------------------------+

Insert weekly dates starting fridays

I'm trying to insert weekly dates in my table, the start date is always on Fridays and the end date is always on thursday. I'm using this code :
CREATE TABLE WEEK AS
WITH generator AS (
SELECT DATE '2015-01-02' + LEVEL - 1 dt
FROM dual
CONNECT BY LEVEL <= DATE '2016-01-21' - DATE '2015-01-02' + 1
)
SELECT to_char(dt, 'YYYY "SEM"IW') "KEY",
dt "DATE_START",
least(next_day(dt - 1, to_char(DATE '2015-01-08', 'DAY')),
last_day(dt)) "DATE_END"
FROM generator
WHERE to_char(dt, 'D') = to_char(DATE '2015-01-02', 'D');
The code is working for weeks on the same month, but if I have a starting date on a month and the finish date on the next month, there's no data inserting in my table.
For example :
Date_ START | DATE_END
29-05-2015 | 31-05-2015
05-06-2015 | 11-05-2015
Instead of 31-05-2015 I should have 04-06-2015.
I think the following is what you're after:
with generator as (select date '2015-05-29' + (level - 1)*7 dt
from dual
connect by level <= (date '2016-01-21' - date '2015-05-29')/7 + 1)
select to_char(dt, 'YYYY "SEM"IW') "KEY",
dt "DATE_START",
dt + 6 "DATE_END"
from generator;
KEY DATE_START DATE_END
---------- ---------- ----------
2015 SEM22 2015-05-29 2015-06-04
2015 SEM23 2015-06-05 2015-06-11
2015 SEM24 2015-06-12 2015-06-18
2015 SEM25 2015-06-19 2015-06-25
<snip>
2016 SEM01 2016-01-08 2016-01-14
2016 SEM02 2016-01-15 2016-01-21
This is assuming that the dates you have specified in the generator subquery have already been determined to be a Friday. Otherwise you could use something like trunc(<date> - 4, 'iw') + 4 or trunc(<date> + 3, 'iw') + 4 (depending on whether you want the previous or next Friday to be included for the date specified) to make sure that the seed date is definitely a Friday.
Maybe just an alternative you can try analytical function here. But as
suggested by Boniest "just adding days" will be better approach. Have
fun
WITH generator AS
(SELECT DATE '2015-01-02' + LEVEL - 1 dt
FROM dual
CONNECT BY LEVEL <= DATE '2016-01-21' - DATE '2015-01-02' + 1
)
-- select * from generator;
SELECT TO_CHAR(dt, 'YYYY "SEM"IW') "KEY",
dt "DATE_START",
lead(dt) over (ORDER BY (dt)) -1 "End Date"
FROM generator
WHERE TO_CHAR(dt, 'D') = TO_CHAR(DATE '2015-01-02', 'D');
----------------------------------OUTPUT-----------------------------------------
**KEY DATE_START End Date**
2015 SEM18 05/01/2015 05/07/2015
2015 SEM19 05/08/2015 05/14/2015
2015 SEM20 05/15/2015 05/21/2015
2015 SEM21 05/22/2015 05/28/2015
2015 SEM22 05/29/2015 06/04/2015
----------------------------------------------------------------------------------