Как соединить две таблицы в запросе 1С с распределением количества (задача распределения партий)?

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

В практике разработки на платформе 1С часто возникает классическая задача: у нас есть две несвязанные напрямую таблицы, содержащие информацию об одном и том же товаре, но в разных разрезах. Например, в первой таблице (Т1) хранится информация о странах происхождения товаров, а во второй таблице (Т2) — о местах их хранения (складах). Нам необходимо «поженить» эти таблицы так, чтобы распределить количество товара из одной таблицы по аналитикам другой. Это типичная задача распределения партий, остатков (ФИФО) или упрощенный вариант транспортной задачи.

Проанализируем пример из темы форума:

Таблица 1 (Источники / Страны):

Таблица 2 (Получатели / Склады):

В результате мы должны получить детальную таблицу, в которой 5 испанских и 2 французские груши будут корректно распределены между складом (4 шт.) и магазином (3 шт.). Давайте разберем по шагам, какими методами можно решить эту задачу в 1С: с помощью запроса и с помощью программного кода.

Метод 1. Распределение в запросе через нарастающие итоги (Метод пересечения интервалов)

Если решать задачу исключительно средствами СУБД (в запросе), то простое левое соединение по номенклатуре нам не подойдет — оно приведет к декартову произведению строк и неверному расчету итогового количества. Чтобы обойти это ограничение, применим математический метод пересечения интервалов на числовой оси.

Представим количество каждого товара в виде непрерывной линейки отрезков. Для каждой строки нам нужно рассчитать границы «от» и «до» относительно общего количества этого товара.

Разберем это на примере груш:

  1. Строим интервалы для Таблицы 1 (Страны):
    • Испания (5 шт.): начинается с 0, заканчивается на 5. Интервал: [0, 5].
    • Франция (2 шт.): идет следом за Испанией. Начинается с 5, заканчивается на 7 (5 + 2). Интервал: [5, 7].
  2. Строим интервалы для Таблицы 2 (Склады):
    • Склад (4 шт.): начинается с 0, заканчивается на 4. Интервал: [0, 4].
    • Магазин (3 шт.): начинается с 4, заканчивается на 7 (4 + 3). Интервал: [4, 7].

Теперь мы можем соединить эти таблицы в запросе по условию пересечения их интервалов:

Т1.КоличествоОт < Т2.КоличествоДо И Т1.КоличествоДо > Т2.КоличествоОт

Распределенное количество для каждой получившейся строки рассчитывается как длина пересечения этих отрезков по формуле:

Минимум(Т1.КоличествоДо, Т2.КоличествоДо) - Максимум(Т1.КоличествоОт, Т2.КоличествоОт)

Рассмотрим, как будет выглядеть реализация расчета интервалов и нарастающих итогов в запросе 1С. Для этого нам понадобится пронумеровать строки таблиц или использовать уникальные ключи связи, чтобы рассчитать сумму «предыдущих» строк.


// Предположим, что Т1 и Т2 уже загружены во временные таблицы ВТ_Т1 и ВТ_Т2
// Каждая ВТ содержит поля: Номенклатура, Количество, Характеристика (Страна/Склад) и НомерСтроки для расчета итога

ВЫБРАТЬ
    Т1.Номенклатура КАК Номенклатура,
    Т1.Характеристика КАК Страна,
    Т1.Количество КАК КоличествоТ1,
    ЕСТЬNULL(СУММА(Т1_Пред.Количество), 0) КАК КоличествоОт,
    ЕСТЬNULL(СУММА(Т1_Пред.Количество), 0) + Т1.Количество КАК КоличествоДо
ПОМЕСТИТЬ ВТ_Т1_Интервалы
ИЗ
    ВТ_Т1 КАК Т1
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Т1 КАК Т1_Пред
        ПО Т1.Номенклатура = Т1_Пред.Номенклатура
        И Т1.НомерСтроки > Т1_Пред.НомерСтроки
СГРУППИРОВАТЬ ПО
    Т1.Номенклатура,
    Т1.Характеристика,
    Т1.Количество,
    Т1.НомерСтроки
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Т2.Номенклатура КАК Номенклатура,
    Т2.Характеристика КАК Склад,
    Т2.Количество КАК КоличествоТ2,
    ЕСТЬNULL(СУММА(Т2_Пред.Количество), 0) КАК КоличествоОт,
    ЕСТЬNULL(СУММА(Т2_Пред.Количество), 0) + Т2.Количество КАК КоличествоДо
ПОМЕСТИТЬ ВТ_Т2_Интервалы
ИЗ
    ВТ_Т2 КАК Т2
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Т2 КАК Т2_Пред
        ПО Т2.Номенклатура = Т2_Пред.Номенклатура
        И Т2.НомерСтроки > Т2_Пред.НомерСтроки
СГРУППИРОВАТЬ ПО
    Т2.Номенклатура,
    Т2.Характеристика,
    Т2.Количество,
    Т2.НомерСтроки
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Т1.Номенклатура КАК Номенклатура,
    Т1.Страна КАК Страна,
    Т2.Склад КАК Склад,
    // Вычисляем пересечение интервалов: Минумум(До) - Максимум(От)
    ВЫБОР 
        КОГДА Т1.КоличествоДо < Т2.КоличествоДо ТЕН Т1.КоличествоДо 
        ИНАЧЕ Т2.КоличествоДо 
    КОНЕЦ - 
    ВЫБОР 
        КОГДА Т1.КоличествоОт > Т2.КоличествоОт ТЕН Т1.КоличествоОт 
        ИНАЧЕ Т2.КоличествоОт 
    КОНЕЦ КАК Количество
ИЗ
    ВТ_Т1_Интервалы КАК Т1
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Т2_Интервалы КАК Т2
        ПО Т1.Номенклатура = Т2.Номенклатура
        И Т1.КоличествоОт < Т2.КоличествоДо
        И Т1.КоличествоДо > Т2.КоличествоОт

Данный подход полностью работает на уровне СУБД, возвращая готовый результат распределения без единой строчки кода на встроенном языке 1С. Однако у него есть критический недостаток, который мы разберем далее.

Метод 2. Программное распределение таблиц значений в коде (Высокопроизводительный алгоритм)

Когда таблицы значений уже находятся в оперативной памяти (в виде объектов ТаблицаЗначений), расчет нарастающих итогов через самосоединение запроса начинает сильно уступать в скорости обычному перебору. Причина в квадратичной сложности СУБД-запроса O(N²) при расчете сумм со сравнением по номерам строк. Если в ваших таблицах более 1000 строк, запрос начнет «подвисать», а на 10 000 строк выполнение может занять минуты.

Выясним, как написать максимально быстрый алгоритм распределения в коде 1С. Для этого мы будем использовать метод двух указателей (индексов) при последовательном обходе коллекций. Это линейный алгоритм со сложностью O(N + M), который работает за доли секунды даже на сотнях тысяч строк.

Рассмотрим пример реализации такого распределения:


Функция РаспределитьТаблицы(Таблица1, Таблица2) Экспорт

    // Создаем результирующую таблицу
    ТЗ_Результат = Новый ТаблицаЗначений;
    ТЗ_Результат.Колонки.Добавить("Номенклатура");
    ТЗ_Результат.Колонки.Добавить("Количество");
    ТЗ_Результат.Колонки.Добавить("Страна");
    ТЗ_Результат.Колонки.Добавить("Склад");

    // Сгруппируем данные по номенклатуре для быстрого позиционирования
    // Предполагаем, что на входе таблицы уже отсортированы по Номенклатуре
    
    Индекс1 = 0;
    Индекс2 = 0;
    Всего1 = Таблица1.Количество();
    Всего2 = Таблица2.Количество();

    Пока Индекс1 < Всего1 И Индекс2 < Всего2 Цикл
        
        Строка1 = Таблица1[Индекс1];
        Строка2 = Таблица2[Индекс2];

        // Если номенклатура не совпадает, сдвигаем указатель отстающей таблицы
        Если Строка1.Номенклатура <> Строка2.Номенклатура Тогда
            Если Строка1.Номенклатура < Строка2.Номенклатура Тогда
                Индекс1 = Индекс1 + 1;
            Иначе
                Индекс2 = Index2 + 1;
            КонецЕсли;
            Продолжить;
        КонецЕсли;

        // Если номенклатура совпала, выполняем распределение объемов
        КоличествоРаспределения = Мин(Строка1.ОстатокКоличества, Строка2.ОстатокКоличества);

        Если КоличествоРаспределения > 0 Тогда
            НоваяСтрока = ТЗ_Результат.Добавить();
            ЗаполнитьЗначенияСвойств(НоваяСтрока, Строка1);
            ЗаполнитьЗначенияСвойств(НоваяСтрока, Строка2);
            НоваяСтрока.Количество = КоличествоРаспределения;

            // Уменьшаем доступные остатки в текущих строках
            Строка1.ОстатокКоличества = Строка1.ОстатокКоличества - КоличествоРаспределения;
            Строка2.ОстатокКоличества = Строка2.ОстатокКоличества - КоличествоРаспределения;
        КонецЕсли;

        // Сдвигаем указатели тех строк, которые полностью исчерпали свой объем
        Если Строка1.ОстатокКоличества = 0 Тогда
            Индекс1 = Индекс1 + 1;
        КонецЕсли;
        Если Строка2.ОстатокКоличества = 0 Тогда
            Индекс2 = Индекс2 + 1;
        КонецЕсли;

    КонецЦикла;

    Возврат ТЗ_Результат;

КонецФункции

Перед запуском этого алгоритма необходимо подготовить исходные таблицы: добавить в них вспомогательную колонку ОстатокКоличества, скопировав туда исходное Количество, и обязательно отсортировать обе таблицы по колонке Номенклатура.

Какое решение выбрать?

Подведем итог и проанализируем сильные и слабые стороны обоих подходов:

  1. Когда использовать Запрос (Метод 1):
    • Если исходные данные физически находятся в таблицах базы данных СУБД, а не в оперативной памяти в виде ТЗ.
    • Если объем обрабатываемых данных заведомо невелик (до 500–1000 строк). В этом случае СУБД легко справится с нагрузкой.
    • Если вам требуется вывести распределенные данные напрямую в отчет СКД (Систему компоновки данных) без использования промежуточного программного кода. К слову, в СКД расчет нарастающих итогов можно реализовать более оптимально через ресурсные функции ВычислитьВыражение().
  2. Когда использовать Программный перебор (Метод 2):
    • Если данные уже получены и находятся в объектах ТаблицаЗначений.
    • Если критически важна скорость работы алгоритма (сотни тысяч распределяемых строк отрабатывают за миллисекунды).
    • Если логика распределения должна учитывать дополнительные нелинейные параметры (например, приоритеты складов, минимальные остатки или сроки годности партий), которые крайне тяжело описать языком декларативных SQL-запросов.

Мы подробно разобрали задачу распределения и сопоставления табличных данных. Выбирайте наиболее подходящий под ваши архитектурные требования метод и внедряйте его в свои проекты!

← На главную