Query date ranges excluding weekends in PostgreSQL - sql

I have the following postgresql table;
id | date_slot
------+-------------------------
1 | [2023-02-08,2023-02-15)
2 | [2023-02-20,2023-02-26)
3 | [2023-02-27,2023-03-29)
I want to make a query that return rows contained in these ranges but exclude weekends
for example the query I made return the following but does not exclude the weekends.
SELECT * FROM table where '2023-02-11'::date <# date_slot;
id | date_slot
------+-------------------------
1 | [2023-02-08,2023-02-15)
2023-02-11 is a weekend so it must not return a result. How can I do that?

Selecting workday-only dateranges (without weekends):
You can check what day of the week it is on the first day in the range using extract() and knowing its length from upper()-lower(), determine if it'll cross a weekend: online demo
select *
from test_table
where '2023-02-11'::date <# date_slot
and extract(isodow from lower(date_slot)
+ (not lower_inc(date_slot))::int)
+( (upper(date_slot) - (not upper_inc(date_slot))::int)
-(lower(date_slot) + (not lower_inc(date_slot))::int) ) < 6 ;
Cases where your ranges have varying upper and lower bound inclusivity are handled by lower_inc() and upper_inc() - their boolean result, when cast to int, just adds or subtracts a day to account for whether it's included by the range or not.
The range is on or includes a weekend if it starts on a weekend day or continues for too long from any other day of the week:
4 days, if it starts on a Monday (isodow=1)
3 days, if it starts on a Tuesday (isodow=2)
2 days, if it starts on a Wednesday (isodow=3)
1 days, if it starts on a Thursday (isodow=4)
0 days, if it starts on a Friday (isodow=5)
This means the isodow of range start date and the range length cannot sum up to more than 5 for the range not to overlap a weekend.
You can also enumerate the dates contained by these ranges using generate_series() and see if they include a Saturday (day 6) or a Sunday (0 as dow, 7 as isodow):
select *
from test_table
where '2023-02-11'::date <# date_slot
and not exists (
select true
from generate_series(
lower(date_slot) + (not lower_inc(date_slot))::int,
upper(date_slot) - (not upper_inc(date_slot))::int,
'1 day'::interval) as alias(d)
where extract(isodow from d) in (6,7) );
Selecting records based on workday-only dates:
First comment got it right
select *
from table_with_dateranges dr,
table_with_dates d
where d.date <# dr.date_slot
and extract(isodow from d.date) not in (6,7);

Related

SQL week number for the whole table

How to create a new column which calculates week number but for the whole table ignoring year?
Desired output is as follows:
Appreciate any help :)
You can do this by calculating 1st day of week of oldest row, and then calculate day diff of 1st day of week of current row and coldest row, after that, divide it by 7 days plus 1 will give you the desired week number across the full table.
Assuming you are using MySQL and the first day of the week is Sunday:
WITH min_week_start AS (
SELECT
SUBDATE(MIN(record_date), dayofweek(MIN(record_date)) - 1) as week_start_date
FROM
record_table
),
record_week_start AS (
SELECT
record_date,
SUBDATE(record_date, dayofweek(record_date) - 1) as week_start_date
FROM
record_table
)
SELECT
record_week_start.record_date,
DATEDIFF(record_week_start.week_start_date, min_week_start.week_start_date) / 7 + 1 as week_num
FROM
record_week_start
CROSS JOIN
min_week_start

How to calculate the number of each weekday between 2 dates in PostgreSQL?

There is a column with dates. I would like to calculate the number of each weekday (Monday to Sunday) from those dates to present date. On Stack Overflow and otherwise, I found answers that included creating functions, I was hoping there's some inbuilt function that would do it. I found another solution here, which mentions DATEPART('day', start - stop) AS days, but that didn't work. If this is an recent update in PostgreSQL then it won't work because the tool we use at work for PostgreSQL doesn't accept some of the recent updates (like PostgreSQL now accepts negative indexing but the tool doesn't).
What I want:
start_date
day_of_week
no_of_days
2022-04-01
1
10
2022-04-01
2
9
2022-05-15
2
3
2022-06-01
5
1
The start_date is the column of dates, which when subtracted from current_date (the other way around) returns the number of each weekday between those two days. There were 10 Mondays between 1st April 2022 and 6th June 2022 (today), and that's the number I want for each day of the week.
How can I achieve this in PostgreSQL? I am on version 12.8.
This "simple" but optimized solution counts the number of occurrences for every weekday in the interval between start_date and the current date:
WITH cte(start_date) AS (
VALUES
('2022-04-01'::date)
, ('2022-05-15')
, ('2022-06-01')
)
SELECT c.start_date, sub.dow, sub.no_of_days
FROM cte c
CROSS JOIN LATERAL (
SELECT dow, COALESCE(ct, 0) AS no_of_days
FROM (
SELECT EXTRACT('isodow' FROM g)::int AS dow, count(*) AS ct
FROM generate_series(start_date, current_date, interval '1 day') g
GROUP BY 1
) g
RIGHT JOIN generate_series(1, 7) dow USING (dow)
) sub
ORDER BY 1, 2;
db<>fiddle here
The upper bound (current_date) is included.
Every weekday is included, even when no_of_days is 0.
For very old dates (resulting in long intervals), an arithmetic solution will be cheaper than simply counting generated days. A bit more challenging, but not that hard.

SQLite select date and increment selection when points to weekend

I want to increment days when I select a date in SQLite and it points to weekends,
here is the code.
SELECT strftime('%Y-%d-%m','now', '+9 days');
Assuming what you want Saturdays and Sundays to be the following Monday then the following may do what you wish :-
SELECT CASE
WHEN strftime('%w','now') = 0 THEN strftime('%Y-%d-%m','now','+1 days') /* add 1 day when Sunday */
WHEN strftime('%w','now')= 6 THEN strftime('%Y-%d-%m','now','+2 days') /* add 2 days when Saturday */
ELSE strftime('%Y-%d-%m','now')
END AS adjusted_date;
If not then as you can see %w returns the days of the week where 0 is Sunday and 6 is Saturday. and you can use CASE WHEN THEN ELSE clause to conditionally returns values.
Date And Time Functions

How to select and group fortnightly in postgreql

I am trying to group the rows in a table fortnightly, but can't seem to work out how to do it - especially, as the date_part function does not have a 'fortnight' keyword argument.
This is what I have so far:
CREATE TABLE foo(
dt DATE NOT NULL,
f1 REAL NOT NULL,
f2 REAL NOT NULL,
f3 REAL NOT NULL,
f4 REAL NOT NULL
);
SELECT AVG((f1+f2+f3+f4)/4) as fld_avg FROM
(
SELECT date_part('year', dt) AS year_part,
date_part('fortnight', dt) AS fortnight_part,
f1, f2, f3, f4
FROM foo
WHERE dt >= date_trunc('day', NOW() - '3 month')
) foo
GROUP BY year_part, fortnight_part
How may I rewrite (or modify) the query above so as to group data fortnightly?
Basic idea
What we need to do, is take intervals of 14 consecutive days and map them to unique buckets and then group by those buckets. These buckets can of any type, int, char, timstamp, as long as we have unique value.
Division
A simple way to accomplish this is division. Divide by 14 days and truncate the result to date precision.
For example, we can extract the number of seconds since 1970-01-01, the UNIX epoch, and divide by the number of seconds in a fortnight: 14 * 24 * 60 * 60 = 14 * 86400 = 1209600. (I'll use Vao Tsun's example data)
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT (EXTRACT(EPOCH FROM d)::int/86400)/14 fortnight FROM c
which yields fortnights since 1970-01-01 (a Thursday):
fortnight
-----------
1251
1252
1254
1254
(4 rows)
The integer values we get, represent the number of fortnights since 1970-01-01, but we don't have to care about this. The important thing is, that it uniquely identifies a fortnight.
Due to 1970-01-01 being a Thursday, all fortnights will start at a Thursday. We might want to vary the starting point of our fortnight to a different day of the week (e.g. Monday) by adding:
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT (EXTRACT(EPOCH FROM d)::int/86400 + 4)/14 fortnight FROM c
By adding four days to Thursday we end up at Monday.
If you rather want fortnights with respect to the beginning of the year, instead of some arbitrary absolute date, such as 1970-01-01, we can use the day of the year instead:
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT EXTRACT(year FROM d) * 26 + EXTRACT(doy FROM d)::int/14 AS fortnight FROM c;
which yields
fortnight
-----------
52467
52468
52469
52470
(4 rows)
We need to multiply the extracted year by 26, because there are 26.1… fortnights in a year.
Truncation
Instead of division another approach is truncation. We map each day of a specific fortnight to the first timestamp of that fortnight.
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT d - make_interval(secs => EXTRACT(EPOCH FROM d)::int % (86400 * 14)) AS fortnight FROM c;
which yields
fortnight
---------------------
2017-12-14 00:00:00
2017-12-28 00:00:00
2018-01-25 00:00:00
2018-01-25 00:00:00
(4 rows)
This might seems a bit more complicated, but has some benefits. The result is still a date/time type and other code does not need to worry about the fact, that we used fortnights.
Again, instead of absolute fortnights, we can calculate this with respect to the beginning of the year:
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT d - make_interval(days => EXTRACT(dow FROM d)::int % 14) AS fortnight FROM c;
which yields
fortnight
---------------------
2017-12-17 00:00:00
2017-12-31 00:00:00
2018-01-21 00:00:00
2018-01-28 00:00:00
(4 rows)
The result is of type timestamp, you might want to have date instead. This can be addressed by casting:
(d - make_interval(days => EXTRACT(dow FROM d)::int % 14))::date
or subtracting int instead of interval from date:
d - (EXTRACT(dow FROM d)::int % 14)
There are much more possibilities. With this scheme, we can calculate the fortnight or any other interval with respect to the beginning of the month, some arbitrary date, etc.
update
fortnight is a two week period - one even the other odd. eg week 1 and 2, 3 and 4, 5 and 6.
closer: 2 is even, mod(2,2)=0 and 1 is odd, mod(1,2)=1
4 is even, mod(4,2)=0 and 3 is odd, mod(3,2)=1
6 is even, mod(6,2)=0 and 5 is odd, mod(5,2)=1
thus you can make an assumption that each one week's in year consecutive number divided by two reminder is 1, and each next one weeks number/2 reminders 0
The general idea is - using the sequential number of week in a year. To avoid Jan 1st to be first and Dec31 (possible be the 53rd - and thus two odds in a row), I use IW
week number of ISO 8601 week-numbering year (01-53; the first Thursday
of the year is in week 1)
then I assume that if one week number will be odd, next will be even, so we divide all the time in parts of two weeks - even+odd.
SQL Example:
o=# with c(d) as (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
select d,to_char(d,'IW'),right(to_char(d,'IW'),1)::int,mod(right(to_char(d,'IW'),1)::int, 2) from c;
d | to_char | right | mod
------------+---------+-------+-----
2017-12-21 | 51 | 1 | 1
2017-12-31 | 52 | 2 | 0
2018-01-26 | 04 | 4 | 0
2018-02-01 | 05 | 5 | 1
(4 rows)
mod is either 0 or 1 - group by this column
https://www.postgresql.org/docs/current/static/functions-math.html
https://www.postgresql.org/docs/current/static/functions-formatting.html
Of course you would need to add outer join on generate_series if you want data without gaps...
I post another answer to explain how I was wrong and why my "smart-n-neat"
way failed...
the schema build and queries are at:
https://www.db-fiddle.com/f/j5i2Td8CvxCVXQQYePKzCe/0
the first (and correct) query:
select distinct w2, avg(c) over (partition by w2)
from d
join generate_series('2016.11.28'::date,'2017.02.23'::date,'2 weeks'::interval) w2
on gs >= w2 and gs < w2 + '2 weeks'::interval
order by w2;
Is a long, simple and correct approach. with idea is to join on two weeks interval. It's working, reliable and all good.
Now the second query:
select distinct div(to_char(gs,'IW')::int,2), min(gs) over w, avg(c) over w
from d
window w as (partition by div(to_char(gs,'IW')::int,2))
order by min;
Is much shorter, neater and smarter, yet has a huge limitation and is unusable. Here's why:
My approach splits next to last two-weeks-interval to two parts: last week of 2016 and first week of 2017, thus dividing the result by half. If you multiply a sum of averages for those two weeks by a half, the result for both queries will match. Alas introducing CASE WHEN logic for the edge year weeks makes neat solution a heavy and overhead. And thus the very point is lost.
TL;DR the neat and lightweight solution works only on interval of one year, farther then two weeks from end or start of the year and lastly if our fortnightly interval starts from Monday.
Now the idea behind lightweight solution: round(2/2, 0)=1 and round(3/2, 0)=1, so you can divide year in intervals of two weeks and use it for grouping by.
Also I deliberately took not this New Year switch, because this 2018 Jan 1 is Monday, so IW is same as WW - which usually is not the case.
Lastly my first answer with odd and even weeks is not viable at all. It divides year not in two-weeks interval, but rather in two parts - for even and odd weeks... I deceived myself with "something close" idea and worked on the reminder, while I should do the opposite the whole value of division...

Adding relative week number column to MySQl results

I have a table with 3 columns: user, value, and date. The main query returns the values for a specific user based on a date range:
SELECT date, value FROM values
WHERE user = '$user' AND
date BETWEEN $start AND $end
What I would like is for the results to also have a column indicating the week number relative to the date range. So if the date range is 1/1/2010 - 1/20/2010, then any results from the first Sun - Sat of that range are week 1, the next Sun - Sat are week 2, etc. If the date range starts on a Saturday, then only results from that one day would be week 1. If the date range starts on Thursday but the first result is on the following Monday, it would be week 2, and there are no week 1 results.
Is this something fairly simple to add to the query? The only ideas I can come up with would be based on the week number for the year or the week number based on the results themselves (where in that second example above, the first result always gets week 1).
Example:
$start = "05/27/2010";
$end = "06/13/2010";
//Query
Result:
week date user value
1 05/28/2010 joe 123
3 06/07/2010 joe 123
3 06/08/2010 joe 123
4 06/13/2010 joe 123
This can easily be done with this simple and obvious expression :)
SELECT ...,
FLOOR(
(
DATEDIFF(date, $start) +
WEEKDAY($start + INTERVAL 1 DAY)
) / 7
) + 1 AS week_number;
Some explanation: this expression just calculates difference between the given date and the start date in days, then converts this number to weeks via FLOOR("difference in days" / 7) + 1. That's simple, but since this works only when $start is Sunday, we should add an offset for other week days: WEEKDAY($start + INTERVAL 1 DAY) which equals to 0 for Sun, 1 for Mon, ..., 6 for Sat.