SQL find latest child status entry on an monthly aggregation - sql

I have two tables: 'project' and 'project_status' in a BigQuery database...
project
Id Name
1 XXXX
2 YYYY
3 ZZZZ
project_status
project_id status_date status
1 2 Jan 2020 Good
2 4 Feb 2020 Bad
3 5 Feb 2020 On track
3 6 March 2020 Bad
I want to produce a monthly report of project status. The status of a project is not updated each month. In this case the most recent status entry up to the month end is taken as the latest status
Month Project Status
Jan 2020 XXXX Good
Feb 2020 XXXX Good
Mar 2020 XXXX Good
Jan 2020 YYYY null
Feb 2020 YYYY Bad
Mar 2020 YYYY Bad
Jan 2020 ZZZZ null
Feb 2020 ZZZZ On track
Mar 2020 ZZZZ Bad
For example, with project 'XXXX', there is no status update for Feb 2020 or March 2020, so the status is to be taken from the most recent status entry of 2 Jan 2020.
Any suggestions on how to build a SQL query to deliver this report?

Consider using GENERATE_DATE_ARRAY AND ARRAY_AGG:
WITH Months AS (
SELECT DATE_SUB(month, INTERVAL 1 DAY) AS last_day
FROM UNNEST(GENERATE_DATE_ARRAY('2020-02-01', '2020-04-02', INTERVAL 1 MONTH)) AS month
),
Projects as (
SELECT 1 AS Id, 'XXXX' AS Name UNION ALL
SELECT 2 AS Id, 'YYYY' AS Name UNION ALL
SELECT 3 AS Id, 'ZZZZ' AS Name
),
Project_Status as (
SELECT 1 AS project_id, PARSE_DATE('%e %b %Y', '2 Jan 2020') AS status_date, 'Good' AS status UNION ALL
SELECT 2, PARSE_DATE('%e %b %Y', '4 Feb 2020'), 'Bad' UNION ALL
SELECT 3, PARSE_DATE('%e %b %Y', '5 Feb 2020'), 'On track' UNION ALL
SELECT 3, PARSE_DATE('%e %b %Y', '6 March 2020'), 'Bad'
)
SELECT Projects.Name as Name, DATE_TRUNC(Months.last_day, MONTH) AS Month,
ARRAY_AGG(Project_Status.status ORDER BY status_date DESC)[OFFSET(0)] AS Status
FROM Projects
CROSS JOIN Months
LEFT JOIN Project_Status ON
Project_Status.project_id = Projects.id
AND Project_Status.status_date <= Months.last_day
GROUP BY Name, Month
The output is:

Related

Get the last 4 weeks prior to current week

I have a table that has DATE_VALUE, FISCAL_WEEK, FISCAL_YEAR
DATE_VALUE FISCAL_WEEK FISCAL_YEAR
24-DEC-21 52 2021
25-DEC-21 52 2021
26-DEC-21 52 2021
27-DEC-21 53 2021
28-DEC-21 53 2021
29-DEC-21 53 2021
30-DEC-21 53 2021
31-DEC-21 53 2021
01-JAN-22 53 2021
02-JAN-22 53 2021
03-JAN-22 1 2022
04-JAN-22 1 2022
05-JAN-22 1 2022
06-JAN-22 1 2022
07-JAN-22 1 2022
08-JAN-22 1 2022
09-JAN-22 1 2022
10-JAN-22 2 2022
11-JAN-22 2 2022
12-JAN-22 2 2022
13-JAN-22 2 2022
14-JAN-22 2 2022
The table goes on for the entire FY 2021 & 2022
I want to get the last 4 fiscal weeks (FW) prior to the current week. Let's assume this week is FW20 FY2022, I am able to get this result:
FISCAL_WEEK FISCAL_YEAR
16 2022
17 2022
18 2022
19 2022
The code used to return the above output is:
SELECT
*
FROM
(
WITH t AS (
SELECT
fiscal_week - 1 lastweek,
fiscal_week - 5 week_x,
fiscal_year
FROM
TABLE
WHERE
Trunc(date_value) = Trunc(sysdate)
)
SELECT
DISTINCT fiscal_week,
t.fiscal_year
FROM
TABLE
OUTER JOIN t ON fiscal_week <> week_x
WHERE
to_char(fiscal_week) BETWEEN lastweek - 4
AND lastweek
ORDER BY
fiscal_week
)
But if the current week was FW04 FY2022, the code above is not able to return this desired output.
FISCAL_WEEK FISCAL_YEAR
53 2021
1 2022
2 2022
3 2022
Similarly, if the current was FY03 FY2022, I want the output to be:
FISCAL_WEEK FISCAL_YEAR
52 2021
53 2021
1 2022
2 2022
How do I need to write the code to get this output? Maybe the case statement could work but I'd like to see if there's any other workaround? Any help would be appreciated.
Thank you!
You may put the condition on the date value not the week to get the required output, then use OFFSET 1 to skip the current week and fetch the next 4 rows only. Try the following:
WITH T AS
(
SELECT DISTINCT fiscal_week, fiscal_year
FROM TABLE_NAME
WHERE Trunc(date_value) <= Trunc(SYSDATE)
ORDER BY fiscal_year DESC, fiscal_week DESC
OFFSET 1 ROWS
FETCH NEXT 4 ROWS ONLY
)
SELECT fiscal_week, fiscal_year
FROM T ORDER BY fiscal_year, fiscal_week
See a demo.
Don't use the FISCAL_WEEK and FISCAL_YEAR columns; just use the DATE column and compare it to a range based on the start of the ISO week:
SELECT DISTINCT fiscal_week, fiscal_year
FROM table_name
WHERE "DATE" < TRUNC(SYSDATE, 'IW')
AND "DATE" >= TRUNC(SYSDATE, 'IW') - INTERVAL '28' DAY
ORDER BY fiscal_year, fiscal_week;
Which, for the sample data:
Create Table table_name("DATE", FISCAL_WEEK, FISCAL_YEAR) AS
SELECT DATE '2021-12-24', 52, 2021 FROM DUAL UNION ALL
SELECT DATE '2021-12-25', 52, 2021 FROM DUAL UNION ALL
SELECT DATE '2021-12-26', 52, 2021 FROM DUAL UNION ALL
SELECT DATE '2021-12-27', 53, 2021 FROM DUAL UNION ALL
SELECT DATE '2021-12-28', 53, 2021 FROM DUAL UNION ALL
SELECT DATE '2021-12-29', 53, 2021 FROM DUAL UNION ALL
SELECT DATE '2021-12-30', 53, 2021 FROM DUAL UNION ALL
SELECT DATE '2021-12-31', 53, 2021 FROM DUAL UNION ALL
SELECT DATE '2022-01-01', 53, 2021 FROM DUAL UNION ALL
SELECT DATE '2022-01-02', 53, 2021 FROM DUAL UNION ALL
SELECT DATE '2022-01-03', 1, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-04', 1, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-05', 1, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-06', 1, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-07', 1, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-08', 1, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-09', 1, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-10', 2, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-11', 2, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-12', 2, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-13', 2, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-01-14', 2, 2022 FROM DUAL UNION ALL
SELECT DATE '2022-02-14', 6, 2022 FROM DUAL;
If SYSDATE was 2022-01-17, would output:
FISCAL_WEEK
FISCAL_YEAR
52
2021
53
2021
1
2022
2
2022
fiddle

How to create Date column from month and year given in oracle sql?

I want to create a date column which would take 'year' and 'month' and give me a date starts from 1st
E_name s_date year month
a 01-08-2012 2012 MAR
a 23-06-2012 2010 DEC
a 19-03-2020 2020 DEC
a 14-02-2020 2020 MAR
b 27-12-2018 2018 DEC
REQUIRED OUTPUT
E_name s_date year month Date
a 01-08-2012 2012 MAR 01-03-2012
a 23-06-2012 2010 DEC 01-12-2010
a 19-03-2020 2020 DEC 01-12-2020
a 14-02-2020 2020 MAR 01-03-2020
b 27-12-2018 2018 DEC 01-12-2018
Concatenate the year and month columns and then use TO_DATE:
SELECT t.*,
TO_DATE(month || year, 'MONYYYY', 'NLS_DATE_LANGUAGE=American') AS "Date"
FROM table_name t
Which, for your sample data:
CREATE TABLE table_name (E_name, s_date, year, month) AS
SELECT 'a', DATE '2012-08-01', 2012, 'MAR' FROM DUAL UNION ALL
SELECT 'a', DATE '2012-06-23', 2010, 'DEC' FROM DUAL UNION ALL
SELECT 'a', DATE '2020-03-19', 2020, 'DEC' FROM DUAL UNION ALL
SELECT 'a', DATE '2020-02-14', 2020, 'MAR' FROM DUAL UNION ALL
SELECT 'b', DATE '2018-12-27', 2018, 'DEC' FROM DUAL;
Outputs:
E_NAME
S_DATE
YEAR
MONTH
Date
a
01-08-2012
2012
MAR
01-03-2012
a
23-06-2012
2010
DEC
01-12-2010
a
19-03-2020
2020
DEC
01-12-2020
a
14-02-2020
2020
MAR
01-03-2020
b
27-12-2018
2018
DEC
01-12-2018
db<>fiddle here

Query Optimization for my code in Oracle SQL

Here is my code:
select
(case when c.yr = 2019 and c.mon = 10 then 'October 2019'
when c.yr = 2019 and c.mon = 11 then 'November 2019'
when c.yr =2019 and c.mon = 12 then 'December 2019' end) as dae
from (
select substr(d,-4,4) as yr, substr(d,1,2) as mon
from
(select '10/11/2019' as d from dual) )c;
`
So I don't want to hard code the dates for the next 5 years, Is there a function that makes this easier.
Here is the Sample Input I want to try
10/11/2019
11/11/2019
12/11/2019
01/11/2020
Expected Output
October 2019
November 2019
December 2019
January 2020
You could use to_date() to turn your string to a date, and then convert it back to a string in the desired format with to_char():
to_char(to_date(d, 'mm/dd/yyyy'), 'Month yyyy')
Demo on DB Fiddle:
with t as (
select '10/11/2019' d from dual
union all select '11/11/2019' from dual
union all select '12/11/2019' from dual
union all select '01/11/2020' from dual
)
select to_char(to_date(d, 'mm/dd/yyyy'), 'Month yyyy') new_dt from t
| NEW_DT |
| :------------- |
| October 2019 |
| November 2019 |
| December 2019 |
| January 2020 |
Use connect by to generate as many dates as you want. Here the gen_dates CTE starts with your start_date and returns a total of 4 months per your example. To increase the number of months to generate, increase the number 4 to a higher number.
with gen_dates(date_in) as (
select add_months('11-OCT-2019', level -1) date_in
from dual
connect by level <= 4
)
select date_in, to_char(date_in, 'Month yyyy') date_out
from gen_dates;
DATE_IN DATE_OUT
--------- --------------
11-OCT-19 October 2019
11-NOV-19 November 2019
11-DEC-19 December 2019
11-JAN-20 January 2020
4 rows selected.

How to create dataset based on specific condition of date?

I have a dataset in the below manner
year mon date org
---- --- --------- ---
2018 mar 21-Mar-18 bb2
2018 mar 19-Mar-18 bbd
2018 feb 17-Feb-18 bbc
2018 feb 15-Feb-18 bba
2018 jan 15-Jan-18 abb
2018 jan 13-Jan-18 abc
and I am trying to get the data repeated in the following manner:
year mon date org
---- --- --------- ---
2018 mar 21-Mar-18 bb2
2018 mar 19-Mar-18 bbd
2018 mar 17-Feb-18 bbc
2018 mar 15-Feb-18 bba
2018 mar 15-Jan-18 abb
2018 mar 13-Jan-18 abc
2018 feb 17-Feb-18 bbc
2018 feb 15-Feb-18 bba
2018 feb 15-Jan-18 abb
2018 feb 13-Jan-18 abc
2018 jan 15-Jan-18 abb
2018 jan 13-Jan-18 abc
Edit:
Sorry about the vague information.
This is a unique requirement, which states as:
" If a user queries for a month, the output should return all the rows of the selected month and the months before it."
So if you look at my existing dataset (this is just dummy data), if I filter for mar I should get 6 rows, for feb 4 rows and 2 rows for jan and so on in a backward manner. So the overall data will consist of 12 rows in a table.
I am working on oracle version 11.2.0.3
any idea or inputs is highly appreciated.
EDIT 2:
Sorry House, I was away for so long. The requirement was scraped so we didn't pursue this route.
However if someone is looking to go in this route, the solution is
self join the table on "org" and Tab_A.column X is >= Tab_B.column X
given that X is a new column( based on year and mon, eg:201803)
To get the latest date for each month/year in the table, you may use max with a group by as follows (table name wasn't given, let's call it myTable):
SELECT year, mon, max(date) FROM myTable GROUP BY year, mon
Then getting your repeated data can be done with a join:
SELECT m2.year, m2.mon, m.date, m.org
FROM myTable AS m
INNER JOIN (SELECT year, mon, max(date) AS maxDate FROM myTable GROUP BY year, mon) AS m2
ON m.date <= m2.maxDate
ORDER BY m2.maxDate DESC, m.date DESC
If you're wanting rows if a user passes in a month and year, this should do the trick:
WITH your_table AS (SELECT 2018 YEAR, 'mar' mon, to_date('21/03/2018', 'dd/mm/yyyy') dt, 'bb2' org FROM dual UNION ALL
SELECT 2018 YEAR, 'mar' mon, to_date('19/03/2018', 'dd/mm/yyyy') dt, 'bbd' org FROM dual UNION ALL
SELECT 2018 YEAR, 'feb' mon, to_date('17/02/2018', 'dd/mm/yyyy') dt, 'bbc' org FROM dual UNION ALL
SELECT 2018 YEAR, 'feb' mon, to_date('15/02/2018', 'dd/mm/yyyy') dt, 'bba' org FROM dual UNION ALL
SELECT 2018 YEAR, 'jan' mon, to_date('15/01/2018', 'dd/mm/yyyy') dt, 'abb' org FROM dual UNION ALL
SELECT 2018 YEAR, 'jan' mon, to_date('13/01/2018', 'dd/mm/yyyy') dt, 'abc' org FROM dual)
-- end of sample data setup
SELECT *
FROM your_table
WHERE to_date(mon||'/'||YEAR, 'mon/yyyy', 'nls_date_language = english') <= to_date(:dt, 'mon/yyyy', 'nls_date_language = english');
:dt = apr/2018
YEAR MON DT ORG
---------- --- ----------- ---
2018 mar 21/03/2018 bb2
2018 mar 19/03/2018 bbd
2018 feb 17/02/2018 bbc
2018 feb 15/02/2018 bba
2018 jan 15/01/2018 abb
2018 jan 13/01/2018 abc
:dt = mar/2018
YEAR MON DT ORG
---------- --- ----------- ---
2018 mar 21/03/2018 bb2
2018 mar 19/03/2018 bbd
2018 feb 17/02/2018 bbc
2018 feb 15/02/2018 bba
2018 jan 15/01/2018 abb
2018 jan 13/01/2018 abc
:dt = feb/2018
YEAR MON DT ORG
---------- --- ----------- ---
2018 feb 17/02/2018 bbc
2018 feb 15/02/2018 bba
2018 jan 15/01/2018 abb
2018 jan 13/01/2018 abc
:dt = jan/2018
YEAR MON DT ORG
---------- --- ----------- ---
2018 jan 15/01/2018 abb
2018 jan 13/01/2018 abc
:dt = dec/2017
no rows selected
You would need to amend the input accordingly; in my query, I've assumed the month being queried for would be in mon/yyyy format; that may or may not be the case.
Also, since we're using mon, I've added in third optional parameter to ensure we're querying with the right date language. The query wouldn't work if the client's language was french, but the data's language is english!
Lastly, this seems like a bad design. You could easily change this table to have just the dt column, and add virtual columns to the table to create the month and year columns (although I wouldn't separate those out; I'd simply do trunc(dt, 'mm') and leave it as an extra date column. Far more flexible, IMO) and index the virtual column(s) accordingly.

How can compute monthly average price of an invoice in ORACLE (Varray&loop)

I have a table for invoices and there are millions of data. In my table, there are invoices and their first and last dates for customers. My target is to compute monthly average price of the invoices. For instance i have:
CUSTOMER_ID INVOICE_ID FIRST_DATE LAST_DATE AMOUNT_OF_INVOICE
9876543 1a 1 Jan 2017 17 Jan 2017 32$
9876543 1b 17 Jan 2017 10 Feb 2017 72$
9876543 1c 10 Feb 2017 7 March 2017 100$
9876543 1d 7 March 2017 1 April 2017 25$
9870011 2a 1 Jan 2017 10 Jan 2017 18$
9870011 2b 10 Jan 2017 10 Feb 2017 62$
9870011 2c 10 Feb 2017 1 April 2017 50$
my target is:
CUSTOMER_ID MONTH MONTHLY_AVERAGE_PRICE
9876543 January 2017 77$ (=16x2+15x3)
9876543 February 2017 103$ (=9x3+19x4)
9876543 March 2017 49$ (=6x4+25x1)
9870011 January 2017 62$ (=9x2+22x2)
9870011 February 2017 37$ (=9x2+19x1)
9870011 March 2017 31$ (=31x1)
For instance I compute 77$ (=16x2+15x3) by:
First invoice which INVOICE_ID is 1a there are 16 days from 1 Jan 2017 to 17 Jan 2017 (not incuding 17 Jan). And the price of invoice is 32$. Therefore average price for one day is 32/16 = 2$. The second invoice is 1b and there are 24 days from 17 Jan 2017 to 10 Feb 2017. Therefore average consumption per day is 3$. And the part of this invoice for January is 15 days (From 17 January to 31 January including 31 January). All in all, for January average consumption: 16x2$+15x3$=77$.
Here I think, I have to use varray for storage the data on months and I have to use a loop to find the days between FIRST_DATE and LAST_DATE. However I couldn't do it. Or are there in other ways?
Oracle Query:
WITH month_invoices ( c_id, i_id, first_date, last_date, month_start, month_end, amount )
AS (
SELECT customer_id,
invoice_id,
first_date,
last_date,
first_date,
LEAST( ADD_MONTHS( TRUNC( first_date, 'MM' ), 1 ), last_date ),
amount_of_invoice
FROM your_table
UNION ALL
SELECT c_id,
i_id,
first_date,
last_date,
month_end,
LEAST( ADD_MONTHS( month_end, 1 ), last_date ),
amount
FROM month_invoices
WHERE month_end < last_date
)
SELECT c_id AS customer_id,
TRUNC( month_start, 'MM' ) AS month,
SUM( amount * ( month_end - month_start ) / ( last_date - first_date ) )
AS average_monthly_price
FROM month_invoices
GROUP BY c_id, TRUNC( month_start, 'MM' )
ORDER BY customer_id, month;
Output:
CUSTOMER_ID MONTH AVERAGE_MONTHLY_PRICE
----------- ---------- ---------------------
9876543 2017-01-01 77
9876543 2017-02-01 103
9876543 2017-03-01 49
9870011 2017-01-01 62
9870011 2017-02-01 37
9870011 2017-03-01 31
This is what I came up with.
The inner query creates a row for each day of each invoice.
The outer query sums them up.
It assumes that invoices will only be a maximum of 999 days long.
select customer_id, month, sum(average_cost_per_day) average_cost_per_day
from (
select max(customer_id) customer_id,
invoice_id,
to_char(first_date + n-1, 'MONTH YYYY') month,
count(1)*max(amount_of_invoice)/max(last_date-first_date) average_cost_per_day
from your_table
inner join (
select level n
from dual
connect by level <= 1000
)
on (first_date + n-1 < last_date)
group by invoice_id, to_char(first_date + n-1, 'MONTH YYYY')
)
group by customer_id, month
order by customer_id desc, to_date(month,'MONTH YYYY');