Как выбрать позиции номенклатуры запросом так, чтобы их общая сумма не превышала заданный лимит?

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

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

Рассмотрим подробнее, как мы можем подойти к решению этой проблемы, используя возможности запросов.

Понимание задачи: выбор до достижения лимита

Прежде чем погружаться в технические детали, давайте убедимся, что мы правильно понимаем суть задачи. У нас есть таблица с номенклатурой и суммами по каждой позиции. Нам нужно выбрать часть этих позиций так, чтобы, если мы просуммируем выбранные позиции, их общая сумма не превысила определенный порог. Важным моментом является то, что порядок выборки может влиять на итоговый набор. Например, если мы выбираем сначала самые дешевые позиции, мы сможем включить их больше, чем если бы мы начали с самых дорогих.

Также важно прояснить условие "не превышает определенный лимит по сумме например >=1000". В контексте "не превышает" обычно подразумевается "меньше или равно" (<=). Предположим, что нам нужно выбрать позиции, чья накопленная сумма меньше или равна заданному лимиту (например, 1000).

Подход через нарастающие итоги (Running Totals)

Один из наиболее эффективных и гибких способов решения этой задачи — использование нарастающих итогов (running totals) в запросе. Этот метод позволяет нам последовательно суммировать значения, пока не будет достигнут или превышен заданный лимит. Фактически, мы имитируем процесс выбора товаров один за другим, добавляя их стоимость к общей сумме.

Разберем по шагам, как это работает:

  1. Предварительная агрегация (группировка): Если одна и та же позиция номенклатуры может встречаться в исходной таблице несколько раз (например, разные строки по одной номенклатуре, но с разными суммами), то сначала нам необходимо сгруппировать данные по Номенклатура и подсчитать общую сумму для каждой уникальной номенклатуры. Это позволит нам работать с суммарной стоимостью каждой позиции.
  2. Сортировка: Следующим критически важным шагом является сортировка агрегированных данных. Порядок сортировки определяет, какие позиции будут "взяты" первыми для накопления суммы. Например, если мы хотим получить максимально возможное количество позиций до достижения лимита, нам следует сортировать их по возрастанию суммы (от самых дешевых к самым дорогим). Если же мы хотим получить самые "ценные" (дорогие) позиции, пока их сумма не превысит лимит, сортировка должна быть по убыванию.
  3. Вычисление нарастающего итога: На этом этапе мы вычисляем накопительную сумму. Для каждой строки в отсортированном наборе мы складываем ее сумму со всеми суммами предыдущих строк. В современных SQL-движках и в 1С (начиная с платформы 8.3.16) для этого используются оконные функции.
  4. Фильтрация: Наконец, мы отфильтровываем результат, оставляя только те строки, у которых вычисленный нарастающий итог не превышает наш лимит.

Этот подход часто ассоциируется с упрощенной версией "задачи о рюкзаке" (Knapsack problem). В классической задаче о рюкзаке ищут оптимальное подмножество предметов с максимальной ценностью при ограничении по весу. Наша задача проще: мы не ищем "оптимальное" подмножество (например, с максимальной суммарной стоимостью), а просто выбираем предметы в определенном порядке, пока не достигнем лимита.

Использование оконных функций в запросах 1С и SQL

Оконные функции — это мощный инструмент в SQL, который позволяет выполнять вычисления над набором строк, связанных с текущей строкой. Они идеально подходят для расчета нарастающих итогов. В 1С, начиная с платформы 8.3.16, также появилась поддержка оконных функций, что делает этот подход доступным и в запросах 1С.

Рассмотрим пример SQL-кода, который демонстрирует этот подход:


WITH АгрегированныеНоменклатуры AS (
    SELECT
        Номенклатура,
        СУММ(Сумма) AS ОбщаяСуммаПоНоменклатуре
    FROM
        ВашаТаблица
    GROUP BY
        Номенклатура
),
НарастающийИтогПоНоменклатуре AS (
    SELECT
        Номенклатура,
        ОбщаяСуммаПоНоменклатуре,
        СУММ(ОбщаяСуммаПоНоменклатуре) OVER (ORDER BY ОбщаяСуммаПоНоменклатуре ASC) AS НакопительнаяСумма
    FROM
        АгрегированныеНоменклатуры
)
SELECT
    Номенклатура,
    ОбщаяСуммаПоНоменклатуре
FROM
    НарастающийИтогПоНоменклатуре
WHERE
    НакопительнаяСумма <= &ЛимитСуммы; -- Например, <= 1000

Разберем этот запрос по частям:

  1. WITH АгрегированныеНоменклатуры AS (...)

    На этом шаге мы создаем временное общее табличное выражение (CTE), которое агрегирует суммы по каждой уникальной номенклатуре. Это важно, если в исходной таблице одна и та же номенклатура может встречаться несколько раз с разными суммами.

    
    SELECT
        Номенклатура,
        СУММ(Сумма) AS ОбщаяСуммаПоНоменклатуре
    FROM
        ВашаТаблица
    GROUP BY
        Номенклатура
    

    Здесь мы группируем строки по полю Номенклатура и вычисляем общую сумму для каждой номенклатурной позиции, присваивая ей псевдоним ОбщаяСуммаПоНоменклатуре.

  2. WITH НарастающийИтогПоНоменклатуре AS (...)

    Следующее CTE использует результат первого для вычисления нарастающего итога. Здесь вступает в игру оконная функция.

    
    SELECT
        Номенклатура,
        ОбщаяСуммаПоНоменклатуре,
        СУММ(ОбщаяСуммаПоНоменклатуре) OVER (ORDER BY ОбщаяСуммаПоНоменклатуре ASC) AS НакопительнаяСумма
    FROM
        АгрегированныеНоменклатуры
    

    Ключевым элементом здесь является СУММ(ОбщаяСуммаПоНоменклатуре) OVER (ORDER BY ОбщаяСуммаПоНоменклатуре ASC):

    • СУММ(ОбщаяСуммаПоНоменклатуре): Указывает, что мы хотим суммировать поле ОбщаяСуммаПоНоменклатуре.
    • OVER (...): Определяет "окно" или набор строк, над которым будет выполняться агрегация.
    • ORDER BY ОбщаяСуммаПоНоменклатуре ASC: Внутри предложения OVER мы указываем порядок сортировки. В данном случае, мы сортируем по возрастанию общей суммы по номенклатуре. Это означает, что нарастающий итог будет считаться, начиная с самых "дешевых" позиций. Если бы мы указали DESC, то начали бы с самых "дорогих".
    • AS НакопительнаяСумма: Присваиваем псевдоним результату оконной функции.
  3. Финальная выборка и фильтрация

    И, наконец, внешний запрос выбирает нужные нам поля из CTE НарастающийИтогПоНоменклатуре и применяет условие фильтрации:

    
    SELECT
        Номенклатура,
        ОбщаяСуммаПоНоменклатуре
    FROM
        НарастающийИтогПоНоменклатуре
    WHERE
        НакопительнаяСумма <= &ЛимитСуммы; -- Например, <= 1000
    

    Здесь мы отбираем только те строки, для которых вычисленная НакопительнаяСумма не превышает заданный нами &ЛимитСуммы.

Этот метод является наиболее рекомендуемым для современных СУБД, включая 1С, так как он обычно более производителен и лаконичен по сравнению с альтернативными подходами, такими как рекурсивные CTE или подзапросы, особенно для больших объемов данных.

Альтернативные подходы

Рекурсивные общие табличные выражения (CTE)

В некоторых случаях, когда оконные функции недоступны или требуется более сложная логика, можно использовать рекурсивные CTE. Этот метод позволяет последовательно выбирать строки и накапливать сумму. Однако он является более сложным в реализации и может быть менее производительным для больших наборов данных. Рекурсивные CTE позволяют строить "цепочки" данных, что может быть полезно для нарастающих итогов, но для нашей конкретной задачи, как правило, оконные функции предпочтительнее из-за их оптимизации для таких видов вычислений.

"Баттерфляй - метод быстрого расчета нарастающего итога"

На таких ресурсах, как Инфостарт, можно найти различные оптимизированные методы расчета нарастающих итогов, в том числе так называемый "Баттерфляй - метод быстрого расчета нарастающего итога в запросе" Ильдаровича. Эти методы часто используют комбинации подзапросов и особых приемов для достижения хорошей производительности в условиях, когда прямые оконные функции недоступны или требуют обхода специфических особенностей платформы. Для изучения этих методов мы рекомендуем обращаться к специализированным статьям и публикациям.

Важные моменты и рекомендации

  1. Выбор порядка сортировки: Как уже было сказано, ORDER BY внутри OVER является ключевым. Если вам нужно выбрать сначала самые дешевые товары, используйте ASC. Если самые дорогие — DESC.
  2. Индексация: Для повышения производительности запроса, особенно на больших таблицах, убедитесь, что поля, по которым происходит группировка (Номенклатура) и сортировка (Сумма или ОбщаяСуммаПоНоменклатуре), проиндексированы. Это значительно ускорит выполнение запроса.
  3. Версия платформы 1С: Помните, что поддержка оконных функций в 1С появилась начиная с платформы 8.3.16. Если вы работаете на более ранней версии, вам придется использовать более сложные конструкции с подзапросами или рекурсивными CTE, либо вычислять нарастающий итог на стороне клиентского приложения.
  4. Точность условия лимита: Всегда четко определяйте, что означает "не превышает". В большинстве случаев это <= (меньше или равно).

Таким образом, задача выборки номенклатуры по лимиту накопленной суммы элегантно и эффективно решается с помощью оконных функций, которые позволяют нам строить нарастающие итоги прямо в теле запроса, делая код чище и производительнее.

← На главную