ASP.NET Web FormによるListViewの表示処理の高速化についてです。
10万件のデータを「0.0005秒程度」で表示するのは物理的に厳しいですが(ページ読み込み+DBアクセス+ASP.NET処理時間を含めての意味なら現実的には 数十ms〜数百ms台が限界)、極限まで高速にページネーション表示する方法は以下の通りです。
完成イメージ
✅ パフォーマンス高速化のアプローチ(概要)
1.SQL側でページング(OFFSET FETCH)を使って最小限のデータを取得
2.SQLにインデックスを正しく設定(ORDER BY列に)
3.ListViewの仮想ページングではなく、自前でデータを制御
4.無駄な全件取得(今のような全読み込み)をしない
5.クライアント側キャッシュ(オプション)
テーブルにインデックスを設定(SQL Server)
CREATE NONCLUSTERED INDEX IX_employee_employeeId
ON [test].[dbo].[employee](employeeId);
修正したコード
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="sample4.aspx.cs" Inherits="WebApplicationWebForm.sample4" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta charset="utf-8" />
<title>ListViewページング</title>
</head>
<body>
<form id="form2" runat="server">
<asp:ListView ID="ListViewClass" runat="server"
AllowPaging="false" AllowSorting="true"
OnItemDataBound="ListViewClass_ItemDataBound"
OnSorting="ListViewClass_Sorting">
<LayoutTemplate>
<table border="1" style="width:100%">
<thead>
<tr>
<th><asp:LinkButton ID="SortEmployeeId" runat="server" CommandName="Sort" CommandArgument="employeeId">従業員ID</asp:LinkButton></th>
<th><asp:LinkButton ID="SortDepartment" runat="server" CommandName="Sort" CommandArgument="department">所属部署</asp:LinkButton></th>
<th><asp:LinkButton ID="SortSection" runat="server" CommandName="Sort" CommandArgument="section">所属課</asp:LinkButton></th>
<th><asp:LinkButton ID="SortJobClass" runat="server" CommandName="Sort" CommandArgument="jobclass">役職</asp:LinkButton></th>
<th><asp:LinkButton ID="SortName" runat="server" CommandName="Sort" CommandArgument="name">従業員名</asp:LinkButton></th>
<th><asp:LinkButton ID="SortSex" runat="server" CommandName="Sort" CommandArgument="sex">性別</asp:LinkButton></th>
<th><asp:LinkButton ID="SortIP" runat="server" CommandName="Sort" CommandArgument="ipaddress">IP</asp:LinkButton></th>
<th><asp:LinkButton ID="SortIPAlloc" runat="server" CommandName="Sort" CommandArgument="ipaddressalocation">割り振り</asp:LinkButton></th>
</tr>
</thead>
<tbody>
<asp:PlaceHolder ID="itemPlaceholder" runat="server" />
</tbody>
</table>
</LayoutTemplate>
<ItemTemplate>
<tr>
<td><%# Eval("EmployeeId") %></td>
<td><%# Eval("Department") %></td>
<td><%# Eval("Section") %></td>
<td><%# Eval("JobClass") %></td>
<td><%# Eval("Name") %></td>
<td><%# Eval("Sex") %></td>
<td><%# Eval("Ipaddress") %></td>
<td><%# Convert.ToBoolean(Eval("IpaddressAlocation")) ? "○" : "×" %></td>
</tr>
</ItemTemplate>
</asp:ListView>
<!-- ✅ ページングリンクをRepeaterで作成 -->
<div style="text-align:center; margin-top:10px;">
<asp:Repeater ID="PagerRepeater" runat="server" OnItemCommand="PagerRepeater_ItemCommand">
<ItemTemplate>
<asp:LinkButton runat="server" CommandName="Page" CommandArgument='<%# Eval("PageIndex") %>'>
<%# Eval("DisplayText") %>
</asp:LinkButton>
</ItemTemplate>
</asp:Repeater>
</div>
</form>
</body>
</html>
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Configuration;
using System.Web.UI.WebControls;
namespace WebApplicationWebForm
{
public partial class sample4 : System.Web.UI.Page
{
private int PageSize => 2;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ViewState["PageIndex"] = 0;
LoadListView();
LoadPager();
}
}
private void LoadListView()
{
int pageIndex = (int)ViewState["PageIndex"];
int offset = pageIndex * PageSize;
string connStr = ConfigurationManager.ConnectionStrings["sqlserver"].ConnectionString;
List<Sample4Employee> employees = new List<Sample4Employee>();
using (SqlConnection conn = new SqlConnection(connStr))
{
string sql = $@"
SELECT employeeId, department, section, jobclass, name, sex, ipaddress, ipaddressalocation
FROM [test].[dbo].[employee]
ORDER BY {SortExpression} {SortDirection}
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@Offset", offset);
cmd.Parameters.AddWithValue("@PageSize", PageSize);
conn.Open();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
employees.Add(new Sample4Employee
{
EmployeeId = Convert.ToInt32(reader["employeeId"]),
Department = reader["department"].ToString(),
Section = reader["section"].ToString(),
JobClass = reader["jobclass"].ToString(),
Name = reader["name"].ToString(),
Sex = reader["sex"].ToString(),
Ipaddress = reader["ipaddress"].ToString(),
IpaddressAlocation = reader["ipaddressalocation"] != DBNull.Value &&
Convert.ToBoolean(reader["ipaddressalocation"])
});
}
}
}
}
ListViewClass.DataSource = employees;
ListViewClass.DataBind();
}
private void LoadPager()
{
int total = GetTotalRowCount();
int totalPages = (int)Math.Ceiling(total / (double)PageSize);
var pagerItems = new List<dynamic>();
// 先頭
pagerItems.Add(new { PageIndex = 0, DisplayText = "先頭" });
// 番号
for (int i = 0; i < totalPages; i++)
{
pagerItems.Add(new { PageIndex = i, DisplayText = (i + 1).ToString() });
}
// 最後尾
pagerItems.Add(new { PageIndex = totalPages - 1, DisplayText = "最後尾" });
PagerRepeater.DataSource = pagerItems;
PagerRepeater.DataBind();
}
private int GetTotalRowCount()
{
string connStr = ConfigurationManager.ConnectionStrings["sqlserver"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
string sql = "SELECT COUNT(*) FROM [test].[dbo].[employee]";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
conn.Open();
return (int)cmd.ExecuteScalar();
}
}
}
protected void PagerRepeater_ItemCommand(object source, RepeaterCommandEventArgs e)
{
if (e.CommandName == "Page")
{
ViewState["PageIndex"] = Convert.ToInt32(e.CommandArgument);
LoadListView();
LoadPager();
}
}
protected void ListViewClass_Sorting(object sender, ListViewSortEventArgs e)
{
if (SortExpression == e.SortExpression)
{
SortDirection = (SortDirection == "ASC") ? "DESC" : "ASC";
}
else
{
SortExpression = e.SortExpression;
SortDirection = "ASC";
}
ViewState["PageIndex"] = 0;
LoadListView();
LoadPager();
}
protected void ListViewClass_ItemDataBound(object sender, ListViewItemEventArgs e)
{
// 任意: 装飾やロジック
}
private string SortExpression
{
get => ViewState["SortExpression"] as string ?? "employeeId";
set => ViewState["SortExpression"] = value;
}
private string SortDirection
{
get => ViewState["SortDirection"] as string ?? "ASC";
set => ViewState["SortDirection"] = value;
}
}
public class Sample4Employee
{
public int EmployeeId { get; set; }
public string Department { get; set; }
public string Section { get; set; }
public string JobClass { get; set; }
public string Name { get; set; }
public string Sex { get; set; }
public string Ipaddress { get; set; }
public bool IpaddressAlocation { get; set; }
}
}
トラブルシューティング
番号リンクがリンクになっていないので、画面の切り替えができない
(修正前のコード)
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="sample4.aspx.cs" Inherits="WebApplicationWebForm.sample4" %>
<!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>ListView表示</title>
</head>
<body>
<form id="form2" runat="server">
<asp:ListView ID="ListViewClass" runat="server"
AllowPaging="false" AllowSorting="true"
OnItemDataBound="ListViewClass_ItemDataBound"
OnSorting="ListViewClass_Sorting"
OnPagePropertiesChanging="ListViewClass_PagePropertiesChanging">
<LayoutTemplate>
<table border="1">
<thead>
<tr>
<th><asp:LinkButton ID="SortEmployeeId" runat="server" CommandName="Sort" CommandArgument="employeeId">従業員ID</asp:LinkButton></th>
<th><asp:LinkButton ID="SortDepartment" runat="server" CommandName="Sort" CommandArgument="department">所属部署</asp:LinkButton></th>
<th><asp:LinkButton ID="SortSection" runat="server" CommandName="Sort" CommandArgument="section">所属課</asp:LinkButton></th>
<th><asp:LinkButton ID="SortJobClass" runat="server" CommandName="Sort" CommandArgument="jobclass">役職</asp:LinkButton></th>
<th><asp:LinkButton ID="SortName" runat="server" CommandName="Sort" CommandArgument="name">従業員名</asp:LinkButton></th>
<th><asp:LinkButton ID="SortSex" runat="server" CommandName="Sort" CommandArgument="sex">性別</asp:LinkButton></th>
<th><asp:LinkButton ID="SortIP" runat="server" CommandName="Sort" CommandArgument="ipaddress">IPアドレス</asp:LinkButton></th>
<th><asp:LinkButton ID="SortIPAlloc" runat="server" CommandName="Sort" CommandArgument="ipaddressalocation">割り振り</asp:LinkButton></th>
</tr>
</thead>
<tbody>
<asp:PlaceHolder ID="itemPlaceholder" runat="server" />
</tbody>
</table>
</LayoutTemplate>
<ItemTemplate>
<tr>
<td><%# Eval("EmployeeId") %></td>
<td><%# Eval("Department") %></td>
<td><%# Eval("Section") %></td>
<td><%# Eval("JobClass") %></td>
<td><%# Eval("Name") %></td>
<td><%# Eval("Sex") %></td>
<td><%# Eval("Ipaddress") %></td>
<td><%# Convert.ToBoolean(Eval("IpaddressAlocation")) ? "○" : "×" %></td>
</tr>
</ItemTemplate>
</asp:ListView>
<div style="margin-top:10px; text-align:center;">
<asp:DataPager ID="DataPager1" runat="server" PagedControlID="ListViewClass" PageSize="2">
<Fields>
<asp:NextPreviousPagerField
ShowFirstPageButton="true"
ShowPreviousPageButton="false"
FirstPageText="先頭"
ButtonType="Link" />
<asp:NumericPagerField
ButtonCount="10"
ButtonType="Link" />
<asp:NextPreviousPagerField
ShowNextPageButton="false"
ShowLastPageButton="true"
LastPageText="最後尾"
ButtonType="Link" />
</Fields>
</asp:DataPager>
</div>
</form>
</body>
</html>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data.SqlClient;
using System.Configuration;
using WebApplicationWebForm.Models;
namespace WebApplicationWebForm
{
public partial class sample4 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// 最初のページ(先頭)からデータ読み込み
DataPager1.SetPageProperties(0, DataPager1.PageSize, true);
//DataPager1.TotalRowCount = GetTotalRowCount();
LoadListView();
}
}
private void LoadListView()
{
int pageSize = DataPager1.PageSize;
int pageIndex = GetPageIndex();
int offset = pageIndex * pageSize;
string connStr = ConfigurationManager.ConnectionStrings["sqlserver"].ConnectionString;
List<Sample4Employee> employeeList = new List<Sample4Employee>();
using (SqlConnection conn = new SqlConnection(connStr))
{
string sql = $@"
SELECT employeeId, department, section, jobclass, name, sex, ipaddress, ipaddressalocation
FROM [test].[dbo].[employee]
ORDER BY {SortExpression} {SortDirection}
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@Offset", offset);
cmd.Parameters.AddWithValue("@PageSize", pageSize);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
employeeList.Add(new Sample4Employee
{
EmployeeId = Convert.ToInt32(reader["employeeId"]),
Department = reader["department"].ToString(),
Section = reader["section"].ToString(),
JobClass = reader["jobclass"].ToString(),
Name = reader["name"].ToString(),
Sex = reader["sex"].ToString(),
Ipaddress = reader["ipaddress"].ToString(),
IpaddressAlocation = reader["ipaddressalocation"] != DBNull.Value ? Convert.ToBoolean(reader["ipaddressalocation"]) : false
});
}
}
}
ListViewClass.DataSource = employeeList;
ListViewClass.DataBind();
}
private int GetPageIndex()
{
return (DataPager1.StartRowIndex / DataPager1.PageSize);
}
private int GetTotalRowCount()
{
string connStr = ConfigurationManager.ConnectionStrings["sqlserver"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
string sql = "SELECT COUNT(*) FROM [test].[dbo].[employee]";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
conn.Open();
return (int)cmd.ExecuteScalar();
}
}
}
protected void ListViewClass_PagePropertiesChanging(object sender, PagePropertiesChangingEventArgs e)
{
DataPager1.SetPageProperties(e.StartRowIndex, e.MaximumRows, false);
LoadListView();
}
protected void ListViewClass_Sorting(object sender, ListViewSortEventArgs e)
{
if (SortExpression == e.SortExpression)
{
SortDirection = (SortDirection == "ASC") ? "DESC" : "ASC";
}
else
{
SortExpression = e.SortExpression;
SortDirection = "ASC";
}
DataPager1.SetPageProperties(0, DataPager1.PageSize, true);
LoadListView();
}
protected void ListViewClass_ItemDataBound(object sender, ListViewItemEventArgs e)
{
if (e.Item.ItemType == ListViewItemType.DataItem)
{
var employee = (Sample4Employee)((ListViewDataItem)e.Item).DataItem;
//var employee = (Employee)((ListViewDataItem)e.Item).DataItem;
// 色分けロジック(必要に応じて)
// if (employee.Sex == "男") e.Item.BackColor = System.Drawing.Color.LightBlue;
// else if (employee.Sex == "女") e.Item.BackColor = System.Drawing.Color.MistyRose;
}
}
// ソート状態保持
private string SortExpression
{
get => ViewState["SortExpression"] as string ?? "employeeId";
set => ViewState["SortExpression"] = value;
}
private string SortDirection
{
get => ViewState["SortDirection"] as string ?? "ASC";
set => ViewState["SortDirection"] = value;
}
}
// モデルクラス
public class Sample4Employee
{
public int EmployeeId { get; set; }
public string Department { get; set; }
public string Section { get; set; }
public string JobClass { get; set; }
public string Name { get; set; }
public string Sex { get; set; }
public string Ipaddress { get; set; }
public bool IpaddressAlocation { get; set; }
}
}
(原因)
「番号リンクが表示されるが押せない(動かない)」という原因は、AllowPaging="false" の設定と DataPager の仕様のミスマッチによるものです。
❌ 今の問題点
AllowPaging="false":ListView 側でのページングはオフ → これは SQL側でOFFSET/FETCHを使うために必要
しかし、DataPager は AllowPaging="true" の前提で動作する部分がある(内部で ListView をトリガーにするため)
結果、ページ番号は出るがリンクとして動作しない状態になります。
✅ 解決策
👉 解決には以下のように ページングのロジックを完全にカスタムで制御する必要があります:
DataPager の代わりに 独自でページ番号リンクを出力(例:リピーターなど)
または AllowPaging="true" にしてListViewの自動ページングに任せる(ただし SQLページングでなくなり非推奨)
✅ 今回の要件(SQL OFFSET/FETCH + 高速)に合わせた対応
✅ 解決方法(推奨)
DataPager を使わず、自作のページ番号リンクを作り、ボタンクリックでページ切替する方法。
✅ 最終形コード(修正版)
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="sample4.aspx.cs" Inherits="WebApplicationWebForm.sample4" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta charset="utf-8" />
<title>ListViewページング</title>
</head>
<body>
<form id="form2" runat="server">
<asp:ListView ID="ListViewClass" runat="server"
AllowPaging="false" AllowSorting="true"
OnItemDataBound="ListViewClass_ItemDataBound"
OnSorting="ListViewClass_Sorting">
<LayoutTemplate>
<table border="1" style="width:100%">
<thead>
<tr>
<th><asp:LinkButton ID="SortEmployeeId" runat="server" CommandName="Sort" CommandArgument="employeeId">従業員ID</asp:LinkButton></th>
<th><asp:LinkButton ID="SortDepartment" runat="server" CommandName="Sort" CommandArgument="department">所属部署</asp:LinkButton></th>
<th><asp:LinkButton ID="SortSection" runat="server" CommandName="Sort" CommandArgument="section">所属課</asp:LinkButton></th>
<th><asp:LinkButton ID="SortJobClass" runat="server" CommandName="Sort" CommandArgument="jobclass">役職</asp:LinkButton></th>
<th><asp:LinkButton ID="SortName" runat="server" CommandName="Sort" CommandArgument="name">従業員名</asp:LinkButton></th>
<th><asp:LinkButton ID="SortSex" runat="server" CommandName="Sort" CommandArgument="sex">性別</asp:LinkButton></th>
<th><asp:LinkButton ID="SortIP" runat="server" CommandName="Sort" CommandArgument="ipaddress">IP</asp:LinkButton></th>
<th><asp:LinkButton ID="SortIPAlloc" runat="server" CommandName="Sort" CommandArgument="ipaddressalocation">割り振り</asp:LinkButton></th>
</tr>
</thead>
<tbody>
<asp:PlaceHolder ID="itemPlaceholder" runat="server" />
</tbody>
</table>
</LayoutTemplate>
<ItemTemplate>
<tr>
<td><%# Eval("EmployeeId") %></td>
<td><%# Eval("Department") %></td>
<td><%# Eval("Section") %></td>
<td><%# Eval("JobClass") %></td>
<td><%# Eval("Name") %></td>
<td><%# Eval("Sex") %></td>
<td><%# Eval("Ipaddress") %></td>
<td><%# Convert.ToBoolean(Eval("IpaddressAlocation")) ? "○" : "×" %></td>
</tr>
</ItemTemplate>
</asp:ListView>
<!-- ✅ ページングリンクをRepeaterで作成 -->
<div style="text-align:center; margin-top:10px;">
<asp:Repeater ID="PagerRepeater" runat="server" OnItemCommand="PagerRepeater_ItemCommand">
<ItemTemplate>
<asp:LinkButton runat="server" CommandName="Page" CommandArgument='<%# Eval("PageIndex") %>'>
<%# Eval("DisplayText") %>
</asp:LinkButton>
</ItemTemplate>
</asp:Repeater>
</div>
</form>
</body>
</html>
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Configuration;
using System.Web.UI.WebControls;
namespace WebApplicationWebForm
{
public partial class sample4 : System.Web.UI.Page
{
private int PageSize => 2;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ViewState["PageIndex"] = 0;
LoadListView();
LoadPager();
}
}
private void LoadListView()
{
int pageIndex = (int)ViewState["PageIndex"];
int offset = pageIndex * PageSize;
string connStr = ConfigurationManager.ConnectionStrings["sqlserver"].ConnectionString;
List<Sample4Employee> employees = new List<Sample4Employee>();
using (SqlConnection conn = new SqlConnection(connStr))
{
string sql = $@"
SELECT employeeId, department, section, jobclass, name, sex, ipaddress, ipaddressalocation
FROM [test].[dbo].[employee]
ORDER BY {SortExpression} {SortDirection}
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@Offset", offset);
cmd.Parameters.AddWithValue("@PageSize", PageSize);
conn.Open();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
employees.Add(new Sample4Employee
{
EmployeeId = Convert.ToInt32(reader["employeeId"]),
Department = reader["department"].ToString(),
Section = reader["section"].ToString(),
JobClass = reader["jobclass"].ToString(),
Name = reader["name"].ToString(),
Sex = reader["sex"].ToString(),
Ipaddress = reader["ipaddress"].ToString(),
IpaddressAlocation = reader["ipaddressalocation"] != DBNull.Value &&
Convert.ToBoolean(reader["ipaddressalocation"])
});
}
}
}
}
ListViewClass.DataSource = employees;
ListViewClass.DataBind();
}
private void LoadPager()
{
int total = GetTotalRowCount();
int totalPages = (int)Math.Ceiling(total / (double)PageSize);
var pagerItems = new List<dynamic>();
// 先頭
pagerItems.Add(new { PageIndex = 0, DisplayText = "先頭" });
// 番号
for (int i = 0; i < totalPages; i++)
{
pagerItems.Add(new { PageIndex = i, DisplayText = (i + 1).ToString() });
}
// 最後尾
pagerItems.Add(new { PageIndex = totalPages - 1, DisplayText = "最後尾" });
PagerRepeater.DataSource = pagerItems;
PagerRepeater.DataBind();
}
private int GetTotalRowCount()
{
string connStr = ConfigurationManager.ConnectionStrings["sqlserver"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
string sql = "SELECT COUNT(*) FROM [test].[dbo].[employee]";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
conn.Open();
return (int)cmd.ExecuteScalar();
}
}
}
protected void PagerRepeater_ItemCommand(object source, RepeaterCommandEventArgs e)
{
if (e.CommandName == "Page")
{
ViewState["PageIndex"] = Convert.ToInt32(e.CommandArgument);
LoadListView();
LoadPager();
}
}
protected void ListViewClass_Sorting(object sender, ListViewSortEventArgs e)
{
if (SortExpression == e.SortExpression)
{
SortDirection = (SortDirection == "ASC") ? "DESC" : "ASC";
}
else
{
SortExpression = e.SortExpression;
SortDirection = "ASC";
}
ViewState["PageIndex"] = 0;
LoadListView();
LoadPager();
}
protected void ListViewClass_ItemDataBound(object sender, ListViewItemEventArgs e)
{
// 任意: 装飾やロジック
}
private string SortExpression
{
get => ViewState["SortExpression"] as string ?? "employeeId";
set => ViewState["SortExpression"] = value;
}
private string SortDirection
{
get => ViewState["SortDirection"] as string ?? "ASC";
set => ViewState["SortDirection"] = value;
}
}
public class Sample4Employee
{
public int EmployeeId { get; set; }
public string Department { get; set; }
public string Section { get; set; }
public string JobClass { get; set; }
public string Name { get; set; }
public string Sex { get; set; }
public string Ipaddress { get; set; }
public bool IpaddressAlocation { get; set; }
}
}

