Как вычислить произведение чисел в запросе 1С, обходя отсутствие агрегатной функции умножения?

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

При работе с данными в системе 1С:Предприятие мы часто сталкиваемся с необходимостью выполнения различных агрегатных операций. Однако, язык запросов 1С имеет определенные ограничения по сравнению с полноценными SQL-серверами. Одной из таких особенностей является отсутствие прямой агрегатной функции для вычисления произведения значений колонки. В то время как СУММА, МАКСИМУМ, МИНИМУМ и КОЛИЧЕСТВО доступны "из коробки", для умножения всех чисел в выборке или группе приходится прибегать к неочевидным методам. Разберем, как мы можем решить эту задачу, используя как стандартные средства 1С, так и возможности Системы Компоновки Данных (СКД), а также математические обходные пути.

Предположим, у нас есть таблица с двумя колонками: "Тип" (строка) и "Коэф" (число), и нам необходимо перемножить все числа из колонки "Коэф" и вывести итоговый результат. Например, для таблицы:

  1. Тип1 | 5
  2. Тип2 | 1
  3. Тип3 | 10

Ожидаемый результат — 50.

Решение 1: Использование логарифмического метода в СКД (начиная с платформы 8.3.20)

Одним из наиболее элегантных и математически обоснованных способов вычисления произведения является использование свойств логарифмов. Мы знаем, что логарифм произведения равен сумме логарифмов сомножителей: Log(x1 * x2 * ... * xN) = Log(x1) + Log(x2) + ... + Log(xN). Отсюда следует, что само произведение можно выразить как экспоненту от суммы логарифмов: x1 * x2 * ... * xN = EXP(SUM(LOG(x1) + LOG(x2) + ... + LOG(xN))).

С версии платформы 1С:Предприятие 8.3.20 в язык запросов были добавлены новые математические функции, включая Log (натуральный логарифм), Exp (экспонента) и Pow (возведение в степень). Это значительно упрощает задачу.

Применение в СКД

В Системе Компоновки Данных мы можем использовать эти функции напрямую в формуле ресурса. Для этого выполним следующие шаги:

  1. Откроем настройки СКД.
  2. Перейдем на вкладку "Ресурсы".
  3. Создадим новый ресурс. В качестве выражения для него укажем:
  4. ВычислитьВыражение("Exp(Сумма(Log(Коэф)))")

    Здесь Коэф — это имя нашей колонки с коэффициентами.

Такой подход позволяет нам вычислить произведение непосредственно в рамках СКД, используя ее возможности по агрегации и вычислению выражений.

Ограничения и важные моменты

Необходимо учитывать следующие аспекты при использовании логарифмического метода:

  1. Положительные числа: Функция Log определена только для положительных чисел. Если в колонке Коэф встречаются нули или отрицательные числа, этот метод потребует дополнительной обработки.
    • Если есть хотя бы один 0, произведение равно 0.
    • Если есть отрицательные числа, их необходимо обрабатывать отдельно, например, вычисляя произведение абсолютных значений и затем определяя общий знак (четное/нечетное количество отрицательных чисел).
  2. Точность вычислений: При работе с дробными числами и очень большими или очень маленькими значениями может возникнуть потеря точности. Максимальная общая длина числа в 1С составляет 37 знаков. Операции с плавающей точкой всегда несут риск округления. Нам следует быть внимательными к требуемой точности в наших расчетах.
  3. Версия платформы: Убедимся, что наша платформа 1С:Предприятие имеет версию 8.3.20 или выше для поддержки функций Log и Exp в языке запросов.

Решение 2: Выгрузка данных в ТаблицуЗначений и обработка на встроенном языке

Если мы сталкиваемся с ограничениями логарифмического метода (например, из-за наличия нулей или отрицательных чисел), или если нам требуется более высокая точность и полный контроль над процессом, можем использовать традиционный подход — выгрузка результатов запроса в ТаблицуЗначений и последующая программная обработка на встроенном языке 1С.

Алгоритм действий

  1. Выполним запрос: Сначала нам необходимо выполнить запрос, который выбирает нужные нам данные (например, колонку "Коэф" из нашей таблицы).
  2. 
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ
        |    ТаблицаДанных.Коэф
        |ИЗ
        |    ТаблицаДанных КАК ТаблицаДанных";
    
    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();
    
  3. Обработаем выборку: Затем в цикле обойдем полученную выборку и перемножим значения.
  4. 
    Произведение = 1;
    Пока Выборка.Следующий() Цикл
        Произведение = Произведение * Выборка.Коэф;
    КонецЦикла;
    
    Сообщить("Итоговое произведение: " + Произведение);
    

Этот подход дает нам полную гибкость. Мы можем легко обрабатывать нули и отрицательные числа:


Произведение = 1;
ЕстьНоль = Ложь;

Пока Выборка.Следующий() Цикл
    Если Выборка.Коэф = 0 Тогда
        ЕстьНоль = Истина;
        Прервать; // Если есть ноль, дальнейшее умножение бессмысленно
    Иначе
        Произведение = Произведение * Выборка.Коэф;
    КонецЕсли;
КонецЦикла;

Если ЕстьНоль Тогда
    Произведение = 0;
КонецЕсли;

Сообщить("Итоговое произведение: " + Произведение);

Преимущества и недостатки

Если нам необходимо вывести результат обратно в СКД, можем создать временную таблицу (или таблицу значений, переданную в СКД как внешний набор данных), заполнить ее вычисленным значением и использовать для финального отчета.

Решение 3: "Извращенский" SQL-хак для ограниченного числа множителей (для старых платформ или строгих требований "только запрос")

Для версий платформы до 8.3.20, где отсутствуют математические функции Log и Exp в запросах, или при очень строгом требовании "только запрос" без встроенного языка 1С, существует крайне сложный и не рекомендуемый для использования подход. Он основан на имитации оконных функций и поворота таблицы (pivot) средствами языка запросов 1С.

Мы должны понимать, что этот метод является крайне неэффективным, сложным для понимания, отладки и сопровождения. Он подходит только для очень ограниченного и заранее известного количества множителей (например, не более 10-20) и при условии, что нет нулей или отрицательных чисел (хотя их обработка также возможна, но усложнит запрос до предела).

Принцип работы

  1. Нумерация записей по группам: Мы используем временные таблицы и специальные приемы с АВТОНОМЕРЗАПИСИ() (с ее ограничениями на ВЫБРАТЬ ПЕРВЫЕ N и обязательную сортировку) для получения сквозной нумерации записей. Затем, с помощью дополнительного соединения, мы перенумеровываем записи внутри каждой группы, чтобы получить порядковый номер множителя для каждого "Типа".
  2. 
    // Шаг 1: Исходные данные (на примере)
    ВЫБРАТЬ
        |    "Тип1" КАК Тип,
        |    5 КАК Коэф
    ПОМЕСТИТЬ ВТ_ИсходныеДанные
    ОБЪЕДИНИТЬ ВСЕ
    ВЫБРАТЬ
        |    "Тип2",
        |    1
    ОБЪЕДИНИТЬ ВСЕ
    ВЫБРАТЬ
        |    "Тип3",
        |    10
    ;
    
    ////////////////////////////////////////////////////////////////////////////////
    // Шаг 2: Получаем сквозной номер записи с сортировкой по Типу и Коэф
    // ВНИМАНИЕ: АВТОНОМЕРЗАПИСИ() имеет ограничения, требует ВЫБРАТЬ ПЕРВЫЕ N
    ВЫБРАТЬ ПЕРВЫЕ 1000000 // Примерное максимальное количество строк
        |    ВТ_ИсходныеДанные.Тип КАК Тип,
        |    ВТ_ИсходныеДанные.Коэф КАК Коэф,
        |    АВТОНОМЕРЗАПИСИ() КАК НПП_Сквозной
    ПОМЕСТИТЬ ВТ_ДанныеСоСквознымНПП
    ИЗ
        |    ВТ_ИсходныеДанные КАК ВТ_ИсходныеДанные
    УПОРЯДОЧИТЬ ПО
        |    Тип,
        |    Коэф
    ;
    
    ////////////////////////////////////////////////////////////////////////////////
    // Шаг 3: Перенумеруем записи внутри каждой группы "Тип"
    ВЫБРАТЬ
        |    Т1.Тип КАК Тип,
        |    Т1.Коэф КАК Коэф,
        |    Т1.НПП_Сквозной - Т2.МинНПП + 1 КАК НПП_Групповой
    ПОМЕСТИТЬ ВТ_ДанныеСГрупповымНПП
    ИЗ
        |    ВТ_ДанныеСоСквознымНПП КАК Т1
        |        ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
        |            |Т.Тип КАК Тип,
        |            |МИНИМУМ(Т.НПП_Сквозной) КАК МинНПП
        |        ИЗ
        |            |ВТ_ДанныеСоСквознымНПП КАК Т
        |        СГРУППИРОВАТЬ ПО
        |            |Т.Тип) КАК Т2
        |        ПО Т1.Тип = Т2.Тип
    ;
    
  3. Поворот таблицы (Pivot): Далее мы "поворачиваем" данные, превращая строки в колонки. Для каждого возможного номера множителя (1, 2, 3... до заранее определенного максимума) создаем отдельную колонку. Если номер текущей записи совпадает с номером колонки, берем ее "Коэф", иначе — NULL. Затем агрегируем по "Типу", используя МАКСИМУМ(), чтобы собрать все значения в одной строке.
  4. 
    // Шаг 4: Поворот таблицы - строки превращаем в столбцы
    ВЫБРАТЬ
        |    Т.Тип КАК Тип,
        |    МАКСИМУМ(ВЫБОР КОГДА Т.НПП_Групповой = 1 ТОГДА Т.Коэф ИНАЧЕ NULL КОНЕЦ) КАК Коэф1,
        |    МАКСИМУМ(ВЫБОР КОГДА Т.НПП_Групповой = 2 ТОГДА Т.Коэф ИНАЧЕ NULL КОНЕЦ) КАК Коэф2,
        |    МАКСИМУМ(ВЫБОР КОГДА Т.НПП_Групповой = 3 ТОГДА Т.Коэф ИНАЧЕ NULL КОНЕЦ) КАК Коэф3,
        |    // ... и так далее для каждого предполагаемого множителя до N
        |    МАКСИМУМ(ВЫБОР КОГДА Т.НПП_Групповой = 10 ТОГДА Т.Коэф ИНАЧЕ NULL КОНЕЦ) КАК Коэф10
    ПОМЕСТИТЬ ВТ_ПовернутыеДанные
    ИЗ
        |    ВТ_ДанныеСГрупповымНПП КАК Т
    СГРУППИРОВАТЬ ПО
        |    Т.Тип
    ;
    
  5. Перемножение колонок: Наконец, перемножаем полученные колонки. Используем ЕСТЬNULL(..., 1), чтобы значения NULL (которые появляются для отсутствующих множителей) не обнуляли произведение, а заменялись на единицу.
  6. 
    // Шаг 5: Перемножение повернутых столбцов
    ВЫБРАТЬ
        |    Т.Тип,
        |    ЕСТЬNULL(Т.Коэф1, 1) * ЕСТЬNULL(Т.Коэф2, 1) * ЕСТЬNULL(Т.Коэф3, 1) *
        |    // ... продолжение умножения всех столбцов до Коэф10
        |    ЕСТЬNULL(Т.Коэф10, 1) КАК Произведение
    ИЗ
        |    ВТ_ПовернутыеДанные КАК Т
    ;
    

Сложности и недостатки

Данный метод демонстрирует теоретическую возможность, но на практике его применение оправдано лишь в исключительных случаях, когда все остальные варианты полностью исчерпаны, и мы готовы принять все его недостатки.

Решение 4: Соединение строк и функция Вычислить (альтернативный подход, требует встроенного языка или СКД)

Еще один интересный, хотя и не чисто запросовый, подход заключается в формировании строки-выражения для умножения и последующем ее вычислении. Этот метод наиболее применим в СКД через выражение ресурса или на встроенном языке.

Принцип работы

Идея состоит в том, чтобы собрать все значения коэффициентов в одну строку, разделяя их знаком умножения (например, "5*1*10"), а затем использовать функцию Вычислить() для получения результата.

Применение в СКД

В СКД этот метод может быть реализован с использованием функции ВычислитьВыражение() и СоединитьСтроки(). Для этого нам потребуется получить массив строковых представлений коэффициентов.

  1. Создадим вычисляемое поле в СКД, которое будет представлять коэффициент как строку:
  2. 
    ВЫБОР
        КОГДА ИсходныеДанные.Коэф ЕСТЬ NULL ТОГДА "1"
        ИНАЧЕ СТРОКА(ИсходныеДанные.Коэф)
    КОНЕЦ
    

    Назовем его, например, КоэфСтрокой. Учитываем NULL, заменяя его на "1", чтобы он не влиял на произведение.

  3. Затем, в ресурсе СКД, мы сможем использовать конструкцию, подобную следующей:
  4. 
    ВычислитьВыражение("Вычислить(СоединитьСтроки(Массив(КоэфСтрокой), ""*"", ""*""))")
    

    Функция Массив() соберет все значения КоэфСтрокой в массив, СоединитьСтроки() объединит их через "*", а Вычислить() произведет вычисление полученного строкового выражения.

Преимущества и недостатки

Общие рекомендации и выводы

Мы видим, что задача вычисления произведения чисел в запросе 1С имеет несколько решений, каждое со своими областями применения и ограничениями. Выбор конкретного метода зависит от версии платформы, количества множителей, требований к точности, наличия нулей/отрицательных чисел и желания использовать встроенный язык 1С.

  1. Для новых платформ (8.3.20+): Самым рекомендуемым и элегантным способом является использование логарифмического метода в СКД с функциями Exp(Сумма(Log(Коэф))). Мы должны всегда помнить о необходимости обработки нулей и отрицательных чисел, а также о потенциальных потерях точности для очень специфических значений.
  2. Для универсальности и контроля: Если требуются высокая точность, обработка всех возможных числовых значений (включая нули и отрицательные), или если мы используем старые версии платформы, лучшим выбором будет выгрузка данных в ТаблицуЗначений и программная обработка на встроенном языке 1С. Это дает нам полный контроль над логикой.
  3. Для специфических случаев с СКД и Вычислить: Метод с формированием строкового выражения и функцией Вычислить() может быть применен в СКД, но мы должны быть осведомлены о его природе как решения на встроенном языке и потенциальных ограничениях.
  4. "Извращенский" SQL-хак: Мы должны избегать сложных SQL-хаков с имитацией оконных функций и поворотом таблиц. Эти решения крайне сложны, неэффективны и непригодны для реальных проектов, где требуется сопровождение и масштабируемость.

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

← На главную