Top-N 查詢是指把結果限制為特定列數的查詢,常見於取出結果集中「最新」或「最好」的幾筆。要有效率地執行,排名必須靠管線化 order by 完成。

必須讓資料庫「知道」你只要幾列#

最單純的做法是抓完所需列數後就把 statement 關掉。可惜最佳化工具在準備執行計畫時無從預見這一點

要挑出最佳計畫,最佳化工具必須知道應用程式最終是否會抓取所有列:

  • 若會抓完,「全表掃描 + 明確排序」可能表現最好。
  • 若只抓十列,管線化 order by 可能更好——即使資料庫得逐列抓取。

SQL 標準長期未涵蓋這項需求。對應的擴充 fetch first 遲至 SQL:2008 才引入,且目前僅 IBM DB2、PostgreSQL 與 SQL Server 2012 提供。一方面它是非核心擴充,另一方面各家資料庫多年來早就各自提供了專有解法。

各資料庫的 Top-N 語法#

以下都在做同一件事:取出所有銷售、從最新的開始,抓滿十列就中止執行

MySQL / PostgreSQLlimit 子句)

SELECT *
  FROM sales
 ORDER BY sale_date DESC
 LIMIT 10;

Oracle Database(偽欄位 ROWNUM,需包一層)

SELECT *
  FROM (
       SELECT *
         FROM sales
        ORDER BY sale_date DESC
       )
 WHERE rownum <= 10;

PostgreSQL(自 8.4 起支援 fetch first,舊的 limit 依然可用)

SELECT *
  FROM sales
 ORDER BY sale_date DESC
 FETCH FIRST 10 ROWS ONLY;

SQL Servertop 子句;2012 起也支援 fetch first

SELECT TOP 10 *
  FROM sales
 ORDER BY sale_date DESC;

上述查詢之所以特別,是因為資料庫認得它們是 top-N 查詢

資料庫只有在一開始就知情,才能為部分結果最佳化查詢。

執行計畫:COUNT STOPKEY#

當最佳化工具知道我們只要十列時,只要可行,它就會偏好管線化 order by:

-------------------------------------------------------------
| Operation                     | Name        | Rows | Cost |
-------------------------------------------------------------
| SELECT STATEMENT              |             |   10 |    9 |
| COUNT STOPKEY                 |             |      |      |
|   VIEW                        |             |   10 |    9 |
|    TABLE ACCESS BY INDEX ROWID| SALES       |1004K |    9 |
|     INDEX FULL SCAN DESCENDING| SALES_DT_PR |   10 |    3 |
-------------------------------------------------------------

Oracle 以 COUNT STOPKEY 操作標示「計畫中的提前終止」——代表它認出了 top-N 語法。

附錄 A「執行計畫」整理了 MySQL、Oracle、PostgreSQL 與 SQL Server 的對應操作。

語法只是一半,索引才是另一半#

用對語法只解決一半問題,有效率地終止執行還要求底層操作以管線化方式執行——也就是 order by 子句必須被索引涵蓋(本例是 SALE_DATEPRODUCT_ID 上的 SALE_DT_PR)。有了它,資料庫就能省去明確排序,邊從索引讀邊把列送給應用程式,抓滿十列即中止——讀取的列不會多於選出的列

反之,若 SALE_DATE 上沒有合適索引可供管線化 order by,資料庫就必須讀取並排序整張表,要讀到表中最後一列,才能交出第一列

--------------------------------------------------
| Operation                | Name  | Rows | Cost |
--------------------------------------------------
| SELECT STATEMENT         |       |   10 | 59558|
| COUNT STOPKEY            |       |      |      |
|  VIEW                    |       |1004K | 59558|
|    SORT ORDER BY STOPKEY |       |1004K | 59558|
|     TABLE ACCESS FULL    | SALES |1004K |  9246|
--------------------------------------------------

這個計畫沒有管線化 order by,幾乎和「從客戶端主動中止」一樣慢。不過使用 top-N 語法仍然較好:資料庫不必具體化完整結果,只需具體化最新的十列,記憶體需求大幅降低。Oracle 以 SORT ORDER BY 上的 STOPKEY 修飾詞標示這項優化。

擴展性:回應時間幾乎與表大小無關#

管線化 top-N 查詢的好處不只是立即的效能收益,還有更好的擴展性

  • 未管線化:回應時間隨資料表大小成長(線性)。
  • 管線化:回應時間只隨被選出的列數成長;換言之幾乎是常數,與表大小無關——只有當 B-tree 深度增加時才會稍微變慢。

圖 7.1:Top-N 查詢的擴展性

具體化(materialized)版本的回應時間隨資料量線性成長,管線化版本則維持恆定。

儘管回應時間不隨表大小成長,它仍隨被選出的列數成長:選兩倍的列,時間就加倍。

這對「載入下一頁」的分頁查詢特別要命——這類查詢常常又從第一筆開始讀起,把前一頁已顯示過的列全部讀進來再丟棄,才終於讀到第二頁的結果。

下一節會提供這個問題的解法。