WHERE vs ON в LEFT JOIN: одно слово замедляет 1С в 60 раз

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

Что за отчёт и почему он важен

Ежемесячная статистика розничных продаж для бухгалтерии: товарооборот по группам, наценка, реализация, себестоимость. Данные из нескольких регистров накопления. На выходе — несколько тысяч строк. Ничего, что должно занимать четыре минуты.

Основной запрос соединял виртуальную таблицу оборотов с регистром движений. LEFT JOIN по четырём полям: продукт, склад, партия, организация. И тут же, в ON-условии, стоял фильтр по периоду — начало и конец отчётного месяца.

Конструкция стандартная. Читается однозначно. На первый взгляд — всё правильно.

Схема LEFT JOIN с фильтром в ON-условии

Первое подозрение — виртуальная таблица

В 1С параметры виртуальных таблиц критичны для производительности, и ошибка там — классика жанра. Проверили. Параметры заданы корректно: период, организация, склады. Не она.

Открыли план запроса. Table Scan на правой таблице — регистр движений. Сотни тысяч строк читаются целиком, хотя за месяц нужна пара тысяч. Фильтр по периоду стоит. Но не работает.

Проблема не в синтаксисе — в семантике

ON-условие LEFT JOIN работает иначе, чем WHERE. При INNER JOIN оптимизатор SQL Server может «протолкнуть» ограничение внутрь таблицы — сначала отфильтровать, потом соединять. При LEFT JOIN — нет.

Причина фундаментальная: LEFT JOIN гарантирует, что каждая строка левой таблицы попадёт в результат. Если заранее сократить правую таблицу, часть строк левой потеряет пару и получит NULL вместо реальных значений. Оптимизатор не имеет права на такую подмену.

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

Сравнение планов запросов: фильтр в ON vs фильтр в WHERE

Почему LEFT JOIN, а не INNER

LEFT JOIN здесь выбран не случайно: не все товары имеют движения по данному регистру, и потерять их из статистики нельзя. INNER JOIN убрал бы строки, по которым нет совпадений — а это именно те позиции, где бухгалтерии нужно увидеть ноль, а не пустоту.

Типичная ловушка: разработчик знает, что LEFT JOIN нужен для полноты данных, но не учитывает, как это влияет на план запроса. В небольших базах разница незаметна. Когда правая таблица вырастает до сотен тысяч строк — 1С начинает тормозить, и отчёт вместо секунд занимает четыре минуты.

Решение: разделить фильтрацию и соединение

Идея простая: не заставлять LEFT JOIN фильтровать — подготовить данные заранее.

Первый запрос пакета: выбрать из регистра записи за нужный период, отфильтровать через WHERE по организации и складам, сгруппировать, проиндексировать по ключу соединения. Результат — компактная временная таблица с парой тысяч строк.

Второй запрос: тот же LEFT JOIN, но к подготовленной временной таблице вместо сырого регистра.

Тридцать строк кода. Секунды вместо минут.

Архитектура решения: пакетный запрос с временной таблицей

Как обнаружить эту проблему у себя

Если в вашей системе есть отчёты на СКД с LEFT JOIN — проверьте, где стоят фильтры. Вот чек-лист:

1. Откройте план запроса. В консоли запросов 1С или через SET STATISTICS PROFILE ON на стороне SQL Server. Ищите Table Scan или Clustered Index Scan на правой таблице LEFT JOIN — это признак того, что фильтр не работает.

2. Проверьте ON-условие. Если в нём есть ограничения по периоду, организации, складу — скорее всего, они не используются оптимизатором. Перенесите их в WHERE (если это INNER JOIN) или в предварительный подзапрос (если LEFT JOIN).

3. Оцените объём правой таблицы. Если в ней меньше 10 тысяч строк — разница может быть незаметна. Если сотни тысяч — вынос в временную таблицу почти наверняка поможет.

4. Не забудьте индексировать временную таблицу. В 1С это делается через ИНДЕКСИРОВАТЬ ПО в пакетном запросе. Без индекса соединение двух временных таблиц может оказаться не быстрее исходного запроса.

Почему это не ловится на код-ревью

WHERE ограничивает данные до соединения. ON определяет правило самого соединения. Одно ключевое слово. В документации по SQL это абзац. В ежемесячном отчёте — четыре минуты, пока бухгалтер ждёт.

Проблема в том, что запрос выглядит правильно. Синтаксически — безупречен. Результат — корректный. Просто медленный. Код-ревью без анализа плана запроса это не поймает. А в 1С планы запросов смотрят, мягко говоря, не все.

Мы встречаем эту конструкцию примерно в каждом третьем проекте, куда приходим разбирать производительность. Отчёт работает год-два нормально. Потом данных становится больше — и «внезапно» тормозит. Причина лежала в коде с первого дня. Другие незаметные антипаттерны запросов описаны в статье про четыре антипаттерна запросов 1С. Особенно остро эта проблема проявляется при закрытии месяца в 1С:ERP, где запросы к регистрам накопления выполняются десятки раз подряд. Измерить прирост производительности после оптимизации помогает метрика APDEX.