千萬條資料,Stack Overflow 是如何實現快速分頁的?

pythontab發表於2018-05-02

Stack Overflow 在分頁機制中使用頁碼代替偏移量,頁碼指向基於 LIMIT 和 OFFSET 的查詢。假設要對 1000 萬條記錄進行分頁,跳到最後一頁會非常慢,但 Stack Overflow 還是想辦法實現了快速分頁。

那麼 Stack Overflow 是如何實現快速分頁的呢?快取熱門查詢並在應用程式程式碼中實現分頁?還是使用了什麼資料庫黑魔法?

實際上,整個分頁過程是非常複雜的。但我會嘗試以一種簡單的方式告訴你其中的原理,而不是寫一個包含很多頁內容的帖子。


假設

說到分頁,基本上是圍繞 pageNumber * pageSize 而展開的。也就是說,要在已排好序的 n 條記錄中獲得當前的集合,可以將 pageNumber 乘以 pageSize,然後再加上 pageSize,就可以返回當前結果。在我們的例子中,它實際上是(pageNumber - 1)* pageSize,因為頁面 1 的索引是 0。

在排序問題上,我們不需要完全排序整個集合,而是對 pageNumber * pageSize 條資料進行排序,這樣就可以得到當前頁面排好序的資料,而剩餘部分可能只進行部分排序。與其排序整個集合並返回前 n 個結果,不如只對集合的前 n 個結果進行排序並返回這些結果。這樣做很合理。

另外需要注意的是,最耗資源的查詢總是那些中間頁。獲取最後 n 個頁面與獲得前 n 個頁面一樣容易:只需進行反向排序即可。比如,在按照日期降序排序時獲取 pageNumber 1 與在按照日期升序排列時獲取 pageNumber n-1 一樣,都很容易。很多排序引擎(資料庫、搜尋引擎等)都使用了這種最佳化方式,我們也一樣。

為了方便討論,我們假定問題就是帖子,反之亦然,因為我會在文中交替使用這兩個名詞。


第 1 步:Tag Engine

我們有一個自己開發的.NET 應用程式,叫作 Tag Engine,它包含了帖子 ID 和後設資料。我們把它看作是一個倒排索引,可以透過資料(如建立日期、標籤、分數等)查詢帖子 ID。

Tag Engine 主要負責基於某些限制條件做一些集合操作,比如它對一系列帖子 ID 集合進行交集、聯合等操作,以便得到最終結果,並且還可以基於後設資料在記憶體中進行排序。

我們使用 pageNumber 和 pageSize 以及一些限制條件(比如 Site ID,因為 Tag Engine 負責處理所有站點的查詢)向 Tag Engine 發起查詢。它在記憶體中進行集合操作(如聯合和交集),然後對結果進行排序,返回相關的帖子 ID 子集。

Tag Engine 還會快取查詢結果(是集合,而不僅僅是請求的頁面),並且可以根據由查詢(頁碼、頁面大小、排序方式等)雜湊生成的快取鍵從特定的快取結果集中快速選擇一個頁面。這樣極大提升了查詢效能。


第 2 步:資料庫

Tag Engine 不包含實際的資料,僅包含 ID 和後設資料。因此,我們用帖子 ID 的結果集來查詢資料庫。查詢看起來像這樣:

Select p.*, pm.ViewCount, u.Id, u.ProfileImageUrl, ...
From Posts p
Join PostMetadata pm On p.Id = pm.PostId
Left Join Users u On p.LastActivityUserId = u.Id
Where p.Id In @Ids";


這裡的 @Ids 是指 Tag Engine 中包含的 ID 列表。這個查詢將返回實際的資料,但事情還沒完。


步驟 3:半冗餘的記憶體排序

如上所述,Tag Engine 可能會返回快取的資料。然而,就其性質而言,快取資料不能保證準確性(因為它們有可能是過去狀態的快照)。相比之下,資料庫始終具有最新的資料。

為了解決這個問題,我們在記憶體中再次對結果頁面進行排序。

不過有一點比較讓人頭疼:最後一次記憶體排序基本上就是呼叫 List.Sort,並傳進去一個排序函式。排序函式因使用者檢視不同的頁面而有所不同:對於“Newest”頁面,它會比較建立日期,而對於“Votes”,它會比較分數等。

如果我們沒有做最後一步,帖子在頁面上顯示時可能會出現亂序,因為它們在 Tag Engine 中的排序反映的是過去的狀態,而不是資料庫的當前狀態。

最後,我們把問題列表顯示出來!


相關文章