【SQL】MAX関数完全ガイド|GROUP BY・最大値の行取得・日付MAX・GREATEST・NULL対策・ウィンドウ関数・HAVINGまで

SQLのMAX関数は、指定した列の最大値を返す集約関数です。売上最高額・最新日付・最大ID——さまざまな場面で使いますが、「最大値を持つ行全体を取得したい」「グループごとの最大値がほしい」といった実務要件になると、書き方に迷うことがあります。

本記事ではMAX関数の基本から、GROUP BY・サブクエリ・ウィンドウ関数を組み合わせた実務パターンまで体系的に解説します。

この記事で分かること

  • MAX の基本構文と数値・日付・文字列での動作
  • GROUP BY でグループ別の最大値を求める方法
  • 最大値を持つ行全体を取得する方法(サブクエリ / ROW_NUMBER)
  • MAX(date) で最新日付・最新レコードを取得する方法
  • MAX と GREATEST の違い(列間 vs 行間)
  • MAX OVER() ウィンドウ関数の使い方
  • NULL の扱いと対策
  • HAVING MAX で絞り込む方法
スポンサーリンク

MAX の基本構文

数値列の最大値
-- 商品テーブルの最高価格を取得
SELECT MAX(price) AS max_price FROM products;
-- 結果: 24,990

-- WHERE で絞り込んでから最大値
SELECT MAX(price) AS max_electronics_price
FROM products
WHERE category = '家電';
日付列の最大値 = 最新日付
-- 最新の注文日を取得
SELECT MAX(order_date) AS latest_order FROM orders;
-- 結果: 2024-03-31

-- 顧客の最終ログイン日
SELECT MAX(login_at) AS last_login FROM users WHERE user_id = 101;
文字列列の最大値
-- 文字列は辞書順(照合順序)で最大値を判定
SELECT MAX(name) FROM employees;
-- 照合順序に依存: 'Z' が先か 'あ' が先かは COLLATION による

-- アルファベットなら 'Z...' が最大
SELECT MAX(product_code) FROM products;
-- 結果: 'Z-999'(辞書順で最後のコード)

GROUP BY と組み合わせる

GROUP BY を使うと、グループごとの最大値を一度に取得できます。

グループ別の最大値
-- カテゴリごとの最高価格
SELECT
    category,
    MAX(price)      AS max_price,
    MIN(price)      AS min_price,
    MAX(price) - MIN(price) AS price_range
FROM products
GROUP BY category
ORDER BY max_price DESC;
-- 結果:
-- 家電    | 249,990 | 1,980 | 248,010
-- 家具    | 89,990  | 5,980 | 84,010
-- 食品    | 4,980   | 198   | 4,782
部署ごとの最新入社日
-- 各部署で最後に入社した日付
SELECT
    department,
    MAX(hire_date) AS latest_hire
FROM employees
GROUP BY department;

最大値を持つ行全体を取得する

MAX(col) は値だけを返します。「最大値を持つ行の他の列も見たい」場合は、サブクエリやウィンドウ関数を使います。

よくある間違い:SELECT name, MAX(price) FROM products はエラーまたは意図しない結果になります。MAX は集約関数なので、GROUP BY なしでは name を指定できません。

方法1:サブクエリで最大値を条件に指定

サブクエリで最大値の行を取得
-- 最高価格の商品を取得
SELECT * FROM products
WHERE price = (SELECT MAX(price) FROM products);
-- 最大値が複数行に存在する場合はすべて返る

-- カテゴリが「家電」の中で最高価格の商品
SELECT * FROM products
WHERE category = '家電'
  AND price = (SELECT MAX(price) FROM products WHERE category = '家電');

方法2:ROW_NUMBER でグループごとの最大行を取得

ROW_NUMBER — 各グループの最大値を持つ行(推奨)
-- カテゴリごとに最高価格の商品を1件ずつ取得
SELECT category, product_name, price
FROM (
    SELECT
        category,
        product_name,
        price,
        ROW_NUMBER() OVER (
            PARTITION BY category
            ORDER BY price DESC
        ) AS rn
    FROM products
) ranked
WHERE rn = 1;
-- 結果: カテゴリごとに最高価格の1件が返る
-- 同額があっても1件(ORDER BY の2番目の列で制御可能)
RANK — 同額が複数あれば全て返す
-- RANK を使うと同率1位が複数あれば全て返る
SELECT category, product_name, price
FROM (
    SELECT
        category,
        product_name,
        price,
        RANK() OVER (
            PARTITION BY category
            ORDER BY price DESC
        ) AS rnk
    FROM products
) ranked
WHERE rnk = 1;

使い分け:

  • 最大値が1件だけでよい → ROW_NUMBER
  • 同率最大を全て返したい → RANK
  • 単純に最大値だけほしい → MAX + サブクエリ

MAX で最新レコードを取得する

日付列に MAX を使うと「最新の日付」を取得できます。実務で最も多いMAXの使い方の1つです。

最新の注文を取得
-- 顧客ごとの最終注文日と最終注文金額
SELECT
    c.customer_id,
    c.name,
    latest.order_date  AS last_order_date,
    latest.amount      AS last_order_amount
FROM customers c
JOIN orders latest
    ON c.customer_id = latest.customer_id
   AND latest.order_date = (
       SELECT MAX(o.order_date)
       FROM orders o
       WHERE o.customer_id = c.customer_id
   );
MAX(id) で最新レコード(AUTO_INCREMENT活用)
-- AUTO_INCREMENT の最大IDが最新レコード
SELECT * FROM logs WHERE id = (SELECT MAX(id) FROM logs);

-- グループごとの最新ログ
SELECT * FROM logs l
WHERE l.id = (
    SELECT MAX(l2.id)
    FROM logs l2
    WHERE l2.user_id = l.user_id
);

MAX と GREATEST の違い

MAX行方向(列の全行から最大値)、GREATEST列方向(同一行の複数列から最大値)です。

関数 方向 使い方
MAX(col) 行方向(縦) 1列の全行から最大値を取得
GREATEST(a, b, c) 列方向(横) 同一行の複数列・値から最大値を取得
MAX と GREATEST の比較
-- MAX: price 列の全行から最大値(集約関数)
SELECT MAX(price) FROM products;
-- 結果: 1行(全体の最高価格)

-- GREATEST: 同一行の複数列から最大値(行ごとに計算)
SELECT
    product_name,
    GREATEST(price_a, price_b, price_c) AS highest_price
FROM price_comparison;
-- 結果: 行ごとに price_a/price_b/price_c の最大値
-- MySQL / PostgreSQL / Oracle で使用可能

-- SQL Server では GREATEST は 2022+ のみ。代替:
SELECT
    product_name,
    (SELECT MAX(v) FROM (VALUES (price_a), (price_b), (price_c)) AS t(v)) AS highest_price
FROM price_comparison;

MAX OVER() — ウィンドウ関数

MAX をウィンドウ関数として使うと、GROUP BY せずに各行に最大値を付与できます。

全体の最大値を各行に付与
-- 各行に全体最大価格と差分を表示
SELECT
    product_name,
    price,
    MAX(price) OVER ()             AS max_price,
    MAX(price) OVER () - price     AS diff_from_max
FROM products;
-- 結果:
-- テレビ   | 49,990 | 249,990 | 200,000
-- 冷蔵庫   | 89,990 | 249,990 | 160,000
-- エアコン | 249,990| 249,990 | 0
パーティション別の最大値
-- カテゴリ別の最高価格を各行に付与
SELECT
    category,
    product_name,
    price,
    MAX(price) OVER (PARTITION BY category) AS category_max
FROM products;
-- 結果:
-- 家電 | テレビ   | 49,990 | 249,990
-- 家電 | エアコン | 249,990| 249,990
-- 食品 | 米       |  2,980 |   4,980
累積最大値(ランニングMAX)
-- 日付順に売上の最大値を更新しながら表示
SELECT
    sale_date,
    amount,
    MAX(amount) OVER (ORDER BY sale_date) AS running_max
FROM sales
ORDER BY sale_date;
-- 結果:
-- 2024-01-01 | 10,000 | 10,000
-- 2024-01-02 | 15,000 | 15,000  ← 更新
-- 2024-01-03 |  8,000 | 15,000  ← 維持
-- 2024-01-04 | 20,000 | 20,000  ← 更新

NULL の扱い

状況 MAX の挙動
NULL を含む列 NULLの行は無視して残りから最大値を返す
対象行が0件 結果はNULL
全行が NULL 結果はNULL
NULL の動作確認と対策
-- データ: price = 100, 200, NULL, 300
SELECT MAX(price) FROM products;
-- 結果: 300(NULLは無視される)

-- 0件の場合
SELECT MAX(price) FROM products WHERE 1 = 0;
-- 結果: NULL(最大値なし)

-- NULL を特定の値に変換
SELECT COALESCE(MAX(price), 0) AS max_price
FROM products WHERE 1 = 0;
-- 結果: 0

HAVING MAX — 最大値による絞り込み

HAVING で最大値を条件に使う
-- カテゴリ別の最高価格が50,000以上のカテゴリだけ抽出
SELECT
    category,
    MAX(price) AS max_price
FROM products
GROUP BY category
HAVING MAX(price) >= 50000
ORDER BY max_price DESC;

-- 最大値と最小値の差が大きいカテゴリ
SELECT
    category,
    MAX(price) AS max_price,
    MIN(price) AS min_price,
    MAX(price) - MIN(price) AS price_range
FROM products
GROUP BY category
HAVING MAX(price) - MIN(price) > 10000;

実務でよく使うパターン

パターン1:最新ステータスの取得

注文の最新ステータスを取得
-- ステータス履歴テーブルから最新のステータスを取得
SELECT
    o.order_id,
    o.order_date,
    sh.status AS current_status
FROM orders o
JOIN status_history sh
    ON o.order_id = sh.order_id
   AND sh.changed_at = (
       SELECT MAX(sh2.changed_at)
       FROM status_history sh2
       WHERE sh2.order_id = o.order_id
   );

パターン2:自動採番の最大値確認

AUTO_INCREMENT の次の値を確認
-- 現在の最大IDを確認
SELECT MAX(id) AS current_max_id FROM employees;

-- 欠番を特定(IDが連続していない箇所)
SELECT a.id + 1 AS gap_start
FROM employees a
WHERE NOT EXISTS (
    SELECT 1 FROM employees b WHERE b.id = a.id + 1
)
AND a.id < (SELECT MAX(id) FROM employees);

パターン3:MAX + CASE WHEN でカテゴリ別の最新日付

カテゴリ別の最新注文日を横並び表示
SELECT
    customer_id,
    MAX(CASE WHEN category = '家電'   THEN order_date END) AS latest_electronics,
    MAX(CASE WHEN category = '食品'   THEN order_date END) AS latest_food,
    MAX(CASE WHEN category = '衣料品' THEN order_date END) AS latest_clothing
FROM orders
GROUP BY customer_id;

集約関数の比較

関数 戻り値 NULLの扱い
MAX(col) 最大値 NULLを無視
MIN(col) 最小値 NULLを無視
SUM(col) 合計 NULLを無視
AVG(col) 平均 NULLの行を分母からも除外
COUNT(col) 件数 NULLをカウントしない
COUNT(*) 全件数 NULLも含めてカウント

MIN関数の詳細はMIN関数完全ガイド、SUM関数の詳細はSUM関数完全ガイド、COUNT関数の詳細はCOUNT関数完全ガイドを参照してください。

まとめ

目的 書き方
列の最大値 SELECT MAX(col) FROM table
グループ別の最大値 MAX(col) … GROUP BY key
最大値を持つ行を取得 WHERE col = (SELECT MAX(col) …) / ROW_NUMBER
最新日付の取得 MAX(date_col)(日付に対するMAX = 最新)
同一行の列間最大値 GREATEST(a, b, c)(MAX ではなく GREATEST)
各行に最大値を付与 MAX(col) OVER (PARTITION BY key)
最大値で絞り込み HAVING MAX(col) >= N
NULL対策 COALESCE(MAX(col), デフォルト値)
  • MAX は NULL を無視する。0件なら NULL を返すため COALESCE で対策する
  • 「最大値の行全体を取得」は ROW_NUMBER が最も汎用的(同率は RANK で対応)
  • MAX(date_col) = 最新日付。ステータス履歴の最新取得などで頻出
  • 列間の最大値は MAX ではなく GREATEST(行内の複数列を比較する関数)
  • MAX OVER() でウィンドウ関数としても使える — GROUP BY なしで各行に最大値を付与