#PHP#Laravel#MySQL#Deferred Join

在 MySQL 中當資料表筆數過多時,使用 limit N offset M 查詢頁面越後面越久,因為 offset 會把 M 筆資料全都抓出來後再丟棄,造成 I/O 的資源浪費。

SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 0 -- 很快
SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 10000000 -- 超慢,會撈取 10000010 筆資料再丟棄前面的 10000000

解決方法有兩種,取決於需不需要跳頁(頁碼),如果服務是無限滾動的分頁方式,可以參考 Laravel 的 Cursor Pagination,他會使用主鍵(Primary Key)透過 Index 搜尋加速查詢。

SELECT id, name FROM users id > 10000000 ORDER BY id ASC LIMIT 10

若需要頁碼的話也是需要索引(Index)的幫助,先撈出需要的 id 再透過 WHERE IN 方式撈取。

SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000 -- 很快,因為只有用到 PK,不需回表查詢(Lookup)
SELECT id, name FROM users id IN (
	SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000
)

但這麼做會有可能因為 MySQL 設定出現

#1235 - This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'

解決方法有兩種

1. 拆成兩個 Query,先將 ids 取出後再 WHERE IN 

SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000;

SELECT id, name FROM users WHERE id IN (10000001, 10000002, 10000003, 10000004, 10000005, 10000006, 10000007, 10000008, 10000009, 10000010);
<?php 

// Simple Paginate
$ids = collect(Users::query()->select('id')->orderBy('id')->simplePaginate(10)->items())->pluck('id');

// Custom Offset and Limit
$ids = Users::query()->select('id')->orderBy('id')->offset(10000000)->limit(10)->get();

$data = Users::query()->select(['id', 'name'])->whereIn('id', $ids)->get();

2. Deferred Join - 自己 JOIN 自己

SELECT users.id, users.name FROM (
	SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000
) AS index_users
JOIN users ON users.id = index_users.id
<?php 

$subQuery = User::query()->select('id')->orderBy('id')->offset(10000000)->limit(10);

$data = User::query()
	->select('users.id', 'users.name')
	->rightJoinSub($subQuery, 'index_users', function ($join) {
        $join->on('users.id', '=', 'index_users.id');
    })
    ->get();

當然這方式還是有極限,真的還是很慢可能就要考慮分片(Sharding)了。