【SQL】グループごとの最大値を持つレコードを抽出する。

  • 2023年11月1日
  • 2023年11月1日
  • DB
  • ,
DB

今回はSQLでグループごとの最大値を抽出してみます。

グループごとの最大値を抽出する方法はいくつかあるので、それぞれみていきます。

使用するデータ

以下のような表で、各食べ物についてそれぞれ最も最近食べた日のレコードを抽出してみます。

id食事食べた日
1ラーメン2023/4/10
2ラーメン2023/6/20
3タコス2023/6/27
4ピザ2023/7/2
5ピザ2023/7/19
6寿司2023/8/8
7寿司2023/9/12
8寿司2023/10/10
9カレー2023/10/19

テーブルを作成するDDLは以下の通りです。

-- テーブルを作成
CREATE TABLE meal_history (
    id INTEGER PRIMARY KEY,
    meal_name VARCHAR(255),
    eaten_date DATE
);

-- データを挿入
INSERT INTO meal_history (id, meal_name, eaten_date) VALUES
    (1, 'ラーメン', '2023-04-10'),
    (2, 'ラーメン', '2023-06-20'),
    (3, 'タコス', '2023-06-27'),
    (4, 'ピザ', '2023-07-02'),
    (5, 'ピザ', '2023-07-19'),
    (6, '寿司', '2023-08-08'),
    (7, '寿司', '2023-09-12'),
    (8, '寿司', '2023-10-10'),
    (9, 'カレー', '2023-10-19');

SQL

サブクエリを使って抽出

まずはサブクエリを用いた方法です。サブクエリとは副問合せとも呼ばれ、SQLの中にSQLを記述する入れ子構造のことを指します。

下記は、サブクエリを使って各食べ物についてそれぞれ最も最近食べた日のレコードを抽出する一例です。

サブクエリは、メインクエリ(m1)の各行に対して実施されます。サブクエリ内では、m1各行のmeal_name(食事)に一致する行について、eaten_date(食べた日)の最大値を取得します。

その日付にヒットするm1の行について、id, meal_name, eaten_dateを取得します(食べ物ごと)

SELECT
    m1.id
    , m1.meal_name
    , m1.eaten_date 
FROM
    meal_history m1 
WHERE
    m1.eaten_date = ( 
        SELECT
            MAX(m2.eaten_date) 
        FROM
            meal_history m2 
        WHERE
            m2.meal_name = m1.meal_name
    );
id食事食べた日
2ラーメン2023/6/20
3タコス2023/6/27
5ピザ2023/7/19
8寿司2023/10/10
9カレー2023/10/19

ただ、個人的になんか直感的に理解しづらいSQLだと思います。

サブクエリを使って抽出②

上記よりもう少し直感的に理解やすいサブクエリを用いたSQLを作ってみます。以下では、サブクエリで取得した結果をINNER JOINしています。

サブクエリの中は、GROUP BYで食事名ごとに日付の最大値を取得した結果です。その結果を元のテーブルとINNER JOINすることで、各食事の日付が最大(最新)のレコードを抽出しています。

SELECT
    m1.id
    , m1.meal_name
    , m1.eaten_date 
FROM
    meal_history m1 
    INNER JOIN ( 
        SELECT
            meal_name
            , MAX(eaten_date) recent_date 
        FROM
            meal_history 
        GROUP BY
            meal_name
    ) m2 
        ON m1.meal_name = m2.meal_name 
        AND m1.eaten_date = m2.recent_date 
ORDER BY
    m1.id;

個人的には、こっちのSQLの方が直感的にどんな処理をしているか理解しやすいです。

WITH句を使って抽出

WITH句を使った方法でも実現可能です。

WITH句はサブクエリに名前を付けて一時的に管理できるものです。SQLの可読性は上がる一方、サブクエリを別テーブルとしてメモリ上に保持するため、メモリを消費してしまうというデメリットもあります。

DBの種類によってはWITH句を使用できない場合もあるので、ご使用のDBでWITH句が利用可能かは事前にご確認ください。

下記では、WITH句で作成したRankedMealsで、食事ごと食べた日ごとに並べ替え、食事ごとに1からの連番を付与しています(下図)。

rn=1のレコードが各食事での最新日付になるので、その行を抽出することで実現しています。

WITH RankedMeals AS ( 
    SELECT
        id
        , meal_name
        , eaten_date
        , ROW_NUMBER() OVER (PARTITION BY meal_name ORDER BY eaten_date DESC) AS rn 
    FROM
        meal_history
) 

SELECT
    id
    , meal_name
    , eaten_date 
FROM
    RankedMeals 
WHERE
    rn = 1;

WITH句内SQLの実行結果は以下の通りです。

RankedMeals

なお、上記SQLはPostgreSQLで動作するものです。

まとめ

SQLでグループごとの最大値を抽出する方法を紹介しました。やり方はいくつかありますが、個人的にはサブクエリでINNER JOINする方法が一番わかりやすい気がします。

SQLについてはパフォーマンスも気にする必要があるので、適切な方法を検討したうえで実装してください。

ではでは👋