В учетных системах нередко возникает задача: за сутки может быть введен не один документ определенного вида, а два и более (например, несколько сменных отчетов, инкассаций или чеков закрытия смены). В итоговой выборке требуется однозначно определить и пометить самый первый документ цепочки за конкретный день, либо отобрать только его.
Разберем по шагам, как решить эту задачу в среде 1С:Предприятие 7.7 при работе с прямыми SQL-запросами через компонент 1С++, а также выясним, как аналогичная логика реализуется в запросах платформы 1С:Предприятие 8.
Проанализируем структуру хранения документов в таблице журнала _1SJOURN платформы 7.7. Для хронологической сортировки в ней используется составное поле DATE_TIME_IDDOC. Это строковое поле фиксированной длины (23 символа), которое включает в себя:
ГГГГММДД;ЧЧММСС;IDDOC в 36-ричной системе счисления.Благодаря такой структуре лексикографический порядок строк полностью совпадает с реальной позицией документа на временной оси 1С. Строковый минимум MIN(DATE_TIME_IDDOC) всегда точно указывает на самый первый документ за выбранный период.
Если перед нами стоит задача получить ровно один первый документ за конкретную известную дату, достаточно использовать ограничение количества строк и правильную сортировку по возрастанию позиции:
SELECT TOP 1
iddoc AS [Док $Документ.СменныйОтчет]
FROM
_1sjourn (NOLOCK)
WHERE
DATE_TIME_IDDOC >= :НачДата AND DATE_TIME_IDDOC <= :КонДата
AND IDDocDef = $ВидДокумента.СменныйОтчет
ORDER BY
DATE_TIME_IDDOC ASC
Обратим внимание на условие по дате: прямое сравнение DATE_TIME_IDDOC = :НачДата приведет к ошибке, так как параметр даты содержит только 8 символов, а поле в базе включает еще время и идентификатор. Поэтому всегда следует использовать строковый срез LEFT(DATE_TIME_IDDOC, 8) = :НачДата либо задавать условие диапазона времени с начала по конец суток.
Рассмотрим более сложную ситуацию: когда строится отчет за интервал в месяц или год, и в выборке нужно вывести все документы, но для первых документов каждого дня установить специальный признак.
Для этого сгруппируем данные по дням, найдем минимальную позицию и свяжем результат с основной таблицей журналов:
SELECT
j.iddoc AS [Док $Документ.СменныйОтчет],
LEFT(j.DATE_TIME_IDDOC, 8) AS [ДатаДок $Дата],
CASE
WHEN min_docs.DATE_TIME_IDDOC IS NOT NULL THEN 1
ELSE 0
END AS ФлагПервыйЗаДень
FROM
_1sjourn j (NOLOCK)
LEFT JOIN
(
SELECT
LEFT(DATE_TIME_IDDOC, 8) AS День,
MIN(DATE_TIME_IDDOC) AS DATE_TIME_IDDOC
FROM
_1sjourn (NOLOCK)
WHERE
IDDocDef = $ВидДокумента.СменныйОтчет
AND DATE_TIME_IDDOC BETWEEN :НачДата AND :КонДата
GROUP BY
LEFT(DATE_TIME_IDDOC, 8)
) min_docs ON j.DATE_TIME_IDDOC = min_docs.DATE_TIME_IDDOC
WHERE
j.IDDocDef = $ВидДокумента.СменныйОтчет
AND j.DATE_TIME_IDDOC BETWEEN :НачДата AND :КонДата
ORDER BY
j.DATE_TIME_IDDOC ASC
Если используется современная версия Microsoft SQL Server, ту же самую задачу можно решить еще компактнее с помощью оконной функции ROW_NUMBER():
SELECT
j.iddoc AS [Док $Документ.СменныйОтчет],
CASE
WHEN ROW_NUMBER() OVER (
PARTITION BY LEFT(j.DATE_TIME_IDDOC, 8)
ORDER BY j.DATE_TIME_IDDOC ASC
) = 1 THEN 1
ELSE 0
END AS ФлагПервыйЗаДень
FROM
_1sjourn j (NOLOCK)
WHERE
j.IDDocDef = $ВидДокумента.СменныйОтчет
AND j.DATE_TIME_IDDOC BETWEEN :НачДата AND :КонДата
ORDER BY
j.DATE_TIME_IDDOC ASC
Выясним, как решается подобная задача на платформе 1С 8. В стандартном языке запросов 1С агрегатные функции МИНИМУМ() и МАКСИМУМ() нельзя применять к типу МоментВремени или к ссылкам на документы. Кроме того, несколько документов могут быть записаны на одну и ту же секунду.
Посмотрим на классическое двухэтапное решение через временные таблицы с группировкой по дню:
ВЫБРАТЬ
СменныйОтчет.Ссылка КАК Ссылка,
НАЧАЛОПЕРИОДА(СменныйОтчет.Дата, ДЕНЬ) КАК День,
СменныйОтчет.Дата КАК Дата
ПОМЕСТИТЬ ВТ_Документы
ИЗ
Документ.СменныйОтчет КАК СменныйОтчет
ГДЕ
СменныйОтчет.Дата МЕЖДУ &НачДата И &КонДата
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Документы.День КАК День,
МИНИМУМ(ВТ_Документы.Дата) КАК МинДата
ПОМЕСТИТЬ ВТ_Минимумы
ИЗ
ВТ_Документы КАК ВТ_Документы
СГРУППИРОВАТЬ ПО
ВТ_Документы.День
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Документы.Ссылка КАК Документ,
ВЫБОР
КОГДА ВТ_Минимумы.МинДата ЕСТЬ НЕ NULL
ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ КАК ЭтоПервыйДокумент
ИЗ
ВТ_Документы КАК ВТ_Документы
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Минимумы КАК ВТ_Минимумы
ПО ВТ_Документы.День = ВТ_Минимумы.День
И ВТ_Документы.Дата = ВТ_Минимумы.МинДата
УПОРЯДОЧИТЬ ПО
ВТ_Документы.Дата ВОЗР
Если существует вероятность записи документов в одну секунду, для исключения дублирования признака первого документа во временной таблице минимумов берут дополнительно МИНИМУМ(Ссылка) с отбором по минимальной дате.
Если результат запроса все равно обходится циклом в коде модуля, вычисление первого документа за сутки часто проще и быстрее выполнить программно, без усложнения структуры SQL-запроса:
Выборка = Запрос.Выполнить().Выбрать();
ТекущийДень = ДАТА(1, 1, 1);
Пока Выборка.Следующий() Цикл
ДеньДокумента = НачалоПериода(Выборка.Дата, "День");
Если ДеньДокумента <> ТекущийДень Тогда
// Это гарантированно первый документ за текущие сутки
ТекущийДень = ДеньДокумента;
ЭтоПервый = Истина;
Иначе
ЭтоПервый = Ложь;
КонецЕсли;
// Дальнейшая обработка строки
КонецЦикла;
Главным условием для такого алгоритма является обязательное упорядочивание выборки запроса по возрастанию даты и момента времени.