【BigQuery】ケース別便利クエリ

# はじめに
便利なクエリをケース別に紹介していきます。筆者が運用保守でよく使用しているクエリたちです。
特にタイムトラベル機能は非常に便利なので、ぜひ使っていただきたいです。

# 使用ケース
## 日次データに欠落がないか調べたい
きちんと毎日のレコードが入っているか確認します。テーブルに日時カラムがあることが条件です。
日別のレコード件数を出すことによってデータが少ない等の違和感に気づくこともできます。

```sql
SELECT
  purchase_date,
  COUNT(*) AS cnt
FROM
  `dataset.table_name`
GROUP BY 1
ORDER BY 1 DESC
```

↓ こんな感じで欠落した日があることに気づくことができる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/705b09dd-5879-ecf0-73de-e024709c9a80.png)

DATEではなくTIMESTAMPならこちら。タイムゾーンを合わせることをお忘れなく。

```sql
SELECT
  DATE(purchase_timestamp, "Asia/Tokyo"),
  COUNT(*) AS cnt
FROM
  `dataset.table_name`
GROUP BY 1
ORDER BY 1 DESC
```

## 過去の状態のテーブルが見たい（7日前まで）
### タイムトラベル機能の使い方
https://cloud.google.com/bigquery/docs/access-historical-data?hl=ja

過去のテーブルに入っていたデータを`FOR SYSTEM_TIME AS OF`で見ることができます。
見たい日時を指定してください。
これを利用すれば、誤って更新したテーブルを元の状態に戻すこともできるので、とても安心感があります。

```sql
SELECT
  *
FROM
  `dataset.table_name`
-- 絶対時間
FOR SYSTEM_TIME AS OF TIMESTAMP('2023-12-01 14:00:00', 'Asia/Tokyo');
-- 相対時間
-- FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 HOUR);
```

遡れるのは最大で7日間です。データセットにタイムトラベルできる期間が設定されています（デフォルトは7日）。

![スクリーンショット 2023-12-17 13.44.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/67eb65e4-47b5-4902-44b6-02f5d168ab48.png)

### タイムトラベルが使えないテーブル

[Viewテーブル](https://cloud.google.com/bigquery/docs/views-intro?hl=ja)は実態を持たないため、タイムトラベル機能を使うことはできません（エラーになります）。
過去の状態が見たければ、そのViewに設定されたクエリを`FOR SYSTEM_TIME AS OF`付きで実行しましょう。

また、[外部テーブル](https://cloud.google.com/bigquery/docs/external-tables?hl=ja)で利用する際にも注意しましょう。
`FOR SYSTEM_TIME AS OF`のクエリは成功しますが、過去に遡るのはテーブルの設定（スキーマやソースURIなど）だけです。つまり外部（GCSなど）に保存されたデータは今の状態しか参照できません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/14e50a4a-58a5-8436-3291-793497b4bd34.png)

## 2つのテーブルの差分を見たい
このような2つのテーブルがあるとします。

<table>
  <caption>users_A</caption>
  <thead>
    <tr>
      <th>id</th> <th>name</th>
    </tr>
  </thead>
  <tr>
    <td>1</td> <td>田中</td>
  </tr>
  <tr>
    <td>2</td> <td>佐藤</td>
  </tr>
  <tr>
    <td>3</td> <td>高橋</td>
  </tr>
  <tr>
    <td>4</td> <td>伊藤</td>
  </tr>
  <tr>
    <td>5</td> <td>武田</td>
  </tr>
</table>

<table>
  <caption>users_B</caption>
  <thead>
    <tr>
      <th>id</th> <th>name</th>
    </tr>
  </thead>
  <tr>
    <td>1</td> <td>田中</td>
  </tr>
  <tr>
    <td>2</td> <td>佐藤</td>
  </tr>
  <tr>
    <td>3</td> <td>高橋</td>
  </tr>
  <tr>
    <td>4</td> <td>鈴木</td>
  </tr>
  <tr>
    <td>5</td> <td>中村</td>
  </tr>
</table>

### AにはあるがBには無いレコードを探す
Aのテーブルには"伊藤"と"武田"がありますが、Bにはありません。
こういった片方で比較して差分を抽出するにはこうします。

```sql
SELECT
  *
FROM
  `dataset.users_A` EXCEPT DISTINCT
SELECT
  *
FROM
  `dataset.users_B`
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/e9635074-b074-d64b-6ee6-d7ee48112f5d.png)

### AとBのどちらか一方にしかないレコードを探す
先ほどとは違い、AとBの双方向で比較して差分を抽出するには、それぞれの差分をUNIONします。
`table_name`カラムをつけると、どちらに存在するレコードか分かるためおすすめです。

```sql
SELECT
  "A" AS table_name
  , *
FROM
  (SELECT * FROM dataset.users_A EXCEPT DISTINCT SELECT * FROM dataset.users_B) -- Aにだけ存在するレコードを抽出する
UNION ALL
SELECT
  "B" AS table_name
  , *
FROM
  (SELECT * FROM dataset.users_B EXCEPT DISTINCT SELECT * FROM dataset.users_A) -- Bにだけ存在するレコードを抽出する
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/2294836b-c64c-cca3-6a14-6ea738d949ba.png)

ただしテーブルの容量が大きいほど、処理にかかるコスト・時間も大きくなるため状況に応じてWHERE句を使いましょう。

例）2023年12月1日のレコードだけを比較する

```sql
SELECT
  "A" AS table_name
  , *
FROM
  (SELECT * FROM dataset.table_A WHERE date = "2023-12-01" EXCEPT DISTINCT SELECT * FROM dataset.table_B WHERE date = "2023-12-01") -- Aにだけ存在するレコードを抽出する
UNION ALL
SELECT
  "B" AS table_name
  , *
FROM
  (SELECT * FROM dataset.table_B WHERE date = "2023-12-01" EXCEPT DISTINCT SELECT * FROM dataset.talbe_A WHERE date = "2023-12-01") -- Bにだけ存在するレコードを抽出する
```

## (応用編) テーブルの更新前後の差分を見たい

このようなテーブルの中身を更新したとします

<table>
  <caption>変更前</caption>
  <thead>
    <tr>
      <th>id</th> <th>name</th>
    </tr>
  </thead>
  <tr>
    <td>1</td> <td>田中</td>
  </tr>
  <tr>
    <td>2</td> <td>佐藤</td>
  </tr>
  <tr>
    <td>3</td> <td>高橋</td>
  </tr>
  <tr>
    <td>4</td> <td>伊藤</td>
  </tr>
  <tr>
    <td>5</td> <td>武田</td>
  </tr>
</table>

<table>
  <caption>変更後</caption>
  <thead>
    <tr>
      <th>id</th> <th>name</th>
    </tr>
  </thead>
  <tr>
    <td>1</td> <td>田中太郎</td>
  </tr>
  <tr>
    <td>2</td> <td>佐藤次郎</td>
  </tr>
  <tr>
    <td>3</td> <td>高橋三郎</td>
  </tr>
  <tr>
    <td>4</td> <td>鈴木四郎</td>
  </tr>
  <tr>
    <td>5</td> <td>中村五郎</td>
  </tr>
</table>

### 差分抽出のクエリ

差分を抽出するには[一時テーブル](https://cloud.google.com/bigquery/docs/multi-statement-queries?hl=ja#create_temporary_table)とタイムトラベル機能、テーブルの比較クエリを組み合わせます。

```sql
-- 一時テーブルの作成クエリ
CREATE OR REPLACE TEMP TABLE users_a_old AS
SELECT
  *
FROM
  `dataset.users_a` FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
;

-- 差分の抽出クエリ
SELECT
  "after" AS table_name
  , *
FROM
  (SELECT * FROM `dataset.users_a` EXCEPT DISTINCT SELECT * FROM users_a_old) -- 変更後にだけ存在するレコードを抽出する
UNION ALL
SELECT
  "before" AS table_name
  , *
FROM
  (SELECT * FROM users_a_old EXCEPT DISTINCT SELECT * FROM `dataset.users_a`) -- 変更前にだけ存在するレコードを抽出する
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/f0887f0b-60d3-de1d-fc62-b18ef4de21dc.png)

### 一時テーブルを使用する理由
上のクエリでなぜ一時テーブルを使用しているのかと疑問に思う方もいらっしゃると思います。
その理由は、**1度のクエリで過去と現在の両方は参照できない**からです。
例えば、下記のクエリはエラーになります。

```sql
SELECT
  *
FROM
  `dataset.users_a` EXCEPT DISTINCT
SELECT
  *
FROM
  `dataset.users_a` FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
```

エラーメッセージ
```text
Table 'dataset.users_a' is referenced with and without 'FOR SYSTEM_TIME AS OF' expression. If a 'FOR SYSTEM_TIME AS OF' expression is used, all references of a table should use the same TIMESTAMP value.
```
訳）テーブル'dataset.users_a'が'FOR SYSTEM_TIME AS OF'式の有無にかかわらず参照されている。FOR SYSTEM_TIME AS OF'式を使用した場合、テーブルの全ての参照は同じTIMESTAMP値を使用しなければなりません。

そのため、過去のテーブルから抽出するクエリと現在のテーブルから抽出するクエリは分けなければなりません。
一時テーブルに過去データを格納することで、差分比較ができるようになります。
ちなみに一時テーブルは24時間で自動削除されるため、テーブルの後片付けは必要ありません。