При разработке медицинских или CRM-систем часто возникает задача: вывести список пациентов, у которых день рождения выпадает на определенный период (например, на текущую неделю или месяц). Основная сложность заключается в том, что в базе данных хранится полная дата рождения с учетом года, а пользователю нужно найти совпадения только по месяцу и дню. В этой статье мы подробно разберем, как реализовать такой отчет на системе компоновки данных (СКД), используя только язык запросов 1С.
Представим ситуацию: пользователь вводит период с 25.12.2023 по 05.01.2024. Нам нужно найти всех пациентов, у которых день рождения попадает в интервал с 25 декабря по 5 января любого года. Если мы используем стандартное условие МЕЖДУ, система будет искать даты именно в указанном диапазоне лет, что нам не подходит.
Проанализируем основные технические препятствия:
ДАТАВРЕМЯ с переменными параметрами (например, подставить текущий год в функцию), а функции работы с датами ограничены.Рассмотрим один из самых популярных подходов — приведение даты рождения к первому году нашей эры (или любому другому фиксированному году). Однако, как было замечено в обсуждении, года «0» в системе 1С не существует, поэтому лучше использовать первый год.
Проанализируем логику: мы вычитаем из даты рождения количество лет, прошедших с 1-го года. Таким образом, любая дата превращается в дату 1-го года с сохранением месяца и дня. Аналогичную операцию мы проводим с границами периода.
ВЫБРАТЬ
ДанныеПациентов.Пациент КАК Пациент,
ДанныеПациентов.ДатаРождения КАК ДатаРождения
ИЗ
РегистрСведений.ДанныеПациентов КАК ДанныеПациентов
ГДЕ
ВЫБОР
КОГДА ГОД(&НачалоПериода) = ГОД(&КонецПериода)
ТОГДА ДОБАВИТЬКДАТЕ(ДанныеПациентов.ДатаРождения, ГОД, -(ГОД(ДанныеПациентов.ДатаРождения) - 1))
МЕЖДУ ДОБАВИТЬКДАТЕ(&НачалоПериода, ГОД, -(ГОД(&НачалоПериода) - 1))
И ДОБАВИТЬКДАТЕ(&КонецПериода, ГОД, -(ГОД(&КонецПериода) - 1))
ИНАЧЕ
// Случай, когда период захватывает конец одного года и начало другого
ДОБАВИТЬКДАТЕ(ДанныеПациентов.ДатаРождения, ГОД, -(ГОД(ДанныеПациентов.ДатаРождения) - 1))
МЕЖДУ ДОБАВИТЬКДАТЕ(&НачалоПериода, ГОД, -(ГОД(&НачалоПериода) - 1))
И ДАТАВРЕМЯ(1, 12, 31, 23, 59, 59)
ИЛИ
ДОБАВИТЬКДАТЕ(ДанныеПациентов.ДатаРождения, ГОД, -(ГОД(ДанныеПациентов.ДатаРождения) - 1))
МЕЖДУ ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)
И ДОБАВИТЬКДАТЕ(&КонецПериода, ГОД, -(ГОД(&КонецПериода) - 1))
КОНЕЦ
Важный момент: использование конструкции -(ГОД(Дата) - 1) позволяет избежать ошибки выхода за границы допустимых дат 1С. Мы гарантированно приводим дату к 0001 году.
Еще один интересный подход заключается в том, чтобы вычислить дату дня рождения пациента в текущем году и проверить, попадает ли она в заданный интервал. Для этого мы используем разность дат в годах.
Рассмотрим пример реализации:
ВЫБРАТЬ
ФизическиеЛица.Ссылка КАК Ссылка,
ДОБАВИТЬКДАТЕ(ФизическиеЛица.ДатаРождения, ГОД, РАЗНОСТЬДАТ(ФизическиеЛица.ДатаРождения, &ДатаКонец, ГОД)) КАК ДеньРожденияВЭтомГоду
ИЗ
Справочник.ФизическиеЛица КАК ФизическиеЛица
ГДЕ
ДОБАВИТЬКДАТЕ(ФизическиеЛица.ДатаРождения, ГОД, РАЗНОСТЬДАТ(ФизическиеЛица.ДатаРождения, &ДатаКонец, ГОД))
МЕЖДУ &ДатаНачало И &ДатаКонец
Этот метод хорош тем, что он автоматически обрабатывает високосные года благодаря внутренним алгоритмам функции ДОБАВИТЬКДАТЕ. Если пациент родился 29 февраля, а текущий год — обычный, платформа корректно вернет 28 февраля. Однако данный метод работает идеально только в том случае, если длина периода не превышает один год.
Если нам нужно максимально надежное решение, которое будет работать с любыми периодами и гарантированно попадать в индексы (при определенных условиях), мы можем применить метод генерации временной таблицы периодов. Разберем сложный запрос, который создает список месяцев в интервале и соединяет его с данными пациентов.
Посмотрим на алгоритм:
// Создаем последовательность чисел
ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВЫБРАТЬ 1 ... ОБЪЕДИНИТЬ ВЫБРАТЬ 9;
// Генерируем список смещений (например, до 100 месяцев)
ВЫБРАТЬ
Т1.Цифра + Т2.Цифра * 10 КАК Смещение
ПОМЕСТИТЬ ВТ_Смещения
ИЗ
ВТ_Цифры КАК Т1, ВТ_Цифры КАК Т2;
// Получаем все даты начала месяцев в периоде
ВЫБРАТЬ
ДОБАВИТЬКДАТЕ(НАЧАЛОПЕРИОДА(&НачалоПериода, МЕСЯЦ), МЕСЯЦ, ВТ_Смещения.Смещение) КАК МесяцПериода
ПОМЕСТИТЬ ВТ_Месяцы
ИЗ
ВТ_Смещения КАК ВТ_Смещения
ГДЕ
ДОБАВИТЬКДАТЕ(НАЧАЛОПЕРИОДА(&НачалоПериода, МЕСЯЦ), МЕСЯЦ, ВТ_Смещения.Смещение) <= &КонецПериода;
// Финальная выборка
ВЫБРАТЬ
ДанныеПациентов.Пациент,
ДОБАВИТЬКДАТЕ(ДАТАВРЕМЯ(1, 1, 1), ГОД, ГОД(ВТ_Месяцы.МесяцПериода) - 1) КАК ТекГод // Вспомогательное
ИЗ
РегистрСведений.ДанныеПациентов КАК ДанныеПациентов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Месяцы КАК ВТ_Месяцы
ПО МЕСЯЦ(ДанныеПациентов.ДатаРождения) = МЕСЯЦ(ВТ_Месяцы.МесяцПериода)
ГДЕ
// Вычисляем ДР в конкретном году из таблицы месяцев и проверяем интервал
ДОБАВИТЬКДАТЕ(ДанныеПациентов.ДатаРождения, ГОД, ГОД(ВТ_Месяцы.МесяцПериода) - ГОД(ДанныеПациентов.ДатаРождения))
МЕЖДУ &НачалоПериода И &КонецПериода
Преимущество: Этот метод абсолютно точен и справляется с любыми переходами через год, так как он фактически «примеряет» день рождения каждого пациента к каждому году, представленному в периоде.
Проанализируем ситуацию с производительностью. Если в вашей базе данных сотни тысяч пациентов, использование функций МЕСЯЦ() или ГОД() в условии ГДЕ приведет к полному сканированию таблицы (Full Scan), так как СУБД не сможет использовать индекс по полю ДатаРождения.
Выясним, как это можно оптимизировать:
ДеньРождения и МесяцРождения. Тогда запрос станет максимально быстрым: ГДЕ МесяцРождения = &НужныйМесяц И ДеньРождения МЕЖДУ &ДеньНач И &ДеньКон.НачалоПериода и КонецПериода так, чтобы они всегда включали время (23:59:59 для конца периода), используя выражение КонецПериода(&ДатаКонец, "День").ДЕНЬГОДА(), помните, что порядковый номер дня (например, 60-й день) в високосном и обычном году соответствует разным датам. Мы рекомендуем избегать функции ДЕНЬГОДА() для этой задачи.Мы рассмотрели несколько способов решения задачи. Для простых отчетов в СКД чаще всего достаточно Варианта 1 с нормализацией дат к первому году. Он компактен и легко вписывается в текст запроса. Если же отчет критичен к точности високосных дат и может формироваться за длительные периоды, лучше использовать Вариант 3 с временной таблицей месяцев. Выбирайте подходящий метод исходя из объема ваших данных и требований к точности.