本文へ移動

ホーム / Data / 第2部 データの基礎

第2部 データの基礎・第2章

分析のための SQL

★★★ 必須11トピック・面接 5問

データエンジニアの基本の道具である SQL を、分析の用途で使いこなす。

別のページで開く →

DBラボを読み込んでいます…

2.1ウィンドウ関数の基本

用語★★★ 必須
ひとことで

行を残したまま、グループ内の順位・累計・前の行との差を計算する。

ウィンドウ関数は、GROUP BY のように行をまとめずに、「同じグループの行たち(ウィンドウ)」を見て計算した値を各行に付け足す関数です。関数() OVER (PARTITION BY グループ ORDER BY 並び) の形で書きます。CTE(WITH 名前 AS (…))は、途中の結果に名前をつけて段階的に書く方法で、長い分析 SQL が読みやすくなります。

GROUP BY は行がまとまり、ウィンドウ関数は行が残るGROUP BY useruser金額A100A200B50B150user合計A300B2004行 → 2行SUM() OVER (PARTITION BY user ORDER BY …)user金額A100A200B50B150user金額累計A100100A200300B5050B1502004行のまま、列が1つ増える
GROUP BY とウィンドウ関数の違い
関数することよく使う場面
ROW_NUMBER()グループ内で 1, 2, 3… と番号をふる(同点でも別の番号)最新の1件・重複の除去
RANK()・DENSE_RANK()同点は同じ順位(RANK は次を飛ばす)ランキング
LAG()・LEAD()前の行・次の行の値を取る前日比・前回の購入からの日数
SUM() OVER (ORDER BY …)累計月ごとの累計売上
AVG() OVER (… ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)移動平均7日移動平均
WITH daily AS (
  SELECT order_date, SUM(amount) AS sales
  FROM orders
  GROUP BY order_date
)
SELECT
  order_date,
  sales,
  sales - LAG(sales) OVER (ORDER BY order_date)           AS diff_prev_day,
  SUM(sales) OVER (ORDER BY order_date)                   AS running_total,
  AVG(sales) OVER (ORDER BY order_date
                   ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM daily
ORDER BY order_date;

重複の除去・最新の1件

ウィンドウ関数の落とし穴

面接ではこう答える

ウィンドウ関数は、GROUP BY のように行をまとめずに、同じグループの行を見て計算した値を各行に足す関数です。OVER の中に PARTITION BY でグループを、ORDER BY で並びを書きます。ROW_NUMBER で最新の1件を取る、LAG で前日比を出す、SUM OVER で累計、ROWS BETWEEN で移動平均を出す、といった分析の定番がすべてこれで書けます。

別のページで開く →

DBラボを読み込んでいます…

2.2CTE で段階的に書く

用語★★★ 必須
ひとことで

WITH で途中の結果に名前をつけ、長い SQL を上から順に読める段階に分ける書き方。

CTE(共通テーブル式)は、WITH 名前 AS (SELECT …) で途中の結果に名前をつける書き方です。サブクエリを何重にも入れ子にする代わりに、上から順に「何をしているか」を読めるようになります。

CTE で段階に分けて書くraw元のテーブルWITH の外daily日別に集計WITH daily ASwith_prev前日の値を付けるWITH … AS最後の SELECT前日比を出す結果
1段ずつ名前をつけると、途中の結果を確かめながら書ける
WITH daily AS (
  SELECT order_date, SUM(amount) AS sales
  FROM orders
  GROUP BY order_date
),
with_prev AS (
  SELECT order_date, sales,
         LAG(sales) OVER (ORDER BY order_date) AS prev_sales
  FROM daily
)
SELECT order_date, sales,
       ROUND(100.0 * (sales - prev_sales) / NULLIF(prev_sales, 0), 1) AS growth_pct
FROM with_prev;
使い方効きどころ
段階に分ける各段階を単独で流して、件数や値を確かめられる
同じ部品を2回使う同じサブクエリを2回書かずに済む
再帰(WITH RECURSIVE)日付の連番を作る、組織の階層をたどる

注意:CTE は読みやすさのための道具で、速くなるとは限りません。BigQuery では、再帰でない CTE を何か所から参照すると、参照の数だけ実行されます。重い CTE を何度も使うなら、一時テーブルや中間のモデルに出します。

面接ではこう答える

CTE は WITH 名前 AS で途中の結果に名前をつける書き方で、入れ子のサブクエリを上から読める段階に分けられます。各段階を単独で流して確かめられるので、長い分析の SQL やライブコーディングで間違いを減らせます。ただし性能のための道具ではなく、DB によっては参照の数だけ計算されるので、重い部品を何度も使うなら中間テーブルにします。

ひとことで

キーごとに ROW_NUMBER で新しい順に番号をふり、1番だけを残す。

重複の除去は、データエンジニアが毎日のように書く形です。CDC や再送で同じ注文が何行も届いたとき、キーごとに一番新しい行だけを残します。

キーごとに番号をふり、1番だけ残すorder_idupdated_at状態rn100111/01 10:00支払済2100111/02 09:00発送済1100211/01 12:00支払済1100211/01 12:00支払済2WHERE rn = 1order_idupdated_at状態100111/02 09:00発送済100211/01 12:00支払済同時刻の再送は、取り込み時刻で順番を決める
ROW_NUMBER で重複を除く(order_id ごとに最新の1行)
WITH ranked AS (
  SELECT *,
         ROW_NUMBER() OVER (
           PARTITION BY order_id
           ORDER BY updated_at DESC, _ingested_at DESC
         ) AS rn
  FROM raw_orders
)
SELECT * FROM ranked WHERE rn = 1;

BigQuery・Snowflake・DuckDB などでは QUALIFY ROW_NUMBER() OVER (…) = 1 と書けば CTE なしで絞り込めます。

同じ形で「ユーザーごとの最新の注文」「商品ごとの最新の価格」も取れます。PARTITION BY に何ごとに1件にしたいか、ORDER BY にどれを残すかを書く、と覚えます。DISTINCT は行のすべての列が同じときしか消せないので、更新された行の重複には使えません。

面接ではこう答える

重複の除去は、ROW_NUMBER を使って PARTITION BY にキー、ORDER BY に更新時刻の新しい順を書き、番号が1の行だけを残します。同じ時刻の行があると結果が実行ごとに変わるので、取り込み時刻などの2つ目の並びを足して結果を決まったものにします。BigQuery や Snowflake では QUALIFY で CTE なしに書けます。

ウィンドウ関数は便利な一方、エラーにならずに違う数字を返す落とし穴がいくつかあります。レビューで見るところでもあります。

同じ日付の行があると、累計の結果が変わる日付金額ORDER BY だけ(RANGE)ROWS を明示11/0110010010011/025013015011/028013023011/032025025011/02 の2行は、RANGE では同じ「仲間」として一度に足される
累計の既定の範囲(RANGE)と ROWS の違い
  • 同点:ORDER BY updated_at だけだと、同じ時刻の行がどちらになるかは実行ごとに変わり得ます。2つ目の並び(取り込み時刻や ID)を足して結果を決まったものにします。
  • 累計の既定の範囲:ORDER BY だけを書くと範囲は RANGE … CURRENT ROW になり、同じ日付の行はまとめて足されます。1行ずつにしたいときは ROWS を明示します。
  • WHERE ではウィンドウ関数を使えない:SQL は WHERE の後にウィンドウ関数を計算するので、CTE やサブクエリで包んでから絞ります。
  • LAG は「前の行」であって「前の日」ではありません。売上のない日があると、2日前と比べてしまいます。日付の表とつないで穴を埋めてから使います。
症状原因直し方
実行するたびに残る行が変わる並びの同点並びに2つ目の列を足す
累計が階段状になる既定の範囲が RANGEROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
前日比が2日前と比べているデータのない日がある日付の表と LEFT JOIN して穴を埋める

「連続ログイン日数の最大」「在庫切れが続いた期間」のように、続いているかたまりを見つける問題を gaps and islands と呼びます。SQL の面接の定番です。決まった解き方は「日付から、並び順の連番を引く」です。連続した日なら、日付も連番も1ずつ増えるので、引いた値が同じになります。

日付 − 連番 が同じなら、続いているログイン日rn日付 − rn11/01110/3111/02210/3111/03310/3111/05411/0111/06511/0110/31 のかたまり → 3日連続11/01 のかたまり → 2日連続かたまりごとに COUNT して、MAX を取る
連続した日を見つける(日付 − 連番)
WITH d AS (            -- 1日に何回ログインしても1行に
  SELECT DISTINCT user_id, login_date FROM logins
),
g AS (
  SELECT user_id, login_date,
         login_date - CAST(ROW_NUMBER() OVER (
           PARTITION BY user_id ORDER BY login_date) AS INT) AS grp
  FROM d
),
streak AS (
  SELECT user_id, grp, COUNT(*) AS days
  FROM g GROUP BY user_id, grp
)
SELECT user_id, MAX(days) AS max_streak
FROM streak GROUP BY user_id;

上は PostgreSQL の書き方(日付から整数を引くと日付になる)です。BigQuery では DATE_SUB(login_date, INTERVAL rn DAY) のように書きます。最初に DISTINCT で1日1行にするのを忘れると、同じ日の2回目のログインで連番がずれます。

NULL は「値が分からない」という特別な状態で、= で比べると真でも偽でもない「不明」になります。WHERE は真の行だけを残すので、NULL が混ざると行が黙って消えます。数字が合わないときの原因の上位です。

右の表の条件は、ON に書くON 側に書くLEFT JOIN o ON … AND o.status = 'paid'顧客支払済の注文A2BNULL(0件)C1注文のない B も残るWHERE 側に書くLEFT JOIN o ON … WHERE o.status = 'paid'顧客支払済の注文A2C1B の行は status が NULL なので消える(結果として INNER JOIN と同じ)
LEFT JOIN の条件を ON に書くか WHERE に書くか
落とし穴何が起きるか直し方
LEFT JOIN の右の条件を WHERE に書く右にない行(NULL)が消え、INNER JOIN と同じになる右の表の条件は ON に書く
NOT IN (サブクエリ) の中に NULL1行も返らなくなるNOT EXISTS を使う
col = NULLいつも不明で、何も当たらないIS NULL。NULL どうしを等しく扱うなら IS NOT DISTINCT FROM(対応する DB で)
COUNT(col) と COUNT(*)COUNT(col) は NULL を数えない何を数えたいかで使い分ける
JOIN のキーに NULLNULL どうしはつながらないキーの NULL を先に調べる。意図して「不明」の行につなぐ
AVG・SUM と NULLNULL は無視される(0 として扱われない)0 とみなすなら COALESCE(col, 0)
ひとことで

小計・合計を1回で出す。DB によって書き方が違う。

レポートでは「地域×商品の売上」と一緒に「地域ごとの小計」「全体の合計」もほしくなります。UNION ALL で何本もつなぐ代わりに、GROUP BY の拡張で1回で出せます。

どの組み合わせの小計が出るかROLLUP(地域, 商品)(地域, 商品)(地域) の小計() 総計CUBE(地域, 商品)(地域, 商品)(地域) の小計(商品) の小計() 総計GROUPING SETS(書いたものだけ)(地域) の小計(商品) の小計() 総計太枠:ROLLUP にはなく CUBE で増える組み合わせ
ROLLUP・CUBE・GROUPING SETS で出る組み合わせ(地域・商品の場合)
書き方出る集計の組み合わせ(region, product の場合)使いどころ
GROUP BY ROLLUP(region, product)(region, product)・(region)・()=総計階層の小計(年→月→日、地域→店舗)
GROUP BY CUBE(region, product)ROLLUP の分+(product) も。すべての組み合わせクロス集計の縦横の小計
GROUP BY GROUPING SETS ((region), (product), ())書いた組み合わせだけ必要な小計だけを選んで出す
SELECT
  CASE WHEN GROUPING(region)  = 1 THEN '(全地域)' ELSE region  END AS region,
  CASE WHEN GROUPING(product) = 1 THEN '(小計)'   ELSE product END AS product,
  SUM(amount) AS sales
FROM sales
GROUP BY ROLLUP(region, product)
ORDER BY GROUPING(region), region, GROUPING(product), product;

小計の行では、まとめた列が NULL になります。もとのデータにも NULL があると見分けられないので、GROUPING(列)(まとめた行なら 1)で判定してラベルをつけます。

DBROLLUPCUBE・GROUPING SETSGROUPING()
PostgreSQL使える使える使える
BigQuery・Snowflake使える使える使える
MySQL 8.0 以降WITH ROLLUP の形使えない使える
MariaDBWITH ROLLUP の形使えない使えない
SQLite使えない使えない使えない

使えない組み合わせは、集計を何本か書いて UNION ALL でつなぎます。

行を列に回す(ピボット)

面接ではこう答える

ROLLUP・CUBE・GROUPING SETS は GROUP BY の拡張で、小計と総計を UNION ALL でつながずに1回で出せます。ROLLUP は年・月・日のような階層の小計、CUBE はすべての組み合わせ、GROUPING SETS は書いた組み合わせだけです。小計の行ではまとめた列が NULL になるので、元の NULL と区別するために GROUPING 関数でラベルをつけます。DB によって使える書き方が違う点にも注意します。

別のページで開く →

DBラボを読み込んでいます…

ピボット(行を列に回す。例:月を横に並べる)は、どの DB でも動く条件つき集計で書くのが確実です。

縦持ちを、条件つき集計で横持ちにする商品月金額A2026-01100A2026-02120B2026-0180B2026-0290SUM(CASE …)商品janfebA100120B8090月が増えたら、列も SQL に書き足す
縦持ち(左)を横持ち(右)に回す
SELECT
  product,
  SUM(CASE WHEN month = '2026-01' THEN amount ELSE 0 END) AS jan,
  SUM(CASE WHEN month = '2026-02' THEN amount ELSE 0 END) AS feb,
  SUM(CASE WHEN month = '2026-03' THEN amount ELSE 0 END) AS mar
FROM monthly_sales
GROUP BY product;

PostgreSQL では SUM(amount) FILTER (WHERE month = '2026-01') とも書けます。BigQuery・Snowflake・SQL Server には PIVOT 句がありますが、構文が少しずつ違います。どの書き方でも列の名前は SQL に書いておく必要があり、月が増えるたびに列を足すことになります。列が動的に増えるものは、縦持ち(月・金額の2列)のまま BI ツールで横に並べるほうが運用が楽です。

ひとことで

DWH では読むデータ量がそのまま料金と時間になる。必要な列とパーティションだけを読む。

クラウド DWH は列指向(列ごとに保存)なので、読む列を減らすほど読むデータ量が減ります。BigQuery のオンデマンド料金のように「読んだ量」で課金される場合、そのまま料金と時間に効きます。

必要な列 × 必要な日だけを読むuser_idamountdatec4c5c6c7c811/0111/0211/0311/0411/05パーティションSELECT * FROM t→ 40マスすべて読むSELECT user_id, amount, dateWHERE date = '2026-11-05'→ 色のついた3マスだけLIMIT をつけても、読む量はほとんど変わらないことが多い
列指向の DWH で読む範囲(例)
  • SELECT * を避ける:100列のテーブルで3列しか使わないなら、3列だけ書けば読む量はおよそ数十分の1です。LIMIT 10 をつけても、多くの場合読む量は減りません(全部読んでから10行返す)。中身を見たいだけならプレビュー機能を使います。
  • パーティション:日付などでテーブルを区切って保存し、WHERE event_date = '2026-11-01' のように条件をつけると、その区切りだけを読みます(パーティションの刈り込み)。列に関数をかける(WHERE DATE(created_at) = … を時刻のパーティションに対して書く等)と刈り込みが効かない場合があるので、実行前の見積もりで確かめます。BigQuery ではパーティションの条件を必須にする設定もあります。
  • クラスタリング:パーティションの中を、よく絞り込む列(user_id・country など)で並べて保存します。その列で絞ると読むブロックが減ります。パーティションは「日付など1つの粗い区切り」、クラスタリングは「その中でよく絞る数列」と使い分けます。
工夫効くわけ目安
使う列だけ書く列指向なので、書いた列しか読まないSELECT * は本番のクエリに残さない
パーティションで絞る対象の日の区切りだけ読む日付の条件は、列に関数をかけずに書く
クラスタリングの列で絞るパーティションの中で読む塊が減るよく WHERE に出る列を数個
JOIN の前に絞る・集計する大きな表どうしの JOIN は読む量も計算も大きい先に CTE で必要な行に減らす
重い集計を中間テーブルにする同じ計算を何度も流さないダッシュボードが毎回同じ集計をしているとき

実行前に「何 GB 読むか」を見る習慣をつけるのが、一番の節約です。

クラウド DWH の料金の仕組み

面接ではこう答える

クラウド DWH は列指向なので、読む列とパーティションを減らすほど、読むデータ量が減り、料金と時間に直接効きます。SELECT * をやめて使う列だけを書き、日付のパーティションに列へ関数をかけない形で条件をつけ、よく絞る列でクラスタリングします。LIMIT をつけても読む量はほとんど減らないので、実行前の見積もりで読む量を確かめる習慣が一番の節約です。

別のページで開く →

DBラボを読み込んでいます…

ひとことで

HyperLogLog などの要約で、ユニーク数を少ない計算量で近似的に数える。

COUNT(DISTINCT user_id) は、全部の ID を覚えて重複を除くので、何億行にもなると重くなります。しかも「日別のユニーク数」を足しても「月のユニーク数」にはなりません(2日来た人を2回数えるため)。

正確な数と、近似の数COUNT(DISTINCT user_id)全部の ID を覚えて重複を除く正確メモリと時間が大きい日別の結果を足し合わせられないAPPROX_COUNT_DISTINCTID を小さなスケッチに要約わずかな誤差がある速く安いスケッチは足し合わせられる(HLL)
ユニーク数の数え方の比較
方法やり方向くもの
近似集計APPROX_COUNT_DISTINCT(user_id)(BigQuery・Snowflake など)。HyperLogLog という要約で数えるダッシュボードの傾向。多少の誤差を許せるもの
スケッチを保存して合算BigQuery の HLL_COUNT.INIT で日別のスケッチを保存し、HLL_COUNT.MERGE で週・月にまとめる期間を自由に選ぶ画面
重複を除いた中間テーブル(日付, user_id)の1日1行のテーブルを作り、そこから数える請求・KPI の公式の数字など、正確さが要るもの

近似を使うときは、利用者に「近似値である」と伝えて合意することが先です。経営の公式の数字や請求に使う数は正確に数えます。

面接ではこう答える

ユニーク数を正確に数えるには全部の ID を覚える必要があり、大きなデータでは重くなります。近似集計は HyperLogLog という要約を使い、わずかな誤差と引き換えに少ないメモリで速く数えます。日別のスケッチを保存しておけば、週や月のユニーク数も合算で出せます。正確さが要る公式の数字には、1日1行に重複を除いた中間テーブルから数える方法を使います。

ひとことで

時刻は UTC で保存し、集計のときに利用者のタイムゾーンで日付に直す。

時刻は UTC で保存し、集計するときに利用者のタイムゾーンに直すのが基本です。日本の「日別の売上」を UTC の日付で区切ると、日本時間の 0時〜9時の注文が前の日に入ってしまいます。

UTC の 15:00 が、日本時間の 0:00(日付の変わり目)UTC 11/01 12:00UTC 15:00UTC 18:00UTC 21:00UTC で 11/01JST では 11/01 22:30UTC で 11/01JST では 11/02 01:30
UTC の日付で区切ると、日本時間の 0〜9時の注文が前の日に入る
やりたいことBigQueryPostgreSQL(timestamptz)
日本時間の日付DATE(ts, 'Asia/Tokyo')(ts AT TIME ZONE 'Asia/Tokyo')::date
月の初めDATE_TRUNC(d, MONTH)date_trunc('month', d)
週の初めDATE_TRUNC(d, WEEK) は日曜始まり。月曜なら WEEK(MONDAY)date_trunc('week', d) は月曜始まり
  • パーティションの日付も UTC か日本時間かを決めて、テーブルの説明に書く。食い違うと、日付で絞ったときに数時間分が漏れる。
  • 期間の条件は「以上・未満」で書く(ts >= '2026-11-01' AND ts < '2026-11-02')。BETWEEN や「23:59:59 まで」は端の値で漏れる。
  • イベントの時刻はスマホの時計で付くことがあり、ずれていることがある。届いた時刻(サーバーの時刻)も一緒に持つ。

面接ではこう答える

時刻は UTC で保存し、日別の集計をするときに利用者のタイムゾーンに直して日付を取ります。日本の売上を UTC の日付で区切ると、日本時間の 0時から 9時までが前の日に入ってしまうからです。期間の条件は以上・未満で書き、週の始まりの曜日が DB によって違うことや、パーティションの日付がどちらの時刻かも確かめます。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • ウィンドウ関数・CTE・集計を自在に使える
  • NULL・同点・タイムゾーンで数字がずれる落とし穴を避ける
  • 大きなデータでの読む量とコスト

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1ユーザーごとの最新の注文を1件ずつ取る SQL を書いてください。

模範回答

ウィンドウ関数を使い、CTE の中で ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ordered_at DESC, order_id DESC) AS rn とユーザーごとに新しい順の番号をふって、外側で WHERE rn = 1 だけを残します。

ポイントは同点の扱いで、同じ時刻の注文があると ORDER BY が日時だけでは結果が毎回変わり得るので、order_id を2つ目の並びに入れて1件に決めています。同点を全部ほしいなら RANK() にして rn = 1 を取ります。MAX(ordered_at) を求めて自分自身と JOIN する方法もありますが、同点で複数行になるのと、テーブルを2回読むので、ウィンドウ関数のほうが素直です。

深掘りされたら

  • ユーザーごとに最新の3件ずつ取るには?
  • このクエリが大きなテーブルで遅いとき、どうしますか?

見られているポイントウィンドウ関数と、同点の扱い

Q2日別の売上と、その7日移動平均を出す SQL を書いてください。

模範回答

まず CTE で日別に集計し、その結果にウィンドウ関数をかけます。AVG(sales) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) で、その日を含む直近7行の平均が出ます。

注意点は、売上のない日があると「7行」が「7日」にならないことです。そこで日付の表(カレンダー)を用意して LEFT JOIN し、売上のない日を 0 で埋めてから移動平均を取ります。また最初の6日は7日分そろっていないので、そのまま出すか、行数を数えて7未満なら NULL にするかを利用者と決めます。

深掘りされたら

  • ROWS と RANGE の違いは何ですか?
  • 前週の同じ曜日と比べるには?

見られているポイントウィンドウの範囲の指定と、欠けた日の扱い

Q3ユーザーごとの「連続ログイン日数の最大」を求めるには、どう書きますか?

模範回答

「ギャップと島」と呼ばれる定番の形で解きます。まずユーザーと日付で重複を除き、ユーザーごとに日付の順で ROW_NUMBER() をふります。連続している日は「日付 − 番号」が同じ値になるので、それをグループのキーにして COUNT(*) すると、連続の長さが出ます。最後にユーザーごとに MAX を取ります。

例えば 11/1・11/2・11/3 にログインしたなら、番号は 1・2・3 で、引いた値はどれも 10/31 になり、1つの島として数えられます。最初に同じ日の複数回のログインを1行にまとめておかないと、番号がずれて正しく数えられない点に注意します。

深掘りされたら

  • LAG を使って解く方法もありますか?
  • タイムゾーンはどう扱いますか?

見られているポイントウィンドウ関数の応用と、重複の前処理

Q4LEFT JOIN したのに、右のテーブルにない行が結果から消えてしまいました。なぜですか?

模範回答

多くの場合、右のテーブルの列に対する条件を WHERE に書いているのが原因です。LEFT JOIN では、右に合う行がないと右の列は NULL になります。そのあと WHERE r.status = 'paid' のように右の列で絞ると、NULL の行は条件を満たさないので落ち、結果として INNER JOIN と同じになります。

右のテーブルを絞りたいなら、その条件は ON のほうに書きます。逆に「右にない行だけ」がほしいなら、WHERE r.id IS NULL と書きます。似た落とし穴に、NOT IN のサブクエリに NULL が1つでもあると結果が0行になる、というものもあるので、NOT EXISTS を使うほうが安全です。

深掘りされたら

  • JOIN で行が増えてしまうのは、どんなときですか?

見られているポイントNULL と JOIN の評価の順番の理解

Q510億行のテーブルで「ユニークユーザー数」を毎日出すのが重いです。どうしますか?

模範回答

まず、正確な数が必要かを利用者と確かめます。傾向を見るダッシュボードなら、多くの DWH にある近似の関数(BigQuery の APPROX_COUNT_DISTINCT など、HyperLogLog という方法)を使うと、小さな誤差と引き換えに、ずっと速く安くなります。

正確な数が必要なら、毎日全期間を数え直すのをやめて、日付でパーティションを切り、必要な期間だけ読むようにします。日別のユニーク数なら、その日の分だけを集計して結果をテーブルに積み上げます。注意点として、日別のユニーク数を足しても月のユニーク数にはならない(同じ人が何日も来る)ので、月の数は別に数えるか、近似の関数が持つ途中の状態(スケッチ)を合わせて出します。

深掘りされたら

  • 近似の誤差はどれくらいですか?
  • 月間のアクティブユーザーを日次で更新するには?

見られているポイント要件の確認と、近似・増分という選択肢