Как отобрать данные из кода 1С: запросы, коллекции, списки и регистры
Содержание
Программный отбор в 1С делают четырьмя путями: условием ГДЕ или параметрами виртуальной таблицы в запросе, методом НайтиСтроки() у таблицы значений, элементами Отбор у динамического списка и набора записей, и структурой отбора в методе менеджера Выбрать(). Для данных из базы почти всегда выигрывает запрос. Для данных, которые уже лежат в памяти, — коллекция с индексом.
Примеры кода написаны для платформы 8.3 на встроенном языке: их можно взять в обработку или модуль формы и сразу проверить. Механизмы общие для платформы и не зависят от конфигурации: одинаково работают в БП 3.0, ЗУП 3.1, УТ 11 и самописных базах.
Какой способ отбора выбрать под задачу
Главная ошибка начинающего разработчика — выгрузить всё в память и фильтровать циклом. Работает на тестовой базе, а на рабочей с сотнями тысяч строк превращается в минуты ожидания.
| Где данные | Способ отбора | Когда подходит |
|---|---|---|
| В базе, нужна выборка | Запрос с ГДЕ и параметрами | Почти всегда, основной путь |
| В базе, остатки и обороты | Параметры виртуальной таблицы | Регистры накопления и бухгалтерии |
| Уже в таблице значений | НайтиСтроки(), Скопировать() | Повторные поиски в одной коллекции |
| На форме, в списке | Список.Отбор, Параметры | Ограничить то, что видит пользователь |
| В регистре, по регистратору | НаборЗаписей.Отбор | Прочитать или перезаписать движения |
| Справочник, одно поле | Менеджер.Выбрать(..., Отбор) | Простой обход без запроса |
Отбор в коллекциях: таблица значений, массив, дерево
Когда данные уже получены, отбор идёт средствами самой коллекции. У таблицы значений для этого есть два метода.
НайтиСтроки и Скопировать
НайтиСтроки() принимает структуру, где ключ — имя колонки, значение — искомое значение. Условие только «равно», ключи объединяются через И.
Отбор = Новый Структура("Склад, Активен", Склад, Истина);
НайденныеСтроки = ТаблицаТоваров.НайтиСтроки(Отбор);
Для Каждого Строка Из НайденныеСтроки Цикл
Сообщить(Строка.Номенклатура);
КонецЦикла;
Метод возвращает массив ссылок на строки исходной таблицы. Если изменить поле в найденной строке, оно изменится и в таблице.
Нужна отдельная таблица с отобранными строками — используйте Скопировать() с той же структурой:
ТоварыСклада = ТаблицаТоваров.Скопировать(Новый Структура("Склад", Склад));
// Можно сразу оставить только нужные колонки
ТоварыСклада = ТаблицаТоваров.Скопировать(Новый Структура("Склад", Склад), "Номенклатура, Количество");
Условия сложнее «равно»
Для «больше», «содержит», «одно из» встроенного метода нет. Есть два честных пути:
- 🔹 небольшая коллекция — обход циклом с проверкой условия;
- 🔹 большая таблица — поместить её в запрос через временную таблицу и отобрать языком запросов.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.УстановитьПараметр("Таблица", ТаблицаТоваров);
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &Таблица КАК Т
|;
|ВЫБРАТЬ
| ВТ.Номенклатура,
| ВТ.Количество
|ИЗ
| ВТ_Товары КАК ВТ
|ГДЕ
| ВТ.Количество > 10";
Результат = Запрос.Выполнить().Выгрузить();
Чтобы таблицу значений можно было передать в запрос, у её колонок должны быть указаны типы (Новый ОписаниеТипов("Число") и т. п.). Таблица, собранная без типов колонок, вызовет ошибку при выполнении запроса.
У массива метод Найти() возвращает индекс первого совпадения или Неопределено. У дерева значений НайтиСтроки() принимает второй параметр ВключатьПодчиненные — с Истина поиск идёт по всем уровням.
Условия отбора в тексте запроса
Запрос — основной инструмент. Значения всегда передавайте через параметры, а не склеивайте в текст: так платформа корректно приведёт типы, а текст не сломается от кавычки в наименовании.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ПометкаУдаления
| И НЕ Номенклатура.ЭтоГруппа
| И Номенклатура.Ссылка В ИЕРАРХИИ (&Группа)
| И Номенклатура.Наименование ПОДОБНО &Шаблон";
Запрос.УстановитьПараметр("Группа", ГруппаНоменклатуры);
Запрос.УстановитьПараметр("Шаблон", "%кабель%");
Выборка = Запрос.Выполнить().Выбрать();
Основные операторы, которыми строят отбор в запросе:
| Оператор | Пример | Что делает |
|---|---|---|
=, <>, >, < | Док.Сумма > &Порог | Сравнение |
МЕЖДУ … И … | Док.Дата МЕЖДУ &Начало И &Конец | Диапазон, границы включены |
В (&Список) | Док.Контрагент В (&Контрагенты) | Одно из значений массива или списка |
В ИЕРАРХИИ | Ном.Ссылка В ИЕРАРХИИ (&Группа) | Группа и всё вложенное |
ПОДОБНО | Ном.Артикул ПОДОБНО "А-%" | Шаблон: % — любые символы, _ — один |
ЕСТЬ NULL | Цены.Цена ЕСТЬ NULL | Нет пары при левом соединении |
ССЫЛКА | Рег.Регистратор ССЫЛКА Документ.РеализацияТоваровУслуг | Тип составного поля |
💡 Для периода по дате документа передавайте конец дня через КонецДня(ДатаОкончания). Иначе документы, проведённые днём последней даты, не попадут в отбор: дата 31.03 без времени означает 31.03 00:00:00.
Параметры виртуальных таблиц
У регистров накопления и бухгалтерии отбор по измерениям ставят в параметры таблицы Остатки, Обороты, ОстаткиИОбороты:
"ВЫБРАТЬ
| Ост.Номенклатура,
| Ост.КоличествоОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&Дата, Склад = &Склад) КАК Ост"
Условие в скобках платформа применяет до расчёта остатков. То же условие в ГДЕ после таблицы сначала посчитает остатки по всем складам, а потом выкинет лишнее. Разница на большом регистре — в разы по времени. Имена регистров и измерений в примерах условные: подставьте свои из дерева метаданных.
Сложные условия: ИЛИ, вложенные запросы, соединения
Скобки в ГДЕ работают как в математике. И сильнее ИЛИ, поэтому смешанные условия всегда берите в скобки явно:
"ГДЕ
| Док.Проведен
| И (Док.Контрагент = &Контрагент ИЛИ Док.Ответственный = &Пользователь)"
Отбор по результату другого запроса
Вложенный запрос в В удобен, когда список значений сам вычисляется:
"ГДЕ
| Контрагенты.Ссылка В
| (ВЫБРАТЬ РАЗЛИЧНЫЕ
| Реализация.Контрагент
| ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
| ГДЕ
| Реализация.Дата МЕЖДУ &Начало И &Конец
| И Реализация.Проведен)"
На больших объёмах вложенный запрос лучше заменить временной таблицей с ИНДЕКСИРОВАТЬ ПО и соединением — оптимизатору СУБД так проще построить план.
Отбор «нет записей»
Найти контрагентов без единой реализации — классика через левое соединение и ЕСТЬ NULL:
"ВЫБРАТЬ
| Контрагенты.Ссылка
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Покупатели КАК ВТ
| ПО Контрагенты.Ссылка = ВТ.Контрагент
|ГДЕ
| ВТ.Контрагент ЕСТЬ NULL"
Условие на поле присоединённой таблицы, поставленное в ГДЕ, превращает левое соединение во внутреннее. Если нужно ограничить присоединяемые строки, переносите условие в ПО.
Динамический отбор: условия собираются во время выполнения
Частая задача: пользователь заполнил на форме часть полей, и отбирать надо только по заполненным. Собирать для этого десять вариантов текста не нужно.
Сборка текста запроса из фрагментов
Условия = Новый Массив;
Условия.Добавить("НЕ Док.ПометкаУдаления");
Если ЗначениеЗаполнено(Контрагент) Тогда
Условия.Добавить("Док.Контрагент = &Контрагент");
Запрос.УстановитьПараметр("Контрагент", Контрагент);
КонецЕсли;
Если ЗначениеЗаполнено(Организация) Тогда
Условия.Добавить("Док.Организация = &Организация");
Запрос.УстановитьПараметр("Организация", Организация);
КонецЕсли;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| " + СтрСоединить(Условия, Символы.ПС + " И ");
Пользовательский ввод в текст не попадает: в нём только имена полей, которые вы написали сами, а значения идут параметрами.
Альтернатива — трюк с «выключателем» в одном статичном тексте: (Док.Контрагент = &Контрагент ИЛИ НЕ &ОтборПоКонтрагенту). Текст проще читать, но СУБД хуже использует индексы на таких условиях.
Объект СхемаЗапроса
Для сложных случаев есть объект СхемаЗапроса: текст разбирается в объектную модель, туда добавляются отборы, и схема снова отдаёт текст.
Схема = Новый СхемаЗапроса;
Схема.УстановитьТекстЗапроса(ТекстЗапроса);
Оператор = Схема.ПакетЗапросов[0].Операторы[0];
Оператор.Отбор.Добавить("Док.Контрагент = &Контрагент");
Запрос.Текст = Схема.ПолучитьТекстЗапроса();
Отбор в схеме компоновки данных
В отчётах на СКД отбор задаётся через настройки компоновщика тем же типом элементов, что и у динамического списка (см. следующий раздел): КомпоновщикНастроек.Настройки.Отбор.Элементы.Добавить(Тип("ЭлементОтбораКомпоновкиДанных")).
Отбор в динамическом списке, наборах записей и выборках менеджера
Динамический список на форме
Отбор, установленный из кода, пользователь не видит в своих настройках и не может снять:
&НаСервере
Процедура УстановитьОтборПоКонтрагенту(Контрагент)
Список.Отбор.Элементы.Очистить();
Элемент = Список.Отбор.Элементы.Добавить(Тип("ЭлементОтбораКомпоновкиДанных"));
Элемент.ЛевоеЗначение = Новый ПолеКомпоновкиДанных("Контрагент");
Элемент.ВидСравнения = ВидСравненияКомпоновкиДанных.Равно;
Элемент.ПравоеЗначение = Контрагент;
Элемент.Использование = Истина;
КонецПроцедуры
Для условия «ИЛИ» сначала добавьте группу типа ГруппаЭлементовОтбораКомпоновкиДанных с ТипГруппы = ТипГруппыЭлементовОтбораКомпоновкиДанных.ГруппаИли, а элементы кладите уже в её Элементы.
В типовых конфигурациях на БСП то же делается одной строкой:
ОбщегоНазначенияКлиентСервер.УстановитьЭлементОтбораДинамическогоСписка(
Список, "Контрагент", Контрагент, ВидСравненияКомпоновкиДанных.Равно, , Истина);
Функция находит существующий элемент по полю и обновляет его, а не плодит дубли при повторном вызове. Если у списка произвольный запрос с параметром &Контрагент, проще задать значение так: Список.Параметры.УстановитьЗначениеПараметра("Контрагент", Контрагент).
Набор записей регистра
НаборЗаписей = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(ДокументСсылка);
НаборЗаписей.Прочитать();
Отбор у набора задаёт не только что прочитать, но и что перезаписать. Записать() с установленным отбором заменит все записи, попадающие под него. Набор без отбора у регистра сведений при записи затронет весь регистр.
Методы менеджера
Справочники.Имя.Выбрать(Родитель, Владелец, Отбор, Порядок) и Документы.Имя.Выбрать(НачалоИнтервала, КонецИнтервала, Отбор, Порядок) принимают отбор структурой с одним ключом — и только по индексированному реквизиту:
Выборка = Справочники.Номенклатура.Выбрать(, , Новый Структура("Артикул", "А-1001"));
Пока Выборка.Следующий() Цикл
// Выборка.Ссылка, Выборка.Наименование
КонецЦикла;
Для поиска одного элемента есть НайтиПоКоду(), НайтиПоНаименованию(), НайтиПоРеквизиту(). Они возвращают первую найденную ссылку или пустую ссылку, если ничего нет. При дублях в справочнике результат непредсказуем — проверяйте уникальность запросом.
Как ускорить отбор: индексы и кэширование
Индекс у таблицы значений
Без индекса НайтиСтроки() перебирает таблицу целиком. В цикле на 10 000 строк × 10 000 поисков это 100 млн сравнений. Индекс по полям отбора убирает перебор:
ТаблицаЦен.Индексы.Добавить("Номенклатура, ВидЦены");
// Дальше НайтиСтроки по этим двум полям идёт по индексу
Индекс помогает, когда набор полей в структуре отбора совпадает с полями индекса. Добавлять индекс ради одного поиска нет смысла — построение тоже стоит времени.
Индексы в базе
- 🔹 у реквизита справочника или документа свойство
Индексироватьв конфигураторе — для полей, по которым часто отбирают; - 🔹 у временной таблицы —
ИНДЕКСИРОВАТЬ ПО ПолепослеПОМЕСТИТЬ, если по ней будет соединение; - 🔹 измерения регистров индексируются платформой, поэтому отбор по первым измерениям быстрее, чем по последним.
Кэширование повторных отборов
Если одно и то же значение запрашивается сотни раз за сеанс (константа, настройка организации), вынесите функцию в общий модуль с Повторное использование возвращаемых значений = На время сеанса или На время вызова. Платформа запомнит результат по набору параметров. Возвращайте из таких функций неизменяемые значения (ФиксированнаяСтруктура, ФиксированныйМассив), иначе вызывающий код испортит кэш.
Для разовой обработки вместо кэша обычно достаточно получить все нужные данные одним запросом до цикла и искать по таблице с индексом.
Поиск по тексту: ПОДОБНО, полнотекстовый поиск и регулярные выражения
ПОДОБНО в запросе
Подходит для «содержит» и «начинается с». Условие с % в начале шаблона индекс не использует, на больших справочниках это заметно. Если в искомой строке могут быть % или _, экранируйте их и укажите символ: ПОДОБНО &Шаблон СПЕЦСИМВОЛ "~".
Полнотекстовый поиск
Работает по словам и словоформам, а не по подстроке. Объект должен быть включён в полнотекстовый поиск в конфигураторе, а индекс — обновлён.
СписокПоиска = ПолнотекстовыйПоиск.СоздатьСписок("договор поставки", 50);
СписокПоиска.ПерваяЧасть();
Для Каждого Найденное Из СписокПоиска Цикл
Сообщить(Найденное.Значение);
КонецЦикла;
ПерваяЧасть() возвращает первую порцию результатов, размер задаётся вторым параметром СоздатьСписок(). Следующие порции — СледующаяЧасть().
Регулярные выражения
Классический способ на Windows — COM-объект VBScript.RegExp. Он работает только там, где код выполняется на Windows: на Linux-сервере его нет.
РегВыр = Новый COMОбъект("VBScript.RegExp");
РегВыр.Pattern = "^\d{10}(\d{2})?$"; // ИНН из 10 или 12 цифр
Если Не РегВыр.Test(СтрокаИНН) Тогда
Сообщить("ИНН введён в неверном формате");
КонецЕсли;
В свежих версиях платформы 8.3 появились встроенные функции для регулярных выражений. Если ваша версия их поддерживает (проверьте синтаксис-помощник по префиксу Стр), используйте их вместо COM — код станет кроссплатформенным.
Ошибки, на которых спотыкаются чаще всего
- ❌ Отбор циклом по выборке всего справочника. Условие переносится в
ГДЕ— и СУБД отдаёт только нужное. - ❌ Значение склеено в текст запроса. Наименование с кавычкой ломает запрос, дата превращается в строку. Только параметры.
- ❌ Разыменование через точку в условии.
Док.Контрагент.ГоловнойКонтрагент = &Хна составном поле порождает соединения со всеми возможными таблицами. Уточните тип черезВЫРАЗИТЬ(… КАК Справочник.Контрагенты). - ❌ Длинный массив в
В (&Список). Тысячи значений раздувают текст запроса к СУБД. Поместите их во временную таблицу и соединитесь с ней. - ❌ Отбор набора записей не установлен перед записью. Вместо движений одного документа можно переписать гораздо больше записей.
- ❌
НайтиСтроки()по полю с другим типом. Строка"100"не равна числу100, и поиск вернёт пустой массив без ошибки.