こんにちは、HRBrainでバックエンドエンジニアをしている稲垣です。
今年、K-POPにドハマりしてしまいましたので、アドカレでは BEST K-POP 2023
みたいな記事でバズリ狙いを考えたのですが、技術要素を盛り込むことができず断念しました。
今年は業務でBigQueryを使う機会が多かったため、今年リリースされたBigQueryの機能の中から ANY_VALUE(HAVING MAX)
について紹介しようと思います。
ユースケース例
ANY_VALUE関数を説明するために、ユースケースとして履歴データの検索を考えてみたいと思います。
以下のように、ユーザごとに好きなフルーツの変更履歴を管理するテーブルがあります。
- id: 主キー
- name: ユーザ名
- fruit: フルーツ名
- date: 登録日
テーブル: user_favorite_fruits
id | name | fruit | date |
---|---|---|---|
1 | takashi | grape | 2023-12-15 |
2 | takashi | orange | 2023-12-01 |
3 | takashi | apple | 2023-11-17 |
4 | takashi | banana | 2023-11-03 |
5 | takashi | mango | 2023-10-20 |
6 | takashi | pineapple | 2023-10-06 |
7 | takashi | kiwi | 2023-09-22 |
8 | takashi | strawberry | 2023-09-08 |
9 | takashi | lemon | 2023-08-25 |
10 | takashi | melon | 2023-08-11 |
11 | hanako | kiwi | 2024-12-05 |
12 | hanako | banana | 2023-11-21 |
13 | hanako | mango | 2023-11-07 |
14 | hanako | apple | 2023-10-24 |
15 | hanako | lemon | 2023-10-10 |
16 | hanako | orange | 2023-09-26 |
17 | hanako | strawberry | 2023-09-12 |
18 | hanako | melon | 2023-08-29 |
19 | hanako | pineapple | 2023-08-15 |
20 | hanako | grape | 2023-08-01 |
このテーブルから各ユーザが特定の日時点で好きなフルーツを抽出します。
前提として、各ユーザはより新しい登録日のフルーツの登録があるまでは、登録日が最新のフルーツが一貫してお気に入りな、一途で良い人だとします。
たとえば、2023-11-10時点で好きなデータを抽出した結果は以下のようになることを期待しています。
id | name | fruit | date |
---|---|---|---|
4 | takashi | banana | 2023-11-03 |
13 | hanako | mango | 2023-11-07 |
SQLを考える
上記の結果を取得できるSQLとして、自分がまず思いついたのは以下のSQLでした。
SELECT
f.id,
f.name,
f.fruit
FROM
`[dataset].user_favorite_fruits` f
INNER JOIN (
SELECT
name,
MAX(date) as max_date
FROM
`[dataset].user_favorite_fruits`
WHERE
date <= '2023-11-10'
GROUP BY
name
) latest_fruits ON f.name = latest_fruits.name AND f.date = latest_fruits.max_date
サブクエリ(latest_fruits)で各ユーザーについて、指定された日付(この例では'2023-11-10')より前の最新の登録日のフルーツのエントリを見つけます。そして、メインクエリでこの情報を使用して、各ユーザーの最新の好きなフルーツを取得します。
他にもROW_NUMBER()を使う方法などもあるようですが、どちらも複数のSELECT句を組み合わせて実現することになります。
ANY_VALUE(HAVING MAX)を使って改善する
ANY_VALUE(HAVING MAX)を使うことで上記のSQLはよりシンプルに書くことができるようになります。
まず、ANY_VALUE関数についてですが、この関数自体はGROUP BY句などで、指定された列以外の列の集約値を決定する際に、任意の単一の値を選択するのに使われます。
(ただし、この際に選択される値は非決定的です。)
以下のような感じで使います。
SELECT
name,
ANY_VALUE(fruit) as fruit,
MAX(date) AS max_date
FROM
`[dataset].user_favorite_fruits`
WHERE
date <= '2023-11-10'
GROUP BY
name
上記のクエリではたとえば以下のような結果を返します。
この結果ではdateはMAX関数で決定していますが、fruitはANY_VALUE関数で取得しているため、なにが選択されているかはわかりません。もちろんdateとの関係もありません。
name | fruit | date |
---|---|---|
takashi | kiwi | 2023-11-03 |
hanako | lemon | 2023-11-07 |
これだけだと、ちょっと使いどころが難しいな、と思っていたのですが、ANY_VALUE(x HAVING MAX y)が一般提供されました
ANY_VALUE() にHAVING MAX句で値に制限を追加することができます。
ANY_VALUE(x HAVING MAX y)と記述することで、集約した結果、yが最大である行に限定しxの値を取得することができるようになっています。
これを使うことで、今回の例では以下のようにクエリを書けるようになります。
SELECT
name,
-- 変更前: ANY_VALUE(fruit) as fruit,
ANY_VALUE(fruit HAVING MAX date) as fruit,
MAX(date) AS max_date
FROM
`[dataset].user_favorite_fruits`
WHERE
date <= '2023-11-10'
GROUP BY
name
どうでしょうか、最初のサブクエリを使った例と比べるとクエリがシンプルになり、格段に読みやすくなったと思います。
注意点として、ANY_VALUE(x HAVING MAX y)は非決定的で1つの値に定めるため、今回の例のように日付単位で履歴を管理していて、最新の同一日付のお気に入りフルーツがある場合は、同じ結果になりません。
(サブクエリを使っている方は同一日付のものをすべて返します。)
なので、最大値・最小値による制約で行が一意に定まりやすいテーブルで使うことでより恩恵を受けることができると思います。
まとめ
今年GAになった ANY_VALUE(x HAVING MAX y) について紹介させていただきました。
自分も、履歴データを扱うユースケースでこの句を活用することができました。
今年もたくさんのアップデートがありましたが、来年もBigQueryのアップデートが楽しみですね!
弊社ではエンジニアを募集しています!
それでは、良い年末をお過ごしください!