【BigQuery】再帰的な階層テーブルをWITH RECURSIVEを使って結合する方法

GLASSでは様々なお客様の広告、サイト、業務データなどを分析しております。分析にはBigQueryを利用することが多く、例えば、業務データをお客様のシステムからBigQueryに転送してSQLを駆使して様々な角度からデータを整形、分析、可視化しています。

今回、お客様が扱う業務データの中に再帰的な自己参照を行う階層テーブルが含まれていたので、BiqQueryにてこれを分析しやすい形に展開する必要がありました。そこで、本記事はBiqQueryで2023年から利用できるようになったWITH RECURSIVEを使ってデータを展開する方法をご紹介いたします。

階層テーブルとは

階層テーブルは名前の通り階層構造を持つデータを表現するのに利用されるテーブル構造です。階層構造を持つデータととしては例えば組織のデータ(事業所>部>課)や商品カテゴリ(生鮮食品>野菜>根菜>人参)などがあります。

これを該当レコードに対する親階層のレコードのIDを使って表現すると以下のような形になります。

カテゴリIDカテゴリ名親カテゴリID
10生鮮食品null
1010野菜10
102010
103010
101010根菜1010
10101010人参101010
10101020大根101010
102010豚肉1020
102020牛肉1020
102030鶏肉1020
カテゴリテーブル

これを以下のような商品テーブルと結合します。カテゴリIDは末端のカテゴリのみが格納されており、その親のカテゴリは、直接は格納されていません

商品ID商品名カテゴリID
A1000豚コマ102010
B2000朝採れ人参10101010
C3000大根(カット)10101020
商品テーブル

今回は例として各商品がどのカテゴリに属しているか、展開するような形で以下のように結合することにします。

商品ID商品名カテゴリIDカテゴリ名
A1000豚コマ102010豚肉
A1000豚コマ1020
A1000豚コマ10生鮮食品
B2000朝採れ人参10101010人参
B2000朝採れ人参101010根菜
B2000朝採れ人参1010野菜
B2000朝採れ人参10生鮮食品
C3000大根(カット)10101020大根
C3000大根(カット)101010根菜
C3000大根(カット)1010野菜
C3000大根(カット)10生鮮食品
結合結果

これを実現するためのSQLはWITH RECURSIVEを使う場合と使わない場合で以下のように書けます。

階層テーブルの結合(WITH RECURESIVEを使わない場合)

-- 最初のカテゴリ (自分自身)
SELECT 
  item.item_id,
  item.item_name,
  category1.category_id AS category_id,
  category1.category_name AS category_name,
FROM `<project_id>.<dataset>.item` AS item
JOIN `<project_id>.<dataset>.category` AS category1 ON item.category_id = category1.category_id

UNION ALL

-- 2階層目のカテゴリ (親カテゴリ)
SELECT 
  item.item_id,
  item.item_name,
  category2.category_id AS category_id,
  category2.category_name AS category_name,
  'レベル2' AS category_level
FROM `<project_id>.<dataset>.item` AS item
JOIN `<project_id>.<dataset>.category` AS category1 ON item.category_id = category1.category_id
JOIN `<project_id>.<dataset>.category` AS category2 ON category1.parent_category_id = category2.category_id

UNION ALL

-- 3階層目のカテゴリ (親の親カテゴリ)
SELECT 
  item.item_id,
  item.item_name,
  category3.category_id AS category_id,
  category3.category_name AS category_name,
FROM `<project_id>.<dataset>.item` AS item
JOIN `<project_id>.<dataset>.category` AS category1 ON item.category_id = category1.category_id
JOIN `<project_id>.<dataset>.category` AS category2 ON category1.parent_category_id = category2.category_id
JOIN `<project_id>.<dataset>.category` AS category3 ON category2.parent_category_id = category3.category_id

UNION ALL

-- 4階層目のカテゴリ (親の親の親カテゴリ)
SELECT 
  item.item_id,
  item.item_name,
  category4.category_id AS category_id,
  category4.category_name AS category_name,
FROM `<project_id>.<dataset>.item` AS item
JOIN `<project_id>.<dataset>.category` AS category1 ON item.category_id = category1.category_id
JOIN `<project_id>.<dataset>.category` AS category2 ON category1.parent_category_id = category2.category_id
JOIN `<project_id>.<dataset>.category` AS category3 ON category2.parent_category_id = category3.category_id
JOIN `<project_id>.<dataset>.category` AS category4 ON category3.parent_category_id = category4.category_id

WITH RECURSIVEを使わない場合は1階層ごとにJOINする必要があります。上記は4階層の場合ですが、5階層以上のデータの場合はさらに記述を追加する必要があります。

つまり、WITH RECURSIVEを使わないこの記述方法は、あらかじめ階層数が分かっている場合にしか利用できません

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

階層テーブルの結合(WITH RECURSIVEを使う場合)

WITH RECURSIVEを使う場合は以下の通りです。

WITH RECURSIVE category_hierarchy AS (
  -- 最初のレベルのカテゴリ
  SELECT 
    item.item_id,
    item.item_name,
    category.category_id,
    category.category_name,
    category.parent_category_id
  FROM `<project_id>.<dataset>.item` AS item
  JOIN `<project_id>.<dataset>.category` AS category ON item.category_id = category.category_id
  
  UNION ALL
  
  -- 再帰的に親カテゴリをたどる
  SELECT
    ch.item_id,
    ch.item_name,
    parent.category_id,
    parent.category_name,
    parent.parent_category_id
  FROM category_hierarchy ch --★1
  JOIN `<project_id>.<dataset>.category` AS parent ON ch.parent_category_id = parent.category_id
)

-- 結果を表示
SELECT item_id, item_name, category_id, category_name FROM category_hierarchy

WITH RECURSIVEを使う場合はコンパクトですね。こちらの場合は階層数があらかじめわかっていなくても対応が可能です。

ポイントはFROM句にWITH RECURSIVEで指定したCTEの名前指定することです(★1)。これにより再帰的な呼び出しを実現しています。プログラムで言えば「最初のレベルのカテゴリ」が末端の処理本体で、「再帰的に親カテゴリをたどる」の部分が再帰呼び出しだと思ってもらえると理解しやすいかと思います。

ただし、階層数が何段でも対応できる便利さの反面、データの誤りで階層構造がループしてしまい、無限の呼び出しが発生するケースが考えられます。そのため、反復回数には500回の上限が設けられています。

結果はWITH RECURSIVEを使わない場合と同様、以下のようになります。

まとめ

今回はWITH RECURSIVEを使って再帰的な階層テーブルを展開する方法を紹介しました。
データ分析を行っていると様々なデータを取り扱うことになると思います。分析対象に階層テーブルが現れたときに今回の記事が参考になれば幸いです。

GLASSでは広告、サイト、業務データなどの様々なデータを可視化、分析してお客様のビジネスをサポートしています。ご興味があれば以下から気軽にお問い合わせください。

マーケティングデータ統合ダッシュボード
GLASSで一緒に働いてみませんか?