💁

BigQueryのSQLにおけるいくつかのTips

に公開

BigQueryをSQLで操作する際につまずいたポイントなどについて整理してみました。
BigQuery固有の挙動ではないものも含まれているかもしれませんが、SQLを書く際の参考になれば幸いです。

今後もBigQueryを利用していく中で新たな発見があれば、随時この内容を更新していく予定です。

1. 構造体の配列を展開するときのLEFT JOIN

以下のようなテーブル user_posts があるとします。
posts カラムは構造体の配列です。

id name posts
1 A [{ title: A, content: X, category_id: 1 }]
2 B [{ title: B, content: Y, category_id: null }]

上記のテーブルに対して、postsSELECT 句で展開し、さらに category テーブルを LEFT JOIN するケースを考えます。

SELECT
  id,
  ARRAY(
    SELECT AS STRUCT
      posts.title,
      categories.name
    FROM user_posts.posts posts
    LEFT JOIN categories ON categories.id = posts.category_id
  )
FROM user_posts

しかし、上記のクエリは以下のエラーになります。

Correlated subqueries that reference other tables are not supported unless they can be de-correlated, such as by transforming them into an efficient JOIN.

この問題への対処法はいくつか考えられますが、私は以下のように対応しています。LEFT JOINJOIN に変更し、NULL の場合はゼロで結合されるようにします。COALESCE 関数は、値が NULL の場合に代替のデフォルト値を指定するために利用できます。

WITH categories AS (
  SELECT id, name FROM categories
  UNION ALL
  SELECT NULL, NULL
)
SELECT
  id,
  ARRAY(
    SELECT AS STRUCT
      posts.title,
      categories.name
    FROM user_posts.posts posts
    JOIN categories ON COALESCE(categories.id, 0) = COALESCE(posts.category_id, 0)
  )
FROM user_posts

2. Range型で同じ値を扱えない

BigQueryには日付やタイムスタンプの区間を表す Range 型がありますが、開始値と終了値が同じ場合はエラーとなります。

SELECT RANGE(DATE '2025-04-01', DATE '2025-04-01')

ただし、日付を DATETIME に変換すれば、この問題を回避できます。

SELECT RANGE(DATETIME '2025-04-01', DATETIME '2025-04-01 23:59:59')

ちなみに、この Range 型を利用して特定の日付で絞り込みをしたい場合は、以下のように RANGE_CONTAINS 関数を使用できます。

# range_date が Range 型と仮定
SELECT * FROM samples WHERE RANGE_CONTAINS(range_date, CURRENT_DATE)

3. 他テーブルのカラムを利用して別テーブルを絞り込んだ結果から1行だけ取得したい場合の対処法

これは少し特殊なケースかもしれません。例えば、以下のようなデータ構造があるとします。

上記のテーブルで、「各ユーザーの登録日時点に所有していた車のうち、最も古い車を1台だけ取得したい」という要件を考えます。この場合、以下の「例1」や「例2」のようなクエリを記述したくなるかもしれませんが、これらはエラーとなるため、目的の結果は得られません。

-- 例1
SELECT
  users.name,
  (
    SELECT
      name
    FROM user_cars
    WHERE user_id = users.id
    AND (
      (user_cars.start_date <= users.registration_date AND user_cars.end_date IS NULL)
      OR (users.registration_date BETWEEN user_cars.start_date AND user_cars.end_date)
    )
    ORDER BY user_cars.start_date LIMIT 1
  )
FROM users

-- 例2
SELECT
  users.name,
  user_cars.name
FROM users
JOIN (
  SELECT
    name
  FROM user_cars
  WHERE user_cars.user_id = users.id
  AND (
    (user_cars.start_date <= users.registration_date AND user_cars.end_date IS NULL)
    OR (users.registration_date BETWEEN user_cars.start_date AND user_cars.end_date)
  )
  ORDER BY user_cars.start_date LIMIT 1
) user_cars

しかし、以下のように QUALIFY 句と ROW_NUMBER() 関数を使用することで、この問題を解決できます。user_id でグループ化し、start_date の昇順で順位を付けた結果、順位が 1user_cars に絞り込みを行っています。

SELECT
  users.name,
  user_cars.name
FROM users
JOIN user_cars ON user_cars.user_id = users.id
  AND (
    (user_cars.start_date <= users.registration_date AND user_cars.end_date IS NULL)
    OR (users.registration_date BETWEEN user_cars.start_date AND user_cars.end_date)
  )
QUALIFY ROW_NUMBER() OVER(PARTITION BY user_cars.user_id ORDER BY user_cars.start_date ASC) = 1

私もまだBigQueryについては学習中ですので、もし他にもっと良い方法をご存知でしたら、ぜひ教えていただけると嬉しいです。

4. 特定のカラムを除外してSELECTする

例えば、created_atupdated_at 以外のすべてのカラムを選択したい場合は、以下の構文を利用できます。

SELECT * EXCEPT(created_at, updated_at)

5. 0除算の回避

SQLで0除算を行おうとするとエラーが発生しますが、BigQueryの SAFE_DIVIDE 関数を使えば、エラーを回避できます。この関数を使用すると、0で割った結果は NULL が返されます。

SELECT SAFE_DIVIDE(x, 0)

6. GROUP BY ALL

グループ化をする場合、通常は集計関数以外の全てのカラムを GROUP BY 句に記述する必要がありました。しかし、GROUP BY ALL を利用することで、SELECT 句に指定されている集計関数以外の全てのカラムで自動的にグループ化を行うことができます。

SELECT
  id,
  name,
  SUM(number)
FROM samples
GROUP BY ALL

Discussion