特定のユーザーのレコードとその前に作成したレコードを取得する

沖縄チームのキリです。前回二番目に大きいデータに一致するレコードを取り出すクエリを書きました。業務で必要になるはずだったのですが実は前回分のデータをくっつけて取得するのが必要な処理でした。

さて以下のようなテーブルがあります。仮にテーブル名をpostsとしておきます。

id user_id content
1 1 A
2 1 B
3 1 C
4 2 A
5 3 A
6 3 B

次のような形でデータを取り出したいとします。idの降順に並べておきます。

id user_id content prev_id prev_content
6 3 B 5 A
5 3 A NULL NULL
4 2 A NULL NULL
3 1 C 2 B
2 1 B 1 A
1 1 A NULL NULL

例によってDEPENDENT SUBQUERYは避けたいです。さてどうしたらいいでしょうか…?まずはuser_idをキーに自己結合してみます。

SELECT posts.id, posts.user_id, posts.content, posts2.id AS post2_id,
  posts2.user_id AS post2_user_id, posts2.content AS post2_content
FROM posts
INNER JOIN posts AS posts2 ON posts.user_id = posts2.user_id
ORDER BY posts.id ASC, posts2.id ASC
id user_id content post2_id post2_user_id post2_content
1 1 A 1 1 A
1 1 A 2 1 B
1 1 A 3 1 C
2 1 B 1 1 A
2 1 B 2 1 B
2 1 B 3 1 C
3 1 C 1 1 A
3 1 C 2 1 B
3 1 C 3 1 C
4 2 A 4 2 A
5 3 A 5 3 A
5 3 A 6 3 B
6 3 B 5 3 A
6 3 B 6 3 B

ユーザーのレコードに同じユーザーのレコードすべてが結合されました。ユーザーごとのレコード数が2乗になってしまうのでテスト用に用意したデータでもなければLIMIT指定しておくべきですね。次にユーザーごとにこれより古いレコードを持つレコードだけを取得したいので、結合条件に『posts.id > posts2.id』を追加します。

SELECT posts.id, posts.user_id, posts.content, posts2.id AS post2_id,
  posts2.user_id AS post2_user_id, posts2.content AS post2_content
FROM posts
INNER JOIN posts AS posts2 ON posts.user_id = posts2.user_id
  AND posts.id > posts2.id
ORDER BY posts.id ASC, posts2.id ASC
id user_id content post2_id post2_user_id post2_content
2 1 B 1 1 A
3 1 C 1 1 A
3 1 C 2 1 B
6 3 B 5 3 A

表を見ているとposts.idが3のレコードが2つありますが、前回分のレコードはこのうちposts2.idが最も大きいものであることに気付くと思います。MAX()関数を使って取り出します。(MySQLは5.7でGROUP BYの挙動が変更され、GROUP BYに指定したカラム、あるいは集計関数を通したカラムしかSELECTに含められません)

SELECT posts.id, MAX(posts2.id) AS prev_id
FROM posts
INNER JOIN posts AS posts2 ON posts.user_id = posts2.user_id
  AND posts.id > posts2.id
GROUP BY posts.id
ORDER BY posts.id ASC
id prev_id
2 1
3 2
6 5

これで特定のレコードと同じユーザーの前回のレコードのidを取得することができました。あとはこのクエリをサブクエリにしてpostsに結合することで目的のデータを取り出すことができます。

SELECT posts.id, posts.user_id, posts.content, prev_posts.id AS prev_id,
  prev_posts.content AS prev_content
FROM posts
LEFT JOIN (
  SELECT posts.id, MAX(posts2.id) AS prev_id
  FROM posts
  INNER JOIN posts AS posts2 ON posts.user_id = posts2.user_id
    AND posts.id > posts2.id
  GROUP BY posts.id
  ORDER BY posts.id ASC
) AS pivot ON posts.id = pivot.id
LEFT JOIN posts AS prev_posts ON pivot.prev_id = prev_posts.id
ORDER BY posts.id DESC
id user_id content prev_id prev_content
6 3 B 5 A
5 3 A NULL NULL
4 2 A NULL NULL
3 1 C 2 B
2 1 B 1 A
1 1 A NULL NULL

いやあ実に大変ですね…私がDB設計するなら素直に前回分のIDを保持するカラムを用意します。ここまで頑張ってクエリを書いたのですが、実は該当箇所に仕様変更というか削除される項目があることがあとからわかりまして、バチバチに削除項目に含まれていました🥺SQLのお勉強になったと思って前向きに受け止めておきます。