Given this schema in a Postgres database:
CREATE TABLE person (
id serial PRIMARY KEY,
name text,
birth_date date,
);
How would I query the table to get the date of each person's next birthday after today?
For example if Bob's birth_date is 2000-06-01 then his next birthday would be 2016-06-01.
Note: I'm not looking for the birth_date + a pre-defined interval but rather the next anniversary of a person's birth.
I've written the equivalent in Python:
def next_birthday(self):
today = datetime.date.today()
next_birthday = self.birth_date.replace(year=today.year)
if next_birthday < today:
next_birthday = next_birthday.replace(year=today.year + 1)
return next_birthday
However I'd like to see if Postgres can do this in a more performant way.
select birth_date,
cast(birth_date + ((extract(year from age(birth_date)) + 1) * interval '1' year) as date) as next_birthday
from person
where name = 'Bob'
The expression (extract(year from age(birth_date)) + 1) * interval '1' year calculates the age at the next birthday in (complete) years. When adding that to the date of birth, this gives the next birthday.
The cast is necessary to get a real date back, because date + interval returns a timestamp (including a time).
If you remove the where condition, you'll get all "next" birthdays.
You can also get a list of the upcoming birthdays in e.g. the next 30 days using something like this:
select next_birthday,
next_birthday - current_date as days_until_next
from (
select birth_date,
cast(birth_date + ((extract(year from age(birth_date)) + 1) * interval '1' year) as date) as next_birthday
from person
) as upcoming
where upcoming.next_birthday <= current_date + 30
order by next_birthday;
SELECT
birth_date
+
cast(
date_part('year', current_date)
-
date_part('year', birth_date)
+
CASE
WHEN
date_part('month', birth_date) <
date_part('month', current_date)
OR (
date_part('month', birth_date) =
date_part('month', current_date)
AND
date_part('day', birth_date) <
date_part('day', current_date)
)
THEN 1
ELSE 0
END || ' year' as interval)
FROM person;
SELECT date birth_date + interval '1 year' as next_birthday
from person;
Related
Suppose I have following data in a table
id createdAt
1 2021-02-26T06:29:03.482Z
2 2021-02-27T06:29:03.482Z
3 2021-03-14T06:29:03.482Z
4 2021-03-17T06:29:03.482Z
I want data of current month. ie, if I generate report in march, I need to fetch results of march, so we need only current month data from table.
wanted output is
id createdAt
3 2021-03-14T06:29:03.482Z
4 2021-03-17T06:29:03.482Z
Anyone please help. Thank you.
You can use date_trunc():
select *
from the_table
where date_trunc('month', createdat) = date_trunc('month', current_timestamp);
date_trunc('month', ...) returns the first day of the month.
However, the above is not able to make use of an index on createdat. To improve performance, use a range query:
select *
from the_table
where createdat >= date_trunc('month', current_timestamp)
and createdat < date_trunc('month', current_timestamp) + interval '1 month'
The expression date_trunc('month', current_timestamp) + interval '1 month' returns the start of the next month (that's way this is compared with <)
You can compare the month and year of a date with the current one. But the index by field will not be used, you can build a separate index by year and month for this.
select *
from your_table
where extract(YEAR FROM createdAt) = extract(YEAR FROM now())
and extract(MONTH FROM createdAt) = extract(MONTH FROM now())
This is my current implementation
SELECT
date_trunc('month', do_date::date)::date as starting_of_the_month,
(date_trunc('month', do_date::date) + interval '1 month' - interval '1 day')::date as ending_of_the_month,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 1
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week1,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 2
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week2,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 3
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week3,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 4
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week4,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 5
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week5
FROM sales_dos
WHERE date_trunc('month', do_date::date)::date >= '2021-02-01' AND date_trunc('month', do_date::date)::date < '2021-02-28'
This is my output for now :
I want the output to display as below :
Week 1 : 2021-02-01 - 2021-02-07
Week 2 : 2021-02-08 - 2021-02-14
Week 3 : 2021-02-15 - 2021-02-21
Week 4 : 2021-02-22 - 2021-02-28
Week 5 : -
Here is another way to do it (example for January 2021).
with
t as (select date_trunc('month', '2021-03-11'::date) as aday), -- any date in Jan-2021
s as
(
select d::date, d::date + 6 ed, extract('isodow' from d) wd
from t, generate_series (aday, aday + interval '1 month - 1 day', interval '1 day') d
)
select format ('Week %s', extract(day from d)::integer / 7 + 1) as weekname, d, ed
from s
where wd = 1;
So what you are looking for is a hybrid ISO with standard Calendar. You are taking the ISO week starting and ending period, but instead of all weeks being exactly 7 days you potentially truncate the 1st and/or last weeks.
The change to need for this is not actually extensive. For initial query returns the in the ISO week begin date instead of the 1st of the month. Then the main query then checks for week 1 and if so produces the 1st of the month. The only twist is determining the ISO week begin date. For this I've just included a function I have had for some time specifically for that. The change to the week_days function are marked --<<<.
create or replace function iso_first_of_week(date_in date)
returns date
language sql
immutable strict
/*
Given a date return the 1st day of the week according to ISO-8601.
I.e. Return the Date if it is Monday otherwise return the preceding Monday
*/
AS $$
with wk_adj(l_days) as (values (array[0,1,2,3,4,5,6]))
select date_in - l_days[ extract (isodow from date_in)::integer ]
from wk_adj;
$$;
create or replace
function week_dates( do_date_in date)
returns table (week_num integer, first_date date, last_date date)
language sql
immutable strict
as $$
with recursive date_list(week_num,first_date,terminate_date) as
( select 1
, iso_first_of_week(do_date_in)::timestamp --<<<
, (date_trunc('month', do_date_in) + interval '1 month' - interval '1 day')::timestamp
union all
select week_num+1, (first_date+interval '7 day'), terminate_date
from date_list
where first_date+interval '6 day' < terminate_date::timestamp
)
select week_num
, case when week_num = 1 --<<<
then date_trunc('month', do_date_in)::date --<<<
else first_date::date --<<<
end --<<<
, case when (first_date+interval '6 day')::date > terminate_date
then terminate_date::date
else (first_date+interval '6 day')::date
end last_date
from date_list;
$$;
---------- Original Reply
You can use a recursive query CTE to get the week number and first date for each week of the month specified. The main query calculates the ending date, shorting the last if necessary. Then wrap that into a SQL function to return the week number and date range for each week. See example.
create or replace
function week_dates( do_date_in date)
returns table (ween_num integer, first_date date, last_date date)
language sql
immutable strict
as $$
with recursive date_list(week_num,first_date,terminate_date) as
( select 1
, date_trunc('month', do_date_in)::timestamp
, (date_trunc('month', do_date_in) + interval '1 month' - interval '1 day')::timestamp
union all
select week_num+1, (first_date+interval '7 day'), terminate_date
from date_list
where first_date+interval '6 day' < terminate_date::timestamp
)
select week_num
, first_date::date
, case when (first_date+interval '6 day')::date > terminate_date
then terminate_date::date
else (first_date+interval '6 day')::date
end last_date
from date_list;
$$;
Response to: "How can i put the output in a single row with week1, week2, week3, week4 and week5". This is essentially the initial output that did not satisfy what you wanted. The term for this type action is PIVOT and is generally understood. It stems from transforming row orientation to column orientation. It is not overly difficult but it is messy.
IMHO this is something that belongs in the presentation layer and is not suitable for SQL. After all you are rearranging the data structure for presentation purposes. Let the database server use its natural format, use the presentation layer to reformat. This allows reuse of the queries instead of rewriting when the presentation is changed or another view of the same data is required.
If you actually want this then just use your initial query, or see the answer from
#Bohemian. However the below shows how this issue can be handled with just SQL (assuming the function week_dates was created).
select week1s
, case when week5e is null
then week4e
else week5e
end "end of month"
, week1s || ' - ' || week1e
, week2s || ' - ' || week2e
, week3s || ' - ' || week3e
, week4s || ' - ' || week4e
, week5s || ' - ' || week5e
from ( select max(case when (week_num=1) then first_date else NULL end) as week1s
, max(case when (week_num=1) then last_date else NULL end) as week1e
, max(case when (week_num=2) then first_date else NULL end) as week2s
, max(case when (week_num=2) then last_date else NULL end) as week2e
, max(case when (week_num=3) then first_date else NULL end) as week3s
, max(case when (week_num=3) then last_date else NULL end) as week3e
, max(case when (week_num=4) then first_date else NULL end) as week4s
, max(case when (week_num=4) then last_date else NULL end) as week4e
, max(case when (week_num=5) then first_date else NULL end) as week5s
, max(case when (week_num=5) then last_date else NULL end) as week5e
from week_dates(current_date)
) w ;
As before I have wrapped the above in a SQL function and provide an example here.
I would first simplify to:
extract(day from do_date)::int / 7 + 1 as week_in_month
then pivot on that using crosstab().
I currently have the following code in Microsoft SQL Server to get users that viewed on two days in a row.
WITH uservideoviewvideo (date, user_id) AS (
SELECT DISTINCT date, user_id
FROM clickstream_videos
WHERE event_name ='video_play'
and user_id IS NOT NULL
)
SELECT currentday.date AS date,
COUNT(currentday.user_id) AS users_view_videos,
COUNT(nextday.user_id) AS users_view_next_day
FROM userviewvideo currentday
LEFT JOIN userviewvideo nextday
ON currentday.user_id = nextday.user_id AND DATEADD(DAY, 1,
currentday.date) = nextday.date
GROUP BY currentday.date
I am trying to get the DATEADD function to work in PostgreSQL but I've been unable to figure out how to get this to work. Any suggestions?
I don't think PostgreSQL really has a DATEADD function. Instead, just do:
+ INTERVAL '1 day'
SQL Server:
Add 1 day to the current date November 21, 2012
SELECT DATEADD(day, 1, GETDATE()); # 2012-11-22 17:22:01.423
PostgreSQL:
Add 1 day to the current date November 21, 2012
SELECT CURRENT_DATE + INTERVAL '1 day'; # 2012-11-22 17:22:01
SELECT CURRENT_DATE + 1; # 2012-11-22 17:22:01
http://www.sqlines.com/postgresql/how-to/dateadd
EDIT:
It might be useful if you're using a dynamic length of time to create a string and then cast it as an interval like:
+ (col_days || ' days')::interval
You can use date + 1 to do the equivalent of dateadd(), but I do not think that your query does what you want to do.
You should use window functions, instead:
with plays as (
select distinct date, user_id
from clickstream_videos
where event_name = 'video_play'
and user_id is not null
), nextdaywatch as (
select date, user_id,
case
when lead(date) over (partition by user_id
order by date) = date + 1 then 1
else 0
end as user_view_next_day
from plays
)
select date,
count(*) as users_view_videos,
sum(user_view_next_day) as users_view_next_day
from nextdaywatch
group by date
order by date;
I am trying to return the data of the orders of the current date and the orders of the same date a year ago.
My idea was to create two similar tables and merge the date by adding WHERE clauses. But it seems to not work.
Could you have a look at my code and see if you identify something wrong?
My outcome of this is totally blank.
Thanks a lot!
WITH
orders_channels AS
(SELECT
'BLH' AS brand,
date_trunc('week', date)::date AS date,
channel,
order_type,
case when (date_trunc('week', date)::date = current_date - interval '1 day') then 'current'
else 'previous' end
as week_type,
sum(orders) AS orders
FROM
de_data.orders_daily_channel_attribution_dashboard
WHERE
date > date_trunc('day', current_date) - interval '1 day'
GROUP BY 1,2,3,4),
wow_orders_channels AS
(SELECT
'BLH' AS brand,
date_trunc('week', date)::date AS date,
channel,
order_type,
case when (date_trunc('week', date)::date = current_date - interval '1 day') then 'current'
else 'previous' end
as week_type,
sum(orders) AS orders
FROM
de_data.orders_daily_channel_attribution_dashboard
WHERE
date >= date_trunc('week', current_date) - INTERVAL '1 year'
GROUP BY 1,2,3,4)
SELECT
*
FROM
(SELECT
o.brand,
date_trunc('week', o.date)::date as week,
'SEO_ACQ' AS name,
o.orders,
wow.orders as wow_orders
FROM
orders_channels o
join wow_orders_channels wow on wow.date = o.date - interval '1 year' and o.order_type = wow.order_type
where
o.channel = 'SEO'
AND o.order_type = 'ACQUISITION'
UNION ALL
SELECT
o.brand,
date_trunc('week', o.date) as week,
'CRM_ORDERS' AS name,
SUM(o.orders),
sum(wow.orders) as wow_orders
FROM
orders_channels o
join wow_orders_channels wow on wow.date = o.date - interval '1 year' and o.order_type = wow.order_type
WHERE
o.channel = 'CRM'
GROUP BY 1,2,3) x
ORDER BY 3,2
I work with a Postgres database. This DB has a table with users, who have a birthdate (date field). Now I want to get all users who have their birthday in the upcoming week....
My first attempt: SELECT id FROM public.users WHERE id IN (lange reeks) AND birthdate > NOW() AND birthdate < NOW() + interval '1 week'
But this does not result, obviously because off the year. How can I work around this problem?
And does anyone know what happen to PG would go with the cases at 29-02 birthday?
We can use a postgres function to do this in a really nice way.
Assuming we have a table people, with a date of birth in the column dob, which is a date, we can create a function that will allow us to index this column ignoring the year. (Thanks to Zoltán Böszörményi):
CREATE OR REPLACE FUNCTION indexable_month_day(date) RETURNS TEXT as $BODY$
SELECT to_char($1, 'MM-DD');
$BODY$ language 'sql' IMMUTABLE STRICT;
CREATE INDEX person_birthday_idx ON people (indexable_month_day(dob));
Now, we need to query against the table, and the index. For instance, to get everyone who has a birthday in April of any year:
SELECT * FROM people
WHERE
indexable_month_day(dob) >= '04-01'
AND
indexable_month_day(dob) < '05-01';
There is one gotcha: if our start/finish period crosses over a year boundary, we need to change the query:
SELECT * FROM people
WHERE
indexable_month_day(dob) >= '12-29'
OR
indexable_month_day(dob) < '01-04';
To make sure we match leap-day birthdays, we need to know if we will 'move' them a day forward or backwards. In my case, it was simpler to just match on both days, so my general query looks like:
SELECT * FROM people
WHERE
indexable_month_day(dob) > '%(start)%'
%(AND|OR)%
indexable_month_day(dob) < '%(finish)%';
I have a django queryset method that makes this all much simpler:
def birthday_between(self, start, finish):
"""Return the members of this queryset whose birthdays
lie on or between start and finish."""
start = start - datetime.timedelta(1)
finish = finish + datetime.timedelta(1)
return self.extra(where=["indexable_month_day(dob) < '%(finish)s' %(andor)s indexable_month_day(dob) > %(start)s" % {
'start': start.strftime('%m-%d'),
'finish': finish.strftime('%m-%d'),
'andor': 'and if start.year == finish.year else 'or'
}]
def birthday_on(self, date):
return self.birthday_between(date, date)
Now, I can do things like:
Person.objects.birthday_on(datetime.date.today())
Matching leap-day birthdays only on the day before, or only the day after is also possible: you just need to change the SQL test to a `>=' or '<=', and not adjust the start/finish in the python function.
I'm not overly confident in this, but it seems to work in my testing. The key here is the OVERLAPS operator, and some date arithmetic.
I assume you have a table:
create temporary table birthdays (name varchar, bday date);
Then I put some stuff into it:
insert into birthdays (name, bday) values
('Aug 24', '1981-08-24'), ('Aug 04', '1982-08-04'), ('Oct 10', '1980-10-10');
This query will give me the people with birthdays in the next week:
select * from
(select *, bday + date_trunc('year', age(bday)) + interval '1 year' as anniversary from birthdays) bd
where
(current_date, current_date + interval '1 week') overlaps (anniversary, anniversary)
The date_trunc truncates the date at the year, so it should get you up to the current year. I wound up having to add one year. This suggests to me I have an off-by-one in there for some reason. Perhaps I just need to find a way to get dates to round up. In any case, there are other ways to do this calculation. age gives you the interval from the date or timestamp to today. I'm trying to add the years between the birthday and today to get a date in the current year.
The real key is using overlaps to find records whose dates overlap. I use the anniversary date twice to get a point-in-time.
Finally, to show the upcoming birthdays of the next 14 days I used this:
SELECT
-- 14 days before birthday of 2000
to_char( to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') - interval '14 days' , 'YYYY-MM-dd') as _14b_b2000,
-- birthday of 2000
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') as date_b2000,
-- current date of 2000
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') as date_c2000,
-- 14 days after current date of 2000
to_char( to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') + interval '14 days' , 'YYYY-MM-dd') as _14a_c2000,
-- 1 year after birthday of 2000
to_char( to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') + interval '1 year' , 'YYYY-MM-dd') as _1ya_b2000
FROM c
WHERE
-- the condition
-- current date of 2000 between 14 days before birthday of 2000 and birthday of 2000
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') between
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') - interval '14 days' and
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd')
or
-- 1 year after birthday of 2000 between current date of 2000 and 14 days after current date of 2000
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') + interval '1 year' between
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') and
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') + interval '14 days'
;
So:
To solve the leap-year issue, I set both birthdate and current date to 2000,
and handle intervals only from this initial correct dates.
To take care of the near end/beginning dates,
I compared first the 2000 current date to the 2000 birthday interval,
and in case current date is at the end of the year, and the birthday is at the beginning,
I compared the 2001 birthday to the 2000 current date interval.
Here's a query that gets the right result, most of the time.
SELECT
(EXTRACT(MONTH FROM DATE '1980-08-05'),
EXTRACT(DAY FROM DATE '1980-08-05'))
IN (
SELECT EXTRACT(MONTH FROM CURRENT_DATE + s.a) AS m,
EXTRACT(DAY FROM CURRENT_DATE + s.a) AS d
FROM GENERATE_SERIES(0, 6) AS s(a)
);
(it doesn't take care of leap years correctly; but you could use extract again to work the subselect in terms of a leap year instead of the current year.
EDIT: Got it working for all cases, and as a useful query rather than a scalar select. I'm using some extra subselects so that I don't have to type the same date or expression twice for month and day, and of course the actual data would be in a table instead of the values expression. You might adapt this differently. It might still stand to improve by making a more intelligent series for weeks containing leap days, since sometimes that interval will only contain 6 days (for non-leap years).
I'll try to explain this from the inside-out; First thing I do is normalize the target date (CURRENT_DATE usually, but explicit in this code) into a year that I know is a leap year, so that February 29th appears among dates. The next step is to generate a relation with all of the month-day pairs that are under consideration; Since there's no easy way to do an interval check in terms of month-day, it's all happening using generate_series,
From there it's a simple matter of extracting the month and day from the target relation (the people alias) and filtering just the rows that are in the subselect.
SELECT *
FROM
(select column1 as birthdate, column2 as name
from (values
(date '1982-08-05', 'Alice'),
(date '1976-02-29', 'Bob'),
(date '1980-06-10', 'Carol'),
(date '1992-06-13', 'David')
) as birthdays) as people
WHERE
((EXTRACT(MONTH FROM people.birthdate),
EXTRACT(DAY FROM people.birthdate)) IN (
SELECT EXTRACT(MONTH FROM thedate.theday + s.a) AS m,
EXTRACT(DAY FROM thedate.theday + s.a) AS d
FROM
(SELECT date (v.column1 -
(extract (YEAR FROM v.column1)-2000) * INTERVAL '1 year'
) as theday
FROM (VALUES (date '2011-06-09')) as v) as thedate,
GENERATE_SERIES(0, 6) AS s(a)
)
)
Operating on days, as I've done here, should work splendidly all the way up until a two month interval (if you wanted to look out that far), since december 31 + two months and change should include the leap day. On the other hand, it's almost certainly more useful to just work on whole months for such a query, in which case you don't really need anything more than extract(month from ....
First find out how old the person currently is using age(), then grab the year from that extract(year from age()). This is how old they are currently in years, so for their age at their next birthday add 1 to the year. Then their next birthday is found by adding an interval of this many years * interval '1 year' to their birthday. Done.
I've used a subselect here to add the next_birth_day column in to the complete table to make the select clause simpler. You can then play with the where conditions to suit your needs.
select *
from (
select *,
(extract(year from age(birth_date)) + 1) * interval '1 year' + birth_date "next_birth_day"
from public.users
) as users_with_upcoming_birth_days
where next_birth_day between now() and now() + '7 days'
This is based on Daniel Lyons's anniversary idea, by calculating the interval between the next birthday and today, with just +/- date arithmetic:
SELECT
today,
birthday,
CASE
WHEN this_year_anniversary >= today
THEN this_year_anniversary
ELSE this_year_anniversary + '1 year'::interval
END - today < '1 week'::interval AS is_upcoming
FROM
(
SELECT
today,
birthday,
birthday + years AS this_year_anniversary
FROM
(
SELECT
today,
birthday,
((
extract(year FROM today) - extract(year from birthday)
) || ' years')::interval AS years
FROM
(VALUES ('2011-02-28'::date)) AS t1 (today),
(VALUES
('1975-02-28'::date),
('1975-03-06'::date),
('1976-02-28'::date),
('1976-02-29'::date),
('1976-03-06'::date)
) AS t2 (birthday)
) AS t
) AS t;
In case you want it to work with leap years:
create or replace function birthdate(date)
returns date
as $$
select (date_trunc('year', now()::date)
+ age($1, 'epoch'::date)
- (extract(year from age($1, 'epoch'::date)) || ' years')::interval
)::date;
$$ language sql stable strict;
Then:
where birthdate(birthdate) between current_date
and current_date + interval '1 week'
See also:
Getting all entries who's Birthday is today in PostgreSQL
Exemple: birthdate between: jan 20 and feb 10
SELECT * FROM users WHERE TO_CHAR(birthdate, '1800-MM-DD') BETWEEN '1800-01-20' AND '1800-02-10'
Why 1800?
No matter may be any year;
In my registration form, I can inform the date of birth (with years) or just the birthday (without year), in which case I saved as 1800 to make it easier to work with the date
Here's my take, which works with leap years too:
CREATE OR REPLACE FUNCTION days_until_birthday(
p_date date
) RETURNS integer AS $$
DECLARE
v_now date;
v_days integer;
v_date_upcoming date;
v_years integer;
BEGIN
v_now = now()::date;
IF (p_date IS NULL OR p_date > v_now) THEN
RETURN NULL;
END IF;
v_years = date_part('year', v_now) - date_part('year', p_date);
v_date_upcoming = p_date + v_years * interval '1 year';
IF (v_date_upcoming < v_now) THEN
v_date_upcoming = v_date_upcoming + interval '1 year';
END IF;
v_days = v_date_upcoming - v_now;
RETURN v_days;
END
$$ LANGUAGE plpgsql IMMUTABLE;
I know this post is old, but I had the same issue and came up with this simple and elegant solution:
It is pretty easy with age() and accounts for lap years... for the people who had their birthdays in the last 20 days:
SELECT * FROM c
WHERE date_trunc('year', age(birthdate)) != date_trunc('year', age(birthdate + interval '20 days'))
I have simply created this year date from original birth date.
( DATE_PART('month', birth_date) || '/' || DATE_PART('day', birth_date) || '/' || DATE_PART('year', now()))::date between :start_date and :end_date
I hope this help.