Приветствуем вас, уважаемые коллеги! Работа с запросами — это один из важнейших навыков в арсенале разработчика 1С. Однако на этапе освоения платформы легко запутаться в том, как именно 1С взаимодействует с базой данных и оперативной памятью. В этой статье мы подробно разберем природу популярной ошибки «Содержимое объекта данных может быть выбрано только во временную таблицу», выясним причины ее возникновения и рассмотрим правильные архитектурные подходы для ее устранения.
Для начала проанализируем ситуацию, в которой оказывается разработчик. Ошибка возникает тогда, когда в секции ИЗ текста запроса указывается параметр-источник (например, &Таблица), содержащий объект, который находится в оперативной памяти сервера приложений 1С (это может быть таблица значений, табличная часть документа или результат другого запроса), но при этом платформа не получает команду записать эти данные в СУБД.
Давайте разберем внутреннюю кухню этого процесса. Система управления базами данных (будь то MS SQL Server, PostgreSQL или любая другая СУБД) ничего не знает об объектах, существующих исключительно в оперативной памяти вашего сервера приложений 1С. Чтобы СУБД могла соединить вашу таблицу значений с физическими таблицами базы данных или просто прочитать из нее строки, платформа 1С должна выполнить промежуточное действие — физически создать реальную таблицу в СУБД (в специальной системной базе данных tempdb) и записать туда строки из оперативной памяти.
Сигналом для платформы о необходимости выполнить это действие служит ключевое слово ПОМЕСТИТЬ. Если вы передаете таблицу значений в запрос как параметр, но не используете конструкцию ПОМЕСТИТЬ ВТ_ИмяВременнойТаблицы, платформа выдает ошибку, так как СУБД не умеет напрямую работать с «воздушными» данными из оперативной памяти.
Посмотрим на классический ошибочный код новичка:
// ОШИБОЧНЫЙ КОД
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТаблицаДанных", МояТаблицаЗначений);
Запрос.Текст =
"ВЫБРАТЬ
| Табл.Номер КАК Номер,
| Табл.Дата КАК Дата
|ИЗ
| &ТаблицаДанных КАК Табл"; // Здесь СУБД выдаст ошибку!
Результат = Запрос.Выполнить();
Очень часто начинающие разработчики совершают логическую ошибку: они пытаются программно получить данные из базы (например, выгрузить документы в таблицу значений) только ради того, чтобы потом передать эту таблицу обратно в запрос. Это в корне неверно, так как приводит к лишней нагрузке на сервер приложений, перерасходу оперативной памяти и замедлению работы системы.
Если данные уже физически записаны в базе данных (например, это документы ЗаказКлиента), то нам вообще не нужны никакие таблицы значений в оперативной памяти. Мы можем и должны обратиться к ним напрямую. Разберем правильный пример получения реквизитов документов прямо из базы данных:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказКлиента.Номер КАК Номер,
| ЗаказКлиента.Дата КАК Дата,
| ЗаказКлиента.Партнер КАК Партнер,
| ЗаказКлиента.СуммаДокумента КАК СуммаДокумента,
| ЗаказКлиента.Менеджер КАК Менеджер
|ИЗ
| Документ.ЗаказКлиента КАК ЗаказКлиента
|ГДЕ
| ЗаказКлиента.Проведен = ИСТИНА";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Обрабатываем полученные данные
Сообщить("Документ № " + Выборка.Номер + " от " + Выборка.Дата);
КонецЦикла;
Этот способ работает максимально быстро и эффективно, так как выборка данных происходит целиком на стороне СУБД, а в оперативную память сервера приложений возвращается только готовый результат.
Бывают ситуации, когда передача таблицы значений в запрос действительно необходима. Например, если данные были загружены из файла Excel загрузчиком отчётов о продажах маркетплейсов из Excel, получены через сторонний Web-сервис или рассчитаны сложным алгоритмом в коде, который невозможно повторить на языке запросов.
В этом случае мы обязаны соблюдать строгие правила работы с временными таблицами. Разберем по шагам, как правильно подготовить и передать таблицу значений в запрос.
Рассмотрим подробнее практический пример реализации этого подхода:
// Шаг 1. Создаем и жестко типизируем таблицу значений в памяти
МояТаблица = Новый ТаблицаЗначений;
// Важно явно указывать ОписаниеТипов для каждой колонки!
МояТаблица.Колонки.Добавить("Номер", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(20)));
МояТаблица.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата"));
МояТаблица.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
// Наполняем таблицу какими-то внешними данными
НоваяСтрока = МояТаблица.Добавить();
НоваяСтрока.Номер = "ВН-0001";
НоваяСтрока.Дата = ТекущаяДата();
НоваяСтрока.Сумма = 150000;
// Шаг 2. Создаем запрос и передаем таблицу в качестве параметра
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ВнешнийИсточник", МояТаблица);
// Используем пакетный запрос. Пакеты разделяются символом ";"
Запрос.Text =
"ВЫБРАТЬ
| ВнИсточник.Номер КАК Номер,
| ВнИсточник.Дата КАК Дата,
| ВнИсточник.Сумма КАК Сумма
|ПОМЕСТИТЬ ВТ_ВнешниеДанные
|ИЗ
| &ВнешнийИсточник КАК ВнИсточник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Данные.Номер КАК Номер,
| Данные.Дата КАК Дата,
| Данные.Сумма КАК Сумма
|ИЗ
| ВТ_ВнешниеДанные КАК Данные";
// Выполняем пакетный запрос
РезультатыПакета = Запрос.ВыполнитьПакет();
// Последний результат в пакете содержит нашу итоговую выборку
ИтоговыйРезультат = РезультатыПакета[РезультатыПакета.Количество() - 1];
Выборка = ИтоговыйРезультат.Выбрать();
Если вы получаете таблицу значений из сторонней функции или внешней обработки и не можете гарантировать, что ее колонки типизированы, на помощь придет функция ВЫРАЗИТЬ непосредственно в тексте запроса. Это позволит СУБД принудительно привести типы полей к нужным стандартам при создании временной таблицы:
Запрос.Текст =
"ВЫБРАТЬ
| ВЫРАЗИТЬ(ВнИсточник.Номер КАК СТРОКА(20)) КАК Номер,
| ВЫРАЗИТЬ(ВнИсточник.Дата КАК ДАТА) КАК Дата,
| ВЫРАЗИТЬ(ВнИсточник.Сумма КАК ЧИСЛО(15, 2)) КАК Сумма
|ПОМЕСТИТЬ ВТ_ВнешниеДанные
|ИЗ
| &ВнешнийИсточник КАК ВнИсточник";
Проанализируем ситуацию, когда передаваемая вами таблица значений содержит тысячи строк, и в последующих пакетах запроса вы планируете соединять ее с физическими таблицами базы данных (например, с табличной частью документа или регистром накопления).
Если оставить временную таблицу без индексов, СУБД для каждого сопоставления строк будет вынуждена полностью сканировать всю временную таблицу (операция Table Scan). Это приведет к колоссальному падению производительности и зависанию системы. Для оптимизации всегда используйте индексирование по полям связи:
Запрос.Текст =
"ВЫБРАТЬ
| ВнИсточник.СсылкаНаДокумент КАК СсылкаНаДокумент,
| ВнИсточник.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_ДанныеДляАнализа
|ИЗ
| &ВнешнийИсточник КАК ВнИсточник
|
|ИНДЕКСИРОВАТЬ ПО
| СсылкаНаДокумент"; // Строим индекс по ключевому полю связи
В коде автора темы фигурирует объект МенеджерВременныхТаблиц. Давайте разберем, когда он действительно необходим, а когда его использование избыточно.
Когда МенеджерВременныхТаблиц НЕ нужен:
Если вы создаете временную таблицу и используете ее в рамках одного и того же объекта Запрос (даже если у вас сложный пакетный запрос из 10-15 пакетов), создавать менеджер в коде вручную не требуется. Платформа 1С автоматически создаст внутренний менеджер, выполнит все пакеты, а по окончании выполнения запроса автоматически очистит оперативную память СУБД и удалит временные таблицы.
Когда МенеджерВременныхТаблиц необходим:
Менеджер временных таблиц нужен только тогда, когда вам необходимо передать результаты выполнения одного запроса в совершенно другой запрос (например, в другой функции, процедуре или при последовательной работе разных обработчиков). Менеджер выступает «контейнером», который сохраняет таблицы между вызовами методов Выполнить() разных объектов запросов.
Посмотрим на пример правильного использования менеджера временных таблиц:
// Создаем общий менеджер
МВТ = Новый МенеджерВременныхТаблиц;
// Первый запрос: создаем временную таблицу
Запрос1 = Новый Запрос;
Запрос1.МенеджерВременныхТаблиц = МВТ;
Запрос1.УстановитьПараметр("ТаблЗначений", МояТЗ);
Запрос1.Текст =
"ВЫБРАТЬ
| ТЗ.Товар КАК Товар
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &ТаблЗначений КАК ТЗ";
Запрос1.Выполнить(); // Временная таблица ВТ_Товары теперь живет внутри МВТ
// ... какой-то промежуточный код ...
// Второй запрос: обращаемся к временной таблице из первого запроса
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МВТ; // Подключаем тот же менеджер
Запрос2.Текст =
"ВЫБРАТЬ
| Товары.Товар КАК Товар,
| Остатки.ВНаличии КАК Остаток
|ИЗ
| ВТ_Товары КАК Товары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
| ПО Товары.Товар = Остатки.Номенклатура";
Результат = Запрос2.Выполнить();
Помните, что после завершения работы с менеджером временных таблиц хорошим тоном является вызов метода МВТ.Закрыть() для немедленного освобождения ресурсов СУБД. Для своевременного освобождения ресурсов и автоматической расстановки инструкций удаления в пакетах можно использовать обработку для оптимизации временных таблиц в запросах.
Надеемся, что этот подробный разбор помог вам разложить по полочкам механизмы работы платформы с СУБД и оперативной памятью. Применяйте полученные знания на практике, пишите оптимальный код и легкой вам разработки!