傳統 DataSet 填滿再分頁會耗盡記憶體。SQL Server 2000/2005 早期無 ROW_NUMBER()、OFFSET/FETCH,常用 NOT IN (SELECT TOP N ...) 實作分頁。本文整理完整可用方案。
CREATE TABLE RedHeadedFile (
ID INT IDENTITY(1,1) PRIMARY KEY,
FileNames NVARCHAR(20),
SendUser NVARCHAR(20)
);
-- 測試資料 50 萬筆
DECLARE @i INT = 1;
WHILE @i <= 500000
BEGIN
INSERT INTO RedHeadedFile (FileNames, SendUser) VALUES (N'我的分頁算法', N'陸俊銘');
SET @i += 1;
END
-- 第 1 頁
SELECT TOP 20 * FROM RedHeadedFile
WHERE ID NOT IN (SELECT TOP 0 ID FROM RedHeadedFile ORDER BY ID ASC)
ORDER BY ID ASC;
-- 第 2 頁
SELECT TOP 20 * FROM RedHeadedFile
WHERE ID NOT IN (SELECT TOP 20 ID FROM RedHeadedFile ORDER BY ID ASC)
ORDER BY ID ASC;
-- 第 N 頁
SELECT TOP @PageSize * FROM RedHeadedFile
WHERE ID NOT IN (SELECT TOP @PageSize * @CurrentPage ID FROM RedHeadedFile ORDER BY ID ASC)
ORDER BY ID ASC;
原理:子查詢取得前 N 頁的 ID,主查詢排除這些 ID,再取 Top PageSize 筆。前提:分頁欄位必須有唯一索引(如 IDENTITY PK),且排序固定。
public partial class WebForm8 : System.Web.UI.Page
{
const int PageSize = 20;
int TotalRecords, TotalPages;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
TotalRecords = GetTotalCount();
TotalPages = (TotalRecords + PageSize - 1) / PageSize; // 天花板除法
ViewState["TotalPages"] = TotalPages;
ViewState["CurrentPage"] = 0;
lblTotalPages.Text = TotalPages.ToString();
lblTotalRecords.Text = TotalRecords.ToString();
txtGoPage.Enabled = TotalRecords > PageSize;
BindData();
}
}
int GetTotalCount()
{
using (var conn = GetConnection())
using (var cmd = new SqlCommand("SELECT COUNT(*) FROM RedHeadedFile", conn))
{
return (int)cmd.ExecuteScalar();
}
}
void BindData()
{
int currentPage = (int)ViewState["CurrentPage"];
int totalPages = (int)ViewState["TotalPages"];
// 按鈕啟用狀態
btnFirst.Enabled = btnPrev.Enabled = currentPage > 0;
btnNext.Enabled = btnLast.Enabled = currentPage < totalPages - 1;
// 核心分頁查詢
string sql = $@"
SELECT TOP {PageSize} * FROM RedHeadedFile
WHERE ID NOT IN (
SELECT TOP {PageSize * currentPage} ID FROM RedHeadedFile ORDER BY ID ASC
)
ORDER BY ID ASC";
using (var conn = GetConnection())
using (var adapter = new SqlDataAdapter(sql, conn))
{
var ds = new DataSet();
adapter.Fill(ds);
dlData.DataSource = ds.Tables[0].DefaultView;
dlData.DataBind();
}
lblCurrentPage.Text = (currentPage + 1).ToString();
txtGoPage.Text = (currentPage + 1).ToString();
}
protected void PageNavigation_Click(object sender, CommandEventArgs e)
{
int currentPage = (int)ViewState["CurrentPage"];
int totalPages = (int)ViewState["TotalPages"];
switch (e.CommandName)
{
case "next": currentPage++; break;
case "prev": currentPage--; break;
case "last": currentPage = totalPages - 1; break;
default: currentPage = 0; break; // first
}
ViewState["CurrentPage"] = currentPage;
BindData();
}
protected void txtGoPage_TextChanged(object sender, EventArgs e)
{
if (int.TryParse(txtGoPage.Text, out int page) && page >= 1 && page <= TotalPages)
{
ViewState["CurrentPage"] = page - 1;
BindData();
}
else
{
ClientScript.RegisterStartupScript(GetType(), "alert", "alert('頁碼超出範圍');", true);
}
}
SqlConnection GetConnection()
{
var conn = new SqlConnection(ConfigurationManager.AppSettings["DSN"]);
conn.Open();
return conn;
}
}
<%# Eval("FileNames") %>
<%# Eval("SendUser") %>
<%# Eval("ID") %>
共 頁 /
筆
首頁
上一頁
下一頁
尾頁
當前第 頁
跳頁
| 方案 | 原理 | 適用場景 | 效能 |
|---|---|---|---|
| NOT IN | 子查詢排除前 N 筆 ID | SQL 2000/2005,單一排序欄位 | 資料量大時子查詢掃描慢 |
| MAX/MIN ID 範圍 | 記錄上頁最大 ID,下頁 WHERE ID > @LastId | 單向瀏覽,無跳頁需求 | 極快,索引查找 |
| ROW_NUMBER() | 視窗函數編號後篩選 | SQL 2005+,複雜排序/多欄位 | 優化器支援好,標準寫法 |
| OFFSET/FETCH | ANSI SQL 標準分頁 | SQL 2012+ | 語法簡潔,效能同 ROW_NUMBER |
| Keyset Pagination (Seek Method) | WHERE (SortCol, PK) > (LastSort, LastPK) | 大數據、高併發、API 分頁 | 最佳,無深分頁效能衰退 |
ORDER BY ID OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY。OFFSET 深分頁效能崩塌。INCLUDE 常用顯示欄位,避免書籤查找。NOT IN 分頁是經典年代的產物,理解其原理有助於維護舊系統。新專案請擁抱現代分頁語法與 Keyset Pagination,配合適當索引,才能在百萬級資料下保持毫秒級回應。