Статьи

Как заполнить разреженные данные предыдущим непустым значением в SQL

Следующее является очень распространенной проблемой во всех технологиях, связанных с данными, и мы рассмотрим два очень простых решения на основе SQL:

Как заполнить ячейки разреженного набора данных «предыдущим непустым значением»?

Эта проблема

Проблема действительно проста, и я повторно использую пример, предоставленный пользователем Stack Overflow aljassi в этом вопросе :

У нас есть таблица, содержащая «разреженные» данные:

01
02
03
04
05
06
07
08
09
10
11
Col1  Col2  Col3  Col4
----------------------
A     0     1     5
B     0     4     0
C     2     0     0
D     0     0     0
E     3     5     0
F     0     3     0
G     0     3     1
H     0     1     5
I     3     5     0

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

01
02
03
04
05
06
07
08
09
10
11
Col1  Col2  Col3  Col4
----------------------
A     0     1     5
B     0     4     5
C     2     4     5
D     2     4     5
E     3     5     5
F     3     3     5
G     3     3     1
H     3     1     5
I     3     5     5

Обратите внимание, что все сгенерированные значения выделены красным, и они соответствуют самому последнему значению синего цвета.

Как это сделать с SQL? Мы рассмотрим два решения:

Решение с использованием оконных функций

Это решение, которое вы должны искать, и в связанном вопросе переполнения стека есть два ответа, которые оба используют оконные функции:

Оба решения примерно эквивалентны. Вот как они работают (используя синтаксис Oracle):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WITH t(col1, col2, col3, col4) AS (
  SELECT 'A', 0, 1, 5 FROM DUAL UNION ALL
  SELECT 'B', 0, 4, 0 FROM DUAL UNION ALL
  SELECT 'C', 2, 0, 0 FROM DUAL UNION ALL
  SELECT 'D', 0, 0, 0 FROM DUAL UNION ALL
  SELECT 'E', 3, 5, 0 FROM DUAL UNION ALL
  SELECT 'F', 0, 3, 0 FROM DUAL UNION ALL
  SELECT 'G', 0, 3, 1 FROM DUAL UNION ALL
  SELECT 'H', 0, 1, 5 FROM DUAL UNION ALL
  SELECT 'I', 3, 5, 0 FROM DUAL
)
SELECT
  col1,
 
  nvl(last_value(nullif(col2, 0))
      IGNORE NULLS OVER (ORDER BY col1), 0) col2,
 
  nvl(last_value(nullif(col3, 0))
      IGNORE NULLS OVER (ORDER BY col1), 0) col3,
 
  nvl(last_value(nullif(col4, 0))
      IGNORE NULLS OVER (ORDER BY col1), 0) col4
FROM t

Теперь давайте разберем эти оконные функции:

NULLIF (colx, 0)

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

01
02
03
04
05
06
07
08
09
10
11
Col1  Col2  Col3  Col4
----------------------
A     NULL  1     5
B     NULL  4     NULL
C     2     NULL  NULL
D     NULL  NULL  NULL
E     3     5     NULL
F     NULL  3     NULL
G     NULL  3     1
H     NULL  1     5
I     3     5     NULL

Мы делаем это потому, что теперь мы можем использовать полезное предложение IGNORE NULLS , которое доступно некоторым функциям ранжирования, в частности, LAST_VALUE() или LAG() . Теперь мы можем написать:

1
last_value(...) IGNORE NULLS OVER (ORDER BY col1)

Где мы берем последнее NULL значение, которое предшествует текущей строке при упорядочении строк по col1 :

  • Если текущая строка содержит NULL значение, мы принимаем это значение.
  • Если текущая строка содержит значение NULL , мы будем идти вверх, пока не достигнем значения, отличного от NULL
  • Если мы идем «вверх» и не достигли значения, отличного от NULL , мы получим NULL

Это приводит к следующему результату:

01
02
03
04
05
06
07
08
09
10
11
Col1  Col2  Col3  Col4
----------------------
A     NULL  1     5
B     NULL  4     5
C     2     4     5
D     2     4     5
E     3     5     5
F     3     3     5
G     3     3     1
H     3     1     5
I     3     5     5

Обратите внимание, что в большинстве оконных функций после указания предложения ORDER BY в качестве значения по умолчанию принимается следующее предложение frame:

1
2
3
4
last_value(...) IGNORE NULLS OVER (
  ORDER BY col1
  ROWS BETWEEN UNBOUNDED PRECEEDING AND CURRENT ROW
)

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

Наконец, поскольку мы не хотим, чтобы эти значения NULL оставались в наших результатах, мы просто удаляем их, используя NVL() (или COALESCE() в других базах данных):

1
nvl(last_value(...) IGNORE NULLS OVER (...), 0)

Легко, не правда ли? Обратите внимание, что в этом конкретном случае LAG() и LAST_VALUE() будут иметь одинаковый эффект.

Решение с использованием предложения MODEL

Всякий раз, когда у вас возникает проблема в (Oracle) SQL, которую становится трудно решить с помощью оконных функций, предложение Oracle MODEL может предложить «простое» решение. Я использую кавычки на «легкий», потому что синтаксис немного сложен для запоминания, но суть его на самом деле не так сложна.

Предложение MODEL — не что иное, как специфический для Oracle диалект для реализации логики, подобной электронной таблице, в базе данных. Я настоятельно рекомендую прочитать соответствующую Белую книгу от Oracle, которая очень хорошо объясняет функциональность:

Вот как вы можете решить проблему с MODEL (и потерпите меня):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
WITH t(col1, col2, col3, col4) AS (
  SELECT 'A', 0, 1, 5 FROM DUAL UNION ALL
  SELECT 'B', 0, 4, 0 FROM DUAL UNION ALL
  SELECT 'C', 2, 0, 0 FROM DUAL UNION ALL
  SELECT 'D', 0, 0, 0 FROM DUAL UNION ALL
  SELECT 'E', 3, 5, 0 FROM DUAL UNION ALL
  SELECT 'F', 0, 3, 0 FROM DUAL UNION ALL
  SELECT 'G', 0, 3, 1 FROM DUAL UNION ALL
  SELECT 'H', 0, 1, 5 FROM DUAL UNION ALL
  SELECT 'I', 3, 5, 0 FROM DUAL
)
SELECT * FROM t
MODEL
  DIMENSION BY (row_number() OVER (ORDER BY col1) rn)
  MEASURES (col1, col2, col3, col4)
  RULES (
    col2[any] = DECODE(col2[cv(rn)], 0, NVL(col2[cv(rn) - 1], 0), col2[cv(rn)]),
    col3[any] = DECODE(col3[cv(rn)], 0, NVL(col3[cv(rn) - 1], 0), col3[cv(rn)]),
    col4[any] = DECODE(col4[cv(rn)], 0, NVL(col4[cv(rn) - 1], 0), col4[cv(rn)])
  )

Здесь есть три пункта, которые представляют интерес:

Предложение DIMENSION BY

Как и в электронной таблице Microsoft Excel, DIMENSION соответствует отдельному последовательному индексу каждой ячейки электронной таблицы, по которому мы хотим получить доступ к этой ячейке. В Excel всегда есть два измерения (одно написано буквами A..Z, AA..ZZ,…), а другое — цифрами (1..infinity).

Используя MODEL , вы можете указать столько размеров, сколько хотите. В нашем примере мы будем использовать только одну, номер строки каждой строки, упорядоченный по col1 (другой вариант использования для оконной функции).

Пункт MEASURES

Предложение MEASURES определяет значения отдельных ячеек для каждой «ячейки». В Microsoft Excel ячейка может иметь только одно значение. В предложении Oracle MODEL мы можем оперировать многими значениями одновременно в «ячейке».

В этом случае мы просто сделаем все столбцы нашими ячейками.

ПРАВИЛА

Это действительно интересная часть предложения MODEL . Здесь мы указываем, по каким правилам мы хотим вычислить значения каждой отдельной ячейки. Синтаксис прост:

1
2
3
4
5
6
RULES (
  <rule 1>,
  <rule 2>,
  ...,
  <rule N>
)

Каждое отдельное правило может реализовывать присваивание формы:

1
2
3
RULES (
  cell[dimension(s)] = rule
)

В нашем случае мы повторим то же правило для ячеек col2 , col3 и col4 и для любого значения измерения rn (для номера строки). Итак, левая часть задания

1
2
3
4
5
RULES (
  col2[any] = rule,
  col3[any] = rule,
  col4[any] = rule,
)

Правая часть — это тривиальное (но не на вид) выражение:

1
DECODE(col2[cv(rn)], 0, NVL(col2[cv(rn) - 1], 0), col2[cv(rn)])

Давайте снова разложим.

ДЕКОДИРОВАТЬ

DECODE — это простая и полезная функция Oracle, которая принимает первый аргумент, сравнивает его с аргументом 2 и, если они совпадают, возвращает аргумент 3, в противном случае — аргумент 4. Она работает как CASE , что немного более детально:

1
2
3
4
5
DECODE(A, B, C, D)
 
-- The same as:
 
CASE A WHEN B THEN C ELSE D END

резюме (гп)

cv() — это специальная «функция» MODEL которая означает «текущее значение». В левой части присваивания мы использовали "any" в качестве спецификатора измерения, поэтому мы применяем это правило для «any» значения rn . Чтобы получить доступ к определенному значению rn , мы просто напишем cv(rn) или «текущее значение rn».

рекурсивность

RULES предложения MODEL могут охватывать рекурсивное дерево (хотя это и не граф, поэтому циклы не допускаются), где каждая ячейка может быть определена на основе предыдущей ячейки, которая снова определяется на основе своего предшественника. Мы делаем это через col2[cv(rn) - 1] , где cv(rn) - 1 означает «текущий номер строки минус один».

Легко, правда? Предоставляется. Синтаксис не является простым, и мы только царапаем поверхность того, что возможно с MODEL .

Вывод

SQL предоставляет отличные способы реализации управляемых данными декларативных спецификаций того, какими должны быть ваши данные. Предложение MODEL немного жуткое, но в то же время чрезвычайно мощное. Гораздо проще, а также немного быстрее — оконные функции, инструмент, который должен быть в цепочке инструментов каждого разработчика, работающего с SQL.

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