Виртуальные таблицы 1С: параметры, которые решают всё

Самый частый антипаттерн, который мы видим в чужом коде, — это виртуальная таблица без параметров. Программист пишет РегистрНакопления.ТоварыНаСкладах.Остатки, потом ставит ГДЕ Номенклатура = &Номенклатура, и удивляется, почему отчёт по одной номенклатуре строится на регистре в десять миллионов записей минуту.

Минуту он строится потому, что виртуальная таблица — это не таблица. Это запрос. И параметры — это не «удобный способ передать значения», а единственный способ объяснить платформе, какую часть регистра вообще трогать. Без них она трогает весь.

Что такое виртуальная таблица на самом деле

В синтаксис-помощнике виртуальная таблица выглядит как обычная таблица. Поля, источник, можно сделать ВЫБРАТЬ * ИЗ. Но в момент компиляции запроса 1С разворачивает её в полноценный SQL: соединения, агрегации, фильтры, сортировки. РегистрНакопления.X.Остатки превращается в запрос вида «возьми таблицу итогов на ближайшую расчётную дату, прибавь движения от расчётной даты до запрошенной, сгруппируй по измерениям, оставь только ненулевые остатки».

Этот развёрнутый запрос всегда выполняется целиком. Если вы передали параметр «Период» — в развёрнутый запрос подставится фильтр по периоду на этапе соединения с движениями. Если вы передали параметр «Условие» (это второй параметр виртуальной таблицы — отбор) — фильтр пойдёт на этап выборки из движений и итогов. То есть вы говорите СУБД: «можешь даже не читать остальное».

А если вы написали отбор в ГДЕ снаружи — СУБД сначала сделает все агрегации по всему регистру, получит миллион строк, и только потом отфильтрует одну. Это не баг платформы — это следствие порядка операций SQL. WHERE применяется после FROM, а FROM в случае виртуальной таблицы — это уже выполненная агрегация.

Виртуальная таблица 1С — параметры внутри vs условие в ГДЕ снаружи

Параметры по типам виртуальных таблиц

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

Регистр накопления, виртуальная таблица «Остатки». Параметры: Период (момент времени, на который нужны остатки), Условие (отбор по измерениям). Период обязателен, если вы хотите остатки не на конец таблицы. Условие обязательно, если вы хотите остатки не по всему регистру. Игнорировать любой из них — значит читать всю таблицу итогов.

Регистр накопления, «Обороты». Параметры: НачалоПериода, КонецПериода, Периодичность (Период, День, Месяц, Год, Регистратор), МетодДополнения, Условие. Здесь критичный параметр — периодичность. Если вам нужны обороты в разрезе месяцев, не запрашивайте их в разрезе регистраторов и потом сами не группируйте — пусть СУБД сразу сгруппирует. Это разница в десять-двадцать раз.

Регистр накопления, «ОстаткиИОбороты». Самый тяжёлый зверь. Параметры те же, что у оборотов плюс «Период». СУБД должна вам собрать остатки на начало, обороты за период, остатки на конец — всё одним запросом. Если вы вызвали ОстаткиИОбороты там, где нужны были только обороты или только остатки, — вы сделали втрое больше работы. Никогда не вызывайте ОстаткиИОбороты по привычке. Только когда действительно нужно всё три набора в одной строке.

Регистр сведений, «СрезПоследних». Параметры: Период, Условие. Без периода вы получите срез на текущий момент — обычно не то, что нужно. Без условия — срез по всем записям, что для регистра курсов валют (например) означает прочитать историю всех валют.

Регистр бухгалтерии. Здесь особая боль: ОстаткиИОборотыДт/Кт, виды субконто, корреспонденции. Параметры передавать обязательно почти всегда — иначе вы получаете пересечение всех счетов со всеми субконто. На корпоративной базе это десятки секунд даже на простом запросе.

Самый частый паттерн ошибки

Берём боевой случай. Программисту нужны остатки по одной номенклатуре на конец дня. Он пишет:

ВЫБРАТЬ
    ОстаткиТовары.Номенклатура,
    ОстаткиТовары.КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки КАК ОстаткиТовары
ГДЕ
    ОстаткиТовары.Номенклатура = &Номенклатура
    И ОстаткиТовары.Период <= &ДатаКонца

Этот код «работает». Он возвращает правильные данные. На небольшой базе он работает быстро. На большой — встаёт. Почему?

Платформа развернёт виртуальную таблицу Остатки без параметров. Это значит, что СУБД построит итоги по всей номенклатуре на момент окончания регистра, а не на нашу дату. Потом снаружи применится фильтр: оставить только нашу номенклатуру и только до нашей даты. Но мы попросили Период <= &ДатаКонца в условии — а виртуальная таблица уже вернула остатки на момент окончания. Это не только медленно, но и логически неверно: мы получили вообще не то.

Правильный вариант:

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

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

Это перекликается с темой WHERE vs ON в LEFT JOIN: там та же логика — фильтр снаружи и фильтр внутри соединения дают принципиально разные планы запроса.

Время выполнения запроса 1С с параметрами виртуальной таблицы и без

Передача отбора через временную таблицу

Часто отбор для виртуальной таблицы вы знаете не как одно значение, а как набор. Например, нужно получить остатки по двадцати конкретным номенклатурам, которые лежат в выборке от пользователя. Соблазн — собрать эти двадцать значений в массив и передать через В (&Массив). Это работает, но плохо.

Хорошее решение — положить значения во временную таблицу, а отбор виртуальной таблицы построить через соединение с этой временной таблицей:

ВЫБРАТЬ
    Т.Номенклатура
ПОМЕСТИТЬ ВТ_Номенклатура
ИЗ
    &ТаблицаНоменклатуры КАК Т
;

ВЫБРАТЬ
    Остатки.Номенклатура,
    Остатки.КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(
        &ДатаКонца,
        Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_Номенклатура)
    ) КАК Остатки

Зачем это нужно? Когда отбор передан как параметр виртуальной таблицы, платформа применяет его на этапе чтения регистра, а не после агрегации. На больших регистрах разница — порядок величины. Кроме того, временная таблица позволяет СУБД понять размер выборки и выбрать правильный план соединения: hash join для большой таблицы, nested loop для маленькой.

Когда параметры не помогают

Бывают случаи, когда даже с правильными параметрами виртуальная таблица работает медленно. Это сигнал смотреть глубже.

Отсутствуют итоги. Регистр накопления хранит итоги отдельно от движений. Если итоги отключены или не пересчитаны, каждая виртуальная таблица заново суммирует все движения. Проверяется в Конфигураторе: «Регистры накопления → Имя → Итоги хранятся → Да». Пересчёт итогов — через регламентное задание или кнопку в настройках регистра.

Граница итогов далеко. Платформа хранит итоги периодами (обычно по месяцам). Если вы запрашиваете остатки на дату, которая близка к границе хранимого периода, — СУБД дополнительно пройдёт по движениям между ближайшим итогом и вашей датой. Если этих движений много, расчёт занимает время. Решение — сдвинуть границу итогов вперёд, через регламент пересчёта.

Регистр перегружен измерениями. Если в регистре десять измерений и индексы построены не по всем, отбор по неиндексированному измерению будет читать таблицу целиком. Это уже тема антипаттернов запросов 1С: проверка планов запросов через ТЖ или SQL Profiler покажет, использует ли СУБД индексы.

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

Маленький чек-лист перед коммитом

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

Первый — указан ли период или дата, на которую запрашиваются данные? Если нет, это скорее всего ошибка — даже если сейчас вы получаете «текущее состояние», это решение должно быть осознанным.

Второй — указан ли отбор внутри параметра виртуальной таблицы? Любые условия, которые относятся к данным регистра, должны быть в параметре, а не в ГДЕ снаружи. Снаружи оставляйте только то, что требует уже агрегированных данных (например, фильтр по полю КоличествоОстаток > 0).

Третий — нужны ли вам именно «ОстаткиИОбороты» или хватит одного из двух? Не зовите тяжёлый вариант, если работаете только с одним набором.

Четвёртый — если нужна группировка по периоду, передайте её в параметре «Периодичность». Не группируйте сами через СГРУППИРОВАТЬ ПО НАЧАЛОПЕРИОДА(Период, Месяц).

Пятый — если отбор по списку значений, заведите временную таблицу и сошлитесь на неё внутри параметра. Это всегда быстрее, чем В (&Массив) на больших регистрах.

Чек-лист параметров виртуальных таблиц 1С перед коммитом запроса

Как искать существующие проблемы

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

Самый быстрый — поиск по строкам в исходниках. Регулярка вроде Регистр(Накопления|Сведений|Бухгалтерии)\.\w+\.(Остатки|Обороты|ОстаткиИОбороты|СрезПоследних)\b\s* найдёт все обращения к виртуальным таблицам. Дальше глазами пробежать места, где после имени таблицы нет открывающей скобки с параметрами. В небольшой конфигурации это пара десятков мест, в крупной — сотни. Но даже сотня — это пара часов работы.

Второй способ — технологический журнал с фильтром по длительности SQL-запросов. Если вы видите запросы вида SELECT ... FROM ... AccumRgT\d+ ... WHERE Period <= ..., длительностью в несколько секунд — это, скорее всего, виртуальная таблица без правильных параметров. Текст SQL подскажет, какая именно.

Третий — посмотреть на план запроса для медленной операции. Если в плане видны scan по таблице итогов на миллионы строк, а возвращается из запроса сотня — у вас явный кандидат на оптимизацию параметров. После добавления параметров скан превращается в seek, и количество прочитанных строк падает в тысячи раз.

Это не оптимизация, это правильное использование

Главное, что хочется сказать про виртуальные таблицы: грамотное использование параметров — это не «оптимизация», это базовое требование. Оптимизация — это когда вы из правильно написанного запроса выжимаете последние проценты. Запрос без параметров — это не «непооптимизированный». Это сломанный.

Если вы видите в чужом коде виртуальную таблицу без параметров, и вокруг есть ГДЕ с фильтрами по данным регистра — это не «можно улучшить». Это нужно исправить, и почти всегда исправление даёт ускорение в десятки раз. Даже если сегодня запрос работает за полсекунды, завтра данных станет в десять раз больше — и тот же запрос будет работать пять секунд. А послезавтра — пятьдесят.

Виртуальные таблицы — это инструмент, который позволяет писать на языке бизнес-логики, а не на языке SQL. Но за это удобство нужно платить вниманием к параметрам. Платформа не подскажет, не предупредит и не вылечит. Она просто молча сделает то, что вы попросили: построит остатки по всему регистру и потом отдаст вам одну строку.