I need a function in PostgreSQL that accepts a date range and returns the dates inside the date range that are Mondays. Anybody have an idea how this could be done?
create function f(dr daterange)
returns setof date as $$
select d::date
from generate_series(
lower(dr), upper(dr), interval '1 day'
) s (d)
where
extract(dow from d) = 1 and
d::date <# dr;
;
$$ language sql;
select f(daterange('2014-01-01', '2014-01-20'));
f
------------
2014-01-06
2014-01-13
The most efficient way should be to find the first Monday and generate a series in steps of 7 days:
CREATE OR REPLACE FUNCTION f_mondays(dr daterange)
RETURNS TABLE (day date) AS
$func$
SELECT generate_series(a + (8 - EXTRACT(ISODOW FROM a)::int) % 7
, z
, interval '7 days')::date
FROM (
SELECT CASE WHEN lower_inc(dr) THEN lower(dr) ELSE lower(dr) + 1 END AS a
, CASE WHEN upper_inc(dr) THEN upper(dr) ELSE upper(dr) - 1 END AS z
) sub
$func$ LANGUAGE sql;
The subquery extracts start (a) and end (z) of the range, adjusted for inclusive and exclusive bounds with range functions.
The expression (8 - EXTRACT(ISODOW FROM a)::int) % 7 returns the number of days until the next monday. 0 if it's Monday already. The manual about EXTRACT().
generate_series() can iterate any given interval - 7 days in this case. The result is a timestamp, so we cast to date.
Only generates Mondays in the range, no WHERE clause needed.
Call:
SELECT day FROM f_mondays('[2014-04-14,2014-05-02)'::daterange);
Returns:
day
----------
2014-04-14
2014-04-21
2014-04-28
SQL Fiddle.
Related
Every month I have to fetch records for the previous month from a Db2 database. How can I write a Db2 query to fetch the last month of data without hard-coding the date range? For example, when run in December 2021, the query would return records dated between '2021-11-01' AND '2021-11-30', and those dates would change dynamically when I run the same query a month later.
It's easy to can precompute the date range in a cte and then use it in the main query. Assuming your table t has a ts column to filter by, you can do:
with
r as (
select
to_date(year(c) || '-' || month(c) || '-01' , 'YYYY-MM-DD') as e,
to_date(year(p) || '-' || month(p) || '-01' , 'YYYY-MM-DD') as b
from (
select current date as c, current date - 1 month as p from sysibm.sysdummy1
) x
)
select *
from t
cross join r
where t.ts >= r.b and t.ts < r.e
See example at db<>fiddle.
There are a few ways to describe the prior month as a date range in a Db2 SQL query. Some datetime SQL functions are not available on Db2 for z/OS, but even then you can still use date arithmetic and the LAST_DAY() function.
First day of last month: LAST_DAY(CURRENT DATE - 2 MONTHS) + 1 DAY
Last day of last month: LAST_DAY(CURRENT DATE - 1 MONTH)
First day of this month: LAST_DAY(CURRENT DATE - 1 MONTH) + 1 DAY
Inclusive-exclusive example (preferred approach):
SELECT ... WHERE someDateTimeColumn >= LAST_DAY(CURRENT DATE - 2 MONTHS) + 1 DAY
AND someDateTimeColumn < LAST_DAY(CURRENT DATE - 1 MONTH) + 1 DAY
Inclusive-inclusive example (calling the DATE() function will prevent implicit type conversion which could skip some some qualifying rows):
SELECT ... WHERE someDateTimeColumn >= LAST_DAY(CURRENT DATE - 2 MONTHS) + 1 DAY
AND DATE(someDateTimeColumn) <= LAST_DAY(CURRENT DATE - 1 MONTH)
If you're querying Db2 for LUW v11.1 or newer, you can also call the THIS_MONTH() function to get the first day of an input month.
First day of last month: THIS_MONTH(CURRENT DATE - 1 MONTH)
First day of this month: THIS_MONTH(CURRENT DATE)
Inclusive-exclusive example:
SELECT ... WHERE someDateTimeColumn >= THIS_MONTH(CURRENT DATE - 1 MONTH)
AND someDateTimeColumn < THIS_MONTH(CURRENT DATE)
I am trying to get the value of bought items of all weekdays of a certain calendar week.
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
group by to_char(angelegt_am, 'Day');
this Query is giving me all values of the year but i don't know how to change it so i get the data from a certain single week.
Solutions like
where to_char(angelegt_am,'IW') = 44
group by to_char(angelegt_am, 'Day')
have a problem, they return grouped values from all years, not only the current year.
One solution could be this one:
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
where to_char(angelegt_am, 'IYYY-"W"IW') = '2020-W44'
group by to_char(angelegt_am, 'Day')
Getting the date from a Week (according to ISO-8601) is not trivial, for example 2021-01-04 is Week 53 of year 2020. The year from ISO-Week can be different to the actual year.
For proper conversion I use these functions:
CREATE OR REPLACE FUNCTION ISOWeek2Date(YEAR INTEGER, WEEK INTEGER) RETURN DATE DETERMINISTIC IS
res DATE;
BEGIN
IF WEEK > 53 OR WEEK < 1 THEN
RAISE VALUE_ERROR;
END IF;
res := NEXT_DAY(TO_DATE( YEAR || '0104', 'YYYYMMDD' ) - 7, 'MONDAY') + ( WEEK - 1 ) * 7;
IF TO_CHAR(res, 'fmIYYY') = YEAR THEN
RETURN res;
ELSE
RAISE VALUE_ERROR;
END IF;
END ISOWeek2Date;
CREATE OR REPLACE FUNCTION ISOWeek2Date(WEEK VARCHAR2) RETURN DATE DETERMINISTIC IS
BEGIN
IF NOT REGEXP_LIKE(WEEK, '^\d{4}-?W\d{2}$') THEN
RAISE VALUE_ERROR;
END IF;
RETURN ISOWeek2Date(REGEXP_SUBSTR(WEEK, '^\d{4}'), REGEXP_SUBSTR(WEEK, '\d{2}$'));
END ISOWeek2Date;
Actually you can extract week number from date and then filter using it:
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
where to_char(angelegt_am, 'iw') = 2 -- specify your week number here
group by to_char(angelegt_am, 'Day')
But please check that you and your DB have the same view on weeks start and end dates
You can further read https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements004.htm#i34510
Here you can find all other format arguments accepted by to_char function under the "Datetime Format Models" topic
Filter the data in your where clause, e.g.:
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
where angelegt_am >= to_date ( :week_start_date, 'yyyy-mm-dd' ) -- change format as appropriate
and angelegt_am < to_date ( :week_start_date, 'yyyy-mm-dd' ) + 7
group by to_char(angelegt_am, 'Day')
I have a table in a PostgreSQL database containing dates and a total count per day.
mydate total
2012-05-12 12
2012-05-14 8
2012-05-13 4
2012-05-12 12
2012-05-15 2
2012-05-17 1
2012-05-18 1
2012-05-21 1
2012-05-25 1
Now I need to get the weekly totals for a given date range.
Ex. I want to get the weekly totals from 2012-05-01 up to 2012-05-31.
I'm looking at this output:
2012-05-01 2012-05-07 0
2012-05-08 2012-05-14 36
2012-05-15 2012-05-22 5
2012-05-23 2012-05-29 1
2012-05-30 2012-05-31 0
This works for any given date range:
CREATE FUNCTION f_tbl_weekly_sumtotals(_range_start date, _range_end date)
RETURNS TABLE (week_start date, week_end date, sum_total bigint)
LANGUAGE sql AS
$func$
SELECT w.week_start, w.week_end, COALESCE(sum(t.total), 0)
FROM (
SELECT week_start::date, LEAST(week_start::date + 6, _range_end) AS week_end
FROM generate_series(_range_start::timestamp
, _range_end::timestamp
, interval '1 week') week_start
) w
LEFT JOIN tbl t ON t.mydate BETWEEN w.week_start and w.week_end
GROUP BY w.week_start, w.week_end
ORDER BY w.week_start
$func$;
Call:
SELECT * FROM f_tbl_weekly_sumtotals('2012-05-01', '2012-05-31');
Major points
I wrapped it in a function for convenience, so the date range has to be provided once only.
The subquery w produces the series of weeks starting from the first day of the given date range. The upper bound is capped with LEAST to stay within the upper bound of the given date range.
Then LEFT JOIN to the data table (tbl in my example) to keep all weeks in the result, even where no data rows are found.
The rest should be obvious. COALESCE to output 0 instead of NULL for empty weeks.
Data types have to match, I assumed mydate date and total int for lack of information. (The sum() of an int is bigint.)
Explanation for my particular use of generate_series():
Generating time series between two dates in PostgreSQL
Using this function
CREATE OR REPLACE FUNCTION last_day(date)
RETURNS date AS
$$
SELECT (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::date;
$$ LANGUAGE 'sql' IMMUTABLE STRICT;
AND generate_series (from 8.4 onwards) we can create the date partitions.
SELECT wk.wk_start,
CAST(
CASE (extract(month from wk.wk_start) = extract(month from wk.wk_start + interval '6 days'))
WHEN true THEN wk.wk_start + interval '6 days'
ELSE last_day(wk.wk_start)
END
AS date) AS wk_end
FROM
(SELECT CAST(generate_series('2012-05-01'::date,'2012-05-31'::date,interval '1 week') AS date) AS wk_start) AS wk;
Then putting it together with the data
CREATE TABLE my_tab(mydate date,total integer);
INSERT INTO my_tab
values
('2012-05-12'::date,12),
('2012-05-14'::date,8),
('2012-05-13'::date,4),
('2012-05-12'::date,12),
('2012-05-15'::date,2),
('2012-05-17'::date,1),
('2012-05-18'::date,1),
('2012-05-21'::date,1),
('2012-05-25'::date,1);
WITH month_by_week AS
(SELECT wk.wk_start,
CAST(
CASE (extract(month from wk.wk_start) = extract(month from wk.wk_start + interval '6 days'))
WHEN true THEN wk.wk_start + interval '6 days'
ELSE last_day(wk.wk_start)
END
AS date) AS wk_end
FROM
(SELECT CAST(generate_series('2012-05-01'::date,'2012-05-31'::date,interval '1 week') AS date) AS wk_start) AS wk
)
SELECT month_by_week.wk_start,
month_by_week.wk_end,
SUM(COALESCE(mt.total,0))
FROM month_by_week
LEFT JOIN my_tab mt ON mt.mydate BETWEEN month_by_week.wk_start AND month_by_week.wk_end
GROUP BY month_by_week.wk_start,
month_by_week.wk_end
ORDER BY month_by_week.wk_start;
I need to query a PostgreSQL database to determine records that fall within today's date and the last day of the previous month. In other words, I'd like to retrieve everything that falls between December 31, 2011 and today. This query will be re-used each month, so next month, the query will be based upon the current date and January 31, 2012.
I've seen this option, but I'd prefer to avoid using a function (if possible).
Both solutions include the last day of the previous month and also include all of "today".
For a date column:
SELECT *
FROM tbl
WHERE my_date BETWEEN date_trunc('month', now())::date - 1
AND now()::date
You can subtract plain integer values from a date (but not from a timestamp) to subtract days. This is the simplest and fastest way.
For a timestamp column:
SELECT *
FROM tbl
WHERE my_timestamp >= date_trunc('month', now()) - interval '1 day'
AND my_timestamp < date_trunc('day' , now()) + interval '1 day'
I use the < operator for the second condition to get precise results (read: "before tomorrow").
I do not cast to date in the second query. Instead I add an interval '1 day', to avoid casting back and forth.
Have a look at date / time types and functions in the manual.
For getting date of previous/last month:
SELECT (date_trunc('month', now())::date - 1) as last_month_date
Result: 2012-11-30
For getting number of days of previous/last month:
SELECT DATE_PART('days', date_trunc('month', now())::date - 1) last_month_days
Result: 30
Try this:
SELECT ...
WHERE date_field between (date_trunc('MONTH', now()) - INTERVAL '1 day')::date
and now()::date
...
Try
select current_date - cast((date_part('day', current_date) + 1) as int)
take from http://wiki.postgresql.org/wiki/Date_LastDay, and modified to return just the days in a month
CREATE OR REPLACE FUNCTION calc_days_in_month(date)
RETURNS double precision AS
$$
SELECT EXTRACT(DAY FROM (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::date);
$$ LANGUAGE 'sql' IMMUTABLE STRICT;
select calc_days_in_month('1999-05-01')
returns 31
Reference is taken from this blog:
You can use below function:
CREATE OR REPLACE FUNCTION fn_GetLastDayOfMonth(DATE)
RETURNS DATE AS
$$
SELECT (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::DATE;
$$ LANGUAGE 'sql'
IMMUTABLE STRICT;
Sample executions:
SELECT *FROM fn_GetLastDayOfMonth(NOW()::DATE);
How do I find the number of fridays between two dates(including both the dates) using a select statement in oracle sql?
This will do it:
select ((next_day(date2-7,'FRI')-next_day(date-1,'FRI'))/7)+1 as num_fridays
from data
Perhaps best if I break that down. The NEXT_DAY function returns the next day that is a (Friday in this case) after the date.
So to find the first Friday after d1 would be:
next_day( d1, 'FRI')
But if d1 is a Friday that would return the following Friday, so we adjust:
next_day( d1-1, 'FRI')
Similarly to find the last Friday up to and including d2 we do:
next_day( d1-7, 'FRI')
Subtracting the 2 gives a number of days: 0 if they are the same date, 7 if they a re a week apart and so on:
next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')
Convert to weeks:
(next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')) / 7
Finally, if they are the same date we get 0, but really there is 1 Friday, and so on so we add one:
((next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')) / 7) + 1
I have to throw in my two cents for using a calendar table. (It's a compulsion.)
select count(*) as num_fridays
from calendar
where day_of_week = 'Fri'
and cal_date between '2011-01-01' and '2011-02-17';
num_fridays
-----------
6
Dead simple to understand. Takes advantage of indexes.
Maybe I should start a 12-step group. Calendar Table Anonymous.
See:
Why should I consider using an auxiliary calendar table?
The article's code is specifically for SQL Server but the techniques are portable to most SQL platforms.
With a Calendar table in place your query could be as simple as
SELECT COUNT(*) AS friday_tally
FROM YourTable AS T1
INNER JOIN Calendar AS C1
ON C1.dt BETWEEN T1.start_date AND T1.end_date
WHERE C1.day_name = 'Friday'; -- could be a numeric code
select sum(case when trim(to_char(to_date('2009-01-01','YYYY-MM-DD')+rownum,'Day')) = 'Friday' then 1 else 0 end) number_of_fridays
from dual
connect by level <= to_date('&end_date','YYYY-MM-DD') - to_date('&start_date','YYYY-MM-DD')+1;
Original source - http://forums.oracle.com/forums/thread.jspa?messageID=3987357&tstart=0
Try modifying this one:
CREATE OR REPLACE FUNCTION F_WORKINGS_DAYS
(V_START_DATE IN DATE, V_END_DATE IN DATE)
RETURN NUMBER IS
DAY_COUNT NUMBER := 0;
CURR_DATE DATE;
BEGIN -- loop through and update
CURR_DATE := V_START_DATE;
WHILE CURR_DATE <= V_END_DATE
LOOP
IF TO_CHAR(CURR_DATE,'DY') NOT IN ('SAT','SUN') -- Change this bit to ignore all but Fridays
THEN DAY_COUNT := DAY_COUNT + 1;
END IF;
CURR_DATE := CURR_DATE + 1;
END LOOP;
RETURN DAY_COUNT;
END F_WORKINGS_DAYS;
/
SELECT (NEXT_DAY('31-MAY-2012','SUN')
-NEXT_DAY('04-MAR-2012','SUN'))/7 FROM DUAL
select ((DATEDIFF(dd,#a,#b)) + DATEPART(dw,(#a-6)))/7