本系列专栏基于杨中科老师的《ASP.NET Core技术内幕与项目实战》,本人记录梳理的学习笔记,有部分的增补和省略。更全面系统的讲解,请看杨老师的视频课:【.NET教程,.Net Core视频教程,杨中科主讲】。
一、IQueryable
IQueryable 是 EF Core 中数据库查询的核心接口,继承自 IEnumerable,专为远程数据源查询设计,是实现高效数据访问的关键。
1. IQueryable 与 IEnumerable 的关系
两者都是可枚举集合接口,但数据处理位置、执行机制有区别:
- IEnumerable(内存集合):执行客户端评估。先把数据库所有数据加载到本地内存,再在内存中执行过滤、排序等操作。(缺点:数据量大时会造成内存溢出、网络传输浪费。)
- IQueryable(数据库查询):执行服务器端评估。EF Core 会将查询表达式翻译为对应的 SQL 语句,直接在数据库中执行筛选后,仅返回需要的数据。(优点:性能极高,是 EF Core 操作数据库的标准用法。)
2. 核心特点
(1)延迟执行(懒加载)
这是 IQueryable 最重要的特性:
- 非终结方法:Where()、OrderBy()、Skip()、Take()、Include()、GroupBy() → 不执行数据库查询,仅构建查询表达式树。
- 终结方法:ToList()/ToListAsync()、ToArray()、FirstOrDefault()、Count()、foreach 遍历 → 立即执行 SQL 查询,获取数据。
执行流程:构建查询规则 → 调用终结方法 → 生成 SQL → 数据库执行 → 返回结果。
(2)查询可复用
IQueryable 本质是待执行的查询逻辑,而非已加载的数据,因此可以多次复用、拼接条件:
// 基础查询:只查询状态为启用的书籍
var query = ctx.Books.Where(b => b.IsEnabled == true);
// 复用基础查询:拼接价格筛选
var cheapBooks = query.Where(b => b.Price < 50);
// 复用基础查询:拼接排序
var orderedBooks = query.OrderBy(b => b.PublishTime);
3. 底层工作原理
(1)数据读取底层依赖:IQueryable 默认使用 DataReader 流式读取数据;
(2)DataReader VS DataTable(ToList/ToArray):
| DataReader | 分批流式读取 | 极小 | 长(持续占用) | 大数据量遍历、无需缓存全部数据 |
| ToList/ToArray | 一次性加载全部到内存 | 较大 | 短(执行即释放) | 数据量小、需要复用数据、上下文销毁后使用 |
(3)必用 ToList/ToArray 的场景:
- DbContext 即将被销毁,仍需要使用查询结果;
- 多个 IQueryable 嵌套查询,避免上下文冲突;
- 长时间处理数据,防止霸占数据库连接。
4. 异步遍历 IQueryable(推荐)
异步编程是 ASP.NET Core 高性能的关键,使用方法:
// 异步查询:不会阻塞主线程
await foreach (var article in ctx.Articles.AsAsyncEnumerable())
{
Console.WriteLine(article.Title);
}
// 等价写法:先转为集合再遍历
var articles = await ctx.Articles.ToListAsync();
foreach (var a in articles)
{
Console.WriteLine(a.Title);
}
注意:异步方法必须搭配 await 使用,方法返回值为 Task/ValueTask。
二、EF Core 分页查询
分页查询是 Web 开发必备功能,核心逻辑:统计总数据量 → 计算总页数 → 跳过指定行数 → 获取当前页数据。
1. 核心逻辑
2. 示例
/// <summary>
/// 分页查询书籍
/// </summary>
/// <param name="pageIndex">页码(从1开始)</param>
/// <param name="pageSize">每页条数</param>
async Task OutputPageAsync(int pageIndex, int pageSize)
{
// 入参校验:防止页码/页数为负数
if (pageIndex < 1) pageIndex = 1;
if (pageSize < 1) pageSize = 10;
using (var ctx = new MyDbContext())
{
// 1. 获取基础 IQueryable 查询(未执行数据库)
IQueryable<Book> booksQuery = ctx.Books;
// 2. 异步统计总条数(执行SQL:SELECT COUNT(*) FROM Books)
long totalCount = await booksQuery.LongCountAsync();
Console.WriteLine($"数据总条数:{totalCount}");
// 3. 计算总页数(Math.Ceiling:向上取整)
long totalPage = (long)Math.Ceiling(totalCount * 1.0 / pageSize);
Console.WriteLine($"总页数:{totalPage}");
// 4. 分页查询(执行SQL:SELECT * FROM Books LIMIT … OFFSET …)
var pageData = await booksQuery
.Skip((pageIndex – 1) * pageSize) // 跳过前N页数据
.Take(pageSize) // 取当前页数据
.ToListAsync(); // 异步执行查询
// 5. 遍历输出
if (pageData.Any())
{
foreach (var item in pageData)
{
Console.WriteLine($"书名:{item.Title},价格:{item.Price}");
}
}
else
{
Console.WriteLine("当前页无数据");
}
}
}
// 调用示例
await OutputPageAsync(1, 10); // 第1页,每页10条
await OutputPageAsync(3, 5); // 第3页,每页5条
3. 优化建议
- 页码从 1 开始:符合用户使用习惯,避免 pageIndex=0 异常;
- 限制最大每页条数:防止恶意请求(如 pageSize=10000)导致性能问题;
- 大数据量分页:优先使用主键分页替代 Skip(Skip 越深性能越差);
- 搭配排序:分页必须配合 OrderBy() 使用,否则数据顺序不稳定(EF Core 强制要求)。

![打卡信奥刷题(3678)用C++实现信奥题 P12274 [蓝桥杯 2024 国 Python B] 设计-171主机测评](https://www.171host.com/wp-content/uploads/2026/11/20261108053830-6af00ad6bdf8d-220x150.png)
