SQLでグループごとに最大のレコードを取得する　ROW_NUMBER関数とRANK関数

指定した購入者ごとの最後の購入時の商品名、購入額、購入日
こちらを取得するSQL文を作成します。

購入者ごとにグループを分けてそれぞれの最終購入日のレコードを取得します。

グループごとに連番をつけるROW_NUMBER関数とRANK関数を使った方法を紹介したいと思います。

# 目次
[1. 検証環境](#1-検証環境)
[2. 対象テーブル](#2-対象テーブル)
　[2-1. usersテーブル](#2-1-usersテーブル)
　[2-2. itemsテーブル](#2-2-itemsテーブル)
　[2-3. ordersテーブル](#2-3-ordersテーブル)
[3. ROW_NUMBER関数](#3-row_number関数)
[4. RANK関数](#4-rank関数)
[5. おまけ](#5-おまけ)
[6. 参考記事](#6-参考記事)

# 1. 検証環境

macOS Monterey 12.3.1
PostgreSQL 14.4

# 2. 対象テーブル

今回例として使用するのは、顧客を管理するusersテーブル、商品を管理するitemsテーブル、購入履歴を管理するordersテーブルの3つです。
（サンプルとして簡単に用意したものです。設計的な不備はお許しください。）

## 2-1. usersテーブル
![users.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517594/bab60a9b-f16f-fae4-dce3-a5365bfda85b.png)

## 2-2. itemsテーブル
![items.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517594/64f1f360-fa67-2ed0-c32d-153c1d41ba85.png)

## 2-3. ordersテーブル
![orders.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517594/6b8409de-ea58-6429-df14-08906e1e34ae.png)

# 3. ROW_NUMBER関数
次のような書き方をします。

```sql
SELECT
	user_name AS 購入者
	, item_name AS 商品名
	, total_price AS 購入額
	, date AS 購入日
FROM
	(
		SELECT
			u.name AS user_name
			, i.name AS item_name
			, i.price * o.quanity AS total_price
			, o.date
			, ROW_NUMBER() OVER (
				PARTITION BY
					u.name
				ORDER BY
					o.date DESC
			) AS rank
		FROM
			orders AS o
		INNER JOIN
			users AS u ON u.id = o.user_id
		INNER JOIN
			items AS i ON i.id = o.item_id
	) AS t
WHERE
	user_name IN ('佐藤', '田中') /* 指定された購入者 */
	AND rank = 1
;
```
結果
![スクリーンショット 2022-10-27 0.39.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517594/6b465ab9-bb0e-27d8-1cb7-1e50d04fb6f5.png)


OVER句の`PARTITION BY`でグループ化するカラムを、`ORDER BY`で表示順を指定します。

ROW_NUMBER関数で取得した連番（ここでいうrank）をwhere句で指定するためには、サブクエリでFROMを指定する必要があります。

ちなみに条件を指定せずに全てのデータを取得するとこのような結果になっています。
![スクリーンショット 2022-10-27 0.43.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517594/c50bf07d-e7d7-f651-c97a-d02204f62a46.png)
この中から指定された購入者の`rank = 1`のレコードを取得しているということになります。

# 4. RANK関数
RANK関数も使い方は同じで次のように書きます。

```sql
SELECT
	user_name AS 購入者
	, item_name AS 商品名
	, total_price AS 購入額
	, date AS 購入日
FROM
	(
		SELECT
			u.name AS user_name
			, i.name AS item_name
			, i.price * o.quanity AS total_price
			, o.date
			, RANK() OVER (
				PARTITION BY
					u.name
				ORDER BY
					o.date DESC
			) AS rank
		FROM
			orders AS o
		INNER JOIN
			users AS u ON u.id = o.user_id
		INNER JOIN
			items AS i ON i.id = o.item_id
	) AS t
WHERE
	user_name IN ('佐藤', '田中') /* 指定された購入者 */
	AND rank = 1
;
```

ROW_NUMBER関数との違いはグループ内の表示順（この例でいう購入日）が同一のものは同じ番号が付けられるという点です。
なので同順位のものがある場合は複数のレコードが取得されます。
必要に応じて使い分ける必要があります。

# 5. おまけ

これ以降は特に参考になる内容ではないです。
このSQL文が出来上がるまでの経緯を私の失敗例と共に残しておきます。

そもそもは、
「指定した購入者の最後の購入時の商品名、購入額、購入日」
を取得するSQL文の作成依頼がありました。
「簡単にできそうだな…」
と思って作成したSQL文がこちらです。

```sql
SELECT
	u.name AS 購入者
	, i.name AS 商品名
	, i.price * o.quanity AS 購入額
	, o.date AS 購入日
FROM
	orders AS o
INNER JOIN
	users AS u ON u.id = o.user_id
INNER JOIN
	items AS i ON i.id = o.item_id
WHERE
	u.name = '佐藤' /* 指定された購入者 */
ORDER BY
	o.date DESC
LIMIT 1
;
```

これで報告した際に、「購入者を複数指定したい場合は？」となったので調べて今回の関数を使用することになりました。

ざっと調べて書いてみたのがこちらです。

```sql:失敗例
SELECT
	u.name AS 購入者
	, i.name AS 商品名
	, i.price * o.quanity AS 購入額
	, o.date AS 購入日
	, ROW_NUMBER() OVER (
		PARTITION BY
			u.name
		ORDER BY
			o.date DESC
	) AS rank
FROM
	orders AS o
INNER JOIN
	users AS u ON u.id = o.user_id
INNER JOIN
	items AS i ON i.id = o.item_id
WHERE
	u.name IN ('佐藤', '田中') /* 指定された購入者 */
	AND rank = 1
;
```

これだと`”rank”なんてないよ！`とエラーが出ます。
なのでwhere句で指定するためにはサブクエリの中で関数を使う必要があるという訳です。

# 6. 参考記事
[【SQL】グループごとに最大の値を持つレコードを取得する方法3選](https://takakisan.com/sql-max-in-each-group/)
[【SQL】PARTITION BYの基礎から列ごとの計算方法まとめ](https://style.potepan.com/articles/22788.html)