Select records by month and year between two dates - sql

I have the table record_b. I want to select the records of an specific month and year between begin_date and end_date.
id
begin_date
end_date
2
2022-09-04
2022-10-03
3
2022-10-04
2022-10-31
4
2022-11-04
2022-12-03
5
2022-12-04
2023-01-03
6
2023-01-04
2023-02-03
7
2023-02-04
null
eg1:
Input: 2023-01
Output should be the record with id 5 and 6
eg2:
Input: 2022-12
Output should be the record with id 4 and 5
I have tried using between however there is a problem evaluating the months after the year.
and v_year BETWEEN EXTRACT(YEAR FROM PC.begin_date)
AND EXTRACT(YEAR FROM PC.end_date)
AND v_month BETWEEN EXTRACT(MONTH FROM PC.begin_date)
AND EXTRACT(MONTH FROM PC.end_date)

A very basic dictate is when you have a date store it as a date.
This can be further extended to when you need to process dates then process dates.
Most of the nothing else will be needed - no conversion, extract, date_part, epoch - just dates.
The task here is to find those rows where a specified Year-Month (yyyy-mm) falls within the period begin and end dates from a table.
Realize that if any portion of the specified year-month falls within the period then the first day of that month (yyyy-mm-01) falls within that period.
You can use the make_date() function to get the first of the specified month. Then JOIN that result with between dates.
with input_val(yr_mon) as (values (:yyyymm)) --select * from input_val
, tgt_date(dt) as
( select 0make_date(substring(yr_mon,1,4)::integer
,substring(yr_mon,6,2)::integer
,01
)
from input_val
) --select * from tgt_date;
select rb.*
from tgt_date t
join record_b rb
on t.dt between date_trunc('month',rb.begin_date)
and date_trunc('month',rb.end_date);
The above however does NOT handle well data point 7 with a null end date (nor would it handle a null start date). But should it?
If so a null value is often interpreted as there in no ending date, which basically says all dates on or after the start date are included.
You can handle the situation by converting the period to a daterange, which will handle it without getting into null processing logic, then use the range containment operator.
with input_val(yr_mon) as (values (:yyyymm)) --select * from input_val
, tgt_date(dt) as
( select make_date(substring(yr_mon,1,4)::integer
,substring(yr_mon,6,2)::integer
,01
)
from input_val
) --select * from tgt_date;
select rb.*
from tgt_date t
join record_b rb
on t.dt <# daterange(date_trunc('month',rb.begin_date)::date
,date_trunc('month',rb.end_date)::date
, '[]'
);
Finally, depending on how you you use the results, you can hide this whole thing within a SQL function, which can then be used in an SQL statement.
create or replace function periods_with_year_month(year_mm text)
returns setof record_b
language sql
as $$
with tgt_date(dt) as
(select make_date(substring(year_mm,1,4)::integer
,substring(year_mm,6,2)::integer
,01
)
)
select rb.*
from tgt_date t
join record_b rb
on t.dt <# daterange( date_trunc('month',rb.begin_date)::date
, date_trunc('month',rb.end_date)::date
, '[]'
);
$$;
See demo here. Unfortunately ,db<>fiddle is non-interactive, so parameters of yyyy-mm are hard coded.

Related

How to create monthly snapshots for the last 6 months?

I'm trying to get detailed data (snapshot) for each month on Business Day=1 for the last 6 months and need to pass 6 different dates (BD1's only) through two date variables.
Two variables will be BOM which will be BD1 for the last 6 months and EOM which will be BD1+1.
For e.g
First snapshot will be
declare #BOM date ='2022-08-01'
declare #EOM date ='2022-09-01'
Second snapshot will be
declare #BOM date ='2022-09-01'
declare #EOM date ='2022-10-01'
and so on for the last 6 months from the current month
Here is what I'm trying to do:
declare #BOM date
set #BOM=
(
select top 6 cast(date_datetime as date) date_datetime
from date_dim
where
datediff(month, date_datetime, getdate()) <= 6
and bd=1
order by date_datetime asc);
declare #EOM date
set #EOM=
(
select top 6 date_datetime
from date_dim
where
datediff(month, date_datetime, getdate()) <= 5
and bd=1
order by date_datetime asc);
But my query does not process it as I'm passing more than 1 value through my BOM & EOM variables in my main query WHERE clause.
I need some help with defining and using these variables in my query so that they can take different snapshots and store it in a table.
As you discovered, you cannot store multiple values in a scalar variable. What you possibly need is to use a table variable (which behaves similarly to a temp table). The table variable can have multiple rows (one for each selected month) and multiple columns (BOM and EOM).
The following code defines such a table variable and populates it with BOM and EOM of the most recent 6 full months from the date_dim table. I used the LEAD() window function to select the corresponding EOM for each BOM.
Lacking any provided sample data to actually query, I added a simple query at the end to just list the selected date ranges and calculated number of business days in each.
-- Table variable to hold selected month information
DECLARE #selected_months TABLE (BOM DATE, EOM DATE)
-- Select last 6 full months
INSERT #selected_months
SELECT *
FROM (
SELECT
date_datetime AS BOM,
LEAD(date_datetime) OVER(ORDER BY date_datetime) AS EOM
FROM date_dim
) D
WHERE DATEDIFF(month, BOM, GETDATE()) BETWEEN 1 AND 6
ORDER BY BOM
-- Sample usage
SELECT M.*, DATEDIFF(day, M.BOM, M.EOM) business_days
FROM #selected_months M
-- JOIN your_data D
-- ON D.your_data_date >= SM.BOM
-- AND D.your_data_date < SM.EOM
GROUP BY M.BOM, M.EOM
ORDER BY M.BOM
Sample results:
BOM
EOM
business_days
2022-08-01
2022-09-05
35
2022-09-05
2022-10-03
28
2022-10-03
2022-11-07
35
2022-11-07
2022-12-05
28
2022-12-05
2023-01-02
28
2023-01-02
2023-02-06
35
See this db<>fiddle for a working demo.

create a temporary sql table using recursion as a loop to populate custom time interval

Suppose you have a table like:
id subscription_start subscription_end segment
1 2016-12-01 2017-02-01 87
2 2016-12-01 2017-01-24 87
...
And wish to generate a temporary table with months.
One way would be to encode the month date as:
with months as (
select
'2016-12-01' as 'first',
'2016-12-31' as 'last'
union
select
'2017-01-01' as 'first',
'2017-01-31' as 'last'
...
) select * from months;
So that I have an output table like:
first_day last_day
2017-01-01 2017-01-31
2017-02-01 2017-02-31
2017-03-01 2017-03-31
I would like to generate a temporary table with a custom interval (above), without manually encoding all the dates.
Say the interval is of 12 months, for each year, for as many years there are in the db.
I'd like to have general approach to compute the months table with the same output as above.
Or, one may adjust the range to a custom interval (months split an year in 12 parts, but one may want to split a time in a custom interval of days).
To start, I was thinking to use recursive query like:
with months(id, first_day, last_day, month) as (
select
id,
first_day,
last_day,
0
where
subscriptions.first_day = min(subscriptions.first_day)
union all
select
id,
first_day,
last_day,
months.month + 1
from
subscriptions
left join months on cast(
strftime('%m', datetime(subscriptions.subscription_start)) as int
) = months.month
where
months.month < 13
)
select
*
from
months
where
month = 1;
but it does not do what I'd expect: here I was attempting to select the first row from the table with the minimum date, and populate a table at interval of months, ranging from 1 to 12. For each month, I was comparing the string date field of my table (e.g. 2017-03-01 = 3 is march).
The query above does work and also seems a bit complicated, but for the sake of learning, which alternative would you propose to create a temporary table months without manually coding the intervals ?

Select list of dates from given range in Firebird

I'm designing a report that returns PurchaseOrder due in future week.
Query that I've added below returns PurchaseOrder due for a particular Commodity, AmountDue and its DeliveryDate.
Obviously it only returns PO_Dates that are in the table. What I want is to also include dates where no PO is expected, i.e. null for those cell.
To me one possibility is to LEFT JOIN the dataset with set of dates of future week on Date column, that will eventually make the result null where no Purchase Order is expected.
In Firebird I don't know how to select list of week long dates and then use it in the join.
SELECT
PURCHASE_ORDER_DET.COMMODITYID AS COM_ID,
PURCHASE_ORDER_DET.DELIVERYDATE + CAST ('29.12.1899' AS DATE) as DLV_DATE,
SUM(PURCHASE_ORDER_DET.REQQUANTITY) as DLV_DUE
FROM
PURCHASE_ORDER_DET
LEFT JOIN PURCHASE_ORDER_HDR on PURCHASE_ORDER_HDR.POH_ID =
PURCHASE_ORDER_DET.POH_ID
WHERE
PURCHASE_ORDER_DET.COMMODITYID = 1
AND PURCHASE_ORDER_HDR.STATUS in (0,1,2)
AND PURCHASE_ORDER_DET.DELIVERYDATE + CAST ('30.12.1899' AS TIMESTAMP) >= '3.01.2019'
AND PURCHASE_ORDER_DET.DELIVERYDATE + CAST ('30.12.1899' AS TIMESTAMP) <= '9.01.2019'
AND PURCHASE_ORDER_DET.DELETED is NULL
Group by
PURCHASE_ORDER_DET.COMMODITYID,
PURCHASE_ORDER_DET.DELIVERYDATE
DataSet
COM_ID DLV_DATE DLV_DUE
1 3.01.2019 50.000000
1 5.01.2019 10.000000
Expected
COM_ID DLV_DATE DLV_DUE
1 3.01.2019 50.000000
1 4.01.2019 null
1 5.01.2019 10.000000
1 6.01.2019 null
1 7.01.2019 null
1 8.01.2019 null
1 9.01.2019 null
Ignoring your odd use of datatypes*, there are several possible solutions:
Use a 'calendar' table that contains dates, and right join to that table (or left join from that table). The downside of course is having to populate this table (but that is a one-off cost).
Use a selectable stored procedure to generate a date range and join on that.
Generate the range in a recursive common table expression in the query itself
Option 1 is pretty self-explanatory.
Option 2 would look something like:
CREATE OR ALTER PROCEDURE date_range(startdate date, enddate date)
RETURNS (dateval date)
AS
BEGIN
dateval = startdate;
while (dateval <= enddate) do
BEGIN
suspend;
dateval = dateval + 1;
END
END
And then use this in your query like:
select date_range.dateval, ...
from date_range(date'2019-01-03', date'2019-01-09') -- use date_range(?, ?) for parameters
left join ...
on date_range.dateval = ...
Option 3 would look something like:
WITH RECURSIVE date_range AS (
SELECT date'2019-01-03' dateval -- start date, use cast(? as date) if you need a parameter
FROM rdb$database
UNION ALL
SELECT dateval + 1
FROM date_range
WHERE dateval < date'2019-01-09' -- end date use ? if you need a parameter
)
SELECT *
FROM date_range
LEFT JOIN ...
ON date_range.dateval = ...
Recursive common table expressions have a maximum recursion depth of 1024, which means that it isn't suitable if you need a span wider than 1024 days.
*: I'd suggest that you start using DATE instead of what looks like the number of days since 30-12-1899. That avoids having to do awkward calculations like you do now. If you do need those number of days, then you can for example use datediff(DAY FROM date'1899-12-30' TO somedatevalue) or somedatevalue - date'1899-12-30' to convert from date to that numeric value.

Oracle - Split a record into multiple records

I have a schedule table for each month schedule. And this table also has days off within that month. I need a result set that will tell working days and off days for that month.
Eg.
CREATE TABLE SCHEDULE(sch_yyyymm varchar2(6), sch varchar2(20), sch_start_date date, sch_end_date date);
INSERT INTO SCHEDULE VALUES('201703','Working Days', to_date('03/01/2017','mm/dd/yyyy'), to_date('03/31/2017','mm/dd/yyyy'));
INSERT INTO SCHEDULE VALUES('201703','Off Day', to_date('03/05/2017','mm/dd/yyyy'), to_date('03/07/2017','mm/dd/yyyy'));
INSERT INTO SCHEDULE VALUES('201703','off Days', to_date('03/08/2017','mm/dd/yyyy'), to_date('03/10/2017','mm/dd/yyyy'));
INSERT INTO SCHEDULE VALUES('201703','off Days', to_date('03/15/2017','mm/dd/yyyy'), to_date('03/15/2017','mm/dd/yyyy'));
Using SQL or PL/SQL I need to split the record with Working Days and Off Days.
From above records I need result set as:
201703 Working Days 03/01/2017 - 03/04/2017
201703 Off Days 03/05/2017 - 03/10/2017
201703 Working Days 03/11/2017 - 03/14/2017
201703 Off Days 03/15/2017 - 03/15/2017
201703 Working Days 03/16/2017 - 03/31/2017
Thank You for your help.
Edit: I've had a bit more of a think, and this approach works fine for your insert records above - however, it misses records where there are not continuous "off day" periods. I need to have a bit more of a think and will then make some changes
I've put together a test using the lead and lag functions and a self join.
The upshot is you self-join the "Off Days" onto the existing tables to find the overlaps. Then calculate the start/end dates on either side of each record. A bit of logic then lets us work out which date to use as the final start/end dates.
SQL fiddle here - I used Postgres as the Oracle function wasn't working but it should translate ok.
select sch,
/* Work out which date to use as this record's Start date */
case when prev_end_date is null then sch_start_date
else off_end_date + 1
end as final_start_date,
/* Work out which date to use as this record's end date */
case when next_start_date is null then sch_end_date
when next_start_date is not null and prev_end_date is not null then next_start_date - 1
else off_start_date - 1
end as final_end_date
from (
select a.*,
b.*,
/* Get the start/end dates for the records on either side of each working day record */
lead( b.off_start_date ) over( partition by a.sch_start_date order by b.off_start_date ) as next_start_date,
lag( b.off_end_date ) over( partition by a.sch_start_date order by b.off_start_date ) as prev_end_date
from (
/* Get all schedule records */
select sch,
sch_start_date,
sch_end_date
from schedule
) as a
left join
(
/* Get all non-working day schedule records */
select sch as off_sch,
sch_start_date as off_start_date,
sch_end_date as off_end_date
from schedule
where sch <> 'Working Days'
) as b
/* Join on "Off Days" that overlap "Working Days" */
on a.sch_start_date <= b.off_end_date
and a.sch_end_date >= b.off_start_date
and a.sch <> b.off_sch
) as c
order by final_start_date
If you had a dates table this would have been easier.
You can construct a dates table using a recursive cte and join on to it. Then use the difference of row number approach to classify rows with same schedules on consecutive dates into one group and then get the min and max of each group which would be the start and end dates for a given sch. I assume there are only 2 sch values Working Days and Off Day.
with dates(dt) as (select date '2017-03-01' from dual
union all
select dt+1 from dates where dt < date '2017-03-31')
,groups as (select sch_yyyymm,dt,sch,
row_number() over(partition by sch_yyyymm order by dt)
- row_number() over(partition by sch_yyyymm,sch order by dt) as grp
from (select s.sch_yyyymm,d.dt,
/*This condition is to avoid a given date with 2 sch values, as 03-01-2017 - 03-31-2017 are working days
on one row and there is an Off Day status for some of these days.
In such cases Off Day would be picked up as sch*/
case when count(*) over(partition by d.dt) > 1 then min(s.sch) over(partition by d.dt) else s.sch end as sch
from dates d
join schedule s on d.dt >= s.sch_start_date and d.dt <= s.sch_end_date
) t
)
select sch_yyyymm,sch,min(dt) as start_date,max(dt) as end_date
from groups
group by sch_yyyymm,sch,grp
I couldn't get the recursive cte running in Oracle. Here is a demo using SQL Server.
Sample Demo in SQL Server

get number of working days from date range in SQL for sqlite without extension functions

I have a table that contains the columns start and end, which contain the start and end date of a date range. I want to select the number of working days (number of days excluding saturday and sunday) using pure sqlite3 functionality from each time range. Is there any way to do this?
I checked the several answers for this problem, but many seem to use functions like DATEDIFF which aren't available in sqlite.
Example: Start date '2015-09-19' and end date '2015-09-22' should result in two working days.
If you had a table like this, your job would be easy.
-- I'm going to drop this table later.
create table calendar (
cal_date date primary key
);
insert into calendar values ('2015-09-18');
insert into calendar values ('2015-09-19');
insert into calendar values ('2015-09-20');
insert into calendar values ('2015-09-21');
insert into calendar values ('2015-09-22');
insert into calendar values ('2015-09-23');
-- Weekdays
select cal_date, strftime('%w', cal_date) day_of_week
from calendar
where day_of_week between 1 and 5;
cal_date day_of_week
---------- -----------
2015-09-18 5
2015-09-21 1
2015-09-22 2
2015-09-23 3
-- Weekdays between two dates
select cal_date, strftime('%w', cal_date) day_of_week
from calendar
where day_of_week between 1 and 5
and cal_date between '2015-09-19' and '2015-09-22';
cal_date day_of_week
---------- -----------
2015-09-21 1
2015-09-22 2
-- Count them
select count(*) num_days
from (select cal_date, strftime('%w', cal_date) day_of_week
from calendar
where day_of_week between 1 and 5
and cal_date between '2015-09-19' and '2015-09-22') x;
num_days
----------
2
So let's manufacture a table like that from start and end dates. To do that, we need a table of integers.
create table integers (
n integer primary key
);
insert into integers values (0);
insert into integers values (1);
insert into integers values (2);
insert into integers values (3);
insert into integers values (4);
insert into integers values (5);
The number of rows you insert is important. The lowest start date plus the largest integer usually needs to return a result later than the latest end date. Typically, you need a lot more than five rows. You've been warned.
create table test (start date, end date);
insert into test values('2015-09-19','2015-09-22');
insert into test values('2015-09-19','2015-09-19');
-- Calendar from date range. Look at your query plan.
-- There might be a better way to do this. I think you're
-- going to have to use a cross join with SQLite, though.
select distinct date(start, '+' || n || ' days') as cal_date
from test, integers
order by cal_date;
cal_date
----------
2015-09-19
2015-09-20
2015-09-21
2015-09-22
2015-09-23
2015-09-24
2015-09-25
Let's expand that a little to include the day of the week, and make it a view.
-- Calendar view from date range
drop table calendar;
create view calendar as
select distinct date(start, '+' || n || ' days') as cal_date,
case strftime('%w', date(start, '+' || n || ' days'))
when 0 then 'Sun'
when 1 then 'Mon'
when 2 then 'Tue'
when 3 then 'Wed'
when 4 then 'Thu'
when 5 then 'Fri'
when 6 then 'Sat'
end as day_of_week
from test, integers;
Now we can count the number of weekdays using a join and an aggregate function.
select start, end, count(cal_date) as num_weekdays
from test
left join calendar on cal_date between start and end
and day_of_week in ('Mon', 'Tue', 'Wed', 'Thu', 'Fri')
group by start, end
order by start, end;
start end num_weekdays
---------- ---------- ------------
2015-09-19 2015-09-19 0
2015-09-19 2015-09-22 2
Have a look at the Date and Time Functions of SQLite. Those are the ones that can be used as part of SQLite-internal queries.
Unfortunately, those are rather basic functions that can be used to map the String representation of a date to something else. The julianday() function is the only one one could calculate differences in days with, but it would not care about Saturday and Sunday.
Maybe there could be an approach based on "day of the week" (0 == Sunday, 6 == Saturday) and using the "seconds since 1970-01-01" to come up with a conditional expression using the time-difference in seconds.