SQL Query building: howto decompose periods of time in different rows - sql

How can I build a SQL Query to decompose some periods, for example in months.
database table:
id fromdate todate value
--------------------------------------------
100 01.01.2015 01.03.2015 10
desired query result:
id fromdate todate value
--------------------------------------------
100 01.01.2015 01.02.2015 5,25
100 01.02.2015 01.03.2015 4,75
where value is based on days between the 2 dates, for example:
value(january) = 31(january nr of days) * 10(original value) / 59(total days) = 5,25
Thank you

For calculations like this you can use date dimension - a table that contains all the dates in your domain as single rows (see this for example).
Once you have date dimension in your database things become simple:
WITH data_by_date AS
( -- Here we join dates to your periods to turn each row in
-- as many rows as there are days in the period.
-- We also turn value field into value_per_day.
SELECT
d.date,
d.month_year,
t.id,
value / (t.todate - t.fromdate) as value_per_day
FROM
dim_date d INNER JOIN
my_table t ON d.date >= t.fromdate AND d.date < t.todate
)
SELECT -- Here we group by results by month.
dd.id,
MIN(dd.date) as fromdate,
MAX(dd.date) as todate,
SUM(dd.value_per_day) as value
FROM data_by_date dd
GROUP BY dd.id, dd.month_year

Use a hierarchical query to generate a list of months for each entry:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TEST (id, fromdate, todate, value ) AS
SELECT 100, DATE '2015-01-01', DATE '2015-03-01', 10 FROM DUAL
UNION ALL SELECT 200, DATE '2014-12-22', DATE '2015-01-06', 30 FROM DUAL
Query 1:
SELECT ID,
fromdate,
todate,
VALUE * ( todate - fromdate ) / ( maxdate - mindate ) AS value
FROM (
SELECT ID,
GREATEST( t.fromdate, m.COLUMN_VALUE ) AS fromdate,
LEAST( t.todate, ADD_MONTHS( m.COLUMN_VALUE, 1 ) ) AS todate,
t.fromdate AS mindate,
t.todate AS maxdate,
t.value
FROM TEST t,
TABLE(
CAST(
MULTISET(
SELECT ADD_MONTHS( TRUNC( t.fromdate, 'MM' ), LEVEL - 1 )
FROM DUAL
CONNECT BY
ADD_MONTHS( TRUNC( t.fromdate, 'MM' ), LEVEL - 1 ) < t.todate
)
AS SYS.ODCIDATELIST
)
) m
)
Results:
| ID | FROMDATE | TODATE | VALUE |
|-----|----------------------------|----------------------------|-------------------|
| 100 | January, 01 2015 00:00:00 | February, 01 2015 00:00:00 | 5.254237288135593 |
| 100 | February, 01 2015 00:00:00 | March, 01 2015 00:00:00 | 4.745762711864407 |
| 200 | December, 22 2014 00:00:00 | January, 01 2015 00:00:00 | 20 |
| 200 | January, 01 2015 00:00:00 | January, 06 2015 00:00:00 | 10 |

Use function add_months() and hierarchical subquery to generate periods for each id:
select id, d1, d2, round(value*(d2-d1)/nod, 2) value
from (
select id, value, todate-fromdate nod, add_months(fromdate, level-1) d1,
least(add_months(fromdate, level), todate) d2
from data
connect by add_months(fromdate, level) <= trunc(add_months(todate, 1)-1)
and id = prior id and prior dbms_random.value is not null )
SQLFiddle demo

Related

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

SQL: Getting all dates between a set of date pairs

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

SQL find effective price of the products based on the date

I have a table with four columns : id,validFrom,validTo and price.
This table contains the price of an article and the duration when that price is effective.
| id| validFrom | validTo | price
|---|-----------|-----------|---------
| 1 | 01-01-17 | 10-01-17 | 30000
| 1 | 04-01-17 | 09-01-17 | 20000
Now, for this inputs in my table my query output should be :
| id| validFrom | validTo | price
|---|-----------|----------|-------
| 1 | 01-01-17 | 03-01-17 | 30000
| 1 | 04-01-17 | 09-01-17 | 20000
| 1 | 10-01-17 | 10-01-17 | 30000
I can compare the dates and check if products with same id have overlapping dates but I have no idea how to split those dates into non-overlapping dates. Also I am not allowed to use PL/SQL.
Is this possible using only SQL ?
Oracle Setup:
CREATE TABLE prices ( id, validFrom, validTo, price ) AS
SELECT 1, DATE '2017-01-01', DATE '2017-01-10', 30000 FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-04', DATE '2017-01-09', 20000 FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-11', DATE '2017-01-15', 10000 FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-16', DATE '2017-01-18', 15000 FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-17', DATE '2017-01-20', 40000 FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-21', DATE '2017-01-24', 28000 FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-23', DATE '2017-01-26', 23000 FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-26', DATE '2017-01-26', 17000 FROM DUAL;
Query:
WITH daily_prices ( id, dt, price, duration ) AS (
-- Unroll the price ranges to individual days
SELECT id,
d.COLUMN_VALUE,
price,
validTo - validFrom
FROM prices p,
TABLE(
CAST(
MULTISET(
SELECT p.validFrom + LEVEL - 1
FROM DUAL
CONNECT BY p.validFrom + LEVEL - 1 <= p.validTo
)
AS SYS.ODCIDATELIST
)
) d
),
min_daily_prices ( id, dt, price ) AS (
-- Where a day falls between multiple ranges group them so the price
-- is for the shortest duration offer and if there are two equally short
-- durations then take the minimum price
SELECT id,
dt,
MIN( price ) KEEP ( DENSE_RANK FIRST ORDER BY duration )
FROM daily_prices
GROUP BY id, dt
),
group_changes ( id, dt, price, has_changed_group ) AS (
-- Find when the price changes or a day is skipped which means a new price
-- group is beginning
SELECT id,
dt,
price,
CASE WHEN dt = LAG( dt ) OVER ( PARTITION BY id ORDER BY dt ) + 1
AND price = LAG( price ) OVER ( PARTITION BY id ORDER BY dt )
THEN 0
ELSE 1
END
FROM min_daily_prices
),
groups ( id, dt, price, grp ) AS (
-- Calculate unique indexes (per id) for each group of price ranges
SELECT id,
dt,
price,
SUM( has_changed_group ) OVER ( PARTITION BY id ORDER BY dt )
FROM group_changes
)
SELECT id,
MIN( dt ) AS validFrom,
MAX( dt ) AS validTo,
MIN( price ) AS price
FROM groups
GROUP BY id, grp
ORDER BY id, validFrom;
Output:
ID VALIDFROM VALIDTO PRICE
---------- -------------------- -------------------- ----------
1 01-JAN-2017 00:00:00 03-JAN-2017 00:00:00 30000
1 04-JAN-2017 00:00:00 09-JAN-2017 00:00:00 20000
1 10-JAN-2017 00:00:00 10-JAN-2017 00:00:00 30000
1 11-JAN-2017 00:00:00 15-JAN-2017 00:00:00 10000
1 16-JAN-2017 00:00:00 18-JAN-2017 00:00:00 15000
1 19-JAN-2017 00:00:00 20-JAN-2017 00:00:00 40000
1 21-JAN-2017 00:00:00 22-JAN-2017 00:00:00 28000
1 23-JAN-2017 00:00:00 25-JAN-2017 00:00:00 23000
1 26-JAN-2017 00:00:00 26-JAN-2017 00:00:00 17000

Date between with only the days and month

I have this date :
Date1 = 14/10/2015
Date2 = 01/10/2011
Date3 = 01/11/2011
I'm trying to make this req :
Date1 between date2 and date3
How can i make this without paying attention to the years (only sql (oracle)).
The req should be true.
Thanks
In Oracle you can get first day of month for date1 and date2 and last day of month for date3 then use between, something like:
WHERE TRUNC(date1, 'MONTH') BETWEEN TRUNC(date2, 'MONTH') AND LAST_DAY(TO_DATE(date3,'MM/DD/YYYY'))
As an alternative you could just cast the numeric representation of MMDD to a number, and check if this number is in a specific range.
select *
from (select to_date('14/10/2015', 'DD/MM/YYYY') as datee from dual union
select to_date('01/10/2011', 'DD/MM/YYYY') as datee from dual union
select to_date('01/11/2011', 'DD/MM/YYYY') as datee from dual)
where to_number(to_char(datee,'MMDD')) between 1014 and 1131
In Oracle you can use the EXTRACT function to get individual parts of the date and use that to compare:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TEST( Date1 ) AS
SELECT DATE '2015-01-01' FROM DUAL
UNION ALL SELECT DATE '2015-01-15' FROM DUAL
UNION ALL SELECT DATE '2015-02-01' FROM DUAL
UNION ALL SELECT DATE '2015-09-01' FROM DUAL
UNION ALL SELECT DATE '2014-01-10' FROM DUAL
UNION ALL SELECT DATE '2014-01-20' FROM DUAL
UNION ALL SELECT DATE '2014-02-02' FROM DUAL
UNION ALL SELECT DATE '2013-01-14' FROM DUAL
Query 1:
WITH Dates ( Date2, Date3 ) AS (
SELECT DATE '2015-01-10', DATE '2015-02-01' FROM DUAL
)
SELECT t.*
FROM TEST t
CROSS JOIN Dates d
WHERE ( EXTRACT( MONTH FROM Date1 ) > EXTRACT( MONTH FROM Date2 )
OR ( EXTRACT( MONTH FROM Date1 ) = EXTRACT( MONTH FROM Date2 )
AND EXTRACT( DAY FROM Date1 ) >= EXTRACT( DAY FROM Date2 )
)
)
AND ( EXTRACT( MONTH FROM Date1 ) < EXTRACT( MONTH FROM Date3 )
OR ( EXTRACT( MONTH FROM Date1 ) = EXTRACT( MONTH FROM Date3 )
AND EXTRACT( DAY FROM Date1 ) <= EXTRACT( DAY FROM Date3 )
)
)
Results:
| DATE1 |
|----------------------------|
| January, 15 2015 00:00:00 |
| February, 01 2015 00:00:00 |
| January, 10 2014 00:00:00 |
| January, 20 2014 00:00:00 |
| January, 14 2013 00:00:00 |
Use the DDD format string to get the number of the day of the year, i.e.:
select to_char(to_date('14/10/2015','DD/MM/YYYY'),'DDD') d1
,to_char(to_date('01/10/2011','DD/MM/YYYY'),'DDD') d2
,to_char(to_date('01/11/2011','DD/MM/YYYY'),'DDD') d3
from dual;
D1 D2 D3
=== === ===
287 274 305
with params as (
select to_date('14/10/2015','DD/MM/YYYY') d1
,to_date('01/10/2011','DD/MM/YYYY') d2
,to_date('01/11/2011','DD/MM/YYYY') d3
from dual)
select 'Yes' a from params
where to_char(d1,'DDD') between to_char(d2,'DDD') and to_char(d3,'DDD');
A
===
Yes

Date Range Query in Oracle

I have following data
Sr. FromDate ToDate Code
1 1990-01-01 2000-08-31 A
2 2000-09-01 2001-05-31 B
3 2001-06-01 2018-12-31 C
and need to write SQL query to find rows having code for
date range= fromdate 1992-01-01 and ToDate 2000-12-31.
Select *
From Table
Where fromDate <= 1992-01-01
and EndDate >=2000-12-31
not returning proper data.
Any help??
Expected output are first two rows which cover part of date mentioned in query.
One of possible query is:
Select * From table where fromdate <= 19920101
UNION
Select * From table where todate >= 20011231
But some how I don't like it and wanted easier alternative.
Switch the operators
select *
from Table
where fromDate >= '1992-01-01'
and EndDate <= '2000-12-31'
You should migrate the dat to a date object:
Select *
from table
where fromDate >= to_date('1992-01-01','YYYY-MM-DD')
and ...
Look here for valid date formats and syntax:
http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions183.htm
Try reverse filtering, I mean change the dates in filtering:
WITH tab(ID, start_date, end_date, code) AS (
SELECT 1, to_date('1990-01-01', 'YYYY-MM-DD'), to_date('2000-08-31', 'YYYY-MM-DD'), 'A' FROM dual UNION ALL
SELECT 2, to_date('2000-09-01', 'YYYY-MM-DD'), to_date('2001-05-31', 'YYYY-MM-DD'), 'B' FROM dual UNION ALL
SELECT 3, to_date('2001-06-01', 'YYYY-MM-DD'), to_date('2018-12-31', 'YYYY-MM-DD'), 'C' FROM dual)
-----------------
--End of data preparation
-----------------
SELECT *
FROM tab
WHERE end_date >= to_date('1992-01-01', 'YYYY-MM-DD')
AND start_date <= to_date('2000-12-31', 'YYYY-MM-DD');
Output
| ID | START_DATE | END_DATE | CODE |
|----|----------------------------------|-------------------------------|------|
| 1 | January, 01 1990 00:00:00+0000 | August, 31 2000 00:00:00+0000 | A |
| 2 | September, 01 2000 00:00:00+0000 | May, 31 2001 00:00:00+0000 | B |
Based on what you said you have to put an OR. Is it really what you need?
Select *
From Table
Where fromDate <= '1992-01-01'
OR EndDate >='2000-12-31'