Запрос 1С изнутри: какой код его вызывает и кто реально читает данные
Содержание
Запрос 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С. Ошибка вида
{(5, 2)}: Поле не найденовозникает здесь, ещё до обращения к базе. - Раскрытие виртуальных таблиц.
Остатки,Обороты,СрезПоследнихразворачиваются в выборку из таблиц итогов и движений регистра. Условия из параметров виртуальной таблицы попадают внутрь этой выборки, а условия изГДЕприменяются уже после неё. - Добавление ограничений доступа. Если в ролях пользователя настроены ограничения на уровне записей, платформа дописывает к запросу их условия. С
ВЫБРАТЬ РАЗРЕШЕННЫЕнедоступные строки просто отбрасываются, без этого слова запрос, наткнувшись на них, завершится ошибкой нехватки прав. - Трансляция в SQL. Имена вроде
Справочник.Номенклатурапревращаются в физические таблицы (_Reference32и подобные), поля — в колонки_Fld1234. Обращение через точку (Товары.Номенклатура.Родитель) порождает дополнительные соединения. - Выполнение в СУБД. Оптимизатор СУБД строит план, читает индексы и отдаёт строки.
- Сборка результата. Сервер 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С условие, которое не попадает в индекс.
Частые вопросы
Выполняется ли запрос на клиенте в файловой базе?
Можно ли выполнить запрос из внешней обработки на тонком клиенте?
&НаСервере формы обработки. При этом база должна разрешать запуск внешних обработок пользователю, а в режиме безопасного запуска часть возможностей ограничена.Снимает ли флаг «Привилегированный» ограничения доступа с запроса?
Почему один и тот же запрос в консоли работает быстро, а в форме медленно?
&НаСервере, пересчёт других реквизитов. Замер производительности покажет, где реально уходит время.Через COM-соединение запрос выполняется на сервере или на клиенте?
Запрос доступен напрямую, без директив. Общие модули, которые вы вызываете через такое соединение, должны иметь флаг «Внешнее соединение».