Как исправить ошибку «Содержимое объекта данных может быть выбрано только во временную таблицу» в запросах 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Приветствуем вас, уважаемые коллеги! Работа с запросами — это один из важнейших навыков в арсенале разработчика 1С. Однако на этапе освоения платформы легко запутаться в том, как именно 1С взаимодействует с базой данных и оперативной памятью. В этой статье мы подробно разберем природу популярной ошибки «Содержимое объекта данных может быть выбрано только во временную таблицу», выясним причины ее возникновения и рассмотрим правильные архитектурные подходы для ее устранения.

Выясняем причину ошибки

Для начала проанализируем ситуацию, в которой оказывается разработчик. Ошибка возникает тогда, когда в секции ИЗ текста запроса указывается параметр-источник (например, &Таблица), содержащий объект, который находится в оперативной памяти сервера приложений 1С (это может быть таблица значений, табличная часть документа или результат другого запроса), но при этом платформа не получает команду записать эти данные в СУБД.

Давайте разберем внутреннюю кухню этого процесса. Система управления базами данных (будь то MS SQL Server, PostgreSQL или любая другая СУБД) ничего не знает об объектах, существующих исключительно в оперативной памяти вашего сервера приложений 1С. Чтобы СУБД могла соединить вашу таблицу значений с физическими таблицами базы данных или просто прочитать из нее строки, платформа 1С должна выполнить промежуточное действие — физически создать реальную таблицу в СУБД (в специальной системной базе данных tempdb) и записать туда строки из оперативной памяти.

Сигналом для платформы о необходимости выполнить это действие служит ключевое слово ПОМЕСТИТЬ. Если вы передаете таблицу значений в запрос как параметр, но не используете конструкцию ПОМЕСТИТЬ ВТ_ИмяВременнойТаблицы, платформа выдает ошибку, так как СУБД не умеет напрямую работать с «воздушными» данными из оперативной памяти.

Посмотрим на классический ошибочный код новичка:


// ОШИБОЧНЫЙ КОД
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТаблицаДанных", МояТаблицаЗначений);
Запрос.Текст = 
    "ВЫБРАТЬ
    |    Табл.Номер КАК Номер,
    |    Табл.Дата КАК Дата
    |ИЗ
    |    &ТаблицаДанных КАК Табл"; // Здесь СУБД выдаст ошибку!
Результат = Запрос.Выполнить();

Решение №1: Прямой запрос к базе данных (самый правильный подход)

Очень часто начинающие разработчики совершают логическую ошибку: они пытаются программно получить данные из базы (например, выгрузить документы в таблицу значений) только ради того, чтобы потом передать эту таблицу обратно в запрос. Это в корне неверно, так как приводит к лишней нагрузке на сервер приложений, перерасходу оперативной памяти и замедлению работы системы.

Если данные уже физически записаны в базе данных (например, это документы ЗаказКлиента), то нам вообще не нужны никакие таблицы значений в оперативной памяти. Мы можем и должны обратиться к ним напрямую. Разберем правильный пример получения реквизитов документов прямо из базы данных:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЗаказКлиента.Номер КАК Номер,
    |    ЗаказКлиента.Дата КАК Дата,
    |    ЗаказКлиента.Партнер КАК Партнер,
    |    ЗаказКлиента.СуммаДокумента КАК СуммаДокумента,
    |    ЗаказКлиента.Менеджер КАК Менеджер
    |ИЗ
    |    Документ.ЗаказКлиента КАК ЗаказКлиента
    |ГДЕ
    |    ЗаказКлиента.Проведен = ИСТИНА";

РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

Пока Выборка.Следующий() Цикл
    // Обрабатываем полученные данные
    Сообщить("Документ № " + Выборка.Номер + " от " + Выборка.Дата);
КонецЦикла;

Этот способ работает максимально быстро и эффективно, так как выборка данных происходит целиком на стороне СУБД, а в оперативную память сервера приложений возвращается только готовый результат.

Решение №2: Использование конструкции ПОМЕСТИТЬ для таблиц в памяти

Бывают ситуации, когда передача таблицы значений в запрос действительно необходима. Например, если данные были загружены из файла Excel загрузчиком отчётов о продажах маркетплейсов из Excel, получены через сторонний Web-сервис или рассчитаны сложным алгоритмом в коде, который невозможно повторить на языке запросов.

В этом случае мы обязаны соблюдать строгие правила работы с временными таблицами. Разберем по шагам, как правильно подготовить и передать таблицу значений в запрос.

  1. Обязательная типизация колонок таблицы значений. Это критически важно! Если вы просто добавите колонку без указания типа данных, платформа 1С не сможет понять, какое поле создавать в СУБД, и выдаст ошибку. Каждая колонка должна иметь жестко заданный тип.
  2. Использование ключевого слова ПОМЕСТИТЬ. Первым пакетом запроса мы должны записать данные во временную таблицу.
  3. Использование пакетного запроса. Вторым пакетом мы уже можем свободно работать с созданной временной таблицей (соединять ее с другими таблицами базы, накладывать условия и сортировать).

Рассмотрим подробнее практический пример реализации этого подхода:


// Шаг 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.Выполнить();

Помните, что после завершения работы с менеджером временных таблиц хорошим тоном является вызов метода МВТ.Закрыть() для немедленного освобождения ресурсов СУБД. Для своевременного освобождения ресурсов и автоматической расстановки инструкций удаления в пакетах можно использовать обработку для оптимизации временных таблиц в запросах.

Надеемся, что этот подробный разбор помог вам разложить по полочкам механизмы работы платформы с СУБД и оперативной памятью. Применяйте полученные знания на практике, пишите оптимальный код и легкой вам разработки!

← На главную