SQL LEFT JOIN only last record of right table — 4 種解決方案

最近工作時剛好遇到了一個需求:「需要取得每位使用者最後的工作記錄。」使用者是一張 table,工作記錄又是ㄧ張 table;原本以為這個簡單的需求用個 leftJoin 就能瞬間搞定,結果卻搞了自己快一個晚上…因此決定把解決方案放上來,給有遇到相同問題朋友們。

這篇文章寫於 2022 年,文中的工具與版本可能已經更新,實作時請以官方文件為準。

本文目錄
  1. 一、使用線上 SQL 語法測試網站來理解&學習這個知識
  2. 二、生成 users、user_logs 兩張 table
  3. 三:撰寫可以找出 right table「所有欄位」的 SQL
  4. 用 table alias name 來內部比對取得最大值
  5. 用 max 函式來找出最大值
  6. 四、撰寫找出 right table「最後 updated_at」的 SQL
  7. 用 GROUP BY 的概念搭配 MAX 函式篩選出最大值
  8. 用 Laravel 現成函式 subquery-joins 找出最大值

最近工作時遇到了一個需求:「需要取得每位使用者最後的工作記錄。」使用者是一張 table,工作記錄又是ㄧ張 table;原本以為這個簡單的需求用個 leftJoin 就能瞬間搞定,結果卻搞了自己快一個晚上…因此決定把解決方案放上來,給有遇到相同問題朋友們。

SQL LEFT JOIN only last record of right table — 4 種解決方案(圖 1)

文章大綱
一、使用線上 SQL 語法測試網站來理解&學習這個知識
二、生成 users、user_logs 兩張 table
三、撰寫可以找出 right table「所有欄位」的 SQL
  ➤ 用 table alias name 來內部比對取得最大值
  ➤ 用 max 函式來找出最大值
四、撰寫找出 right table「最後 updated_at」的 SQL
  ➤ 用 GROUP BY 的概念搭配 MAX 函式篩選出最大值
  ➤ 用 Laravel 現成函式 subquery-joins 找出最大值

一、使用線上 SQL 語法測試網站來理解&學習這個知識

考慮到讀者未必手上都剛好有能夠運行的資料庫環境,所以這邊推薦兩個實用的線上 SQL 語法測試網站(SQL fiddle、MySQL Online)

如果不知道如何使用,可以參考我之前的文章:「線上 SQL 語法測試網站(SQL fiddle、MySQL Online)」


二、生成 users、user_logs 兩張 table

CREATE TABLE users (
  id INT,
  name varchar(50)
);
CREATE TABLE user_logs (
  id INT,
  user_id INT,
  log varchar(50),
  updated_at timestamp
);

INSERT INTO users VALUES (1, 'John');
INSERT INTO users VALUES (2, 'Yannis');
INSERT INTO users VALUES (3, 'Victor');
INSERT INTO users VALUES (4, 'Jimmy');
INSERT INTO users VALUES (5, 'Richard');

INSERT INTO user_logs VALUES (1, 1 ,"John 工作記錄 1","2022-01-01 00:00:00");
INSERT INTO user_logs VALUES (2, 1 ,"John 工作記錄 big","2022-01-09 00:00:00");
INSERT INTO user_logs VALUES (3, 1 ,"John 工作記錄 2","2022-01-05 00:00:00");
INSERT INTO user_logs VALUES (4, 1 ,"John 工作記錄 3","2022-01-06 00:00:00");
INSERT INTO user_logs VALUES (5, 4 ,"Jimmy 工作記錄 1","2022-01-01 00:00:00");
INSERT INTO user_logs VALUES (6, 4 ,"Jimmy 工作記錄 2","2022-01-03 00:00:00");
INSERT INTO user_logs VALUES (7, 4 ,"Jimmy 工作記錄 big","2022-01-05 00:00:00");
INSERT INTO user_logs VALUES (8, 5 ,"Richard 工作記錄 big","2022-01-06 00:00:00");
INSERT INTO user_logs VALUES (9, 5 ,"Richard 工作記錄 1","2022-01-05 00:00:00");

原始碼:init_users_user_logs.sql(GitHub Gist)


三:撰寫可以找出 right table「所有欄位」的 SQL

用 table alias name 來內部比對取得最大值

SELECT t1.name,
       t2.log,
       t2.updated_at
FROM `users` t1
LEFT JOIN
  (SELECT t.*
   FROM user_logs t
   WHERE NOT EXISTS
       (SELECT 1
        FROM user_logs
        WHERE user_id = t.user_id
          AND updated_at > t.updated_at ) ) t2 ON t2.user_id = t1.id
ORDER BY t1.id

原始碼:last_record_method1.sql(GitHub Gist)

使用SQL fiddle 線上編譯的結果
使用SQL fiddle 線上編譯的結果

用 max 函式來找出最大值

SELECT users.name,
       user_logs.log,
       user_logs.updated_at
FROM users
LEFT JOIN user_logs ON `users`.`id` = `user_logs`.`user_id`
AND `user_logs`.`updated_at` =
  (SELECT max(updated_at)
   FROM user_logs
   WHERE user_logs.user_id = users.id)
ORDER BY users.id

原始碼:last_record_method2.sql(GitHub Gist)

使用SQL fiddle 線上編譯的結果
使用SQL fiddle 線上編譯的結果

特別提醒:上述方案在同個 user 的 user_logs 有[年月日時分秒]完全相同的「updated_at」時,會跳出複數筆資料;但此情境在正常環境中不會發生,若想避免此狀況,可以使用 distinct 函式來將 user 過濾。


四、撰寫找出 right table「最後 updated_at」的 SQL

有時我們只是要找出使用者最後更新的日期(updated_at),就不必大費周章抓出所有欄位,下面也提供兩個方案。

用 GROUP BY 的概念搭配 MAX 函式篩選出最大值

SELECT name,
       updated_at
FROM `users`
LEFT JOIN
  (SELECT user_id,
          MAX(updated_at) AS updated_at
   FROM `user_logs`
   GROUP BY `user_id`) AS `user_logs` ON `users`.`id` = `user_logs`.`user_id`

原始碼:last_record_method3.sql(GitHub Gist)

使用SQL fiddle 線上編譯的結果
使用SQL fiddle 線上編譯的結果

用 Laravel 現成函式 subquery-joins 找出最大值

$latestLogs = DB::table('user_logs')
                ->select('user_id', DB::raw('MAX(updated_at) as last_log_created_at'))
                ->groupBy('user_id');

$users = DB::table('users')
        ->joinSub($latestLogs, 'latest_logs', function ($join) {
            $join->on('users.id', '=', 'latest_logs.user_id');
        })->get();

原始碼:last_record_method4.php(GitHub Gist)


希望今天這篇文章有幫助讀者解決工作或是專案上的需求喔~

Talk

留言討論

留言載入中…