SQL оператор IN( ..list.. ) для 1С

30.09.25

База данных - HighLoad оптимизация

Возможные проблемы быстродействия при использовании SQL оператора IN( ..list.. ) для 1С.

Введение

Недавно обсуждали с коллегой использование условия Where … IN( ..list.. ). Пример запроса 1С ниже, но оператор IN( ..list.. ) может использоваться в любом контексте, необязательно виртуальная таблица остатков. Запрос написан для УТ, без ограничения общности. Поведение запроса зависит от СУБД, есть разные варианты.

 
 Мировая паутина предупреждает...

 

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

Запрос.УстановитьПараметр("ВсяНоменклатура", ВсяНоменклатура);
РезультатЗапроса = Запрос.Выполнить();

СУБД MS SQL

В документации написано: Явное включение очень большого количества значений (много тысяч значений, разделенных запятыми) в круглые скобки в предложение IN может привести к интенсивному расходованию ресурсов и возврату ошибки 8623 (out of internal resources) или 8632 (An expression services limit has been reached). Чтобы избежать этой проблемы, храните элементы списка IN в таблице и используйте вложенный запрос SELECT в предложении IN.

Пока искал информацию, вспомнил, что Виктор Богачев что-то рассказывал нам об этом операторе, но нигде не смог найти подробности. Награда  ждЁт // того, кто найдЁт! (С)

Посмотрим, как обрабатывается код 1С из примера в технологическом журнале logcfg.xml, события DBMSSQL и SDBL для СУБД MS SQL. Сначала выполним запрос для массива &ВсяНоменклатура 150 элементов, потом сделаем тоже для 100 элементов.

 
Текст logcfg.xml
 
 фрагменты журнала

При сравнении событий SDBL видим одинаковые фрагменты Fld7882 IN (***СПИСОК ...***) которые соответствуют фрагменту запроса Номенклатура В (&ВсяНоменклатура). Программа 1С:Предприятие передает запрос в СУБД одинаково для массива 100 элементов и массива 150 элементов.

Однако, при сравнении событий DBMSSQL видим изменения:

Для 100 элементов СУБД использует массив

T2._Fld7882RRef IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

Для 150 элементов СУБД использует временную таблицу #tt31.

T2._Fld7882RRef IN (SELECT ... FROM #tt31 ...)

Наконец, вспомнил! Виктор Богачев рассказывал нам, что массивы больше 128 элементов преобразуются во временные таблицы. В событии SDBL используется массив, в событии DBMSSQL используется временная таблица для массива 150 элементов. Версия MS SQL Server 2016.

СУБД PostgreSQL

Посмотрим, как обрабатывается код 1С из примера в технологическом журнале logcfg.xml, события DBPOSTGRS. Размер массива 1500 элементов. Версия СУБД postgresql_13.21_1.1C_x64. Пробуйте на других версиях, мне интересно. 

 
 текст logcfg.xml
 
 фрагмент журнала

Видно, что СУБД PostgreSQL использует массив T2._Fld37189RRef IN ( ***СПИСОК 1500*** ), использует Nested Loop.
СУБД ORACLE

В документации указано внутреннее ограничение Oracle на максимальное значение в 1000 значений в списке.

Заключение

Мы обсудили, как разные СУБД обрабатывают запрос с оператором IN( ..list.. ). Нужно учитывать, что при большом объеме списка проблемой может стать не только выполнение запроса, не использование в плане запроса индекса, но и передача большого объема данных list на сервер СУБД. Использование подзапроса из индексированной таблицы вместо list выглядит более надежным.

Некоторые крутые вещи, которые мы слышим на вебинарах, приходят к сознательному пониманию гораздо позже )).

Дополнение от 02/10/2025

Проверил две гипотезы на примере списка 150 элементов.

(1) Временная таблица #tt1 создана платформой 1С, команда INSERT INTO, метка времени 51:43.656000. Очищена платформой 1С, команда truncate table, метка времени 51:43.719003

(2) Соединение временной таблицы #tt1 (псевдоним T3) с таблицей AccumRgT7888 (псевдоним T2) происходит оператором Nested Loops, без использования индекса. Оператор Stream Aggregate применяется для группировки.

 
 фрагмент журнала

 

Спасибо всем за толковые комментарии.

Вступайте в нашу телеграмм-группу Инфостарт

См. также

HighLoad оптимизация Программист 1С v8.3 1C:ERP Бесплатно (free)

Приведем примеры использования различных в динамических списках и посмотрим, почему это плохо.

18.02.2025    6728    ivanov660    39    

61

HighLoad оптимизация Технологический журнал Системный администратор Программист Бесплатно (free)

Обсудим поиск и разбор причин длительных серверных вызовов CALL, SCALL.

24.06.2024    9259    ivanov660    13    

62

HighLoad оптимизация Программист 1С v8.3 Бесплатно (free)

Метод очень медленно работает, когда параметр приемник содержит намного меньше свойств, чем источник.

06.06.2024    14908    Evg-Lylyk    73    

45

HighLoad оптимизация Программист 1С v8.3 1C:Бухгалтерия Бесплатно (free)

Анализ простого плана запроса. Оптимизация нагрузки на ЦП сервера СУБД используя типовые индексы.

13.03.2024    7277    spyke    29    

53

HighLoad оптимизация Программист 1С v8.3 Бесплатно (free)

Оказывается, в типовых конфигурациях 1С есть, что улучшить!

13.03.2024    10494    vasilev2015    22    

45

HighLoad оптимизация Инструменты администратора БД Системный администратор Программист 1С v8.3 1C:Бухгалтерия Абонемент ($m)

Обработка для простого и удобного анализа настроек, нагрузки и проблем с SQL сервером с упором на использование оного для 1С. Анализ текущих запросов на sql, ожиданий, конвертация запроса в 1С и рекомендации, где может тормозить.

5 стартмани

15.02.2024    17565    327    ZAOSTG    100    

123
Вознаграждение за ответ
Показать полностью
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. paulwist 01.10.25 14:44 Сейчас в теме
Для 100 элементов СУБД использует массив

T2._Fld7882RRef IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

Для 150 элементов СУБД использует временную таблицу #tt31.

T2._Fld7882RRef IN (SEL ECT ... FR OM #tt31 ...)


Эээ, а причём тут СУБД MSSQL, на основании чего вы сделали такой вывод??
2. vasilev2015 2813 01.10.25 14:48 Сейчас в теме
(1) на основании текста события DBMSSQL, фрагмент журнала приложен.
3. paulwist 01.10.25 14:54 Сейчас в теме
(2)
на основании текста события DBMSSQL, фрагмент журнала приложен.


Хех, вы полагаете, что СУБД самостоятельно создало времянку+заполнило её какими-то данными???

Можете привести ссылку из документации вендора, что MSSQL так умеет делать?

PS Попробуйте в профайлере трассирнуть кусок с IN > 150 значений и посмотрите какой логин/УЗ создаёт временную таблицу, ... обнаружите, что это сделал юзер от имени которого запущен сервис 1С.
4. vasilev2015 2813 01.10.25 15:15 Сейчас в теме
(3) В публикации не указано, что СУБД создает временную таблицу самостоятельно. Скорее всего временную таблицу создает платформа 1С. Спасибо за уточнение. Присылайте трассировку.
5. starik-2005 3192 01.10.25 16:13 Сейчас в теме
На стековерфлоу вопрос о том, использует ли IN индекс:
Обычно так и происходит, если только предложение IN не охватывает слишком большую часть таблицы, и тогда выполняется сканирование таблицы. Лучший способ выяснить это в вашем конкретном случае — запустить запрос в анализаторе запросов и проверить план выполнения.
Т.е. если у вас есть организация, и по ней есть индекс, то все упирается в то, насколько много организаций. Если там три организации и по каждой миллион записей, то какой смысл использовать индекс? Так что и "В" и "=" не будут использовать индекс. А если это ID (ссылка), то индекс будет использоваться - это как бы само собой разумеется. И нужно понимать, что в жизни куда больше случаев с вариантами между этими двумя, и по ним ясность наступает только при выполнении запроса. Т.е. для поля с высокой селективностью будет юзаться индекс, а для поля с низкой и при "=" он скорее всего юзаться не будет. Т.е. системе во втором случае что индекс целиком вычитывать, а потом таблицу, что сразу таблицу - разницы мало.
6. vasilev2015 2813 01.10.25 16:19 Сейчас в теме
(5) Если внутри IN( ... ) запрос из индексированной таблицы - скорее всего, индекс будет использоваться.
Если внутри IN( ... ) список (массив) - скорее всего, индекс НЕ будет использоваться.
7. starik-2005 3192 01.10.25 16:33 Сейчас в теме
(6) Есть несколько вариантов, как найти значения в списке. Самый простой и незамысловатый - просто пройтись по всем записям таблицы и сравнить с искомым. Если у нас есть 1500 значений в списке и миллион значений в таблице, то для этого нужно миллион умножить на 1500 = полтора лярда. Это, предположу, долго. Но умные людишки придумали хеш-таблицы, бинарный поиск, может быть даже что-то еще. Хеш-таблица - это такое соответствие. На него некоторым образом указывает "HashAggregate (cost=18.00..33.00 rows=1500 width=32) (actual time=0.694..1.041 rows=1500 loops=1)". Тут 1500 строк и 1 цикл.
Обычный индекс в СУБД - это бинарное дерево, по нему значений ищется за log 2 N - это, предположу, ваш index scan. В итоге данные, которые были засканированы в субд, искались с этой хеш-таблицы.
План запроса для мелкомягких я не обнаружил, но могу предположить, что все примерно также, за исключением ограничения самого мелкомягкого скуля на количество значений в IN.
vasilev2015; +1 Ответить
8. vasilev2015 2813 01.10.25 16:37 Сейчас в теме
(7) Поддерживаю, согласен.
9. starik-2005 3192 01.10.25 16:47 Сейчас в теме
(8) Там неточность. Скан индекса вычитывает весь индекс, а потом каждую его запись с гуидом номенклатуры ищет в хеш-таблице. В итоге O(N * 1..Log2(1500)). Но все-равно это все достаточно быстро. Можем ли мы сказать, что индекс тут не используется? Ну, наверное, не можем. Можем ли мы сказать, что индекс тут используется неоптимально? Конечно, т.к. мы тут не ищем в индексе за Log 2 N, а ищем все значения из него в хеш-таблице. С другой стороны, если искать 1500 значений в индексе, то стоит их предварительно упорядочить, тогда это будет быстрее, т.к. индекс - двоичное дерево. В итоге будет О(1500 * Log 2 N'), где N' - это срезы правее предыдущего найденного. Быстрее ли это, чем O(N * 1..Log2(1500))? Я не знаю )))
10. vasilev2015 2813 01.10.25 16:55 Сейчас в теме
(9) Найду план запроса MS SQL, посмотрим вместе. Мне тоже интересно стало ))
15. vasilev2015 2813 03.10.25 08:35 Сейчас в теме
(9) Добавил более подробный план запроса для MS SQL в статью. В этом частном случае индекс не используется.
16. paulwist 03.10.25 11:25 Сейчас в теме
(15)
В этом частном случае индекс не используется.


Ммм, а индекс на какой таблице не используется?? Что-то я не увидел Table/Clustered Index Scan.
17. vasilev2015 2813 03.10.25 11:32 Сейчас в теме
(16) По моему мнению, здесь оператор IN реализован через внутреннее соединение. Индекс не используется при соединении таблиц. Операторы Nested Loops не используют индекс. Псевдонимы таблиц T2, T3.
19. paulwist 03.10.25 12:48 Сейчас в теме
(17)
Операторы Nested Loops не используют индекс.


Ну давайте разберём.

1. Подзапрос из временной таблички Т3 (#tt1), сначала ищутся значения IsNotNull с использованием поиска по кластерному индексу. (тут конечно, есть некоторый нюанс, физически идёт сканирование кластерного индекса, на это указывает ORDERED FORWARD - есть такая особенность в показе плана MSSQL. Почему сканирование, потому что IsNotNull низко селективное/кардинальное значение)
Результат помещается в буферный кэш.

|--Clustered Index Seek(OBJECT:([tempdb].[dbo].[#tt1] AS [T3]), SEEK:([T3].[_Q_000_F_000RRef] IsNotNull) ORDERED FORWARD)


2. Поскольку оператор IN (подзапрос), то оптимизатор убирает дублирующие значения из получившегося набора данных/буферного кэша из п.1

|--Stream Aggregate(GROUP BY:([T3].[_Q_000_F_000RRef]))


3. По значениям из буферного кэша, ищутся соответствия (через NL) в таблице Т2 (_AccumRgT7888]), причём поиск осуществляется по/в индексу (тут опять наблюдаем ORDERED FORWARD - это говорит, что идёт сканирование индекса. Почему, тут два ответа либо низкая кардинальность, либо значения лежат подряд и достаточно отсканировать страницы индекса один раз спустившись по дереву, либо сканирование индекса дешевле хождению по дереву до листьев)

Index Seek(OBJECT:([].[dbo].[_AccumRgT7888].[_AccumR7888_ByDims7887_TR] AS [T2]), SEEK:([T2].[_Period]=[@P1] AND [T2].[_Fld7882RRef]=[tempdb].[dbo].[#tt1].[_Q_000_F_000RRef] as [T3].[_Q_000_F_000RRef]ORDERED FORWARD)


4. Ну и заключительный NL - это по данным полученным в п.3 лезем уже в саму таблицу (кластерный индекс) за недостающими для запроса полями

|--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[_Period], [T2].[_Fld7881RRef], [T2].[_Fld7882RRef], [T2].[_Fld7883RRef], [T2].[_Fld7884RRef],[T2].[_Fld7885RRef], [Expr1011]) WITH ORDERED PREFETCH)
....
|--Clustered Index Seek


Собственно всё. Как видим поиск в индексах при NL используется.
18. starik-2005 3192 03.10.25 11:52 Сейчас в теме
(15)
В этом частном случае индекс не используется.
А что в данном случае "Индекс"?
150, 1, 150, 0.00313, 0.000322, 23, 0.00345, 1,                       |    |    |--Clustered Index Seek(OBJECT:([tempdb].[dbo].[#tt1] AS [T3]), SEEK:([T3].[_Q_000_F_000RRef] IsNotNull) ORDERED FORWARD)
4, 150, 59.3, 0.00313, 0.000222, 95, 0.129, 39.4,                       |    |--Index Seek(OBJECT:([].[dbo].[_AccumRgT7888].[_AccumR7888_ByDims7887_TR] AS [T2]), SEEK:([T2].[_Period]=[@P1] AND [T2].[_Fld7882RRef]=[tempdb].[dbo].[#tt1].[_Q_000_F_000RRef] as [T3].[_Q_000_F_000RRef]) ORDERED FORWARD)
2, 4, 2.2E+003, 0.00313, 0.000158, 20, 7.66, 2.34E+003,                       |--Clustered Index Seek(OBJECT:([].[dbo].

Ну и что тут у нас?
Ищем в кластерном индексе 150 наших записей во временной таблице. Получаем упорядоченный список. Ищем эти 150 значений в кластерном индексе по соединению регистра.
Т.е. получили записи из регистра и записи из временной таблицы, по всей видимости уже упорядоченные (ORDERED FORWARD, т.е. ищем уже по упорядоченному списку, поэтому получаем упорядоченные данные).
Дальше ищем 150 записей в записях регистра, чтобы соединить их. Ищем в цикле. Но как ищем? Если по упорядоченному списку, то двоичный поиск, т.е. log 2 N - это так же быстро, как по индексу, только еще быстрее, т.к. это не нужно читать из базы - из базы мы по кластерному индексу все извлекли.
Т.е.:
1. Прочитали из базы по кластерному индексу.
2. Соединили прочитанные выборки через поиск в цикле. Но это не значит, то для поиска мы использовали длительный алгоритм перебора всех записей - у нас списки упорядоченные.

В общем не стоит считать, что nested loop - это плохо. Ведь то же соединение - это поиск данных из левой таблицы в правой. Если в обоих таблицах данные упорядочены, то мы можем идти по первой и второй таблице только вперед. Получается эффективно - O(N+M), если индекс есть только в правой таблице, то O(N * Log 2 M). Если нет в обоих, то O(N * M). И скульный сервер такие шарады решать умеет хорошо. И два последних варианта - это поиск в цикле. При том скул для последнего варианта легко может создать хеш-таблицу для поиска, в итоге все сведется к еще более эффективному O(N * 1..Log 2 N).
12. paulwist 02.10.25 09:33 Сейчас в теме +1 $m
(6)
Если внутри IN( ... ) запрос из индексированной таблицы - скорее всего, индекс будет использоваться.
Если внутри IN( ... ) список (массив) - скорее всего, индекс НЕ будет использоваться.


Что гадать-то, будет-не будет.

Вот репро-код, когда IN (список) используется индекс и дешевле чем IN (sel ect)

use tempdb
go
cre ate   table #t (id int, name char(100) )
cre ate   index idx_id on #t (id)

cre ate   table #tIN (id int , name char(100) )
cre ate   index idx_id on #tIN (id)

declare @i int = 0
while @i < 3000
begin
	ins ert into #t (id) values (@i)
	set @i = @i + 1
end

ins ert into #tIN (id) values (2), (3)

set statistics io on
go
sel ect * fr om #t t where t.id in (2, 3) 

sel ect * fr om #t t where t.id in (select id fr om #tIN) 
se t statistics io off
go

dr op   table #t
dr op   table #tIN

Показать


Ну и статистика
IN (список) = 6 страниц прочитано
IN (select) = 6+2=8 страниц прочитано

(2 rows affected)
Таблица "#t Сканирований 2, логических операций чтения 6

(1 row affected)

(2 rows affected)
Таблица "#t. Сканирований 2, логических операций чтения 6
Таблица "#tIN Сканирований 1, логических операций чтения 2

(1 row affected)
Показать


Планы, сверху когда IN (список), снизу IN (sele ct).
Прикрепленные файлы:
14. vasilev2015 2813 02.10.25 10:03 Сейчас в теме
(12) Аплодирую стоя за такой подробный пост. Но перейти от частных случаев к общему правилу иногда бывает очень трудно логически.
11. PerlAmutor 160 01.10.25 19:23 Сейчас в теме
Осталось теперь убедить обычных пользователей не настраивать отборы в динамических списках и СКД отчетах, куда помещаются все документы из таблицы по условию "В Списке()". Я с таким сталкивался, пользователь добавил 20к ссылок на документы по условию "НЕ В Списке()". Там ни то чтобы отчет долго формировался, там сама форма отчета открывалась несколько минут.
vasilev2015; +1 Ответить
13. vasilev2015 2813 02.10.25 09:57 Сейчас в теме
(11) "Если безобразие нельзя предотвратить, его нужно возглавить!" (С)

Сделать, заполнить регистр ИсключенияСписка и поставить условие в запрос
Левое Соединение РегистрСведений.ИсключенияСписка КАК ИсключенияСписка
....
Где ИсключенияСписка.Документ is null

???
Для отправки сообщения требуется регистрация/авторизация