sugiii8's tech blog.
post: 2022-12-26
update: 2023-01-05
INDEX

postgresで直SQLを書くことになったので復習してみた - 集約周り

thumbnail

ORMではなく、直接SQLを書く必要が出てきたので復習してみた。
要件としては以下のような感じです。

売上テーブル(sales_amounts)

以下のフィールドを持っている

  • 顧客ID(client_id): number
  • 金額(amount): number
  • 対象日時[何日分の売上か] (target_date): date


要件

  • 前月分の売上と過去三ヶ月分の売上平均の比率を出したい
  • 売上の推移を確認する目的


仕様

  • 金額は月ごとに積み上げ形式
  • シンプルに月で集約して最大値を取ってくれば月の売上は取得できる


SQL

前月の売上

  • target_dateで絞り込む
  • 顧客IDとtarget_dateを年月で切ったもので集約
  • 金額の最大値を取得
    SELECT
        client_id,
        to_char(target_date, 'YYYY-MM') as ym,
        MAX(amount)
    FROM sales_amounts
    WHERE target_date >= date_trunc('month', now()) - interval '1 month'
    AND target_date < date_trunc('month', now())
    GROUP BY client_id, ym


(現在月を含まない)過去3ヶ月分の売上の平均

まず過去3ヶ月分の売上を取得する。前月売上同様年月でグルーピングした状態にする

SELECT
    client_id,
    to_char(target_date, 'YYYY-MM') as ym,
    MAX(amount)
FROM sales_amounts
WHERE target_date >= date_trunc('month', now()) - interval '3 month'
AND target_date < date_trunc('month', now())
GROUP BY client_id, ym


上記をサブクエリとして、顧客IDで集約する
さらに平均を出す

    SELECT
        client_id,
        round(avg(amount)) as total_amount_avg_3month
    FROM
        (
          SELECT
              client_id,
              to_char(target_date, 'YYYY-MM') as ym,
              MAX(amount) as amount
          FROM sales_amounts
          WHERE target_date >= date_trunc('month', now()) - interval '3 month'
          AND target_date < date_trunc('month', now())
          GROUP BY client_id, ym
       )
     GROUP BY client_id


上記2つのSQLを更にサブクエリにしてjoinする
売上の上昇率と下降率両方を取得するため 直近/過去3ヶ月過去3ヶ月/直近 の値を出す

最終形はこんな感じ

SELECT
  A.client_id,
  round(((NULLIF(B.last_amount, 0) / NULLIF(A.amount_avg_3month, 0) - 1) * 100)) as ratio_last_to_3month,
  round(((NULLIF(A.amount_avg_3month, 0) / NULLIF(B.last_amount, 0) - 1) * 100)) as ratio_3month_to_last,
FROM


(
  SELECT
    client_id,
    round(avg(amount)) as amount_avg_3month
  FROM
      (
        SELECT
          client_id,
          to_char(target_date, 'YYYY-MM') as ym,
          MAX(amount) as amount
        FROM sales_amounts
        WHERE target_date >= date_trunc('month', now()) - interval '3 month'
        AND target_date < date_trunc('month', now())
        GROUP BY client_id, ym
      ) as three_month_sales_amounts
  GROUP BY client_id
) as A



INNER JOIN
(
  SELECT
    client_id,
    to_char(target_date, 'YYYY-MM') as ym,
    MAX(amount) as last_amount
  FROM sales_amounts
  WHERE target_date >= date_trunc('month', now()) - interval '1 month'
  AND target_date < date_trunc('month', now())
  GROUP BY client_id, ym
  ORDER BY client_id, ym
) as B


ON A.client_id = B.client_id


注意点

  • 今回のクエリはアプリケーション経由で叩かれるものではないのでパフォーマンスの考慮はしていない(あとでやる)


最後に

  • 自分で振り返ったときに何を取得しようとしていたかがわからなくなりそうなので、途中経過を記載しておくのは良いと思った
  • とはいえあんまりSQLを直接書くのは極力さけたい(今回は特殊な事情による)