【BigQueryでループ処理】を書く

# 概要
WHERE句にある絞り込み条件で同じSQLを流用しながら条件を変えつつforループのように実行したい時のBQでループを書く方法。

## 問題設定

今回は「アクセスログの中から、集計対象となるログをマスタテーブルに保持されているパラメータで絞り込みUU数を計算する」ということを想定してSQLを書く。

※この程度の処理であれば、マスタテーブルとログテーブルを突合してパラメータでGROUP BY すれば集計できますが、今回はループ処理の説明のために簡単な問題をテーマに扱って解説します。

## 使用するデータテーブル

▼masterテーブル
|parameter  |
|---|
|a  |
|b  |
|c  |
|...  |

▼アクセスログ(access_log) テーブル
|user_id  |session  |time_stamp  |page_url  |event  |
|---|---|---|---|---|
|Aさん  |XXX  |1  |https://hogehoge.com　  |NULL  |
|Aさん  |XXX  |2  |https://hogehoge.com　  |a  |
|Bさん  |YYY  |3  |https://hogehoge.com　  |b  |
|...  |...  |...  |...　  |...  |

- user_id：サイトに訪問したユーザのID
- session：セッション
- time_stamp：ページを閲覧した時の時刻
- page_url：閲覧したページのURL
- event：ユーザが事前定義されたアクションを行った際にパラメータが格納される（アクションしない場合はNULL）

## 処理フロー

処理フローを日本語で書くと、次のようなイメージ。

①初期値定義
②ループ回数(n)の計算
③処理開始
　(i)ループインデックス(i)回目のパラメータ値を抽出
　(ii)tempテーブルを作成
　(iii)各イテレーションの処理を書く
　(iv)ループ1回目ならoutputテーブル作成、2回目以降なら、outputテーブルにインサート
　(v)インデックス i = i+1 でインデックスを増やす
　(vi)インデックスiとループ回数(n)を比較。nより大きければループ終了


## 作成したSQL

```SQL
--------------------------------------------------------
-- 変数定義 & 初期化
--------------------------------------------------------
-- ループ中のindex
DECLARE i INT64 DEFAULT 1;
-- ループしたい回数
DECLARE n INT64 DEFAULT 0;


--------------------------------------------------------
-- ループ回数の計算
--------------------------------------------------------
SET n = (
  --マスターテーブルの"ユニーク"なレコード数を数えて何回ループさせるかを計算
  WITH master AS (
    SELECT 
      parameter
    FROM
      `master_table`
  )
  SELECT COUNT(DISTINCT parameter)
  FROM master
);


--------------------------------------------------------
-- 処理開始

LOOP
    -- i = i でループしているときに条件判定で使うための変数値を抽出
    -- target_parameterにparameterがセットされる
    SET target_parameter = (
        WITH master AS (
            SELECT 
                DISTINCT parameter
            FROM
                `master_table`
        )
        -- 順番(インデックス)をつける
        ,order_master AS (
            SELECT
                parameter
                ,ROW_NUMBER() OVER(ORDER BY parameter DESC) AS ROW_NUMBER
            FROM
                master
        )
        -- インデックス通りの順番に並び替える
        ,ordered_master AS (
            SELECT
                parameter
                ,ROW_NUMBER
            FROM
                order_master
            ORDER BY
                ROW_NUMBER
        )
        -- ループインデックス(i)番目のparameterを抽出
        SELECT parameter
        FROM ordered_master
        WHERE ROW_NUMBER = i
    );


--------------------------------------------------------
-- ループ処理


--------------------------------------------------------
-- 1ループ毎に結果をtempというテーブルに格納する
CREATE OR REPLACE TABLE temp
  AS (

------------------------------------------------------------------------------------------------
--****************************実際の処理はここに書く*************************************************
--------------------------------------------------------------
--target_parameterを含んでいるアクセスログを抽出
WITH log AS (
    SELECT
        user_id     -- ユーザID
        ,session    -- セッション
        ,time_stamp -- ページを閲覧した時の時刻
        ,page_url   -- 閲覧したページのURL
    FROM
        `access_log`
    WHERE
    	event = target_parameter -- eventというカラムにtarget_parameterが含まれているレコードに限定
)

--------------------------------------------------------------
-- ユニークユーザ数のカウント

SELECT
  COUNT(DISTINCT user_id) AS uu
FROM
  log

--********************************処理終了*******************************************************
------------------------------------------------------------------------------------------------
);


--------------------------------------------------------
-- tempを都度参照し、ループ1回目なら新しくテーブルを作り、2回目以降なら結果を追加する
  IF i = 1 THEN
    CREATE OR REPLACE TABLE output AS (SELECT * FROM temp);
  ELSE
    INSERT INTO output SELECT * FROM temp;
  END IF;



  SET i = i + 1;
  IF i > n THEN
    LEAVE;
  END IF;
END LOOP;

```

## ループ処理で書くメリット

ループを使わずともUNIONやWITH句を大量に作れば同じことはできますが、2つ問題があるかと思います

### ①実行可能性の問題

BigQueryではwith句やUNIONを多用するとリソース不足に陥りSQLが実行できなくなることがあります。ループを使うことでリソース不足に陥るリスクを低減できます

### ②開発/運用保守などの人間側の問題

ループ処理で書きたいということは同じような処理を繰り返し実行したいということですが、これを素直にWITH句などで記載すると、基本となるwith句をひたすら「コピペし、with句内のparameterを変えて…」ということの繰り返しになると思います。（記事下部に記載している「WITH句を大量に作る場合のSQLのイメージ」を参照）
人間が手作業をする量が増えれば増えるほど、ミス発生の可能性が高まるため、コピペする量が増えればparameterの変更漏れが発生してしまう可能性が高くなりますつまり、**開発でのミスが発生する可能性が高まります。**

また、付与されているパラメータの仕様が変わったりすると、**全てのwith句で改修しなくてはいけなくなるため運用保守が大変になります。**（筆者の実務ではパラメータを100個単位で変更することがザラにあったため、もしループではなく、with句を大量に使った場合はちょっとした変更が起きるたびに100個単位での変更が必要となり残業しないといけなくなっていたかもしれません）

**ループ処理を使えば、パラメータの仕様が変わってもマスタ側のパラメータをアップデートすれば即座に対応できるため上記のような運用保守の手間から解放されます。**


#### WITH句を大量に作る場合のSQLのイメージ

```SQL
WITH table_a AS (
  SELECT
    user_id     -- ユーザID
    ,session    -- セッション
    ,time_stamp -- ページを閲覧した時の時刻
    ,page_url   -- 閲覧したページのURL
  FROM
    `access_log`
  WHERE
     event = 'a'
),
table_b AS (
  SELECT
    user_id     -- ユーザID
    ,session    -- セッション
    ,time_stamp -- ページを閲覧した時の時刻
    ,page_url   -- 閲覧したページのURL
  FROM
    `access_log`
  WHERE
     event = 'b'
),
table_c AS (
  SELECT
    user_id     -- ユーザID
    ,session    -- セッション
    ,time_stamp -- ページを閲覧した時の時刻
    ,page_url   -- 閲覧したページのURL
  FROM
    `access_log`
  WHERE
     event = 'c'
)
...
...
...

SELECT COUNT(DISTINCT user_id) FROM table_a
UNION ALL
SELECT COUNT(DISTINCT user_id) FROM table_b
UNION ALL 
SELECT COUNT(DISTINCT user_id) FROM table_c
...

```