При разработке сложных алгоритмов обработки данных или длительных отчетов мы часто используем пакетные запросы. Это удобно: можно создать временные таблицы, обработать данные поэтапно и получить итоговый результат в одном вызове. Однако иногда возникает ситуация, которая ставит в тупик даже опытных разработчиков: во время выполнения "длинного" запроса в базе появляются новые данные (например, создается документ), но запрос их упорно не видит, даже если обращение к этим данным происходит в самом конце пакета. Разберем эту ситуацию подробно, выясним технические причины такого поведения платформы и СУБД, а также научимся обходить это ограничение.
Представим сценарий:
1. Запускается выполнение пакетного запроса, который состоит из создания нескольких временных таблиц и итоговой выборки.
2. Выполнение занимает определенное время (например, 10–20 секунд).
3. Параллельно с этим (в другом сеансе или фоновом задании) в базу записывается новый документ.
4. В середине или конце нашего пакетного запроса есть инструкция, которая должна выбрать этот новый документ.
Результат: Запрос возвращает Null или пустую выборку, как будто документа не существует. При этом, если проверить базу сразу после выполнения запроса, документ на месте.
Многие ошибочно полагают, что записи блокируются для чтения, или что 1С как-то кэширует данные. На самом деле причина кроется глубже — в архитектуре работы современных СУБД.
Чтобы понять, почему так происходит, нам нужно опуститься на уровень взаимодействия платформы 1С и СУБД (чаще всего это MS SQL Server или PostgreSQL).
В большинстве современных инсталляций 1С используется уровень изоляции транзакций Read Committed (Чтение подтвержденных данных).
* В PostgreSQL это поведение по умолчанию.
* В MS SQL Server 1С включает режим READ_COMMITTED_SNAPSHOT.
Ключевое понятие здесь — Снимок (Snapshot).
Когда СУБД получает команду на выполнение запроса SELECT, она делает "мгновенный снимок" состояния данных на момент начала выполнения команды. Запрос видит только те данные, которые были зафиксированы (committed) до момента начала этого снимка.
Платформа 1С при выполнении метода Запрос.Выполнить() или Запрос.ВыполнитьПакет() транслирует весь текст запроса (все временные таблицы и выборки) в один большой SQL-оператор (или блок инструкций), который отправляется на сервер баз данных одной посылкой.
Для СУБД это выглядит как единый оператор (Single Statement).
Рассмотрим хронологию событий:
1. Время 00:00:01 — Платформа 1С отправляет пакетный запрос в СУБД. СУБД фиксирует Снимок №1. Все данные, которые будут прочитаны в рамках этого пакета (будь то первая временная таблица или десятая), берутся из этого снимка.
2. Время 00:00:05 — Параллельный процесс создает и записывает новый документ. Эти изменения попадают в базу данных.
3. Время 00:00:10 — Наш пакетный запрос доходит до инструкции выбора этого документа. Но он работает со Снимком №1, сделанным в 00:00:01. В этом снимке документа еще нет.
Именно поэтому запрос "слеп" к изменениям, произошедшим после старта его выполнения. Это не ошибка, а механизм обеспечения целостности данных (Statement-level Read Consistency). Если бы запрос видел меняющиеся данные в процессе работы, это могло бы привести к "фантомным" результатам и логическим ошибкам в расчетах внутри одного алгоритма.
Если наша логика требует, чтобы мы видели самые актуальные данные, появившиеся уже после старта обработки, нам необходимо заставить СУБД обновить снимок данных.
Так как снимок создается в момент начала выполнения команды SQL, нам нужно разбить один большой пакетный запрос на несколько отдельных команд. В терминах 1С это означает разбиение текста запроса на несколько вызовов метода Выполнить().
Однако, если мы просто выполним два разных запроса, мы потеряем контекст — временные таблицы, созданные в первом запросе, будут уничтожены сразу после его завершения и будут недоступны во втором.
Для решения этой задачи в платформе 1С предусмотрен объект МенеджерВременныхТаблиц.
1. Создаем экземпляр объекта МенеджерВременныхТаблиц.
2. Формируем первый запрос (или первую часть пакета). Подключаем к нему менеджер.
3. Выполняем первый запрос. Результат (временные таблицы) сохраняется в менеджере. В этот момент СУБД делает первый снимок данных.
4. Выполняем какие-либо промежуточные действия (если нужно). В это время параллельные процессы могут создать новые данные.
5. Формируем второй запрос (вторую часть пакета). Подключаем к нему тот же самый менеджер временных таблиц.
6. Выполняем второй запрос.
В момент запуска второго запроса СУБД получит новую команду и сделает новый снимок данных, который уже будет включать изменения, произошедшие после завершения первого запроса. При этом временные таблицы из первого шага останутся доступными благодаря менеджеру.
Рассмотрим, как это выглядит на практике.
В этом случае, если документ будет создан пока выполняется расчет ВТ_Данные, итоговая выборка его не увидит.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ...
|ПОМЕСТИТЬ ВТ_Данные
|ИЗ
| ...; // Длительная операция
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Док.Ссылка
|ИЗ
| Документ.НужныйДокумент КАК Док
|ГДЕ
| Док.Проведен"; // Этот запрос не увидит новые документы
Результат = Запрос.Выполнить();
Разбиваем процесс на этапы, чтобы обновить "видимость" данных.
// 1. Создаем менеджер временных таблиц
МенеджерВТ = Новый МенеджерВременныхТаблиц;
// 2. Формируем первую часть запроса (подготовка данных)
Запрос1 = Новый Запрос;
Запрос1.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос1.Текст =
"ВЫБРАТЬ
| Товары.Ссылка,
| Товары.Цена
|ПОМЕСТИТЬ ВТ_ПредварительныеДанные
|ИЗ
| Справочник.Товары КАК Товары";
// Выполняем первую часть. СУБД делает Снимок №1.
// Таблица ВТ_ПредварительныеДанные сохраняется в МенеджереВТ.
Запрос1.Выполнить();
// Здесь может пройти время, или мы специально ждем появления данных.
// Предположим, в этот момент параллельно создается документ "НужныйДокумент".
// 3. Формируем вторую часть запроса
Запрос2 = Новый Запрос;
// ВАЖНО: Подключаем тот же менеджер, чтобы видеть ВТ_ПредварительныеДанные
Запрос2.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос2.Текст =
"ВЫБРАТЬ
| Док.Ссылка,
| ВТ.Цена
|ИЗ
| Документ.НужныйДокумент КАК Док
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПредварительныеДанные КАК ВТ
| ПО Док.Товар = ВТ.Ссылка
|ГДЕ
| Док.Проведен"; // Здесь мы увидим даже те документы, что появились секунду назад
// Выполняем вторую часть. СУБД получает новую команду и делает Снимок №2.
Результат = Запрос2.Выполнить();
При использовании данного метода необходимо учитывать несколько моментов:
* Транзакции 1С: Если весь ваш код обернут в явную транзакцию 1С (НачатьТранзакцию() ... ЗафиксироватьТранзакцию()), поведение может зависеть от используемой СУБД. В PostgreSQL при уровне изоляции REPEATABLE READ (используется в некоторых конфигурациях для строгой отчетности) снимок фиксируется на старте транзакции, а не запроса. В таком случае разбиение запроса не поможет — вы все равно будете видеть данные на момент НачатьТранзакцию(). Однако в типовом режиме (Read Committed) метод с разбивкой запросов работает корректно.
* Производительность: Разбиение запроса на части увеличивает накладные расходы на взаимодействие между сервером 1С и СУБД (сетевые задержки, парсинг запроса). Не стоит дробить запросы слишком мелко без необходимости. Используйте этот метод только там, где критически важно видеть "свежие" данные в середине алгоритма.
* Очистка ресурсов: Временные таблицы, хранящиеся в МенеджерВременныхТаблиц, живут до тех пор, пока жив сам объект менеджера или пока вы их явно не удалите (командой УНИЧТОЖИТЬ в запросе). Если менеджер объявлен в глобальной переменной или долгоживущей процедуре, не забывайте закрывать его (МенеджерВТ.Закрыть() в 8.3.13+ или просто обнулять переменную), чтобы не "раздувать" tempdb.
Ситуация, когда запрос не видит только что созданный документ, является нормальным поведением базы данных, работающей в режиме Read Committed Snapshot. СУБД гарантирует целостность данных на момент старта команды. Чтобы работать с актуальными данными, появившимися в процессе выполнения алгоритма, используйте разделение логики на несколько запросов, связанных через МенеджерВременныхТаблиц. Это заставит СУБД обновлять снимок данных при каждом новом вызове.