В практике разработки и администрирования 1С часто встречаются ситуации, когда регламентное задание или дополнительная обработка, запускаемая по расписанию, работает некорректно. Особенно запутанным случай становится, когда под полными правами (Администратор) все кажется рабочим, а под пользователем (например, Кассир) задание "буксует".
Давайте подробно разберем реальный кейс: обработка должна выгружать чеки каждые несколько минут, но фактически ничего не происходит. Мы пройдем путь от диагностики настроек расписания до глубокой оптимизации запроса на языке 1С.
В рассматриваемой ситуации мы имеем следующую вводную: есть дополнительная обработка в конфигурации "1С:Розница 2.3". Настроено расписание (каждые 300 секунд) и назначен ответственный пользователь. Однако результат (файл выгрузки) не появляется.
Первое, что мы обычно делаем — открываем Журнал регистрации. Но здесь нас может ждать сюрприз: журнал пуст или не содержит явных ошибок выполнения.
Почему так происходит? Давайте проанализируем настройки регламентного задания:
Именно второй пункт является критическим. Если код обработки выполняется дольше 30 секунд, платформа принудительно прерывает фоновое задание. В зависимости от настроек технологического журнала и версии платформы, такое прерывание ("убийство" процесса) может не успеть записать красивый стек ошибки в стандартный журнал регистрации, либо транзакция откатывается полностью, и мы не видим следов деятельности.
В нашем случае выяснилось, что простейшая выгрузка чеков за текущий день выполнялась около 3 минут. Естественно, с настройкой "завершать через 30 секунд" задание никогда не доходило до конца.
Давайте посмотрим на код запроса, который вызывал такую задержку. На первый взгляд он кажется логичным: нужно выбрать товары из чеков за сегодня.
Исходный текст запроса:
Запрос = новый Запрос("ВЫБРАТЬ
""1"" КАК Азс,
ЧекККМТовары.Номенклатура.Наименование КАК Номенклатура,
ЧекККМТовары.Количество КАК Количество,
ЧекККМТовары.Сумма КАК Сумма,
ЧекККМТовары.Ссылка.Дата КАК Дата
ИЗ
Документ.ЧекККМ.Товары КАК ЧекККМТовары
ГДЕ
ЧекККМТовары.Ссылка.Проведен = Истина
И НАЧАЛОПЕРИОДА(ЧекККМТовары.Ссылка.Дата, ДЕНЬ) = &Дата
УПОРЯДОЧИТЬ ПО
Дата");
Запрос.УстановитьПараметр("Дата", НачалоДня(ТекущаяДата()));
Давайте разберем, почему этот запрос, возвращающий всего пару строк результата, работает минуты. Здесь допущены две грубые ошибки, критично влияющие на производительность SQL-сервера.
Обратите внимание на конструкцию:
НАЧАЛОПЕРИОДА(ЧекККМТовары.Ссылка.Дата, ДЕНЬ) = &Дата
Когда мы применяем функцию (в данном случае НАЧАЛОПЕРИОДА) к полю таблицы базы данных (Дата), SQL-оптимизатор не может использовать индекс по этому полю. Ему приходится выполнять сканирование всей таблицы (Table Scan или Index Scan), вычислять значение функции для каждой записи и только потом сравнивать с параметром. Если в базе миллионы чеков, это занимает огромное время.
Запрос строится к табличной части Документ.ЧекККМ.Товары. Однако в условии отбора мы обращаемся к реквизитам шапки документа:
ЧекККМТовары.Ссылка.ПроведенЧекККМТовары.Ссылка.ДатаКаждое обращение через Ссылка.Поле при выборе из табличной части создает неявное левое соединение (Left Join) с таблицей шапки документа. В сочетании с первой ошибкой (невозможность использовать индекс по дате), СУБД вынуждена соединять огромные массивы данных, а затем фильтровать их неоптимальным способом.
Чтобы задание выполнялось мгновенно и укладывалось в отведенные 30 секунд, нам нужно переписать запрос так, чтобы он использовал индексы. Для этого мы должны избавиться от функций в условиях и оптимизировать соединения.
Рассмотрим правильный подход:
Вот как должен выглядеть оптимизированный код:
// Определяем границы периода заранее
НачПериода = НачалоДня(ТекущаяДата());
КонПериода = КонецДня(ТекущаяДата());
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ""1"" КАК Азс,
| ЧекККМТовары.Номенклатура.Наименование КАК Номенклатура,
| ЧекККМТовары.Количество КАК Количество,
| ЧекККМТовары.Сумма КАК Сумма,
| ЧекККМ.Дата КАК Дата
|ИЗ
| Документ.ЧекККМ.Товары КАК ЧекККМТовары
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЧекККМ КАК ЧекККМ
| ПО ЧекККМТовары.Ссылка = ЧекККМ.Ссылка
|ГДЕ
| ЧекККМ.Дата МЕЖДУ &НачДата И &КонДата
| И ЧекККМ.Проведен = ИСТИНА
|
|УПОРЯДОЧИТЬ ПО
| Дата";
Запрос.УстановитьПараметр("НачДата", НачПериода);
Запрос.УстановитьПараметр("КонДата", КонПериода);
Почему этот вариант работает быстрее?
МЕЖДУ (или >= и <=) позволяет SQL-серверу использовать индекс по полю Дата. Он сразу находит нужный диапазон записей, не перебирая всю таблицу.Примечание: Еще более производительный вариант (особенно на очень больших базах) — сначала выбрать ссылки на документы во временную таблицу по отбору дат, а затем соединить эту временную таблицу с табличной частью товаров. Но в рамках задачи "выгрузка за один день" предложенного выше варианта с МЕЖДУ будет более чем достаточно.
После оптимизации кода не забудьте проверить окружение, так как мы говорим о запуске "на сервере" под определенным пользователем.
\\Server\Share\File.txt предпочтительнее локальных дисков C:\....Для корректной работы вам может потребоваться зайти в справочник "Дополнительные отчеты и обработки", найти вашу обработку и в ее настройках разрешить выполнение небезопасных действий или отключить безопасный режим (если политика безопасности компании это позволяет).
Если ваша обработка по расписанию "молчит" и не оставляет следов в журнале:
НАЧАЛОПЕРИОДА, ГОД, МЕСЯЦ) в условиях запроса — главная причина замедления.МЕЖДУ).