Как отрицание и дизъюнкция в условии ГДЕ влияют на скорость запроса 1С

· 11 мин

Содержание

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

Для фильтрации в запросах 1С это означает простую последовательность. Сначала переформулируйте условие так, чтобы в нём не было отрицания. Затем замените ИЛИ по одному полю на В (...). Если остались ИЛИ по разным полям на большой таблице — разбейте запрос на части. И каждый шаг проверяйте по плану выполнения, а не на глаз.

Как НЕ и ИЛИ переводятся друг в друга: законы де Моргана

Любое условие с отрицанием можно переписать без него, и наоборот. Для этого есть два закона де Моргана:

  • 🔁 НЕ (А ИЛИ Б) равно НЕ А И НЕ Б;
  • 🔁 НЕ (А И Б) равно НЕ А ИЛИ НЕ Б.

То есть вопрос «НЕ или ИЛИ» часто сводится к записи одного и того же условия. Два варианта ниже отбирают одинаковые строки:

// Вариант 1: отрицание над скобкой
ГДЕ
    НЕ (Реализация.ПометкаУдаления ИЛИ НЕ Реализация.Проведен)

// Вариант 2: то же самое после раскрытия скобки
ГДЕ
    НЕ Реализация.ПометкаУдаления
    И Реализация.Проведен

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

Подвох: NULL ломает эквивалентность

Законы де Моргана работают для логики «истина/ложь». В запросах есть третье состояние — NULL. Оно появляется после левого соединения, когда парной строки нет, и у реквизитов составного типа.

Сравнение с NULL не даёт ни «истину», ни «ложь». Отрицание от такого результата тоже не даёт «истину». Поэтому строка с NULL не пройдёт ни условие Поле = 1, ни условие НЕ Поле = 1.

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

Если такие строки нужны, допишите их явно: НЕ Договоры.ВидДоговора = &ВидДоговора ИЛИ Договоры.Ссылка ЕСТЬ NULL. Либо оберните поле в ЕСТЬNULL(Договоры.ВидДоговора, ЗНАЧЕНИЕ(Перечисление.ВидыДоговоровКонтрагентов.ПустаяСсылка)). Имя перечисления в этом примере зависит от конфигурации — проверьте его в дереве метаданных.

Отрицание в условии ГДЕ после левого соединения молча превращает его во внутреннее. Отчёт «теряет» строки, и ошибку замечают только при сверке итогов.

Почему условие с НЕ обычно читает всю таблицу

Индекс помогает, когда нужно найти немного строк из многих. Условие Склад = &Склад выбирает одну ветку индекса. Условие НЕ Склад = &Склад просит «всё, кроме одной ветки» — и это обычно 80–99% таблицы.

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

Типичные отрицания, которые дают сканирование:

  • 🚫 НЕ Поле = &Значение и Поле <> &Значение;
  • 🚫 НЕ Поле В (&Список);
  • 🚫 НЕ Поле ПОДОБНО "%текст%" — впрочем, ПОДОБНО с процентом в начале не использует индекс и без НЕ;
  • 🚫 НЕ Поле В (ВЫБРАТЬ ...) — отрицание с вложенным запросом.

Когда отрицание безопасно

НЕ не страшно, если рядом стоит избирательное положительное условие. Запрос ГДЕ Документ.Дата МЕЖДУ &Начало И &Конец И НЕ Документ.ПометкаУдаления сначала отберёт документы за период по индексу, а признак пометки проверит уже на этой выборке.

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

Замена отрицания положительным списком

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

// Было: отрицание
ГДЕ
    НЕ Заказы.Статус = ЗНАЧЕНИЕ(Перечисление.СтатусыЗаказов.Закрыт)

// Стало: положительный список
ГДЕ
    Заказы.Статус В (&ОткрытыеСтатусы)

Список статусов передайте параметром через Запрос.УстановитьПараметр("ОткрытыеСтатусы", МассивСтатусов). Имя перечисления выше условное — у каждой конфигурации своё. Плюс такой записи не только в индексе: если в перечисление добавят новый статус, он не попадёт в отчёт без вашего решения.

Когда ИЛИ работает быстро, а когда нет

ИЛИ бывает двух видов, и ведут они себя по-разному.

ИЛИ по одному полю — безопасно. Условие Склад = &Склад1 ИЛИ Склад = &Склад2 равно Склад В (&Склад1, &Склад2). СУБД делает по индексу два коротких поиска. Запись через В короче и не разрастается, когда складов станет десять.

ИЛИ по разным полям — источник медленных запросов. Условие Контрагент = &Контрагент ИЛИ Ответственный = &Ответственный нельзя закрыть одним индексом: строки разбросаны по двум разным упорядочиваниям. СУБД может объединить два поиска, но часто предпочитает просто прочитать всё.

Сравнение вариантов отбора:

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

Индексирование реквизита задаётся в Конфигураторе свойством «Индексировать» в палитре свойств реквизита. Измерения регистров входят в основной индекс в порядке следования, поэтому отбор по первому измерению эффективнее, чем по последнему.

Составной тип — скрытое ИЛИ

Если реквизит имеет составной тип (например, «Документ-основание» с несколькими видами документов), платформа хранит его в нескольких колонках. Простое сравнение Основание = &Документ в SQL может превратиться в набор условий по этим колонкам.

Помогает уточнение типа: Основание ССЫЛКА Документ.ЗаказКлиента или ВЫРАЗИТЬ(Основание КАК Документ.ЗаказКлиента). Так запрос перестаёт проверять все возможные типы.

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

Мерить «на глаз» по секундомеру ненадёжно: второй запуск быстрее из-за кеша СУБД. Сравнивайте планы и число прочитанных строк.

Шаг 1. Получите текст SQL

Включите технологический журнал. В файле logcfg.xml (каталог conf платформы) настройте сбор событий DBMSSQL или DBPOSTGRS и добавьте элемент <plansql/>, чтобы для MS SQL в журнал писались планы. После настройки журнала пусть один пользователь выполнит медленный отчёт.

Технологический журнал на рабочем сервере быстро растёт. Ограничьте сбор по длительности (например, событиями дольше 1 секунды) и по базе, а после замера отключите его.

Шаг 2. Выполните запрос в СУБД

  • 🗄️ MS SQL Server: в SQL Server Management Studio включите «Показать действительный план выполнения» и выполните текст из журнала. Ищите операторы Index Scan и Clustered Index Scan на больших таблицах — это и есть полное чтение. Index Seek — поиск по индексу.
  • 🐘 PostgreSQL: выполните EXPLAIN (ANALYZE, BUFFERS) перед текстом запроса. Seq Scan — сканирование, Index Scan и Bitmap Index Scan — работа с индексом. BitmapOr означает, что СУБД сумела объединить два индекса для условия с ИЛИ.
EXPLAIN (ANALYZE, BUFFERS)
SELECT ... -- текст запроса из технологического журнала

Шаг 3. Сравните цифры

Смотрите не на «стоимость» в процентах, а на фактическое число прочитанных строк и страниц. Вариант, который читает 3 тысячи строк вместо 2 миллионов, выигрывает при любой нагрузке.

Если доступа к СУБД нет, сравнивайте варианты в консоли запросов 1С на копии базы: запускайте каждый 3–5 раз подряд и берите среднее без первого запуска.

Переписываем медленные условия: три примера

Пример 1. ИЛИ по разным полям → ОБЪЕДИНИТЬ ВСЕ

Задача: найти документы реализации, где контрагент — заданный или ответственный — заданный сотрудник.

// Медленно на большой таблице: ИЛИ по разным полям
ВЫБРАТЬ
    Р.Ссылка
ИЗ
    Документ.РеализацияТоваровУслуг КАК Р
ГДЕ
    Р.Контрагент = &Контрагент
    ИЛИ Р.Ответственный = &Ответственный
// Быстрее: каждая часть ищет по своему индексу
ВЫБРАТЬ
    Р.Ссылка
ИЗ
    Документ.РеализацияТоваровУслуг КАК Р
ГДЕ
    Р.Контрагент = &Контрагент

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Р.Ссылка
ИЗ
    Документ.РеализацияТоваровУслуг КАК Р
ГДЕ
    Р.Ответственный = &Ответственный
    И НЕ Р.Контрагент = &Контрагент

Здесь НЕ и ИЛИ работают вместе. ОБЪЕДИНИТЬ ВСЕ не убирает дубли, поэтому документ, подходящий под оба условия, попал бы в результат дважды. Отрицание во второй части отсекает такие строки. Это быстрее, чем ОБЪЕДИНИТЬ без ВСЕ, которое сортирует весь результат ради удаления повторов.

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

Вторая часть безопасна и по NULL: у документа реквизит «Контрагент» не NULL, а пустая ссылка. Если вы разбиваете запрос с левым соединением, добавьте во вторую часть проверку ЕСТЬ NULL.

Пример 2. НЕ В с вложенным запросом → левое соединение

Задача: контрагенты, которым за период ничего не продавали.

// Вариант с отрицанием и вложенным запросом
ВЫБРАТЬ
    К.Ссылка
ИЗ
    Справочник.Контрагенты КАК К
ГДЕ
    НЕ К.Ссылка В
        (ВЫБРАТЬ
            Р.Контрагент
        ИЗ
            Документ.РеализацияТоваровУслуг КАК Р
        ГДЕ
            Р.Дата МЕЖДУ &НачалоПериода И &КонецПериода)
// Вариант с левым соединением
ВЫБРАТЬ
    К.Ссылка
ИЗ
    Справочник.Контрагенты КАК К
        ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Р
        ПО Р.Контрагент = К.Ссылка
            И Р.Дата МЕЖДУ &НачалоПериода И &КонецПериода
ГДЕ
    Р.Ссылка ЕСТЬ NULL

Условие по дате стоит в ПО, а не в ГДЕ — иначе снова сработает подвох с NULL из первого раздела. На MS SQL оба варианта часто дают одинаковый план. На PostgreSQL отрицание с подзапросом нередко выполняется заметно дольше. Решайте по плану на своей СУБД.

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

Задача: остатки по всем складам, кроме транзитного.

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

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

Регистр ТоварыНаСкладах взят из УТ 11; в вашей конфигурации имя и набор измерений могут быть другими. Принцип общий: условие в параметре виртуальной таблицы отбирает данные до расчёта остатков, а условие в ГДЕ — после.

Правила для быстрых условий отбора

Проверка условия ГДЕ перед сдачей запроса
Выполнено: 0 / 7

Ещё несколько приёмов, которые помогают на практике:

  1. Сложный фильтр — во временную таблицу. Отберите нужные ссылки отдельным пакетом с ПОМЕСТИТЬ, добавьте ИНДЕКСИРОВАТЬ ПО и соединяйтесь с ней. Так одно громоздкое условие с ИЛИ превращается в соединение.
  2. Не применяйте функции к полю в условии. ГОД(Документ.Дата) = 2025 или ВЫБОР КОГДА ... КОНЕЦ в ГДЕ закрывают дорогу индексу. Пишите диапазоном Дата МЕЖДУ &Начало И &Конец.
  3. Учитывайте ограничения доступа. Если в базе включены RLS-ограничения, платформа добавляет к вашему условию свои. Медленный запрос у пользователя и быстрый у администратора — повод смотреть именно туда.
  4. Не дробите ради дробления. На таблице в 5 тысяч строк сканирование занимает миллисекунды. Переписывать ИЛИ в объединение имеет смысл на таблицах от сотен тысяч строк.

Частые ошибки при отборе через НЕ и ИЛИ

  • ❌ Забытые скобки. И выполняется раньше ИЛИ. Условие А ИЛИ Б И В означает А ИЛИ (Б И В). Отчёт выводит лишние строки, и это не про скорость, а про неверные цифры.
  • ❌ НЕ без скобок. НЕ А И Б — это (НЕ А) И Б, а не НЕ (А И Б). Если имели в виду второе, поставьте скобки явно.
  • ❌ ОБЪЕДИНИТЬ ВСЕ без отсечения пересечений. Суммы в отчёте задваиваются для строк, которые подходят под оба условия.
  • ❌ Пустая ссылка и NULL как одно и то же. Незаполненный реквизит документа — это пустая ссылка, а отсутствие строки в соединении — NULL. Проверка = ЗНАЧЕНИЕ(...ПустаяСсылка) не поймает NULL, а ЕСТЬ NULL не поймает пустую ссылку.
Проверьте себя
Чему равно условие НЕ (А ИЛИ Б)?
Какую запись лучше выбрать вместо Склад = &С1 ИЛИ Склад = &С2?
Пройдёт ли строка с NULL в поле условие НЕ Поле = 1?

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

Есть ли в языке запросов 1С оператор «не равно»?
Да, <>. Запись Поле <> &Значение и НЕ Поле = &Значение равнозначны и по смыслу, и по скорости.
Можно ли писать булево поле в ГДЕ без сравнения с ИСТИНА?
Можно: ГДЕ Документ.Проведен И НЕ Документ.ПометкаУдаления. Сравнение = ИСТИНА ничего не добавляет.
Почему запрос быстрый в файловой базе и медленный на сервере?
Объём и план другие. В копии на 10 тысяч строк сканирование незаметно, в рабочей базе на 5 миллионов — нет. Сравнивайте на данных реального размера.
ОБЪЕДИНИТЬ или ОБЪЕДИНИТЬ ВСЕ при замене ИЛИ?
ОБЪЕДИНИТЬ ВСЕ с отрицанием первого условия во второй части. Просто ОБЪЕДИНИТЬ даёт верный результат, но тратит время на удаление дублей.
Помогает ли индекс при условии ПОДОБНО?
Только если шаблон начинается с текста: ПОДОБНО "Иван%". Шаблон "%Иван%" читает таблицу целиком, с НЕ или без.