How to query checking if a month is between two dates? - sql

I'm looking for query in postgresql to check whether a month is between two dates or not.
I know how to check if a date is between two date or not. Postgres also have a function to do that.
Let's say I have a a_table with rows:
ID | start_date (timestamp) | end_date (timestamp)
1 | 2019-07-20 00:00 | 2020-03-20 00:00
2 | 2019-08-20 00:00 | 2020-08-30 00:00
I have a to return the row that include a month between the start_date and end_date.
Let's say i have a month 2019-08.
So when i count
Select count(*) from a_table
Where [some where clause]
it returns 2 rows, ID 1 and ID 2
AND when i have a month 2020-01 it only return ID 1

You can use date range for this.
It's not clear to me what should happen if the start/end date in the table only covers part of a month.
If you only want to consider the full month, use the "contains" operator
select count(*)
from the_table
where daterange(start_date::date, end_date::date, '[]') #> daterange('2019-08-01'::date, '2019-09-01'::date, '[)');
The <# is the "is contained" operator which tests if the left range (the values from the table) is contained in the right hand range (the month you want to test). The comparison is done with an "open interval", which means '2019-09-01' is excluded from it. The above would not consider rows that do not contain the full August.
If you want to include partial matches as well, use the "overlaps" operator && instead:
select count(*)
from the_table
where daterange(start_date::date, end_date::date, '[]') && daterange('2019-08-01'::date, '2019-09-01'::date, '[)');

You can use to_date() in your where clause. So that your where clause would be like this.
WHERE '2019-08' BETWEEN to_date(start_date, 'YYYY-MM') and to_date(end_date ,'YYYY-MM')

I would recommend writing this as:
WHERE end_date >= TO_DATE('2019-08', 'YYYY-MM') AND
start_date < TO_DATE('2019-08', 'YYYY-MM') + INTERVAL '1 MONTH'
That is, the period includes at least one day of the month, because it starts before the end of the month and ends after the start of the month.
In addition, this has no functions on the columns in the table. So, if an index is available on either column, then it can be used. If you define the start/end as a range, then that provides other opportunities for using indexes.

Related

Oracle: counting sum of flags between two dates

I have two tables:
The first one contains the beginning and the end of an interval:
------------------------------
DATE_START | DATE_END
------------------------------
2020-01-04 2023-07-04
------------------------------
The second one is the calendar with dates and boolean "special day" indicator:
------------------------------
DATE | SPECIAL_DAY_FLAG
------------------------------
2020-01-04 1
------------------------------
2020-01-05 0
------------------------------
So I need a query, that returns only rows from the first table, where number of "special days" between DATE_START and DATE_END is greater than 14.
You can join to your second table (which I've called 'calendar') based on the date range from the first table (which I've called 'intervals'); filter those joined records on the flag value, and count how many matches there are; and finally use having to check the count is 14 or more:
select i.date_start, i.date_end,
count(c.calendar_date) as special_days
from intervals i
join calendar c on c.calendar_date >= i.date_start and c.calendar_date <= i.date_end
where c.special_day_flag = 1
group by i.date_start, i.date_end
having count(c.calendar_date) >= 14
fiddle
... where I've assumed the interval is inclusive - if it isn't then change to < end_date.
If you wanted to count total matched days as well as special days you could also use conditional aggregation; which would need to be in a subquery to then filter by number of special days.

Compare date filed with month and year in Postgres

I have a date field in one of my tables and the column name is from_dt. Now I have to compare a month and year combination against this from_dt field and check whether the month has already passed. The current database function uses separate conditions for the month and the year, but this is wrong as it will compare month and year separately. The current code is like this
SELECT bill_rate, currency FROM table_name WHERE
emp_id = employee_id_param
AND EXTRACT(MONTH FROM from_dt) <= month_param
AND EXTRACT(YEAR FROM from_dt) <= year_param
Now the fromt_dt field has value 2021-10-11. If I give month_param as 01 and year_param as 2022, this condition will not work as the month 10 is greater than 1, which I have given. Basically, I need to check whether 01-2022 (Jan 2022) is greater than r equal to 2021-10-01(October 1st, 2021). It would be very much helpful if someone can shed some light here.
If you just want to check whether one date is >= then another:
# select '2022-01-01'::date >= '2021-10-11'::date;
?column?
----------
t
If you want to restrict to year/month then:
select date_trunc('month','2022-01-01'::date) >= date_trunc('month', '2021-10-11'::date);
?column?
----------
t
Where the date_trunc components are:
select date_trunc('month','2022-01-01'::date) ;
date_trunc
------------------------
2022-01-01 00:00:00-08
select date_trunc('month','2021-10-11'::date) ;
date_trunc
------------------------
2021-10-01 00:00:00-07
See Postgres date_trunc for more information.
Assuming the given year_param and month_param are integers you can use the make_date function to create the first of the year_month and date_trunc to get the first on the month from the table. Just compare those values. (See date functions) So:
select bill_rate, currency
from table_name
where emp_id = employee_id_param
and date_trunc('month',from_dt) =
make_date( year_param, month_param, 01);

SQL - Select query not displaying all dates

I have a table that has a start date, an end date, and the pay period information according to the start and end dates. When I try to find the pay period information with a date range, the very first pay period information does not show in the result.
For example, when I run the following query:
select *
FROM PayPeriod
where start_date between '2020-12-01' and '2020-12-21'
I should see the following result:
Start_date End_date Pay_perild
2020-11-22 2020-12-05 2020-12-wk1
2020-12-06 2020-12-19 2020-12-wk3
2020-12-20 2021-01-02 2021-01-wk1
Instead, I get:
Start_date End_date Pay_period
2020-12-06 2020-12-19 2020-12-wk3
2020-12-20 2021-01-02 2021-01-wk1
The date range and the pay period that includes '2020-12-01' is omitted. Why isn't it showing, and how do I correct this?
Looks like I've got what you wanted. You need to intersect two time intervals.
To find intersecting intervals of two tables (say TableA and TableB, I use tables as more general case to distinguish two intervals by their meaning/role/whatever) you need to compare begin date of one table with end date of another for both tables (putting each of them to "one" and "another" role):
TableA.start_date < TableB.end_date
and TableB.start_date < TableA.end_date
It is the rule for the case where your intervals are continuous, e.g. end_date of one period is "just before" (like real numbers) of the start_dare of another (so all items in the interval will have start_date <= item_date < end_date). For discrete intervals (like days, where duration of one day will have identical values of start_date and end_date) there would be <= in intersection condition.
So, your query will look like
DECLARE #period_from date = CONVERT ('2020-12-01' 23),
#period_to date = CONVERT ('2020-12-21', 23);
select *
FROM PayPeriod
where start_date < #period_to /*or <= depending on inclusion of end_date*/
and #period_from < end_date /*or <= depending on inclusion of end_date*/
The query is returning the result as you instructed. It's working perfectly. In your query you put the date range condition one the start_date:
where start_date between '2020-12-01' and '2020-12-21'
The first row that you expect has start_date = 2020-11-22. This date is not in the rage you specified in the condition.
If you want the first row in the result set simply you need to change the condition.
If you have to put condition on the start date you have to make the date range wider in the condition. For example:
SELECT *
FROM PayPeriod
WHERE start_date between '2020-11-21' and '2020-12-21'
Based on your application requirement you have to arrange the condition.

Convert Year+WeekOfYear+DayOfWeek to a date

I have date values identified by a year, the week number within that year and the weekday and want to convert those into simple dates.
I couldn't find a function or another simple way to combine those, so I came up with a workaround using generate_series to get all dates in a range and JOIN the extracted values of those with my data:
SELECT data.*, days.d result
FROM ( VALUES (2017, 33, 3) ) data(d_year, d_week, d_weekday)
JOIN (
SELECT
-- the potential castdate
d::date d
-- year-week-dayofweek combination for JOINing
, EXTRACT('year' FROM d) d_year, EXTRACT('week' FROM d) d_week, EXTRACT('dow' FROM d) d_weekday
FROM generate_series('2015-01-01', '2019-12-31', INTERVAL '1day') AS days(d)
) days
USING(d_year, d_week, d_weekday)
Result is:
+--------+--------+-----------+------------+
| d_year | d_week | d_weekday | result |
+--------+--------+-----------+------------+
| 2017 | 33 | 3 | 16.08.2017 |
+--------+--------+-----------+------------+
While this works, this seems like overkill for such a simple task. Moreover, if one doesn't have a fixed range, this might not even work.
Is there an easier way to this?
demo:db<>fiddle
you can use the to_date() function, which takes an date string as argument, as well as a format pattern. So if the date string may be '2017-33-3', you could take this pattern to clarify each date part:
'IYYY-IW-ID'
'ID': The tricky part is: Does your week start with Sunday oder with Monday? This question influences the solution because it would shift the week numbers in an unexpected ways if you don't think about it. Thanks to your expected output, I saw you need 'ID' (ISO week day, week starts mondays) instead of 'D' (week day, week start sundays.)
'IW': Because we are taking the ISO week day, we need the ISO week of year as well (instead of 'WW': week of year)
'IYYY': Similar to (2)
More information about date patterns (especially the ISO thing): Postgres documentation
SELECT to_date(d_year || '-' || d_week || '-' || d_weekday, 'IYYY-IW-ID')
If you used the standard week pattern: 'YYYY-WW-D', your result would be 2017-08-13 (see fiddle)
Of course, this works also without the - characters, but it might be less readable:
SELECT to_date(d_year || d_week || d_weekday, 'IYYYIWID')

Choose active employes per month with dates formatted dd/mm/yyyy

I'm having a hard time explaining this through writing, so please be patient.
I'm making this project in which I have to choose a month and a year to know all the active employees during that month of the year.. but in my database I'm storing the dates when they started and when they finished in dd/mm/yyyy format.
So if I have an employee who worked for 4 months eg. from 01/01/2013 to 01/05/2013 I'll have him in four months. I'd need to make him appear 4 tables(one for every active month) with the other employees that are active during those months. In this case those will be: January, February, March and April of 2013.
The problem is I have no idea how to make a query here or php processing to achieve this.
All I can think is something like (I'd run this query for every month, passing the year and month as argument)
pg_query= "SELECT employee_name FROM employees
WHERE month_and_year between start_date AND finish_date"
But that can't be done, mainly because month_and_year must be a column not a variable.
Ideas anyone?
UPDATE
Yes, I'm very sorry that I forgot to say I was using DATE as data type.
The easiest solution I found was to use EXTRACT
select * from employees where extract (year FROM start_date)>='2013'
AND extract (month FROM start_date)='06' AND extract (month FROM finish_date)<='07'
This gives me all records from june of 2013 you sure can substite the literal variables for any variable of your preference
There is no need to create a range to make an overlap:
select to_char(d, 'YYYY-MM') as "Month", e.name
from
(
select generate_series(
'2013-01-01'::date, '2013-05-01', '1 month'
)::date
) s(d)
inner join
employee e on
date_trunc('month', e.start_date)::date <= s.d
and coalesce(e.finish_date, 'infinity') > s.d
order by 1, 2
SQL Fiddle
If you want the months with no active employees to show then change the inner for a left join
Erwin, about your comment:
the second expression would have to be coalesce(e.finish_date, 'infinity') >= s.d
Notice the requirement:
So if I have an employee who worked for 4 months eg. from 01/01/2013 to 01/05/2013 I'll have him in four months
From that I understand that the last active day is indeed the previous day from finish.
If I use your "fix" I will include employee f in month 05 from my example. He finished in 2013-05-01:
('f', '2013-04-17', '2013-05-01'),
SQL Fiddle with your fix
Assuming that you really are not storing dates as character strings, but are only outputting them that way, then you can do:
SELECT employee_name
FROM employees
WHERE start_date <= <last date of month> and
(finish_date >= <first date of month> or finish_date is null)
If you are storing them in this format, then you can do some fiddling with years and months.
This version turns the "dates" into strings of the form "YYYYMM". Just express the month you want like this and you can do the comparison:
select employee_name
from employees e
where right(start_date, 4)||substr(start_date, 4, 2) <= 'YYYYMM' and
(right(finish_date, 4)||substr(finish_date, 4, 2) >= 'YYYYMM' or finish_date is null)
NOTE: the expression 'YYYYMM' is meant to be the month/year you are looking for.
First, you can generate multiple date intervals easily with generate_series(). To get lower and upper bound add an interval of 1 month to the start:
SELECT g::date AS d_lower
, (g + interval '1 month')::date AS d_upper
FROM generate_series('2013-01-01'::date, '2013-04-01', '1 month') g;
Produces:
d_lower | d_upper
------------+------------
2013-01-01 | 2013-02-01
2013-02-01 | 2013-03-01
2013-03-01 | 2013-04-01
2013-04-01 | 2013-05-01
The upper border of the time range is the first of the next month. This is on purpose, since we are going to use the standard SQL OVERLAPS operator further down. Quoting the manual at said location:
Each time period is considered to represent the half-open interval
start <= time < end [...]
Next, you use a LEFT [OUTER] JOIN to connect employees to these date ranges:
SELECT to_char(m.d_lower, 'YYYY-MM') AS month_and_year, e.*
FROM (
SELECT g::date AS d_lower
, (g + interval '1 month')::date AS d_upper
FROM generate_series('2013-01-01'::date, '2013-04-01', '1 month') g
) m
LEFT JOIN employees e ON (m.d_lower, m.d_upper)
OVERLAPS (e.start_date, COALESCE(e.finish_date, 'infinity'))
ORDER BY 1;
The LEFT JOIN includes date ranges even if no matching employees are found.
Use COALESCE(e.finish_date, 'infinity')) for employees without a finish_date. They are considered to be still employed. Or maybe use current_date in place of infinity.
Use to_char() to get a nicely formatted month_and_year value.
You can easily select any columns you need from employees. In my example I take all columns with e.*.
The 1 in ORDER BY 1 is a positional parameter to simplify the code. Orders by the first column month_and_year.
To make this fast, create an multi-column index on these expressions. Like
CREATE INDEX employees_start_finish_idx
ON employees (start_date, COALESCE(finish_date, 'infinity') DESC);
Note the descending order on the second index-column.
If you should have committed the folly of storing temporal data as string types (text or varchar) with the pattern 'DD/MM/YYYY' instead of date or timestamp or timestamptz, convert the string to date with to_date(). Example:
SELECT to_date('01/03/2013'::text, 'DD/MM/YYYY')
Change the last line of the query to:
...
OVERLAPS (to_date(e.start_date, 'DD/MM/YYYY')
,COALESCE(to_date(e.finish_date, 'DD/MM/YYYY'), 'infinity'))
You can even have a functional index like that. But really, you should use a date or timestamp column.