Часто в практике разработки на 1С возникает задача сопоставления данных из двух Таблиц Значений (ТЗ). Проанализируем классическую ситуацию: у нас есть первая таблица с затратами (колонки: счет затрат, статья, сумма, коррсчет) и вторая таблица с правилами распределения (колонки: раздел, статья, коррсчет затрат). Нам необходимо найти правило для каждой строки затрат. При этом алгоритм поиска должен быть приоритетным: сначала мы ищем точное совпадение по ключу Статья + Коррсчет, а если такого правила нет, используем резервное совпадение только по ключу Статья.
Многие разработчики сразу задумываются о переборе в цикле, однако это не самый оптимальный подход. Рассмотрим подробнее, как реализовать такую логику элегантно и быстро с помощью запросов и объектной модели 1С.
Разберем по шагам решение задачи с использованием языка запросов. Основная идея заключается в том, чтобы поместить обе таблицы во временные таблицы (ВТ) базы данных, а затем соединить основную таблицу затрат с таблицей правил дважды: первый раз по полному ключу, второй раз — по частичному.
Посмотрим на пример реализации такого запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПравилаФормированияФХДСтатьиЗатрат.Ссылка КАК Ссылка,
| ПравилаФормированияФХДСтатьиЗатрат.СтатьяЗатрат КАК СтатьяЗатрат,
| ПравилаФормированияФХДСчетУчета.СчетУчета КАК СчетУчета
|ПОМЕСТИТЬ ВТПравила
|ИЗ
| Справочник.ПравилаФормированияФХД.СтатьиЗатрат КАК ПравилаФормированияФХДСтатьиЗатрат
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.ПравилаФормированияФХД.СчетУчета КАК ПравилаФормированияФХДСчетУчета
| ПО ПравилаФормированияФХДСтатьиЗатрат.Ссылка = ПравилаФормированияФХДСчетУчета.Ссылка
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Источник.Счет КАК Счет,
| Источник.СтатьяЗатрат КАК СтатьяЗатрат,
| Источник.КоррСчет КАК КоррСчет,
| Источник.СуммаОборотДт КАК СуммаОборотДт
|ПОМЕСТИТЬ ВТЗатраты
|ИЗ
| &ВнешнийИсточник КАК Источник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТЗатраты.СтатьяЗатрат КАК СтатьяЗатрат,
| ВТЗатраты.СуммаОборотДт КАК СуммаОборотДт,
| ЕСТЬNULL(ВТПравила.Ссылка, ВТПравила1.Ссылка) КАК Правило
|ИЗ
| ВТЗатраты КАК ВТЗатраты
| ЛЕВОЕ СОЕДИНЕНИЕ ВТПравила КАК ВТПравила
| ПО ВТЗатраты.СтатьяЗатрат = ВТПравила.СтатьяЗатрат
| И ВТЗатраты.КоррСчет = ВТПравила.СчетУчета
| ЛЕВОЕ СОЕДИНЕНИЕ ВТПравила КАК ВТПравила1
| ПО ВТЗатраты.СтатьяЗатрат = ВТПравила1.СтатьяЗатрат
| И ВТПравила1.СчетУчета = ЗНАЧЕНИЕ(ПланСчетов.Хозрасчетный.ПустаяСсылка)";
Что здесь происходит? Мы используем функцию ЕСТЬNULL. Она проверяет: если первое соединение (ВТПравила, где совпали и статья, и счет) вернуло пустой результат (NULL), то платформа возьмет значение из второго соединения (ВТПравила1, где совпала только статья).
Важный нюанс: защита от задвоения строк. Обратите внимание на условие второго левого соединения: И ВТПравила1.СчетУчета = ЗНАЧЕНИЕ(ПланСчетов.Хозрасчетный.ПустаяСсылка). Если его не добавить, запрос присоединит к одной строке затрат все доступные правила для данной статьи, что приведет к эффекту "декартова произведения" (строки затрат задвоятся или затроятся). Мы явно указываем, что резервное правило — это то, у которого коррсчет не заполнен.
Обязательная типизация: Передавая Таблицу Значений как параметр &ВнешнийИсточник, обязательно убедитесь, что все колонки в ТЗ имеют строгое Описание Типов. Иначе запрос завершится ошибкой при попытке создать временную таблицу.
Рассмотрим альтернативный способ внутри запроса, который избавляет нас от двойного левого соединения. Мы можем добавить вычисляемое поле с приоритетом при формировании временной таблицы правил:
ВЫБОР
КОГДА СчетУчета <> ЗНАЧЕНИЕ(ПланСчетов.Хозрасчетный.ПустаяСсылка) ТОГДА 1
ИНАЧЕ 2
КОНЕЦ КАК Приоритет
При таком подходе мы соединяем таблицы только один раз по ключу СтатьяЗатрат. В результате к одной строке затрат может прикрепиться несколько правил (с приоритетом 1 и приоритетом 2). Чтобы оставить только самое подходящее, мы используем группировку и отбираем строку с МИНИМУМ(Приоритет). Этот метод делает текст запроса более читаемым при увеличении количества условий поиска.
Проанализируем ситуацию, когда объемы данных небольшие. Инициализация менеджера временных таблиц и выполнение запроса могут оказаться избыточными. В таком случае эффективнее использовать встроенный платформенный метод НайтиСтроки.
Реализуем алгоритм последовательного поиска через структуру:
СтруктураПоиска = Новый Структура("СтатьяЗатрат, Коррсчет");
Для Каждого СтрокаЗатрат Из ТЗЗатраты Цикл
// Попытка 1: Ищем по двум ключам
СтруктураПоиска.СтатьяЗатрат = СтрокаЗатрат.СтатьяЗатрат;
СтруктураПоиска.Коррсчет = СтрокаЗатрат.Коррсчет;
НайденныеПравила = ТЗПравила.НайтиСтроки(СтруктураПоиска);
// Попытка 2: Если не нашли, ищем только по статье
Если НайденныеПравила.Количество() = 0 Тогда
СтруктураПоиска.Удалить("Коррсчет"); // Убираем счет из условий
НайденныеПравила = ТЗПравила.НайтиСтроки(СтруктураПоиска);
// Возвращаем ключ обратно для следующей итерации цикла
СтруктураПоиска.Вставить("Коррсчет");
КонецЕсли;
Если НайденныеПравила.Количество() > 0 Тогда
// Берем первое найденное правило
НужноеПравило = НайденныеПравила[0];
СтрокаЗатрат.Правило = НужноеПравило.Ссылка;
КонецЕсли;
КонецЦикла;
Оптимизация объектного поиска: Чтобы метод НайтиСтроки работал максимально быстро и не сканировал всю таблицу линейно, перед началом цикла мы обязательно должны проиндексировать колонки поиска. Выполним следующую команду:
ТЗПравила.Индексы.Добавить("СтатьяЗатрат, Коррсчет");
Создание индекса выстраивает бинарное дерево значений в оперативной памяти сервера 1С, благодаря чему поиск даже по таблицам на десятки тысяч строк происходит практически мгновенно.
Подводя итог, можно сказать, что выбор между запросом и объектной моделью зависит от контекста вашей задачи. Если данные в дальнейшем участвуют в сложных выборках базы данных — смело используйте Временные Таблицы и функцию ЕСТЬNULL. Если же задача решается в модуле обработки на уже сформированных коллекциях — проиндексируйте таблицу и примените метод НайтиСтроки со структурой ключей.