c#软件开发学习笔记--分组、游标与临时表、分页

数据库高级查询学习总结

目录


一、分组查询与统计

1.1 GROUP BY 分组

1.1.1 基本语法
sql 复制代码
SELECT 聚合函数, 分组列 
FROM 表名 
[JOIN 关联表 ON 关联条件]
GROUP BY 分组列

核心原则

  • GROUP BY 后的分组列,一般要出现在 SELECT 展示项中
  • SELECT 中不要出现非聚合且非分组的列,否则会报错
1.1.2 常用聚合函数
函数 说明
COUNT(*) 统计记录总数
COUNT(列名) 统计该列非NULL值的数量
AVG(列名) 求平均值
MAX(列名) 求最大值
MIN(列名) 求最小值
SUM(列名) 求和
1.1.3 实际应用示例
sql 复制代码
-- 1. 统计每个班级有多少人
SELECT COUNT(*) AS 总人数, ClassName 
FROM Students
INNER JOIN StudentClass ON StudentClass.ClassId = Students.ClassId
GROUP BY ClassName

-- 2. 按性别统计人数
SELECT Sex, COUNT(*) AS 人数 
FROM Students
GROUP BY Sex
1.1.4 NULL 值处理注意事项
  • NULL 值不参与聚合运算
  • 好处:请假没有参加考试的学生(成绩为NULL),在统计平均分时不参与计算
  • 坏处:可能被利用于作弊------故意让成绩差的学生不参加考试,提高班级平均分
  • 解决方案:如果需要将NULL作为0参与计算,可使用临时表或ISNULL函数处理

1.2 HAVING 分组筛选

1.2.1 为什么需要 HAVING

WHERE 子句只能在分组之前 过滤原始数据行,无法对分组统计后的结果进行筛选。这就需要使用 HAVING 关键字。

1.2.2 基本语法
sql 复制代码
SELECT 聚合函数, 分组列
FROM 表名
GROUP BY 分组列
HAVING 聚合函数条件
1.2.3 应用示例
sql 复制代码
-- 查询C#和DB平均分都大于70的班级
SELECT ClassName,
       COUNT(*) AS 总人数,
       MAX(CSharp) AS C#最高分,
       MAX(SQLServer) AS DB最高分,
       C#平均分 = AVG(CSharp),
       DB平均分 = AVG(SQLServer)
FROM Students
LEFT OUTER JOIN StudentClass ON StudentClass.ClassId = Students.ClassId
LEFT OUTER JOIN ScoreList ON ScoreList.StudentId = Students.StudentId
GROUP BY ClassName
HAVING AVG(CSharp) > 70 AND AVG(SQLServer) > 70

-- 使用HAVING查询重复的分数
SELECT Score, COUNT(*) AS 重复次数 
FROM ScoreList 
GROUP BY Score
HAVING COUNT(Score) > 1

1.3 查询重复数据(HAVING 经典应用)

1.3.1 场景

当发现数据表中有重复数据(脏数据),需要找到这些重复数据进行删除或处理。

1.3.2 实现方法
sql 复制代码
-- 方法一:先查重复字段,再用子查询查完整记录(推荐,易理解)
-- 步骤1:找出哪些StudentId有重复
SELECT StudentId 
FROM ScoreList 
GROUP BY StudentId 
HAVING COUNT(*) > 1

-- 步骤2:查询这些重复记录的具体内容
SELECT * FROM ScoreList
WHERE StudentId IN (
    SELECT StudentId 
    FROM ScoreList 
    GROUP BY StudentId 
    HAVING COUNT(*) > 1
)
ORDER BY StudentId

-- 方法二:使用相关子查询(类似双层for循环)
SELECT * FROM ScoreList
WHERE (
    SELECT COUNT(*) 
    FROM ScoreList s 
    WHERE s.StudentId = ScoreList.StudentId
) > 1
ORDER BY StudentId

1.4 WHERE vs GROUP BY vs HAVING 执行顺序

#mermaid-svg-X79EhUO8WJHp9QUx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X79EhUO8WJHp9QUx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X79EhUO8WJHp9QUx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X79EhUO8WJHp9QUx .error-icon{fill:#552222;}#mermaid-svg-X79EhUO8WJHp9QUx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X79EhUO8WJHp9QUx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X79EhUO8WJHp9QUx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X79EhUO8WJHp9QUx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X79EhUO8WJHp9QUx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X79EhUO8WJHp9QUx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X79EhUO8WJHp9QUx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X79EhUO8WJHp9QUx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X79EhUO8WJHp9QUx .marker.cross{stroke:#333333;}#mermaid-svg-X79EhUO8WJHp9QUx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X79EhUO8WJHp9QUx p{margin:0;}#mermaid-svg-X79EhUO8WJHp9QUx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X79EhUO8WJHp9QUx .cluster-label text{fill:#333;}#mermaid-svg-X79EhUO8WJHp9QUx .cluster-label span{color:#333;}#mermaid-svg-X79EhUO8WJHp9QUx .cluster-label span p{background-color:transparent;}#mermaid-svg-X79EhUO8WJHp9QUx .label text,#mermaid-svg-X79EhUO8WJHp9QUx span{fill:#333;color:#333;}#mermaid-svg-X79EhUO8WJHp9QUx .node rect,#mermaid-svg-X79EhUO8WJHp9QUx .node circle,#mermaid-svg-X79EhUO8WJHp9QUx .node ellipse,#mermaid-svg-X79EhUO8WJHp9QUx .node polygon,#mermaid-svg-X79EhUO8WJHp9QUx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X79EhUO8WJHp9QUx .rough-node .label text,#mermaid-svg-X79EhUO8WJHp9QUx .node .label text,#mermaid-svg-X79EhUO8WJHp9QUx .image-shape .label,#mermaid-svg-X79EhUO8WJHp9QUx .icon-shape .label{text-anchor:middle;}#mermaid-svg-X79EhUO8WJHp9QUx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X79EhUO8WJHp9QUx .rough-node .label,#mermaid-svg-X79EhUO8WJHp9QUx .node .label,#mermaid-svg-X79EhUO8WJHp9QUx .image-shape .label,#mermaid-svg-X79EhUO8WJHp9QUx .icon-shape .label{text-align:center;}#mermaid-svg-X79EhUO8WJHp9QUx .node.clickable{cursor:pointer;}#mermaid-svg-X79EhUO8WJHp9QUx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X79EhUO8WJHp9QUx .arrowheadPath{fill:#333333;}#mermaid-svg-X79EhUO8WJHp9QUx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X79EhUO8WJHp9QUx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X79EhUO8WJHp9QUx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X79EhUO8WJHp9QUx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X79EhUO8WJHp9QUx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X79EhUO8WJHp9QUx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X79EhUO8WJHp9QUx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X79EhUO8WJHp9QUx .cluster text{fill:#333;}#mermaid-svg-X79EhUO8WJHp9QUx .cluster span{color:#333;}#mermaid-svg-X79EhUO8WJHp9QUx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X79EhUO8WJHp9QUx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X79EhUO8WJHp9QUx rect.text{fill:none;stroke-width:0;}#mermaid-svg-X79EhUO8WJHp9QUx .icon-shape,#mermaid-svg-X79EhUO8WJHp9QUx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X79EhUO8WJHp9QUx .icon-shape p,#mermaid-svg-X79EhUO8WJHp9QUx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X79EhUO8WJHp9QUx .icon-shape .label rect,#mermaid-svg-X79EhUO8WJHp9QUx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X79EhUO8WJHp9QUx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X79EhUO8WJHp9QUx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X79EhUO8WJHp9QUx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WHERE 过滤原始数据
GROUP BY 分组统计
HAVING 过滤分组结果

子句 作用阶段 过滤对象
WHERE 分组前 原始数据行
GROUP BY 分组中 将数据行分组并计算统计值
HAVING 分组后 分组统计后的结果集

1.5 补充:NOT IN 用法

sql 复制代码
-- 查找没有参加考试的学生(成绩表中没有记录的学生)
SELECT * 
FROM Students 
WHERE StudentId NOT IN (SELECT StudentId FROM ScoreList)

-- 统计缺考人数
SELECT absentCount = COUNT(*) 
FROM Students 
WHERE StudentId NOT IN (SELECT StudentId FROM ScoreList)

-- 等价写法:使用左外连接 + IS NULL判断
SELECT * 
FROM Students
LEFT OUTER JOIN ScoreList ON Students.StudentId = ScoreList.StudentId
WHERE ScoreList.Score IS NULL

二、游标与临时表

2.1 临时表

2.1.1 什么是临时表

临时表是临时存储数据的表,存储在 tempdb 系统数据库中,类似于 C# 中的局部变量,使用 # 前缀命名。

2.1.2 临时表的特点
  • 临时表名以 # 开头(局部临时表),如 #TempTable
  • 存储在 tempdb 数据库中
  • 当前会话结束后自动删除
  • 也可以手动删除
2.1.3 创建与删除临时表
sql 复制代码
-- 判断临时表是否存在,存在则先删除
IF OBJECT_ID('tempdb..#TempTable') IS NOT NULL
   DROP TABLE #TempTable

-- 创建临时表
CREATE TABLE #TempTable
(
    Score INT NOT NULL,
    ScoreCount INT NULL
)
2.1.4 临时表数据插入方式
sql 复制代码
-- 方式一:使用 INSERT INTO ... VALUES 逐条插入(通常配合游标循环)
INSERT INTO #TempTable(Score, ScoreCount) 
VALUES(@Score, @ScoreCount)

-- 方式二:使用 INSERT INTO ... SELECT 批量插入
INSERT INTO #TempTable
SELECT ScoreList.Score, COUNT(Score) AS ScoreCount 
FROM ScoreList
WHERE Score > 90 AND Score <= 100
GROUP BY Score

-- 查询临时表数据
SELECT * FROM #TempTable

2.2 游标(Cursor)

2.2.1 什么是游标

游标是一种对结果集进行逐行处理的机制,类似于编程语言中的 foreach 循环,可以对查询结果的每一行进行遍历和操作。

2.2.2 游标的使用步骤

游标使用分为 5个固定步骤

复制代码
1. 声明游标 → 2. 打开游标 → 3. 循环读取数据 → 4. 关闭游标 → 5. 释放游标
2.2.3 游标基本语法
sql 复制代码
-- ====================== 游标五步法 ======================

-- 步骤1:声明变量(用于接收每行数据)
DECLARE @Score INT;
DECLARE @ScoreCount INT;

-- 步骤1:声明游标
DECLARE Score_cursor CURSOR FOR
SELECT ScoreList.Score, COUNT(Score) AS ScoreCount 
FROM ScoreList
GROUP BY Score

-- 步骤2:打开游标
OPEN Score_cursor

-- 步骤3:读取第一行数据,存入变量
FETCH NEXT FROM Score_cursor INTO @Score, @ScoreCount

-- 步骤3:循环读取(@@FETCH_STATUS = 0 表示读取成功)
WHILE @@FETCH_STATUS = 0
BEGIN
    -- 对当前行数据进行处理(此处示例:插入临时表)
    INSERT INTO #TempTable(Score, ScoreCount) 
    VALUES(@Score, @ScoreCount);
    
    -- 读取下一行数据(非常重要,否则死循环!)
    FETCH NEXT FROM Score_cursor INTO @Score, @ScoreCount
END

-- 步骤4:关闭游标
CLOSE Score_cursor

-- 步骤5:释放游标
DEALLOCATE Score_cursor
2.2.4 @@FETCH_STATUS 状态说明
返回值 含义
0 FETCH 语句读取成功
-1 FETCH 语句失败或者行不在结果集中
-2 被提取的行不存在
2.2.5 局部变量 vs 全局变量
类型 前缀 说明
局部变量 @ 用户自定义,如 @Score@StudentId
全局变量 @@ SQL Server 系统定义,如 @@FETCH_STATUS
2.2.6 游标完整示例:遍历学生表
sql 复制代码
-- 声明接收变量
DECLARE @StudentId INT;
DECLARE @StudentName VARCHAR(20);
DECLARE @Age INT;
DECLARE @Sex NVARCHAR(10);
DECLARE @Address NVARCHAR(10);
DECLARE @ClassId NVARCHAR(10);

-- 1. 声明游标:查询学生ID >= 3的学生
DECLARE My_Cursor CURSOR FOR 
SELECT * FROM Students WHERE StudentId >= 3;

-- 2. 打开游标
OPEN My_Cursor;

-- 3. 读取第一行
FETCH NEXT FROM My_Cursor INTO @StudentId, @StudentName, @Age, @Sex, @Address, @ClassId;

-- 4. 循环遍历
WHILE @@FETCH_STATUS = 0
BEGIN
    PRINT @StudentId;
    PRINT @StudentName;
    PRINT @Age;
    PRINT '--------'
    
    -- 读取下一行(必须!)
    FETCH NEXT FROM My_Cursor INTO @StudentId, @StudentName, @Age, @Sex, @Address, @ClassId;
END

-- 5. 关闭游标
CLOSE My_Cursor;

-- 6. 释放游标
DEALLOCATE My_Cursor;

2.3 游标与临时表小结

  1. 临时表

    • #表名 命名,存储在 tempdb
    • 适合临时存储中间计算结果
    • 两种插入方式:VALUES(逐条)和 SELECT(批量)
  2. 游标五步法

    • DECLARE cursor_name CURSOR FOR ... 声明
    • OPEN cursor_name 打开
    • FETCH NEXT ... INTO 循环读取
    • CLOSE cursor_name 关闭
    • DEALLOCATE cursor_name 释放
  3. 注意事项

    • 循环内必须写 FETCH NEXT,否则会造成死循环
    • 游标使用完毕必须关闭和释放,避免资源泄漏
    • 大量数据处理时,游标性能较低,优先考虑集合操作

三、分页查询

3.1 分页查询概述

当数据表中数据量较大时,一次性加载所有数据会影响性能和用户体验,因此需要分页显示数据。

3.1.1 分页核心参数
参数 说明
CurrentPage 当前页码(从1开始)
PageSize 每页显示条数
TotalCount 总记录数
TotalPage 总页数
3.1.2 页码范围计算公式
复制代码
起始行号 = (CurrentPage - 1) * PageSize + 1
结束行号 = CurrentPage * PageSize

示例:PageSize=3

  • 第1页:RowId ≥ 1 且 RowId ≤ 3
  • 第2页:RowId ≥ 4 且 RowId ≤ 6
  • 第3页:RowId ≥ 7 且 RowId ≤ 9

3.2 方式一:ROW_NUMBER() 函数分页(兼容 SQL Server 2005+)

3.2.1 基本语法
sql 复制代码
SELECT * FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY 排序列) AS RowId, * 
    FROM 表名
) AS 别名
WHERE RowId BETWEEN 起始行号 AND 结束行号
3.2.2 参数化示例
sql 复制代码
-- 声明变量
DECLARE @CurrentPage INT;
DECLARE @PageSize INT;

-- 赋值
SET @CurrentPage = 1;  -- 第1页
SET @PageSize = 3;     -- 每页3条

-- 分页查询
SELECT * FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY Age) AS RowId, * 
    FROM Students
) AS S
WHERE RowId BETWEEN (@CurrentPage - 1) * @PageSize + 1 
            AND @CurrentPage * @PageSize

说明

  • ROW_NUMBER():为结果集的每一行生成一个递增的序号
  • OVER (ORDER BY Age):按 Age 列排序后再生成序号
  • 子查询必须起别名(此处为 S),否则语法错误

3.3 方式二:OFFSET...FETCH NEXT 分页(SQL Server 2012+)

3.3.1 基本语法
sql 复制代码
SELECT * FROM 表名
ORDER BY 排序列
OFFSET 跳过的行数 ROWS
FETCH NEXT 取的行数 ROWS ONLY
3.3.2 参数化示例
sql 复制代码
-- 声明变量
DECLARE @CurrentPage INT;
DECLARE @PageSize INT;

-- 赋值
SET @CurrentPage = 1;
SET @PageSize = 3;

-- 分页查询
SELECT * FROM Students
ORDER BY StudentId
OFFSET (@CurrentPage - 1) * @PageSize ROWS       -- 跳过前N行
FETCH NEXT @PageSize ROWS ONLY                     -- 取接下来的N行

说明

  • OFFSET:指定跳过多少行
  • FETCH NEXT:指定在跳过之后返回多少行
  • 必须配合 ORDER BY 使用,否则语法错误
  • 代码更简洁,性能与 ROW_NUMBER 相当或略优

3.4 两种分页方式对比

对比项 ROW_NUMBER() OFFSET...FETCH
最低版本 SQL Server 2005 SQL Server 2012
语法复杂度 需要子查询,稍复杂 简洁直观
是否必须ORDER BY 是(在OVER内) 是(外层必须)
适用场景 需要行号或兼容旧版本 2012以上版本推荐

3.5 C# WinForm 分页完整实现

以下是基于 C# WinForm + SQL Server + OFFSET...FETCH NEXT 的完整分页实现代码:

3.5.1 核心变量定义
csharp 复制代码
/// <summary>
/// 数据库连接字符串(从App.config配置文件读取)
/// </summary>
public readonly string connectionString = ConfigurationManager.ConnectionStrings["conString"].ConnectionString;

/// <summary>
/// 当前页码(从1开始)
/// </summary>
public int PageIndex = 1;

/// <summary>
/// 每页显示条数
/// </summary>
public int PageSize = 3;

/// <summary>
/// 总页数
/// </summary>
public int TotalPage = 0;
3.5.2 分页数据绑定方法
csharp 复制代码
/// <summary>
/// 绑定分页数据到DataGridView
/// 包含:条件查询、参数化、OFFSET...FETCH分页、总数统计、按钮状态控制
/// </summary>
private void BindGridView()
{
    // ========== 1. 解析查询条件(int.TryParse 安全转换)==========
    int.TryParse(txtStartScore.Text, out int scoreStart);
    int.TryParse(txtEndScore.Text, out int scoreEnd);

    // ========== 2. 定义SQL参数 ==========
    SqlParameter[] parameters = new SqlParameter[]
    {
        new SqlParameter("@PageIndex", PageIndex),
        new SqlParameter("@PageSize", PageSize),
        new SqlParameter("@Name", $"%{txtName.Text}%"),
        new SqlParameter("@ScoreStart", scoreStart),
        new SqlParameter("@ScoreEnd", scoreEnd),
    };

    // ========== 3. 构建查询语句(基于视图 vm_student)==========
    // 统计总数SQL(不带分页)
    string sql1 = "select count(*) from vm_student where 1 = 1";
    
    // 分页查询SQL(不带分页条件,后续追加)
    string sql2 = "select * from vm_student where 1 = 1";

    // ========== 4. 动态拼接查询条件 ==========
    // 姓名模糊查询
    if (!string.IsNullOrWhiteSpace(txtName.Text))
    {
        sql1 += " and stu_name like @Name";
        sql2 += " and stu_name like @Name";
    }
    // C#分数下限
    if (!string.IsNullOrWhiteSpace(txtStartScore.Text))
    {
        sql1 += " and score_c# >= @ScoreStart";
        sql2 += " and score_c# >= @ScoreStart";
    }
    // C#分数上限
    if (!string.IsNullOrWhiteSpace(txtEndScore.Text))
    {
        sql1 += " and score_c# <= @ScoreEnd";
        sql2 += " and score_c# <= @ScoreEnd";
    }

    // 追加 OFFSET...FETCH NEXT 分页语句
    sql2 += " order by stu_id offset (@PageIndex - 1) * @PageSize rows fetch next @PageSize rows only";

    // ========== 5. 执行分页查询并绑定数据 ==========
    DataSet dataSet = SqlHelper.ExecuteDataset(connectionString, CommandType.Text, sql2, parameters);
    dataGridView1.DataSource = dataSet.Tables[0];
    // 列宽自适应填充
    dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;

    // ========== 6. 计算总页数(使用 Math.Ceiling 向上取整)==========
    object result = SqlHelper.ExecuteScalar(connectionString, CommandType.Text, sql1, parameters);
    if (int.TryParse(result.ToString(), out int totalCount))
    {
        // Math.Ceiling:将浮点结果向上取整,再转为int
        TotalPage = (int)Math.Ceiling((float)totalCount / PageSize);
    }

    // 显示当前页/总页数
    UpdatePageInfo(TotalPage);

    // ========== 7. 按钮状态控制 ==========
    if (PageIndex == 1)
    {
        // 第一页:禁用首页和上一页
        btnFirst.Enabled = false;
        btnPrev.Enabled = false;
        btnNext.Enabled = true;
        btnLast.Enabled = true;
    }
    else if (PageIndex == TotalPage)
    {
        // 最后一页:禁用下一页和末页
        btnFirst.Enabled = true;
        btnPrev.Enabled = true;
        btnNext.Enabled = false;
        btnLast.Enabled = false;
    }
    else
    {
        // 中间页:全部启用
        btnFirst.Enabled = true;
        btnPrev.Enabled = true;
        btnNext.Enabled = true;
        btnLast.Enabled = true;
    }
}

/// <summary>
/// 更新页码显示信息
/// </summary>
/// <param name="totalPage">总页数</param>
private void UpdatePageInfo(int totalPage)
{
    lblPageInfo.Text = $"{PageIndex}/{totalPage}";
}
3.5.3 分页按钮事件处理
csharp 复制代码
/// <summary>
/// 窗体加载事件:初始化分页数据和每页条数下拉框
/// </summary>
private void Form1_Load(object sender, EventArgs e)
{
    BindGridView();
    cmbPageSize.SelectedIndex = 0;
}

/// <summary>
/// 查询按钮:根据条件查询并刷新分页
/// </summary>
private void btnQuery_Click(object sender, EventArgs e)
{
    BindGridView();
}

/// <summary>
/// 首页按钮:跳转到第1页(添加判断避免重复绑定)
/// </summary>
private void btnFirst_Click(object sender, EventArgs e)
{
    if (PageIndex != 1)
    {
        PageIndex = 1;
        BindGridView();
    }
}

/// <summary>
/// 上一页按钮:页码减1
/// </summary>
private void btnPrev_Click(object sender, EventArgs e)
{
    if (PageIndex > 1)
    {
        PageIndex--;
    }
    BindGridView();
}

/// <summary>
/// 下一页按钮:页码加1
/// </summary>
private void btnNext_Click(object sender, EventArgs e)
{
    if (PageIndex < TotalPage)
    {
        PageIndex++;
    }
    BindGridView();
}

/// <summary>
/// 末页按钮:跳转到最后一页(添加判断避免重复绑定)
/// </summary>
private void btnLast_Click(object sender, EventArgs e)
{
    if (PageIndex != TotalPage)
    {
        PageIndex = TotalPage;
        BindGridView();
    }
}

/// <summary>
/// 跳转按钮:跳转到指定页码(带输入合法性验证)
/// </summary>
private void btnRedirectTo_Click(object sender, EventArgs e)
{
    // 验证是否为合法整数
    if (int.TryParse(txtPageIndex.Text, out int pageIndex))
    {
        // 验证页码范围是否有效
        if (pageIndex <= TotalPage && pageIndex >= 1)
        {
            PageIndex = pageIndex;
            BindGridView();
            return;
        }
    }
    MessageBox.Show("请输入正确的页码!");
}

/// <summary>
/// 每页条数下拉框改变事件:切换每页条数并重置为第1页
/// </summary>
private void cmbPageSize_SelectedIndexChanged(object sender, EventArgs e)
{
    PageSize = Convert.ToInt32(cmbPageSize.SelectedItem);
    PageIndex = 1;
    BindGridView();

    btnFirst.Enabled = false;
    btnPrev.Enabled = false;
}

3.6 分页查询小结

  1. 两种SQL分页方式

    • ROW_NUMBER():兼容 SQL Server 2005+,需要子查询 + BETWEEN 范围
    • OFFSET...FETCH NEXT:SQL Server 2012+,语法更简洁(date714项目采用此方式)
  2. C#分页实现要点

    • 连接字符串:从 App.config 配置文件读取(ConfigurationManager
    • 两个SQL:一个查总数(COUNT(*)),一个查分页数据
    • 分页方式:OFFSET (@PageIndex-1)*@PageSize ROWS FETCH NEXT @PageSize ROWS ONLY
    • 参数化查询:使用 SqlParameter + int.TryParse 安全解析,防止SQL注入和格式异常
    • 动态条件:支持姓名模糊查询(LIKE @Name)、C#分数范围查询(>= / <=
    • 总页数计算:(int)Math.Ceiling((float)totalCount / PageSize) 浮点数向上取整
    • 跳转验证:int.TryParse + 范围判断 + 错误提示,避免非法跳转
    • 按钮优化:PageIndex != 1 条件判断,避免第一页重复点击首页时重复绑定
    • 辅助方法:UpdatePageInfo() 独立封装页码显示更新,代码更清晰
  3. 关键步骤

    • 定义分页参数(PageIndex/PageSize)→ 安全解析输入 → 构建两条SQL
    • 动态拼接查询条件 → 追加 OFFSET...FETCH 分页语句
    • 执行分页查询绑定 DataGridView → 执行 COUNT 计算总页数
    • Math.Ceiling 向上取整算总页 → 调用 UpdatePageInfo 显示页码
    • 根据当前页位置控制按钮启用状态 → 跳转按钮做合法性校验

相关推荐
rick9771 小时前
用 C# 从零搭建本地知识库:RAG 系统完整实战
c#
治愈系贝壳1 小时前
ARM ---day1
linux·arm开发·学习
qdprobot1 小时前
齐护AIOT科创社团学习套件桌面总程AiTall V3版人工智能Mixly图形化编程AI小智物联网AI具身智能应用椴木科创作品
人工智能·物联网·学习
遇乐的果园1 小时前
前端学习笔记-vue列表处理
前端·vue.js·学习
He BianGu2 小时前
【笔记】WPF中 Brush 的类型、继承关系、使用方式和注意事项
笔记·wpf
小弥儿2 小时前
GitHub今日热榜 | 2026-07-16:AI知识库与反AI味设计成今日双主线
人工智能·学习·github
菩提树下的打坐2 小时前
CI 中的测试分层:让金字塔真正落地
学习·ci/cd
阿米亚波2 小时前
【C++ STL】std::forward_list
开发语言·c++·笔记·stl·visual studio·forward_list
菩提树下的打坐2 小时前
flaky 测试治理:用工程化方式根治“上次过这次不过“
学习