グループ化演算(GROUPING) 

# はじめに

いままで使ったことが無かったけど便利そうな `GROUPING` というのを見つけたので紹介します。

# GROUP BY CUBE

こういう SQL があったとして

```sql
WITH t AS (
  SELECT *
    FROM (VALUES('太郎', 'リンゴ', 10),
                ('太郎', 'バナナ', 2),
                ('太郎', 'バナナ', 8),
                ('二郎', 'バナナ', 9),
                ('二郎', 'バナナ', 1),
                ('三郎', 'パイナップル', 1),
                ('三郎', 'リンゴ', 4),
                ('三郎', 'リンゴ', 5)) AS t("name", "item", "amount")
)

  SELECT "name"
       , "item"
       , SUM("amount") AS amount_sum
       , COUNT(DISTINCT "name") AS name_count
       , COUNT(DISTINCT "item") AS item_count
    FROM t
GROUP BY CUBE(1, 2)
;
```

以下のような結果になります

```txt
 name |     item     | amount_sum | name_count | item_count
------+--------------+------------+------------+------------
 三郎 | パイナップル |          1 |          1 |          1
 三郎 | リンゴ       |          9 |          1 |          1
 三郎 |              |         10 |          1 |          2
 二郎 | バナナ       |         10 |          1 |          1
 二郎 |              |         10 |          1 |          1
 太郎 | バナナ       |         10 |          1 |          1
 太郎 | リンゴ       |         10 |          1 |          1
 太郎 |              |         20 |          1 |          2
      |              |         40 |          3 |          3
      | バナナ       |         20 |          2 |          1
      | パイナップル |          1 |          1 |          1
      | リンゴ       |         19 |          2 |          1
(12 rows)
```

# 問題

`GROUP BY CUBE` を使うと、どの行がどのレベルの小計なのかとか総計はどれなのかとかわかりにくくなってしまいます。

# GROUPING

そこで役に立ちそうなのが `GROUPING` です。

https://www.postgresql.jp/document/12/html/functions-aggregate.html
> グループ化演算はグループ化セット（7.2.4参照）と一緒に使われ、結果の行を区別するものです。 GROUPING演算の引数は実際には評価されませんが、関連する問い合わせのGROUP BY句にある式と正確に一致する必要があります。 最も右側の引数が最下位ビットになるようにビットが割り当てられます。 各ビットは、対応する式が結果の行を生成するグループ化セットのグループ化条件に含まれていれば0、そうでなければ1です。 例えば以下のようになります。

とりあえず使ってみます。

```sql
WITH t AS (
  SELECT *
    FROM (VALUES('太郎', 'リンゴ', 10),
                ('太郎', 'バナナ', 2),
                ('太郎', 'バナナ', 8),
                ('二郎', 'バナナ', 9),
                ('二郎', 'バナナ', 1),
                ('三郎', 'パイナップル', 1),
                ('三郎', 'リンゴ', 4),
                ('三郎', 'リンゴ', 5)) AS t("name", "item", "amount")
)

  SELECT "name"
       , "item"
       , SUM("amount") AS amount_sum
       , COUNT(DISTINCT "name") AS name_count
       , COUNT(DISTINCT "item") AS item_count
       , GROUPING("name", "item")
    FROM t
GROUP BY CUBE(1, 2)
ORDER BY 6
;
```

結果は↓

```txt
 name |     item     | amount_sum | name_count | item_count | grouping
------+--------------+------------+------------+------------+----------
 三郎 | パイナップル |          1 |          1 |          1 |        0
 三郎 | リンゴ       |          9 |          1 |          1 |        0
 二郎 | バナナ       |         10 |          1 |          1 |        0
 太郎 | バナナ       |         10 |          1 |          1 |        0
 太郎 | リンゴ       |         10 |          1 |          1 |        0
 二郎 |              |         10 |          1 |          1 |        1
 太郎 |              |         20 |          1 |          2 |        1
 三郎 |              |         10 |          1 |          2 |        1
      | リンゴ       |         19 |          2 |          1 |        2
      | パイナップル |          1 |          1 |          1 |        2
      | バナナ       |         20 |          2 |          1 |        2
      |              |         40 |          3 |          3 |        3
(12 rows)
```

`grouping` に `0` か `1` か `2` か `3` が現れています。
これはそれぞれ `B'00'`、 `B'01'`、`B'10'`、 `B'11'` に対応しています。
2 bit あるのは引数が 2 個だからで、それぞれの bit に `name` と `item` が対応していて、対応する bit が立っている列の計である事を表しています。全ての bit が立っている値（今回の場合は 3）は総計になります。

# 分かりやすく分類してみる

```sql
WITH t AS (
  SELECT *
    FROM (VALUES('太郎', 'リンゴ', 10),
                ('太郎', 'バナナ', 2),
                ('太郎', 'バナナ', 8),
                ('二郎', 'バナナ', 9),
                ('二郎', 'バナナ', 1),
                ('三郎', 'パイナップル', 1),
                ('三郎', 'リンゴ', 4),
                ('三郎', 'リンゴ', 5)) AS t("name", "item", "amount")
)

  SELECT "name"
       , "item"
       , SUM("amount") AS amount_sum
       , COUNT(DISTINCT "name") AS name_count
       , COUNT(DISTINCT "item") AS item_count
       , CASE GROUPING("name", "item")
         WHEN 0 THEN '明細'
         WHEN 1 THEN 'name 小計'
         WHEN 2 THEN 'item 小計'
         WHEN 3 THEN '総計'
         ELSE 'あり得ない'
         END
    FROM t
GROUP BY CUBE(1, 2)
ORDER BY GROUPING("name", "item")
;
```

`SELECT` の `GROUPING` は `CASE` で振り分けて、ソートは `GROUPING` そのものにしてみます。

```txt
 name |     item     | amount_sum | name_count | item_count |   case
------+--------------+------------+------------+------------+-----------
 三郎 | パイナップル |          1 |          1 |          1 | 明細
 三郎 | リンゴ       |          9 |          1 |          1 | 明細
 二郎 | バナナ       |         10 |          1 |          1 | 明細
 太郎 | バナナ       |         10 |          1 |          1 | 明細
 太郎 | リンゴ       |         10 |          1 |          1 | 明細
 二郎 |              |         10 |          1 |          1 | name 小計
 太郎 |              |         20 |          1 |          2 | name 小計
 三郎 |              |         10 |          1 |          2 | name 小計
      | リンゴ       |         19 |          2 |          1 | item 小計
      | パイナップル |          1 |          1 |          1 | item 小計
      | バナナ       |         20 |          2 |          1 | item 小計
      |              |         40 |          3 |          3 | 総計
(12 rows)
```

なんとなく分かりやすくなりました。便利。

# まとめ

どこの列が `NULL` だったら、みたいな条件で小計かどうかを判定するより、`GROUPING` を使った方がスッキリと分類出来そうで便利だと思いました。
最近は集計は BigQuery ばかり使ってるんですが、PostgreSQL にある `CUBE` が使えなくて困ってます。早く `BigQuery` にも `CUBE` が来て欲しいです（ついでに `GROUPING` も）。