Запрос 1С изнутри: какой код его вызывает и кто реально читает данные

· 11 мин

Содержание

Запрос 1С в управляемом приложении выполняется только в серверном контексте: в процедуре с директивой &НаСервере или &НаСервереБезКонтекста, в общем модуле с флагом «Сервер», в модуле объекта или менеджера. На тонком клиенте и в веб-клиенте объекта Запрос нет вообще: строка Новый Запрос в клиентской процедуре даже не скомпилируется.

Сами данные читает не 1С, а СУБД. Платформа на сервере переводит текст запроса на языке 1С в SQL нужного диалекта, отдаёт его PostgreSQL или MS SQL и собирает ответ. Разберём, как устроены клиентский, серверный и комбинированный контексты, сколько стоит каждый вызов сервера и чем ловить медленные запросы.

Как устроена платформа: клиент, сервер приложений и СУБД

Любая база 1С 8.3 работает по трёхзвенной схеме, даже если вы её не видите.

  • 🖥️ Клиент — тонкий клиент, веб-клиент или толстый клиент. Рисует формы, принимает ввод, выполняет код с директивой &НаКлиенте.
  • ⚙️ Сервер 1С — рабочие процессы rphost. Здесь живут объекты базы данных, выполняется проведение, здесь же собираются запросы.
  • 🗄️ СУБД — PostgreSQL, MS SQL Server, IBM Db2 или Oracle Database. Хранит таблицы и выполняет SQL-код, который ей прислал сервер 1С.

В клиент-серверном варианте эти звенья — разные процессы, часто на разных машинах. Клиент вообще не подключается к СУБД напрямую: у пользователя нет ни строки подключения, ни прав в PostgreSQL.

В файловом варианте звенья те же, но логически. Серверную часть исполняет сам процесс клиента (или процесс веб-сервера при публикации), а роль СУБД играет встроенный файловый движок, который читает 1Cv8.1CD. Код при этом пишется точно так же: директивы и ограничения едины для обоих вариантов.

Отсюда главное правило переносимости: код, который корректно разделён на клиентский и серверный в файловой базе, без правок заработает в клиент-серверной. Обратное неверно, если в файловой базе «случайно сработало» что-то лишнее в толстом клиенте.

Директивы компиляции: где окажется ваш код

Директива над процедурой указывает, в каком контексте её компилировать и исполнять. В модуле управляемой формы их четыре.

ДирективаГде выполняетсяДоступ к данным формыМожно ли выполнить запрос
&НаКлиентена клиентеда, к реквизитам формынет
&НаСерверена сервереда, форма передаётся на серверда
&НаСервереБезКонтекстана серверенет, только параметрыда
&НаКлиентеНаСервереБезКонтекстаи там, и тамнеттолько в серверной ветке

Последняя директива и есть комбинированный режим. Процедура компилируется дважды и может вызываться как с клиента, так и с сервера, причём без перехода между ними: вызвали на клиенте — выполнилась на клиенте. Поэтому прямой Новый Запрос в ней вызовет ошибку компиляции клиентской части.

Если запрос всё-таки нужен в такой процедуре, отделите серверную ветку инструкцией препроцессора:

&НаКлиентеНаСервереБезКонтекста
Функция КоличествоСтрок(Ссылка)
	#Если Сервер Тогда
	Запрос = Новый Запрос(
	"ВЫБРАТЬ
	|	КОЛИЧЕСТВО(*) КАК Количество
	|ИЗ
	|	Документ.РеализацияТоваровУслуг.Товары КАК Товары
	|ГДЕ
	|	Товары.Ссылка = &Ссылка");
	Запрос.УстановитьПараметр("Ссылка", Ссылка);
	Выборка = Запрос.Выполнить().Выбрать();
	Возврат ?(Выборка.Следующий(), Выборка.Количество, 0);
	#Иначе
	Возврат 0;
	#КонецЕсли
КонецФункции

Имя документа здесь условное: подставьте свой объект метаданных.

Флаги общих модулей

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

  • ✅ Сервер — модуль компилируется на сервере, в нём можно писать запросы .
  • ✅ Вызов сервера — экспортные методы серверного модуля можно вызывать с клиента. Без этого флага клиент их просто не увидит.
  • ✅ Клиент (управляемое приложение) — модуль доступен тонкому и веб-клиенту, запросов в нём быть не может.
  • ✅ Внешнее соединение — модуль доступен при подключении через COM-соединение.
  • ✅ Привилегированный — код выполняется без проверки прав, запросы обходят ограничения доступа на уровне записей.

Если отметить одновременно «Клиент» и «Сервер», получится тот же комбинированный модуль: серверный код внутри него придётся обрамлять #Если Сервер Тогда.

Толстый клиент и обычное приложение

В толстом клиенте объект Запрос доступен и в клиентском коде. Это наследие обычных форм. Но и здесь в клиент-серверной базе текст запроса уходит на сервер 1С, там превращается в SQL, а результат передаётся обратно в клиентский процесс. Дешевле от того, что запрос написан в модуле обычной формы, он не становится.

В модулях объектов и менеджеров типовых конфигураций часто встречается обёртка:

#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда
// процедуры, работающие с запросами и объектами
#КонецЕсли

Она прячет серверный код от тонкого клиента и оставляет его доступным там, где объект Запрос существует.

Путь запроса от кода до СУБД

Вызов Запрос.Выполнить() на сервере проходит несколько этапов.

  1. Разбор текста. Платформа проверяет синтаксис языка запросов 1С. Ошибка вида {(5, 2)}: Поле не найдено возникает здесь, ещё до обращения к базе.
  2. Раскрытие виртуальных таблиц. Остатки, Обороты, СрезПоследних разворачиваются в выборку из таблиц итогов и движений регистра. Условия из параметров виртуальной таблицы попадают внутрь этой выборки, а условия из ГДЕ применяются уже после неё.
  3. Добавление ограничений доступа. Если в ролях пользователя настроены ограничения на уровне записей, платформа дописывает к запросу их условия. С ВЫБРАТЬ РАЗРЕШЕННЫЕ недоступные строки просто отбрасываются, без этого слова запрос, наткнувшись на них, завершится ошибкой нехватки прав.
  4. Трансляция в SQL. Имена вроде Справочник.Номенклатура превращаются в физические таблицы (_Reference32 и подобные), поля — в колонки _Fld1234. Обращение через точку (Товары.Номенклатура.Родитель) порождает дополнительные соединения.
  5. Выполнение в СУБД. Оптимизатор СУБД строит план, читает индексы и отдаёт строки.
  6. Сборка результата. Сервер 1С получает строки и превращает их в объект РезультатЗапроса.

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

Каждое обращение через точку к ссылочному полю — это ещё одно соединение таблиц в SQL. Три уровня точек по составному типу (например, по регистратору) способны превратить быстрый запрос в многосекундный.

Как передавать данные между клиентом и сервером

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

На тонкий клиент нельзя передать РезультатЗапроса, ВыборкаИзРезультатаЗапроса и ТаблицаЗначений. Этих типов на клиенте нет. Рабочие способы:

  • 📥 Записать результат в реквизит формы. В процедуре &НаСервере выгрузите таблицу и загрузите её в табличную часть или реквизит-таблицу. После возврата платформа сама передаст изменённые данные формы на клиент.
  • 📦 Вернуть массив структур или соответствие. Подходит для &НаСервереБезКонтекста: эти типы сериализуются и доступны на клиенте.
  • 🗃️ Поместить во временное хранилище. ПоместитьВоВременноеХранилище вернёт адрес, по которому данные можно забрать в следующем серверном вызове, не гоняя их на клиент.

Пример заполнения табличной части формы остатками:

&НаКлиенте
Процедура ЗаполнитьОстатки(Команда)
	ЗаполнитьОстаткиНаСервере();
КонецПроцедуры

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

И вариант без контекста формы, который возвращает массив структур:

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

Имя регистра в обоих примерах условное.

Чем &НаСервере дороже &НаСервереБезКонтекста

При вызове &НаСервере платформа отправляет на сервер данные формы, а после выполнения возвращает их обратно. Если в форме документ на 2 000 строк, они путешествуют туда и обратно на каждый такой вызов, даже когда процедура читает одну константу.

&НаСервереБезКонтекста передаёт только параметры и возвращаемое значение. Для запросов, которым не нужны реквизиты формы, это заметно быстрее, особенно на медленных каналах и в веб-клиенте.

Где разместить процедуру с запросом?

Ошибки контекста и потери производительности

Ошибки здесь делятся на две группы: те, что не дают коду запуститься, и те, что тихо замедляют работу.

Не компилируется или падает:

  • ❌ Тип не определен (Запрос) — запрос написан в клиентской процедуре. Перенесите его в &НаСервере или общий серверный модуль.
  • ❌ Переменная не определена (Объект) — в процедуре &НаСервереБезКонтекста обращаются к данным формы. Их там нет: передайте нужные значения параметрами.
  • ❌ Клиент «не видит» функцию общего модуля — у модуля не отмечен флаг «Вызов сервера» или функция не объявлена с Экспорт.
  • ❌ Ошибка при возврате результата на клиент — функция пытается вернуть ТаблицуЗначений или РезультатЗапроса. Преобразуйте в массив структур.

Работает, но медленно:

  • 🐢 Запрос в цикле. Сто строк документа — сто обращений к СУБД. Соберите ссылки в массив и выполните один запрос с условием В (&Список).
  • 🐢 Серверный вызов в цикле на клиенте. Каждая итерация — сетевой круг до сервера и обратно. Отправьте весь набор данных одним вызовом.
  • 🐢 Условие в ГДЕ вместо параметров виртуальной таблицы. Остатки() сначала посчитает итоги по всем складам, а потом отфильтрует. Условие внутри скобок Остатки(&Дата, Склад = &Склад) ограничит выборку сразу.
  • 🐢 Разыменование составных полей через точку. Используйте ВЫРАЗИТЬ(Регистратор КАК Документ.…), чтобы платформа не соединяла таблицы всех типов документов.
  • 🐢 Лишние поля и ВЫБРАТЬ *. Тянуть все колонки ради одной — лишний трафик между СУБД, сервером и клиентом.

Плохой и хороший вариант для заполнения цен:

// Плохо: запрос на каждую строку
Для Каждого Строка Из Товары Цикл
	Строка.Цена = ПолучитьЦенуНаСервере(Строка.Номенклатура);
КонецЦикла;

// Хорошо: один серверный вызов, один запрос с условием В (&Список)
Цены = ПолучитьЦеныНаСервере(Товары.Выгрузить(, "Номенклатура").ВыгрузитьКолонку("Номенклатура"));

Второй вариант выполняется на сервере (таблица значений существует только там), функция возвращает соответствие «номенклатура → цена», и цикл заполнения тоже идёт без новых обращений к СУБД.

Чем анализировать и отлаживать запросы

Чтобы найти медленный запрос, не нужно гадать. У платформы есть инструменты для каждого уровня.

ИнструментЧто показываетКогда брать
Конструктор запросаструктуру, соединения, параметры виртуальных таблицпри написании и правке текста
Отладчикзначение результата запроса, параметры, временные таблицыкогда результат «не тот»
Замер производительностивремя каждой строки кода и число вызововкогда тормозит конкретная операция
Технологический журналтекст SQL, длительность, блокировкикогда нужно увидеть, что ушло в СУБД
Профайлер СУБД, EXPLAINплан выполнения SQLкогда запрос медленный в самой СУБД

Замер производительности

Запустите отладку из Конфигуратора и включите замер в меню Отладка → Замер производительности. Выполните медленную операцию, остановите замер. В результате видно, какая строка заняла больше всего времени и сколько раз она вызывалась. Строка Запрос.Выполнить() с сотнями вызовов — верный признак запроса в цикле.

Для серверного кода отладку на сервере нужно разрешить: служба агента запускается с ключом -debug.

Перезапуск службы сервера 1С для изменения ключей запуска обрывает все сеансы пользователей. Делайте это в нерабочее время и после предупреждения коллег.

Количество серверных вызовов и объём переданных данных клиент покажет, если запустить его с параметром /DisplayPerformance. Удобно проверить, сколько кругов до сервера делает открытие тяжёлой формы.

Технологический журнал

Техжурнал включается файлом logcfg.xml в каталоге conf платформы, например C:\Program Files\1cv8\conf\. Минимальная настройка для перехвата запросов:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
	<log location="D:\techlog" history="2">
		<event>
			<eq property="name" value="DBMSSQL"/>
		</event>
		<event>
			<eq property="name" value="DBPOSTGRS"/>
		</event>
		<event>
			<eq property="name" value="SDBL"/>
		</event>
		<property name="all"/>
	</log>
</config>

События DBMSSQL и DBPOSTGRS содержат реальный SQL-текст для MS SQL и PostgreSQL, SDBL — запрос на уровне 1С. Параметр history задаёт срок хранения в часах.

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

Полученный SQL можно вставить в консоль СУБД и посмотреть план: EXPLAIN ANALYZE в PostgreSQL или «Включить действительный план выполнения» в SQL Server Management Studio. Если план показывает полное сканирование большой таблицы, ищите в запросе 1С условие, которое не попадает в индекс.

Проверка медленного запроса
Выполнено: 0 / 6

Частые вопросы

Выполняется ли запрос на клиенте в файловой базе?
Нет. Код с запросом всё равно должен быть серверным, просто серверную часть в файловом варианте исполняет процесс клиента. Директивы при этом обязательны так же, как в клиент-серверной базе.
Можно ли выполнить запрос из внешней обработки на тонком клиенте?
Да, если запрос стоит в процедуре &НаСервере формы обработки. При этом база должна разрешать запуск внешних обработок пользователю, а в режиме безопасного запуска часть возможностей ограничена.
Снимает ли флаг «Привилегированный» ограничения доступа с запроса?
Да, код привилегированного модуля выполняется без проверки прав, включая ограничения на уровне записей. Выносите туда минимум кода и не передавайте в него произвольный текст запроса с клиента.
Почему один и тот же запрос в консоли работает быстро, а в форме медленно?
Чаще всего виновата не СУБД, а то, что вокруг: вызов в цикле, передача большой формы через &НаСервере, пересчёт других реквизитов. Замер производительности покажет, где реально уходит время.
Через COM-соединение запрос выполняется на сервере или на клиенте?
Внешнее соединение — отдельный контекст, в котором объект Запрос доступен напрямую, без директив. Общие модули, которые вы вызываете через такое соединение, должны иметь флаг «Внешнее соединение».