Group By Month between two dates - sql

I have a table that lists all employees and their respective start and end dates
I want to be able to count the number of active employees in each month. Is there a way to do this via a single query (eg groupBy) rather than generating multiple queries for each month?
=================================
Employee ID | StartDt | EndDt
123 | 01 Feb 2022 |
234 | 01 jan 2022 | 28 Feb 2022
456 | 01 dec 2021 | 28 Feb 2022
As an example, the table above should return:
Dec 2021: 1
Jan 2022: 2
Feb 2022: 3
Mar 2022: 1
Apr 2022: 1

You can generate a calendar and join to that:
WITH calendar (month) AS (
SELECT ADD_MONTHS(min_startdt, LEVEL - 1)
FROM (
SELECT MIN(startdt) AS min_startdt,
MAX(COALESCE(enddt, SYSDATE)) AS max_enddt
FROM employees
)
CONNECT BY LEVEL - 1 <= MONTHS_BETWEEN(max_enddt, min_startdt)
)
SELECT c.month,
COUNT(e.employee_id)
FROM calendar c
LEFT OUTER JOIN employees e
ON (e.startdt <= c.month AND (c.month <= e.enddt OR e.enddt IS NULL))
GROUP BY
c.month
Which, for the sample data:
CREATE TABLE employees (Employee_ID, StartDt, EndDt ) AS
SELECT 123, DATE '2022-02-01', NULL FROM DUAL UNION ALL
SELECT 234, DATE '2022-01-01', DATE '2022-02-28' FROM DUAL UNION ALL
SELECT 456, DATE '2021-12-01', DATE '2022-02-28' FROM DUAL;
Outputs:
MONTH
COUNT(E.EMPLOYEE_ID)
2021-12-01 00:00:00
1
2022-01-01 00:00:00
2
2022-02-01 00:00:00
3
2022-03-01 00:00:00
1
2022-04-01 00:00:00
1
db<>fiddle here

Related

Aggregate monthly rows created date and ended date

I need to adapt a graph from the current BI implementation to an SQL one. This graph reflects the amount of requests received and each one of these requests have 3 fields that are relevant for this query: the id, created date and the end date.
The graph looks like this https://i.stack.imgur.com/NRIjr.png:
+----+--------------+-------------+
| ID | CREATE_DATE | END_DATE |
+----+--------------+-------------+
| | | |
| 1 | 2022-01-01 | 2022-02-10 |
| | | |
| 2 | 2022-01-03 | 2022-03-01 |
| | | |
| 3 | 2022-02-01 | 2022-04-01 |
| | | |
| 4 | 2022-03-01 | null |
+----+--------------+-------------+
So for this particular example we'd have something like this:
January: active: 2 (requests 1 and 2), finished: 0;
February: active 2 (requests 2, 3), finished 1 (request 1);
March: active 2 (requests 3, 4) finished 1 (request 2)
So for each month I want the active requests for that particular month (those that their ended date goes after that particular month or is null) and the requests that finished during that month (this one might be split to another query, of course) I tried this query, but of course, it doesn't take into account the requests that ended in a particular month, and only gives me the cumulative sum
Edit: I forgot to mention that one of the requirements is that the beggining and end date of the graph might be set by the user. So maybe I want to see the months from April-2022 to April-2020 and see the 2 year behaviour!
WITH cte AS ( SELECT
date_trunc('month',
r.date_init) AS mon,
count(r.id) AS mon_sum
FROM
"FOLLOWUP"."CAT_REQUEST" r
GROUP BY
1 ) SELECT
to_char(mon,
'YYYY-mm') AS mon_text,
COALESCE(sum(c.mon_sum)
OVER (ORDER BY mon),
0) AS running_sum
FROM
generate_series('2022-01-01', '2023-12-25',
interval '1 month') mon
LEFT JOIN
cte c USING (mon)
ORDER BY
mon
I wrote query for you using some different business logic. But, result is will be same result which you needed. Sample query:
with month_list as (
select 1 as id, 'Yanuary' as mname union all
select 2 as id, 'Febriary' as mname union all
select 3 as id, 'Marth' as mname union all
select 4 as id, 'April' as mname union all
select 5 as id, 'May' as mname union all
select 6 as id, 'June' as mname union all
select 7 as id, 'Jule' as mname union all
select 8 as id, 'August' as mname union all
select 9 as id, 'September' as mname union all
select 10 as id, 'October' as mname union all
select 11 as id, 'November' as mname union all
select 12 as id, 'December' as mname
),
test_table as (
select
id,
create_date,
end_date,
extract(month from create_date) as month1,
extract(month from end_date) as month2
from
your_table
)
select
t1.mname,
count(*) as "actived"
from
month_list t1
inner join
test_table t2 on (t1.id >= t2.month1) and (t1.id < t2.month2)
group by
t1.id, t1.mname
order by
t1.id
/* --- Result:
mname actived
--------------------
Yanuary 2
Febriary 2
Marth 1
*/
PostgreSQL has many date & time functions and types.
I write some samples for you:
For example, in my samples function now() our chosen date.
-- get previos 12 month from date (return timestampt)
select now() - '12 month'::interval as newdate
-- Return:
2021-04-03 18:22:48.344 +0400
-- if you need only date, you can cast this to date
select (now() - '12 month'::interval)::date as newdate
-- Return:
2021-04-03
-- generate data from previous 12 month to selected date increase by month:
SELECT t1.datelist::date
from generate_series
(
now()-'12 month'::interval,
now(),
'1 month'
)
AS t1(datelist)
-- Return:
2021-04-03
2021-05-03
2021-06-03
2021-07-03
2021-08-03
2021-09-03
2021-10-03
2021-11-03
2021-12-03
2022-01-03
2022-02-03
2022-03-03
2022-04-03
-- generate data from previous 12 month to selected date increase by month with extracting month names and year:
-- this sample may be as you needed.
SELECT
extract(year from t1.datelist) as "year",
TO_CHAR(t1.datelist, 'Month') as "month",
trim(TO_CHAR(t1.datelist, 'Month')) || '-' || trim(to_char(t1.datelist, 'yyyy')) as "formatted_date"
from generate_series
(
now()-'12 month'::interval,
now(),
'1 month'
)
AS t1(datelist)
-- Return:
year month formatted_date
------------------------------------
2021 April April-2021
2021 May May-2021
2021 June June-2021
2021 July July-2021
2021 August August-2021
2021 September September-2021
2021 October October-2021
2021 November November-2021
2021 December December-2021
2022 January January-2022
2022 February February-2022
2022 March March-2022
2022 April April-2022

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

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.

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

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

Oracle count days per month

I wrote this SQL statement to calculate the days for each month
(select count(*) DAYs FROM
(
select trunc(ADD_MONTHS(sysdate,-1),'MM') + level -1 Dates from dual connect by
level <= ADD_MONTHS(trunc(sysdate,'MM'),1)-1 - trunc(sysdate,'MM')+1
) Where To_char(dates,'DY') NOT IN ('SA','SO'))
At the moment this statement ignores Saturdays and Sundays and it calculates the days from the month before the sysdate (June).
June has 22 days without weekends but sadly my statement says it has 23. I found out it includes the 1st July, which is wrong.
Do you know how I can tell my little statement it only calculates the days from the month I want to get not including days from another month?
Doing this sort of thing is always going to look not pretty... here's one way, which does it for the entire current year. You can restrict to a single month by adding an additional statement to the where clause:
select to_char(trunc(sysdate, 'y') + level - 1, 'fmMON') as month, count(*)
from dual
where to_char(trunc(sysdate, 'y') + level - 1, 'fmDY', 'nls_date_language=english') not in ('SAT','SUN')
connect by level <= trunc(add_months(sysdate, 12), 'y') - trunc(sysdate, 'y')
group by to_char(trunc(sysdate, 'y') + level - 1, 'fmMON')
As I said, not pretty.
Note a couple of things:
Use of the fm format model modifier to remove leading spaces
Explicit use of nls_date_language to ensure it'll work in all environments
I've added 12 months to the current date and then truncated it to the first of January to get the first day of the new year for simplicity
If you want to do this by month it might be worth looking at the LAST_DAY() function
The same statement (using LAST_DAY()) for the previous month only would be:
select count(*)
from dual
where to_char(trunc(sysdate, 'y') + level - 1, 'fmDY', 'nls_date_language=english') not in ('SAT','SUN')
connect by level <= last_day(add_months(trunc(sysdate, 'mm'), -1)) - add_months(trunc(sysdate, 'mm'), -1) + 1
Firstly, your inner query (select trunc(ADD_MONTHS(sysdate,-1),'MM') + level -1 Dates from dual connect by level <= ADD_MONTHS(trunc(sysdate,'MM'),1)-1 - trunc(sysdate,'MM')+1) returns the days of the month plus one extra day from the next month.
Secondly, a simpler query could use the LAST_DAY function which gets the last day of the month.
Finally, use the 'D' date format to get the day of the week as a number.
SELECT COUNT(*) FROM (
SELECT TO_CHAR(TRUNC(SYSDATE,'MM') + ROWNUM - 1, 'D') d
FROM dual CONNECT BY LEVEL <= TO_NUMBER(TO_CHAR(LAST_DAY(SYSDATE),'DD'))
) WHERE d BETWEEN 1 AND 5;
Without having to generate all days of the month and then count them:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE FUNCTION WORK_DAYS_IN_MONTH(
dt DATE
) RETURN NUMBER DETERMINISTIC
AS
first_day DATE := TRUNC( dt, 'MM' );
remainder NUMBER := LAST_DAY( dt ) - ( first_day + INTERVAL '27' DAY );
BEGIN
RETURN 20 + CASE first_day - TRUNC( first_day, 'IW' )
WHEN 0 THEN remainder -- Monday
WHEN 1 THEN remainder -- Tuesday
WHEN 2 THEN remainder -- Wednesday
WHEN 3 THEN LEAST( remainder, 2 ) -- Thursday
WHEN 4 THEN LEAST( remainder, 1 ) -- Friday
WHEN 5 THEN GREATEST( remainder-2, 0 ) -- Saturday
ELSE GREATEST( remainder-1, 0 ) -- Sunday
END;
END;
//
Query 1:
SELECT ADD_MONTHS( DATE '2014-12-01', LEVEL ) AS "Month",
WORK_DAYS_IN_MONTH( ADD_MONTHS( DATE '2014-12-01', LEVEL ) ) AS "# Work Days"
FROM DUAL
CONNECT BY LEVEL <= 12
Results:
| Month | # Work Days |
|-----------------------------|-------------|
| January, 01 2015 00:00:00 | 22 |
| February, 01 2015 00:00:00 | 20 |
| March, 01 2015 00:00:00 | 22 |
| April, 01 2015 00:00:00 | 22 |
| May, 01 2015 00:00:00 | 21 |
| June, 01 2015 00:00:00 | 22 |
| July, 01 2015 00:00:00 | 23 |
| August, 01 2015 00:00:00 | 21 |
| September, 01 2015 00:00:00 | 22 |
| October, 01 2015 00:00:00 | 22 |
| November, 01 2015 00:00:00 | 21 |
| December, 01 2015 00:00:00 | 23 |