iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

Redashで初めてSQLを書いてみた話

はじめに

こんにちは!iimonでエンジニアをしているつかちゃんです!

SQLという単語は入社してからずっと耳にしていました。でも自分が触ることはなく、なんとなく「データベースに問い合わせるやつ」くらいのイメージしか持っていませんでした。

今回、Redash経由でデータを集計する機会があり、初めてSQLを自分で書くことになりました。実際に書いて、エラーになって、直して、結果が返ってきたとき「あ、これがSQLか!」となりました。

使ったのはRedashから Athena に接続し、federated query という仕組みでデータを取得するというものでした。SQLの書き方でつまずいたポイントを、仮想の例を使って整理してみます。

用語 説明
Redash データベースへのクエリ実行や結果の可視化ができるBIツール
Federated Query 外部のデータソースを直接SQLで検索する仕組み

題材: 注文テーブルとユーザーテーブル

説明用に、こんな2つのテーブルがあるとします。

orders(注文テーブル) ← 誰が何を注文したか

id user_id item status
1 101 りんご done(完了)
2 102 みかん done(完了)
3 101 ぶどう canceled(キャンセル)

users(ユーザーテーブル) ← ユーザーの一覧

id name
101 Alice
102 Bob
103 Carol

Carolは注文が1件もありません。

ここから「ユーザーごとの完了済み注文数」を集計したい、というのがやりたいことです。

欲しい結果はこれです:

結果: Alice → 1件、Bob → 1件、Carol → 0件

Carolは注文が1件もないので0になってほしいのがポイントです。

つまずきポイント①: JOINの条件をどこに書くか問題

一番時間を使ったのがここでした。

最初、こんな感じで「とりあえずJOINしてWHEREで絞ればいいんでしょ?」というノリで書いていました。

SELECT
  u.id,
  u.name,
  COUNT(o.id) AS done_order_count
FROM users u
LEFT JOIN orders o
  ON u.id = o.user_id
WHERE o.status = 'done'
GROUP BY u.id, u.name

実行結果:

id name done_order_count
101 Alice 1
102 Bob 1

Carolが消えてしまっています。LEFT JOINしているのに、です。

原因は、JOIN条件(ON句)とフィルタ条件(WHERE句)を混同していたこと でした。

  • WHERE o.status = 'done'は「JOINした後の行」に対してかかる条件
  • Carolはorders側がNULLなので、o.status = 'done'という条件自体を満たせず、行ごと弾かれてしまう
  • つまりLEFT JOINしているのに、実質INNER JOINと同じ挙動になっていた

正しくは、絞り込み条件をON句側に入れる必要がありました。

SELECT
  u.id,
  u.name,
  COUNT(o.id) AS done_order_count
FROM users u
LEFT JOIN orders o
  ON u.id = o.user_id AND o.status = 'done'
GROUP BY u.id, u.name

実行結果:

id name done_order_count
101 Alice 1
102 Bob 1
103 Carol 0

こうすると、Carolもdone_order_count = 0として結果に残ります。

つまずきポイント②: GROUP BYと集計列の対応

集計(COUNTSUM)を使うとき、SELECTに書いた列は基本的に全部GROUP BYに入れるか、集計関数でくるむ必要がある、というルールにも地味に苦戦しました。

-- これはエラーになる
SELECT
  u.id,
  u.name,
  u.created_at,
  COUNT(o.id) AS done_order_count
FROM users u
LEFT JOIN orders o
  ON u.id = o.user_id AND o.status = 'done'
GROUP BY u.id, u.name

実行結果:

ERROR: Column 'u.created_at' must appear in the GROUP BY clause or be used in an aggregate function

u.created_atSELECTに追加しただけでエラーになって、「なんで?」と思ったんですが、GROUP BYに含まれていない列(かつ集計関数でもない列)はそのまま選択できない、というSQLのルールに当たっていただけでした。

-- GROUP BYにも追加すればOK
SELECT
  u.id,
  u.name,
  u.created_at,
  COUNT(o.id) AS done_order_count
FROM users u
LEFT JOIN orders o
  ON u.id = o.user_id AND o.status = 'done'
GROUP BY u.id, u.name, u.created_at

実行結果:

id name created_at done_order_count
101 Alice 2024-01-01 1
102 Bob 2024-02-15 1
103 Carol 2024-03-10 0

TypeScriptの型エラーみたいに「ルールに沿ってないと教えてくれる」感じで、わかってしまえば納得感はありました。

つまずきポイント③: JSONカラムから日付を取り出して期間絞り込み

こんなクエリを書く場面がありました。

WHERE
  date_parse(json_extract_scalar(params, '$.created_at'), '%Y-%m-%dT%H:%i:%s.%fZ') >= date_add('day', -7, current_date)
  AND date_parse(json_extract_scalar(params, '$.created_at'), '%Y-%m-%dT%H:%i:%s.%fZ') < current_date

複雑に見えますが、分解してみると3つの部品で出来ていました。

① json_extract_scalar: JSONから値を取り出す

paramsカラムにはJSON文字列が入っていることがあります。例えばこんな形です。

{"created_at": "2025-06-24T10:30:00.000Z", "user_id": 101}

ここからcreated_atの値だけ取り出すのがjson_extract_scalarです。

json_extract_scalar(params, '$.created_at')
-- => '2025-06-24T10:30:00.000Z' (文字列)

$.created_at$はJSON全体を指していて、.created_atでキーを指定するイメージです。ただしこの時点ではあくまで文字列です。

② date_parse: 文字列→日付型に変換する

取り出した文字列'2025-06-24T10:30:00.000Z'は、このままでは日付として比較できません。そこでdate_parseで日付型に変換します。

date_parse('2025-06-24T10:30:00.000Z', '%Y-%m-%dT%H:%i:%s.%fZ')
-- => timestamp型の値

第2引数がフォーマット指定で、文字列の形式に合わせて書く必要があります。%Yが年、%mが月、%dが日、%H:%i:%sが時分秒、%fがミリ秒です。フォーマットが1文字でも違うとエラーになるので、ここは正確に合わせる必要がありました。

③ date_add と current_date: 上限を書き忘れると今日分まで入ってくる

-- 最初はこう書いていた(下限だけ指定)
WHERE
  date_parse(json_extract_scalar(params, '$.created_at'), '%Y-%m-%dT%H:%i:%s.%fZ')
    >= date_add('day', -7, current_date)  -- 6/23以降

実行結果: 6/23〜6/30の8日分が返ってきてしまいます。

date_add('day', -7, current_date)は7日前(6/23)を返すので、「6/23以降」という下限だけの条件になります。上限を指定していないので、今日(6/30)のデータも含まれてしまい、結果が6/23〜6/30の8日分になっていました。

欲しかったのは6/23〜6/29の7日分だったので、上限として< current_date(今日より前)を追加するのが正解でした。

-- 上限を追加して7日分に絞る
WHERE
  date_parse(json_extract_scalar(params, '$.created_at'), '%Y-%m-%dT%H:%i:%s.%fZ')
    >= date_add('day', -7, current_date)  -- 6/23以降
  AND date_parse(json_extract_scalar(params, '$.created_at'), '%Y-%m-%dT%H:%i:%s.%fZ')
    < current_date                         -- 6/30より前(=6/29まで)

実行結果: 6/23〜6/29の7日分が返ってきます。

< current_dateにすることで今日を除外でき、ちょうど7日分になります。期間を絞るときは上限・下限の両方を意識する必要があると学びました。

学んだこと

  • ON vs WHERE: LEFT JOINを使うときは、絞り込み条件をONに書くかWHEREに書くかで結果が変わる。「結合する条件」と「絞り込む条件」を分けて考える
  • GROUP BYの対応: 集計するときは、SELECTに書く列とGROUP BYに書く列の対応関係を常に意識する
  • JSONと日付の変換は3ステップ: json_extract_scalarで文字列取り出し → date_parseで日付型に変換 → date_add/current_dateで範囲を作る、という順番で分解すると読みやすくなる
  • 期間は上限・下限の両方を書く: 下限(>= 7日前)だけだと今日分まで含まれてしまう。< current_dateで上限も指定して初めて「昨日まで7日分」になる

おわりに

最初は何を書けばいいかイメージすらできず、かなり手探りでした。ただ一つひとつ分解して考えれば、複雑に見えるクエリも理解できると気づいてからだいぶスムーズになりました。次は集計結果をRedashのダッシュボードで可視化するところまでやってみたいと思います。

最後まで読んでくださりありがとうございました。

現在弊社ではエンジニアを募集しています! この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

参考文献