В процессе разработки на платформе 1С:Предприятие 7.7 с использованием SQL-версии базы данных часто возникает задача получения последних событий для каждого объекта. Типичный пример — определение даты и времени самой последней закупки для каждого клиента. Несмотря на почтенный возраст платформы, оптимизация таких запросов остается актуальной для обеспечения быстродействия крупных баз данных. В этой статье мы разберем несколько подходов к решению данной задачи: от классических агрегатных функций до современных оконных функций и методов прямого обращения к индексам SQL Server.
Рассмотрим наиболее распространенный и методически верный подход с использованием агрегатной функции MAX(). Многие разработчики опасаются, что поиск максимального значения заставляет SQL Server перебирать все записи, однако при наличии правильного индекса это не так. Проанализируем стандартный запрос:
SELECT
Клиенты.CODE AS Код,
MAX(Дисконт.DATE_TIME_IDDOC) AS ДатаДокумента
FROM
$Регистр.Дисконт AS Дисконт WITH (NOLOCK)
INNER JOIN
$Справочник.Клиенты AS Клиенты WITH (NOLOCK) ON $Дисконт.Клиент = Клиенты.ID
GROUP BY
Клиенты.CODE
ORDER BY
Клиенты.CODE
Почему это работает быстро: В 1С 7.7 поле DATE_TIME_IDDOC является составной строкой, включающей дату, время и уникальный идентификатор документа. Поскольку это строка, SQL Server выполняет лексикографическое сравнение. При наличии индекса по полю $Дисконт.Клиент и включенном поле позиции документа, серверу не нужно «сравнивать» все даты — он просто переходит к последнему значению в соответствующей ветке индексного дерева. Это называется операцией Index Group Aggregate.
Если нам необходимо исключить глобальную группировку по всей таблице (например, если справочник клиентов огромен, а нам нужны данные только по определенной выборке), можно воспользоваться подзапросом в блоке SELECT. Разберем этот пример:
SELECT
Клиенты.CODE AS Код,
(SELECT TOP 1
ВнутрДисконт.DATE_TIME_IDDOC
FROM
$Регистр.Дисконт AS ВнутрДисконт WITH (NOLOCK)
WHERE
$ВнутрДисконт.Клиент = Клиенты.ID
ORDER BY
ВнутрДисконт.DATE_TIME_IDDOC DESC) AS DateLast
FROM
$Справочник.Клиенты AS Клиенты WITH (NOLOCK)
ORDER BY
Клиенты.CODE
Проанализируем ситуацию: этот метод заставляет SQL Server выполнять поиск для каждой строки таблицы клиентов. Если у вас есть составной индекс (Клиент, DATE_TIME_IDDOC), этот запрос будет работать мгновенно, так как операция TOP 1 в сочетании с ORDER BY DESC просто берет первую запись из конца индекса для конкретного клиента.
Если ваша база данных 1С 7.7 работает на SQL Server 2005, 2008 или более новых версиях, мы можем применить оконную функцию ROW_NUMBER(). Это позволяет пронумеровать закупки каждого клиента в обратном хронологическом порядке и выбрать только первую запись. Посмотрим на пример кода:
SELECT
Продажи.КодКлиента,
Продажи.ДатаДок
FROM
(SELECT
$Клиенты.CODE AS КодКлиента,
Дисконт.DATE_TIME_IDDOC AS ДатаДок,
ROW_NUMBER() OVER (PARTITION BY $Дисконт.Клиент ORDER BY Дисконт.DATE_TIME_IDDOC DESC) AS НомерПродажи
FROM
$Регистр.Дисконт AS Дисконт WITH (NOLOCK)
INNER JOIN
$Справочник.Клиенты AS Клиенты WITH (NOLOCK) ON $Дисконт.Клиент = Клиенты.ID
) AS Продажи
WHERE
Продажи.НомерПродажи = 1
Основное преимущество этого метода заключается в том, что оконные функции отлично оптимизированы для многопоточного выполнения (Parallelism). Однако стоит помнить, что для простых задач агрегации MAX() часто оказывается менее ресурсоемким, так как требует меньше оперативной памяти для построения сегментированных окон данных.
Рассмотрим современную альтернативу подзапросам — оператор CROSS APPLY. Он позволяет «применить» логику выбора одной (последней) строки к каждой строке основной таблицы. Это часто эффективнее обычных соединений LEFT JOIN с подзапросами.
SELECT
Клиенты.CODE,
ПоследняяЗакупка.DATE_TIME_IDDOC
FROM
$Справочник.Клиенты AS Клиенты WITH (NOLOCK)
CROSS APPLY (
SELECT TOP 1
D.DATE_TIME_IDDOC
FROM
$Регистр.Дисконт AS D WITH (NOLOCK)
WHERE
$D.Клиент = Клиенты.ID
ORDER BY
D.DATE_TIME_IDDOC DESC
) AS ПоследняяЗакупка
Этот подход особенно хорош тем, что SQL Server может построить очень эффективный план выполнения, используя Nested Loops с поиском по индексу (Index Seek).
Выясним причину, почему одни запросы «летают», а другие «вешают» базу. В 1С 7.7 структура индексов задается через конфигуратор. Чтобы наши запросы по датам закупок работали быстро, необходимо:
Клиент в регистре Дисконт.Это действие создаст в SQL-таблице регистра составной индекс, где первым полем будет ID клиента, а вторым — позиция документа (то самое поле DATE_TIME_IDDOC). Без такого индекса SQL Server будет вынужден выполнять полное сканирование таблицы (Table Scan), что на миллионах записей приведет к катастрофическому падению производительности.
Разберем подробнее структуру этого поля. В 1С 7.7 оно имеет строковый тип данных. Это критически важно, так как:
YYYYMMDD....MAX() работает очень быстро, так как это простая лексикографическая операция.WHERE DATE_TIME_IDDOC BETWEEN '20200101' AND '20241231', чтобы отсечь заведомо некорректные данные.Если ваша таблица регистра содержит данные за 10-15 лет, поиск последней закупки по всей истории может быть избыточным. Проанализируем стратегию ограничения периода:
Добавление условия на ограничение поиска последним годом или полугодием значительно ускоряет работу даже при наличии индексов. SQL-серверу придется просматривать гораздо меньшую часть индексного дерева. Если по конкретному клиенту закупка в свежем периоде не найдена, можно выполнить второй, «тяжелый» запрос уже по всей истории, но только для оставшихся клиентов.
Подведем итог: использование MAX() совместно с GROUP BY является наиболее стабильным и проверенным решением для 1С 7.7. Однако для достижения максимальной производительности всегда проверяйте наличие индекса «Отбор движений» по аналитическому разрезу (клиенту) и рассматривайте возможность использования оконных функций на современных версиях SQL Server.