SQL — Как выполнить оконную функцию, если есть нулевое значение?

#sql #sql-server #window-functions

Вопрос:

Во-первых, у меня есть эта информация:

  1. Вес А
  2. Вес B
  3. Отношение B к A: 1 ко многим

Таким образом, ниже может быть получен результат:

A_Id Вес А Вес B B_Id
1 3 16 1
2 5 16 1
3 6 16 1
4 7 16 1
5 2 12 2
6 6 12 2

Теперь добавим еще два столбца: Sum Weight A By B_Id , Accumulative Difference (рассмотрим ниже в виде таблицы t2 )

A_Id Вес А Вес Суммы A По B_Id Вес B B_Id Накопительная Разница
1 3 21 16 1 5
2 5 21 16 1 5
3 6 21 16 1 5
4 7 21 16 1 5
5 2 8 12 2 1
6 6 8 12 2 1

Например, выше,

  1. накопительная разница в первом ряду => 21 — 16 = 5
  2. накопительная разница в пятом ряду => (21 8) — (16 12) = 1

Итак, моя цель состоит в том, чтобы вычислить такое « Accumulative Difference Весь результат должен быть отображен в отчете.

Технически, используя «Оконные функции», этого можно достичь без проблем. Во-первых, я должен создать еще 2 столбца: Accumulate Weight A By B_Id , Accumulate Weight B . Затем просто найдите разницу между ними.

На самом деле мне нужно еще 3 колонки:

  • [Строка По B_Id]
  • [Суммарный Вес A По B_Id]
  • [Накопить Вес B]
A_Id Вес А Вес Суммы A По B_Id Вес B B_Id Строка По B_Id Накопите Вес A На B_Id Накопить Вес В Накопительная Разница
1 3 21 16 1 1 21 16 5
2 5 21 16 1 2 21 16 5
3 6 21 16 1 3 21 16 5
4 7 21 16 1 4 21 16 5
5 2 8 12 2 1 29 28 1
6 6 8 12 2 2 29 28 1

sample SQL (to generate t2 ):


SELECT
*,
[Accumulate Weight A By B_Id] = SUM(WeightA) OVER (PARTITION BY … ORDER BY B_Id),
[Accumulate Weight B] = SUM(WeightB) OVER (PARTITION BY … ORDER BY B_Id)
FROM t2
— (…) could be by date year month
— Accumulate Weight B can set to only 1st row, etc

 ;WITH tableA AS (
SELECT [A_Id] = 1, [Weight] = 3, [B_Id] = 1, [date] = '2021-10-01'
UNION
SELECT [A_Id] = 2, [Weight] = 5, [B_Id] = 1, [date] = '2021-10-02'
UNION
SELECT [A_Id] = 3, [Weight] = 6, [B_Id] = 1, [date] = '2021-10-03'
UNION
SELECT [A_Id] = 4, [Weight] = 7, [B_Id] = 1, [date] = '2021-10-04'
UNION
SELECT [A_Id] = 5, [Weight] = 2, [B_Id] = 2, [date] = '2021-10-05'
UNION
SELECT [A_Id] = 6, [Weight] = 6, [B_Id] = 2, [date] = '2021-10-06'
    
--Uncomment for testing NULL value
--UNION
--SELECT [A_Id] = 7, [Weight] = 9, [B_Id] = NULL, [date] = '2021-10-07'
--UNION
--SELECT [A_Id] = 8, [Weight] = 10, [B_Id] = 3, [date] = '2021-10-08'
    
),
tableB AS (
     SELECT [B_Id] = 1, [Weight] = 16, [date] = '2021-10-03'
     UNION
     SELECT [B_Id] = 2, [Weight] = 12, [date] = '2021-10-06'

    --Uncomment for testing NULL value
    --UNION
    --SELECT [B_Id] = 3, [Weight] = 8, [date] = '2021-10-08'
),
t1a AS (
    SELECT 
        [A_Id] = tableA.A_Id,
        [WeightA] = tableA.Weight,
        [WeightB] = tableB.Weight,
        [B_Id] = tableB.B_Id,
        [Row By B_Id] = ROW_NUMBER() OVER(PARTITION BY tableB.B_Id ORDER BY A_Id)
    FROM 
        tableA 
    FULL JOIN tableB ON tableA.B_Id = tableB.B_Id
),
t1b AS (
    SELECT
        *,
        [Sum Weight A By B_Id] = SUM(WeightA) OVER (ORDER BY B_Id),
        [Accumulate Weight B] = SUM(CASE WHEN [Row By B_Id] = 1 THEN WeightB ELSE 0 END) OVER (ORDER BY B_Id)
    FROM t1a
),
t2 AS (
    SELECT 
        *,
        [Accumulate Difference] = [Sum Weight A By B_Id] - [Accumulate Weight B]
    FROM t1b
)
SELECT 
    *
FROM t2
 

Теперь возникает проблема, ЕСЛИ один из B_Id них есть NULL . (Раскомментируйте детали, чтобы сгенерировать нулевой B_Id)

Ниже приведен мой ожидаемый результат, особенно в выделенной строке:

A_Id Вес А Вес Суммы A По B_Id Вес B B_Id Накопите Вес A На B_Id Накопить Вес В Накопительная Разница
1 3 21 16 1 21 16 5
2 5 21 16 1 21 16 5
3 6 21 16 1 21 16 5
4 7 21 16 1 21 16 5
5 2 8 12 2 29 28 1
6 6 8 12 2 29 28 1
7 9 9 0 нулевой 38 28 10
8 7 10 8 3 48 36 12
9 3 10 8 3 48 36 12

Однако с моим примерным запросом это не работает. Вместо этого ниже появится:

текущий результат

Вместо этого в первой строке отображается пустой B_Id. (Заказ испорчен)

Итак, мой вопрос, как справиться с такой ситуацией? (Сохраните исходную строку, как в ожидаемом результате)

Как, например, почему порядок таков? (поднято @ThorstenKettner)

Порядок по умолчанию, основан B_TransactionDatetime . Если B_Id равно NULL, то он будет основан на A_TransactionDatetime . Итак, я вычисляю другой столбец RefDateTime = COALESCE(B_TransactionDatetime, A_TransactionDatetime) и упорядочиваю его на основе этого.

пс:

Вдохновленный @ThorstenKettner, я должен использовать функцию RefDateTime в окне, т. е.:

 [Sum Weight A By B_Id] = SUM(WeightA) OVER (ORDER BY RefDateTime),
[Accumulate Weight B] = SUM(CASE WHEN [Row By B_Id] = 1 THEN WeightB ELSE 0 END) OVER (ORDER BY RefDateTime)
 

Дело закрыто.

Комментарии:

1. Вы используете - , чтобы представлять NULL s здесь? Это было бы менее запутанно, если бы вы использовали NULL его . '-' это строка, которая подразумевает, что вы храните числовые данные в varchar ; основной недостаток дизайна.

2. Вы также говорите о наборе данных с a B_Id NULL , но ни один из ваших примеров данных не демонстрирует эту проблему.

3. @Larnu спасибо за отзыв. я только что добавил колонку B_Id . Между тем, » — » не равно НУЛЮ, вы можете считать, что это то же значение, что и в предыдущей строке. (я поставил дефис, чтобы сфокусировать другие значения)

4. Тогда вам нужно исправить свои данные, zeroflaw. - <> The value of the previous row . Если все несколько строк имеют одинаковое значение, они также должны быть указаны в ваших образцах данных.

5. Таким образом, есть A без B. Однако я не понимаю, по каким критериям вы упорядочиваете свои строки. Сначала я, хотя по b.id, а.вес, но положение b.id Значение NULL противоречит этому, а также порядку строк для b.id 3.

Ответ №1:

Вы хотите внешнее соединение B с A, потому что не у каждого A есть связанный B.

Затем вы смотрите на строки по блокам. Один блок-это либо все строки.принадлежащие одному B, либо одна строка A, в которой нет B. b_id будет хорошим ключом группы для первого, в то время как a_id подойдет для второго. Для комбинированного ключа существуют различные варианты. COALESCE(b_id, a_id) не является одним из них, потому что мы могли бы иметь a_id 1 и b_id 1 в результирующем наборе, но не хотим, чтобы они находились в одной группе. Одно из решений простое COALESCE(b_id, -a_id) , при условии, что ваши идентификаторы, конечно, не могут быть отрицательными.

Теперь все ваши вычисления основаны на агрегированных группах, т. Е. вас не интересуют отдельные значения A, когда они принадлежат группе B. По этой причине я бы сразу же объединил и снова присоединился только к одной строке » А » в самом конце.

Порядок строк таков COALESCE(b_date, a_date) .

  with grouped as
    (
      select
        coalesce(b.b_id, -a.a_id) as grp_id,
        max(coalesce(b.date, a.date)) as grp_date,
        coalesce(max(b.weight), 0) as b_weight,
        sum(a.weight) as a_weight
      from a
      left join b on b.b_id =a.b_id
      group by coalesce(b.b_id, -a.a_id)
    )
    , calculated as
    (
      select
        grp_id,
        grp_date,
        b_weight,
        a_weight,
        sum(a_weight - b_weight) over (order by grp_date) as running_diff
        from grouped
    )
    select *
    from calculated c
    join a on a.b_id = c.grp_id or a.a_id = -c.grp_id
    order by c.grp_date, a.date;
 

Надеюсь, я все понял правильно. У меня нет компьютера под рукой, и я печатаю это на своем мобильном телефоне, что оказывается сложнее, чем я думал 🙂

Комментарии:

1. Пожалуйста, кто-нибудь отредактируйте этот код для меня. Он не работает с моего мобильного телефона. {} Кнопки нет, и отступ из четырех пробелов, похоже, тоже не работает должным образом для меня.

2.Я отредактировал для вас 🙂 Ваш ответ вдохновил меня на использование RefDateTime COALESCE(b_date, a_date) функции «Быть ORDERed в окне». (это сделало бы работу). Тем временем я узнал, что COALESCE(b_id, -a_id) могу служить группой. Потрясающе.

Ответ №2:

Вы можете использовать coalesce().

 SELECT 
 *,
 [Accumulate Weight A By B_Id] = SUM(WeightA) OVER (PARTITION BY B_id ORDER BY B_Id),
 [Accumulate Weight B] = SUM(WeightB) OVER (PARTITION BY B_id ORDER BY B_Id),
 SUM(coalesce(WeightA,0)-coalesce(WeightB,0)) OVER (PARTITION BY B_id ORDER BY B_Id) difference

FROM t2
 

PS: На самом деле ваш первоначальный запрос кажется мне неправильным, если бы это было правильно, то это подошло бы.
Вероятно, вам следует привести примеры данных A и B. Для меня имеет больше смысла суммировать (), прежде чем присоединяться к ним вообще.

Комментарии:

1. Привет, приношу свои извинения. Я добавил больше информации в вопрос.

Ответ №3:

Вам придется внести изменения, но это должно помочь.`

 SELECT [Accumulate Weight A By B_Id] = SUM(WeightA) OVER (
        PARTITION BY...ORDER BY B_Id
        )
    ,[Accumulate Weight B] = SUM(WeightB) OVER (
        PARTITION BY...ORDER BY B_Id
        )
FROM t2
WHERE B_Id IS NOT NULL

UNION

SELECT [Accumulate Weight A By B_Id] = SUM(TAB.WeightA) OVER (
        PARTITION BY TAB.ROW_NUM ORDER BY B_Id
        )
    ,[Accumulate Weight B] = SUM(TAB.WeightB) OVER (
        PARTITION BY TAB.ROW_NUM ORDER BY B_Id
        )
FROM (
    SELECT WeightA
        ,WeightB
        ,B_Id
        ,ROW_NUMBER() OVER (
            ORDER BY B_ID
            ) AS ROW_NUM
    FROM T2
    WHERE B_ID IS NULL
    ) TAB
 

`

Комментарии:

1. Привет, приношу свои извинения. я добавил больше информации в вопрос. Как видно из вашего запроса, означает ли это, что, наконец, порядок изменится?