2025年5月2日 星期五

[研究]ASP.NET,GridView 伺服器端分頁、資料庫分頁 (只讀取顯示於畫面的資料)

[研究]ASP.NET,GridView 伺服器端分頁、資料庫分頁 (只讀取顯示於畫面的資料)

2025-05-02

環境:Visual Studio 2022 + ASP.NET + WebForm + Web Application + C# + SQL Server 2019 + SQL Server Management Studio (SSMS) 19

********************************************************************************

在 ASP.NET Web Forms 中,SqlDataSource 搭配 GridView 預設的分頁功能是「預先載入所有資料(全部)再進行記憶體分頁」,這種方式不適合大量資料。若你希望做到「按下一頁才向 SQL Server 查詢下一頁的 10 筆資料(資料庫分頁)」,你需要啟用「伺服器端分頁(Server-side paging)」。

範例

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"
    Inherits="WebApplication1.Default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:MyDBConnectionString %>"
            SelectCommand="SELECT * FROM (
  SELECT 
    *, 
    ROW_NUMBER() OVER (ORDER BY [SN]) AS RowNum
  FROM [MyTable]
) AS RowConstrainedResult
WHERE RowNum BETWEEN ((@PageIndex-1) * @PageSize + 1) AND (@PageIndex * @PageSize)
ORDER BY RowNum">
            <SelectParameters>
                <asp:ControlParameter Name="PageIndex" ControlID="TextBox_PageIndex" PropertyName="Text" Type="Int32" />
                <asp:ControlParameter Name="PageSize" ControlID="TextBox_PageSize" PropertyName="Text" Type="Int32" />
            </SelectParameters>
        </asp:SqlDataSource>
        <asp:GridView ID="GridView1" runat="server" AllowSorting="True"
            AutoGenerateColumns="False" DataKeyNames="SN" DataSourceID="SqlDataSource1"
            OnDataBound="GridView1_DataBound">
            <Columns>
                <asp:BoundField DataField="RowNum" HeaderText="##" SortExpression="RowNum" ReadOnly="True" />
                <asp:BoundField DataField="SN" HeaderText="SN" SortExpression="SN" InsertVisible="False" ReadOnly="True" />
                <asp:BoundField DataField="Field1" HeaderText="Field1" SortExpression="Field1" />
                <asp:BoundField DataField="Field2" HeaderText="Field2" SortExpression="Field2" />
            </Columns>
        </asp:GridView>
        共&nbsp;<asp:Label ID="Label_RecordCount" runat="server" ForeColor="Blue"></asp:Label>&nbsp;筆,
        共&nbsp;<asp:Label ID="Label_PageCount" runat="server" ForeColor="Blue"></asp:Label>&nbsp;頁,
        <asp:Button ID="Button_First" runat="server" Text="第一頁" OnClick="Button_First_Click" />
        <asp:Button ID="Button_Prev" runat="server" Text="上一頁" OnClick="Button_Prev_Click" />
        <asp:Button ID="Button_Next" runat="server" Text="下一頁" OnClick="Button_Next_Click" />
        <asp:Button ID="Button_Last" runat="server" Text="最後一頁" OnClick="Button_Last_Click" />
        第<asp:TextBox ID="TextBox_PageIndex" runat="server" Width="100px"></asp:TextBox>頁,
        每頁<asp:TextBox ID="TextBox_PageSize" runat="server" Width="100px"></asp:TextBox>筆&nbsp;
        <asp:Button ID="Button_Go" runat="server" Text="顯示" OnClick="Button_Go_Click" />
    </form>
</body>
</html>


Default.aspx.cs

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.SqlClient;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebApplication1
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                GridView1.VirtualItemCount = GetTotalRecordCount(); // 例如 45
                TextBox_PageIndex.Text = "1";
                TextBox_PageSize.Text = "10";
            }
        }
        
        // 取得資料總筆數
        private int GetTotalRecordCount()
        {
            using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["MyDBConnectionString"].ConnectionString))
            {
                SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM [MyDB].[dbo].[MyTable]", conn);
                conn.Open();
                return (int)cmd.ExecuteScalar();
            }
        }

        protected void GridView1_DataBound(object sender, EventArgs e)
        {
            Label_RecordCount.Text = GridView1.VirtualItemCount.ToString();
            Int32.TryParse(TextBox_PageSize.Text, out int pageSize);
            Label_PageCount.Text = ((GridView1.VirtualItemCount+ pageSize-1) / pageSize).ToString();
        }

        protected void Button_First_Click(object sender, EventArgs e)
        {
            TextBox_PageIndex.Text = "1";
            GridView1.DataBind();
        }

        protected void Button_Prev_Click(object sender, EventArgs e)
        {
            int pageIndex = 0;
            Int32.TryParse(TextBox_PageIndex.Text, out pageIndex);
            if (pageIndex <= 1)
            {
                pageIndex = 1;
            }
            else
            {
                pageIndex = pageIndex - 1;
            }
            TextBox_PageIndex.Text = pageIndex.ToString();
            GridView1.DataBind();
        }

        protected void Button_Next_Click(object sender, EventArgs e)
        {
            int pageIndex = 0;
            int pageCount = 0;
            Int32.TryParse(TextBox_PageIndex.Text, out pageIndex);
            Int32.TryParse(Label_PageCount.Text, out pageCount);
            if (pageIndex >= pageCount)
            {
                pageIndex = pageCount;
            }
            else
            {
                pageIndex = pageIndex + 1;
            }
            TextBox_PageIndex.Text = pageIndex.ToString();
            GridView1.DataBind();
        }

        protected void Button_Last_Click(object sender, EventArgs e)
        {
            TextBox_PageIndex.Text = Label_PageCount.Text;
            GridView1.DataBind();
        }

        protected void Button_Go_Click(object sender, EventArgs e)
        {
            GridView1.DataBind();
        }
    }
}

執行


********************************************************************************

記憶體分頁(In-Memory Paging)vs.伺服器端分頁(Server-side Paging)比較

項目記憶體分頁(In-Memory Paging)伺服器端分頁(Server-side Paging)
適用資料量小量(< 500~1000 筆)大量(上千筆以上)
初次載入速度較慢(一次載入所有資料)較快(只載入目前頁的資料)
換頁速度快(資料已在記憶體)稍慢(每次換頁都需查詢資料庫)
系統記憶體消耗高(資料全在記憶體中)低(每次只存目前頁的資料)
網路傳輸量高(一次傳送全部資料)低(每次傳送一頁資料)
對資料庫負擔低(只查詢一次)中~高(每頁都查詢一次)
開發簡單度簡單(不需寫 SQL 分頁語法)較複雜(需搭配 OFFSET/FETCH 或 ROW_NUMBER)
需要 SQL Server 2012 或以上
不適合資料量大是(易造成記憶體爆炸)否(專為大量資料設計)
支援即時資料更新差(資料已載入,不易反映更新)好(每頁都重新查詢)
資料總筆數統計自動已知需額外查詢 COUNT(*)

********************************************************************************

SqlDataSource + GridView 占用的記憶體何時釋放?

元件記憶體釋放時機說明
SqlDataSourceASP.NET 頁面生命週期結束後,由 GC 自動回收(前提是沒有手動綁定給全域變數、Session 等)SqlDataSource 是伺服器控制項,綁定資料後資料會保留到 Page 完成處理
GridView頁面結束後,整個控制項樹(包含 GridView.DataSource)都會被清除若沒有保存 ViewState 的大型資料,就不會殘留記憶體
GridView.DataSource若來自外部(如 DataTable),則會隨作用域結束+無其他參考被 GC 回收除非被綁定在 Session、ViewState、Cache 等長期儲存區

********************************************************************************

2025-05-05 註:敝人實際測試了,「資料庫分頁」雖然降低了讀取的量,在後續 C# 和 ASP.NET 減少了時間和工作量;但卻增加了從 DB 中取得的時間,增加了 DB 的工作量,增加 C# 和 ASP.NET 的程式的複雜度和開發時時間,最好在 SSMS 中測試撈取的時間,看看看值不值得。



********************************************************************************

(完)

相關

沒有留言:

張貼留言