Выбор дня рождения Postgres - PullRequest
       40

Выбор дня рождения Postgres

5 голосов
/ 02 августа 2011

Я работаю с базой данных Postgres.В этой БД есть таблица с пользователями, у которых есть дата рождения (поле даты).Теперь я хочу получить всех пользователей, у которых есть день рождения на следующей неделе ....

Моя первая попытка: SELECT id FROM public.users WHERE id IN (lange reeks) AND birthdate > NOW() AND birthdate < NOW() + interval '1 week'

Но это не результат, очевидно, потому что не в год.Как я могу обойти эту проблему?

И кто-нибудь знает, что с PG случится с делами в день рождения 29-02?

Ответы [ 11 ]

9 голосов
/ 23 июля 2013

Мы можем использовать функцию postgres, чтобы сделать это действительно хорошим способом.

Предполагая, что у нас есть таблица people с датой рождения в столбце dob, которая является датой,мы можем создать функцию, которая позволит нам индексировать этот столбец, игнорируя год.(Спасибо Золтану Бёззёрменьи ):

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));

Теперь нам нужно выполнить запрос к таблице и индексу.Например, чтобы получить всех, у кого есть день рождения в апреле любого года:

SELECT * FROM people 
WHERE 
    indexable_month_day(dob) >= '04-01'
AND 
    indexable_month_day(dob) < '05-01';

Есть одна ошибка: если наш период начала / окончания пересекает границу года, нам нужно изменить запрос:

SELECT * FROM people 
WHERE 
    indexable_month_day(dob) >= '12-29'
OR 
    indexable_month_day(dob) < '01-04';

Чтобы убедиться, что мы совпадаем с днями рождения високосного дня, нам нужно знать, будем ли мы «перемещать» их на день вперед или назад.В моем случае проще было сопоставить оба дня, поэтому мой общий запрос выглядит так:

SELECT * FROM people 
WHERE 
    indexable_month_day(dob) > '%(start)%'
%(AND|OR)%
    indexable_month_day(dob) < '%(finish)%';

У меня есть метод набора запросов django, который делает все это намного проще:

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)

Теперь я могу делать такие вещи, как:

Person.objects.birthday_on(datetime.date.today())

Возможно совпадение дней рождений високосного дня только на предыдущий день или только на следующий день: вам просто нужно изменить тест SQL на `> = 'или' <= 'и не корректируйте начало / конец в функции python. </p>

2 голосов
/ 14 ноября 2014

Наконец, чтобы показать предстоящие дни рождения следующих 14 дней, я использовал это:

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' 
;

Итак: Чтобы решить проблему високосного года, я установил дату рождения и текущую дату на 2000, и обрабатывать интервалы только с этой первоначальной правильной даты.

Заботиться о ближайшем конце / начале даты, Сначала я сравнил текущую дату 2000 года с интервалом рождения 2000 года, и если текущая дата в конце года, а день рождения в начале, Я сравнил день рождения 2001 года с текущим интервалом дат 2000 года.

2 голосов
/ 12 января 2013

Сначала выясните, сколько лет человек использует age(), а затем выберите год от этого extract(year from age()). Это сколько им лет в настоящее время, поэтому для их возраста на следующий день рождения прибавьте 1 к году. Затем их следующий день рождения определяется путем добавления интервала из этих многих лет * interval '1 year' к их дню рождения. Готово.

Я использовал здесь выборку, чтобы добавить столбец next_birth_day в полную таблицу, чтобы упростить предложение select. Затем вы можете поиграть с теми условиями, которые вам больше подходят.

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'
2 голосов
/ 02 августа 2011

Я не слишком уверен в этом, но, похоже, в моем тестировании это работает.Ключевым моментом здесь является оператор OVERLAPS и некоторая арифметика дат.

Я предполагаю, что у вас есть таблица:

create temporary table birthdays (name varchar, bday date);

Затем я добавлю в нее некоторые вещи:

insert into birthdays (name, bday) values 
('Aug 24', '1981-08-24'), ('Aug 04', '1982-08-04'), ('Oct 10', '1980-10-10');

Этот запрос даст мне людей с днями рождения на следующей неделе:

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)

date_trunc усекает дату в году, поэтому он должен привести вас к текущему году.Я закончил тем, что добавил один год.Это наводит меня на мысль, что по какой-то причине у меня там один на один.Возможно, мне просто нужно найти способ, чтобы сузить даты.В любом случае, есть другие способы сделать этот расчет.age дает вам интервал от даты или отметки времени до сегодняшнего дня.Я пытаюсь добавить годы между днем ​​рождения и сегодняшним днем, чтобы получить дату в текущем году.

Реальный ключ использует overlaps, чтобы найти записи, чьи даты перекрываются.Я использую дату годовщины дважды, чтобы получить момент времени.

1 голос
/ 02 августа 2011

Вот запрос, который в большинстве случаев дает правильный результат.

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)
);

(он не заботится о високосных годах правильно, но вы можете снова использовать extract для обработки подвыбора в виде високосного года вместо текущего года.

РЕДАКТИРОВАТЬ: Работает для всех случаев, и в качестве полезного запроса, а не скалярного выбора. Я использую некоторые дополнительные подвыборы, чтобы мне не приходилось вводить одну и ту же дату или выражение дважды для месяца и дня, и, конечно, фактические данные были бы в таблице вместо выражения values. Вы можете адаптировать это по-другому. Возможно, он еще улучшится, составив более интеллектуальный ряд для недель, содержащий високосные дни, поскольку иногда этот интервал будет содержать только 6 дней (для не високосных лет).

Я постараюсь объяснить это изнутри; Сначала я нормализую целевую дату (обычно CURRENT_DATE, но явно указанную в этом коде) в год, который, как я знаю, является високосным, так что 29 февраля появляется среди дат. Следующим шагом является создание связи со всеми рассматриваемыми парами месяц-день; Поскольку нет простого способа сделать интервальную проверку с точки зрения дня месяца, все это происходит с помощью generate_series,

Оттуда достаточно просто извлечь месяц и день из целевого отношения (псевдоним people) и отфильтровать только те строки, которые есть в подвыборке.

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)
        )
    )

Работа в дни, как я это делал здесь, должна работать великолепно вплоть до двухмесячного интервала (если вы хотите посмотреть так далеко), поскольку 31 декабря + два месяца и изменения должны включать високосный день , С другой стороны, почти наверняка более полезно просто работать целыми месяцами для такого запроса, и в этом случае вам действительно не нужно ничего, кроме extract(month from ....

0 голосов
/ 26 июня 2017

Я просто создал этот год с даты рождения.

( DATE_PART('month', birth_date) || '/' || DATE_PART('day', birth_date) || '/' || DATE_PART('year', now()))::date between :start_date and :end_date

Надеюсь, это поможет.

0 голосов
/ 07 июня 2017

Я знаю, что этот пост старый, но у меня возникла та же проблема, и я пришел к такому простому и элегантному решению: с age () довольно легко и приходится лет круга ... для людей, у которых были дни рождения впоследние 20 дней:

SELECT * FROM c 
WHERE date_trunc('year', age(birthdate)) != date_trunc('year', age(birthdate + interval '20 days'))
0 голосов
/ 01 мая 2014

Вот мой дубль, который работает и с високосными годами:

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;
0 голосов
/ 19 августа 2013

Например: дата рождения: между 20 января и 10 февраля. 10

SELECT * FROM users WHERE TO_CHAR(birthdate, '1800-MM-DD') BETWEEN '1800-01-20' AND '1800-02-10'

Почему 1800?Независимо от того, может быть любой год;

В моей регистрационной форме я могу сообщить дату рождения (с указанием лет) или просто день рождения (без указания года), и в этом случае я сэкономил 1800, чтобы упроститьработа с датой

0 голосов
/ 03 августа 2011

Если вы хотите, чтобы он работал с високосными годами:

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;

Тогда:

where birthdate(birthdate) between current_date
                            and current_date + interval '1 week'

Смотри также:

Получение всех записей о том, у кого сегодня день рождения в PostgreSQL

...