Почему программный запрос в 1С возвращает пустой результат или NULL, а в динамическом списке всё работает?

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

Каждый начинающий разработчик в среде «1С:Предприятие 8» рано или поздно сталкивается с удивительным феноменом. В конфигурации создается простейший динамический список, в его схему компоновки или текст запроса вставляется элементарный выбор полей (например, получение номеров документов), и на форме отображаются сотни записей. Но стоит написать абсолютно аналогичный запрос программно в модуле, запустить отладчик и посмотреть на результат — и система возвращает пустоту, Неопределено или странное значение, которое разработчик интерпретирует как NULL.

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

В чем главная разница между динамическим списком и запросом в коде?

Прежде всего, проанализируем архитектурные различия этих двух механизмов.

Динамический список (объект типа ДинамическийСписок) — это специализированный высокоуровневый объект платформы, предназначенный для отображения списков данных в интерфейсе. Когда вы привязываете запрос к динамическому списку, платформа берет на себя всю тяжелую работу «под капотом»:

  1. Постраничная выборка (пагинация). Список никогда не считывает все данные из базы сразу. Он запрашивает у СУБД ровно столько строк, сколько пользователь видит на экране прямо сейчас (обычно от 20 до 45 записей). При прокрутке списка платформа генерирует новые фоновые запросы.
  2. Автоматическое чтение и вывод. Платформа сама считывает данные из базы, распределяет их по колонкам таблицы на форме и визуализирует их. Разработчику не нужно писать ни одной строчки кода для перебора строк.

Программный запрос (объект типа Запрос) — это низкоуровневый инструмент прямого обращения к данным. Когда мы пишем код программного выполнения запроса, мы обязуемся самостоятельно управлять всем жизненным циклом данных:

  1. Инициализировать объект запроса через Новый Запрос.
  2. Передать текст SQL-подобного запроса в свойство Текст.
  3. Передать параметры в СУБД при помощи метода УстановитьПараметр().
  4. Дать команду СУБД выполнить поиск и расчет через метод Выполнить().
  5. Самостоятельно получить и обойти результаты из оперативной памяти сервера приложений. Платформа ничего не выведет сама, пока мы явно не попросим её об этом.

---

Почему отладчик показывает пустой результат или NULL?

Выясним причину, по которой автор темы на форуме увидел значение NULL (или Неопределено) в окне «Табло» отладчика после выполнения кода:

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

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

Если же вы попытаетесь обратиться напрямую к полям выборки, которая еще не была инициализирована методом Следующий(), например, написав в Табло Выборка.Номер, система выдаст ошибку чтения значения или вернет Неопределено. Разработчик-новичок часто путает это состояние с отсутствием записей в базе данных или системным значением NULL.

Посмотрим подробнее, как правильно работать с результатами выполнения запроса в коде.

---

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

Если нам необходимо программно перебрать каждую запись, полученную из базы данных, мы должны использовать объект ВыборкаИзРезультатаЗапроса. По умолчанию, когда мы вызываем метод Выбрать(), указатель выборки стоит в позиции перед первой записью. Чтобы начать чтение данных, необходимо сдвинуть указатель вперед с помощью метода Следующий(). Этот метод возвращает значение Истина, если впереди есть строка с данными, и Ложь, если записи закончились.

Рассмотрим пример правильного кода с использованием цикла:


// 1. Инициализируем запрос
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ДЗ.Номер КАК НомерЗаказа,
    |   ДЗ.Дата КАК ДатаЗаказа
    |ИЗ
    |   Документ.ЗаказКлиента КАК ДЗ";

// 2. Выполняем запрос на СУБД и получаем РезультатЗапроса
РезультатЗапроса = Запрос.Выполнить();

// Проверяем, есть ли вообще данные в результате, чтобы не тратить ресурсы
Если Не РезультатЗапроса.Пустой() Тогда
    
    // 3. Создаем выборку для обхода
    Выборка = РезультатЗапроса.Выбрать();
    
    // 4. Запускаем цикл обхода записей
    Пока Выборка.Следующий() Цикл
        // Теперь мы можем обратиться к конкретному полю текущей строки
        Сообщить("Обрабатывается заказ № " + Выборка.НомерЗаказа + " от " + Формат(Выборка.ДатаЗаказа, "ДФ=dd.MM.yyyy"));
    КонецЦикла;
    
КонецЕсли;

Обратите внимание на использование метода РезультатЗапроса.Пустой(). Это быстрая и легковесная операция, которая позволяет моментально узнать, вернула ли база данных хоть что-то, без выделения памяти под обход элементов.

---

Вариант решения 2: Выгрузка в Таблицу значений и ее правильный просмотр

В некоторых случаях нам удобнее сразу получить всю таблицу данных в оперативной памяти сервера, чтобы передать ее в другой метод, заполнить реквизит формы или быстро проверить количество строк. Для этого используется метод Выгрузить(), возвращающий ТаблицуЗначений.

Проанализируем пример кода:


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

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

// Получаем количество строк
КоличествоСтрок = ТаблицаРезультатов.Количество();
Сообщить("Всего выгружено документов: " + КоличествоСтрок);

Важный лайфхак для работы в отладчике: если вы остановили выполнение кода точкой останова на строке после выгрузки таблицы и хотите воочию увидеть, какие данные туда попали:

  1. Выделите переменную ТаблицаРезультатов в тексте модуля или добавьте её в Табло отладчика.
  2. Нажмите клавиши Shift + F9 (или дважды кликните по переменной в Табло).
  3. В открывшемся окне просмотра значений нажмите на маленькую лупу (или клавишу F2).
  4. Перед вами откроется полноценное табличное представление выгруженных данных со всеми строками и колонками, где вы сможете визуально убедиться в наличии номеров и дат документов.

---

Вариант решения 3: Использование конструктора запросов с обработкой результатов

Платформа 1С:Предприятие предоставляет мощный инструмент автоматической генерации шаблонного кода, который полностью исключает опечатки и логические ошибки при обходе выборки. Это инструмент «Конструктор запроса с обработкой результата».

Разберем по шагам, как им воспользоваться:

  1. Установите курсор в то место программного модуля, где вы хотите написать запрос.
  2. Нажмите на правую кнопку мыши для вызова контекстного меню.
  3. Выберите пункт «Конструктор запроса с обработкой результата...».
  4. В появившемся окне выбора выберите необходимый тип обработки:
    • «Обход результата» — для создания стандартного цикла Пока Выборка.Следующий().
    • «Выгрузка в таблицу значений» — для автоматического создания кода выгрузки.
    • «Вывод в табличный документ» — если вам нужно сразу вывести отчет на экран.
  5. Нажмите «ОК», после чего откроется стандартный конструктор запросов. Перетащите нужные таблицы и поля (например, из таблицы документов Документ.ЗаказКлиента выберите поле Номер).
  6. Нажмите кнопку «ОК» в конструкторе запроса.

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

---

Анализируем сопутствующие ошибки: «мусорный» код

Давайте обратим внимание на код автора из сообщения №12:


Процедура ЗаполнениеТаблицы() Экспорт
    Запрос = Новый Запрос;
    Документы.ЗаказКлиента.ПолучитьСсылку(); // Что это?
    Запрос.Текст = ...
КонецПроцедуры

В этой процедуре присутствует абсолютно бессмысленная и вредная строка: Документы.ЗаказКлиента.ПолучитьСсылку();. Метод менеджера документа ПолучитьСсылку() генерирует новый уникальный идентификатор (GUID) в рамках СУБД и возвращает пустую ссылку на документ этого типа.

Поскольку результат работы этого метода никуда не записывается (нет присвоения переменной вида НоваяСсылка = Документы...), эта строка выполняется «в пустоту». Она бесполезно тратит системные ресурсы на генерацию UUID и засоряет код программного модуля. Старайтесь избегать подобных конструкций при написании процедур.

---

Важное предостережение: опасность полной выгрузки больших таблиц

В сообщении №1 один из участников обсуждения задал критически важный вопрос: «А сколько доков в базе? Если база рабочая и пухлая, то зачем все собирать-то, тормозить же начнет?».

Это классическое архитектурное правило разработки в 1С. Программный запрос без ограничений (без конструкции ГДЕ или без использования ключевого слова ПЕРВЫЕ) на пустой тестовой базе с 5–10 документами выполнится мгновенно. Но если этот же код запустить в рабочей базе крупного предприятия, где хранятся миллионы заказов клиентов, произойдет следующее:

Поэтому, если вам нужно программно прочитать данные, всегда используйте точные отборы по периоду, организации или пометке удаления в секции ГДЕ вашего запроса, либо ограничивайте выборку, если вам нужны только свежие записи:


// Пример запроса только по актуальным и проведенным заказам за конкретный период
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ ПЕРВЫЕ 100
    |   ДЗ.Ссылка КАК Ссылка,
    |   ДЗ.Номер КАК Номер
    |ИЗ
    |   Документ.ЗаказКлиента КАК ДЗ
    |ГДЕ
    |   ДЗ.Проведен = ИСТИНА
    |   И ДЗ.Дата МЕЖДУ &НачалоПериода И &КонецПериода";
    
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ТекущаяДата()));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(ТекущаяДата()));

---

Краткие выводы: памятка разработчику

Для закрепления материала сформулируем основные правила работы с результатами запросов:

← На главную