我们在进行数据库查询时,通常并不是为了取得整个表的数据,而是某些符合过滤条件的记录。比如:

var unassociatedSudokus = await _dbContext.DbSudokus
.Where(s => !relatedSudokuIds.Contains(s.ID))
.ToListAsync();

这里 relatedSudokuIds 元素不多,过滤条件 s => !relatedSudokuIds.Contains(s.ID) 简单,运算量不大。所以,一切正常。

但我们经常会加入别的过滤条件,比如:

var unassociatedSudokus = await _dbContext.DbSudokus
.Where(s => !relatedSudokuIds.Contains(s.ID) && string.Join(string.Empty, s.Ans) == targetAns)
.ToListAsync();

过滤条件 string.Join(string.Empty, s.Ans) == targetAns 还算简单,但因为在读取数据表的时候现场装配 s.Ans,速度变慢,记录量大时,延迟明显。

推荐方案,是在数据库设计层面优化,添加一个存储拼接后结果的字段并建立索引。一般来说,可以从根本上解决读取速读慢的问题。

偏偏我的应用,DbSudokus 表非常大,而需要这种查询的场景,却不多。我舍不得多加一个 Column,让 DbSudokus 数据表无谓地臃肿。

另一个方案,数据库加载阶段简单过滤,将拼接之类的复杂过滤运算放在数据库加载之后,在内存里过滤:

  var unassociatedSudokus = await _dbContext.DbSudokus
.Where(s => !relatedSudokuIds.Contains(s.ID))
.ToListAsync(); unassociatedSudokus = [.. unassociatedSudokus.Where(s => string.Join(string.Empty, s.Ans) == targetAns)];

代价是一次性加载全部数据,内存占用过多。尤其在我的应用的情形,数据表本身很大,过滤后的结果集很小,总觉得不划算。

于是想到用 Channel。

Channel 主要是通过流处理的方式来平衡性能和内存占用。原理是:

  1. 先从数据库分批读取数据(避免一次性加载全部数据)
  2. 通过 Channel 将数据逐个或批量传递到消费者
  3. 在消费者端进行内存中的字符串拼接和比较等耗时运算
  4. 只保留符合条件的结果

这样既避免了在数据库中执行复杂操作,如字符串拼接等,可能无法有效利用索引,又避免了一次性加载所有数据导致的高内存占用。数据一边读取一边处理,通过批次大小和通道容量,可以限制同时加载到内存中的数据量,而且流处理,不需要等待全部数据加载完成。预计到结果集很小时,大部分数据需要被过滤掉,使用 Channel 优势明显,在流处理读取过程中尽早过滤掉不需要的数据,自然降低了内存占用。

我的想法,把过滤条件切分两部分:

第一,简单的部分,放在数据库加载阶段,有 Channel 的生产者处理,并且可接受消费者的通知,提前结束数据库读入:

// 生产者任务:支持提前终止
var producerTask = Task.Run(async () =>
{
try
{
var page = 0;
while (!stopProcessing) // 当消费者发现足够结果时可以提前停止
{
var batch = await _dbContext.DbSudokus
.Where(s => !relatedSudokuIds.Contains(s.ID))
.Skip(page * batchSize)
.Take(batchSize)
.ToListAsync(cancellationToken); if (batch.Count == 0)
break; foreach (var sudoku in batch)
{
// 再次检查是否需要停止,避免写入多余数据
if (stopProcessing) break; await channel.Writer.WriteAsync(sudoku, cancellationToken);
} page++;
}
}
finally
{
channel.Writer.Complete();
}
});

第二,复杂的部分,

 // 消费者任务:找到足够结果后可以提前停止
var consumerTask = Task.Run(async () =>
{
await foreach (var sudoku in channel.Reader.ReadAllAsync(cancellationToken))
{
// 检查是否已经找到足够的结果
if (maxResults.HasValue && result.Count >= maxResults.Value)
{
stopProcessing = true;
break;
} // 内存中过滤
var ansString = string.Join(string.Empty, sudoku.Ans);
if (ansString == targetAns)
{
lock (result)
{
result.Add(sudoku);
}
}
}
});

如果知道大概的结果数量,可以设置 maxResults 参数,得到额外的提前终止的好处。例如,如果通常只需要找到 1-2 条匹配结果,就可以将 maxResults 设为 2,系统会在找到 2 条结果后立即停止所有操作。

进一步,我们可以把上面的做法泛型化,核心是分离数据库端和内存端筛选逻辑,以兼顾性能和灵活性。具体做法,是把筛选逻辑包装成委托,作为参数传入。

最后,给出我的实现代码。这是一个 LINQ 风格 IQueryable 扩展方法,具有高度通用性,适用于任何实体类型和筛选场景,调用很方便。替换现有代码,几乎没有侵入性。我就是在自己应用的生产性代码中,原行替换的。

using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions; namespace Zhally.Sudoku.Data; public static class QueryFilterExtensions
{
/// <summary>
/// 流式筛选IQueryable数据,平衡性能和内存占用
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <param name="query">原始查询</param>
/// <param name="productionFilter">数据库端筛选表达式(生产阶段)</param>
/// <param name="consumptionFilter">内存端筛选委托(消费阶段)</param>
/// <param name="batchSize">批次大小</param>
/// <param name="maxResults">最大结果数量(达到后提前终止)</param>
/// <returns>筛选后的结果列表</returns>
public static async Task<List<T>> FilterWithChannelAsync<T>(
this IQueryable<T> query,
Expression<Func<T, bool>> productionFilter,
Func<T, bool> consumptionFilter,
int batchSize = 100,
int? maxResults = null)
where T : class
{
// 创建有界通道控制内存占用
var channel = Channel.CreateBounded<T>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = true
}); var result = new List<T>();
var cancellationToken = CancellationToken.None;
bool stopProcessing = false; // 消费者任务:处理并筛选数据
var consumerTask = Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken))
{
// 检查是否已达到最大结果数
if (maxResults.HasValue && result.Count >= maxResults.Value)
{
stopProcessing = true;
break;
} // 应用内存筛选条件
if (consumptionFilter(item))
{
lock (result)
{
result.Add(item);
}
}
}
}); // 生产者任务:从数据库分批读取数据
var producerTask = Task.Run(async () =>
{
try
{
var page = 0;
while (!stopProcessing)
{
// 应用数据库筛选并分页查询
var batch = await query
.Where(productionFilter)
.Skip(page * batchSize)
.Take(batchSize)
.ToListAsync(cancellationToken); if (batch.Count == 0)
break; // 没有更多数据 // 将批次数据写入通道
foreach (var item in batch)
{
if (stopProcessing) break;
await channel.Writer.WriteAsync(item, cancellationToken);
} page++;
}
}
finally
{
channel.Writer.Complete(); // 通知消费者数据已写完
}
}); // 等待所有任务完成
await Task.WhenAll(producerTask, consumerTask); return result;
}
}

这种设计特别适合以下场景:

  • 需要在数据库端做初步筛选,再在内存中做复杂筛选
  • 预期结果集较小,但源数据集可能很大
  • 希望平衡数据库负载和内存占用

还可以根据实际需求调整批次大小和通道容量,以获得最佳性能。

使用示例:

var result = await _dbContext.DbSudokus
.FilterWithChannelAsync(
// 数据库端筛选:排除关联的Sudoku
s => !relatedSudokuIds.Contains(s.ID),
// 内存端筛选:比较拼接后的答案
s => string.Join(string.Empty, s.Ans) == targetAns,
batchSize: 100,
maxResults: null
);

Maui 实践:用 Channel 实现数据库查询时读取速度与内存占用的平衡的更多相关文章

  1. MySQL查询当天数据以及大量查询时提升速度

    select * from 表名 where to_days(字段名) = to_days(now()) 一.数据库设计方面1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 ord ...

  2. PHP查询MySQL大量数据的内存占用分析

    这篇文章主要是从原理, 手册和源码分析在PHP中查询MySQL返回大量结果时, 内存占用的问题, 同时对使用MySQL C API也有涉及. 昨天, 有同事在PHP讨论群里提到, 他做的一个项目由于M ...

  3. php返回数据库查询时出现Resource id #2

    1.使用php调用MySQL数据库的过程是不是先用mysql_query(SELECT*...)或mysql_list_dbs()等查询函数返回结果指针(mysql查询函数中还有没有这样的返回指针函数 ...

  4. 在数据库查询时解决大量in 关键字的方法

    有时候在前台界面需要批量处理一些数据时,经常要用到update table set fields=value where keyid in ('1','2',....) 但如果这个数据量如果超过100 ...

  5. C#连接oracle 数据库查询时输入中文查询不出来,用plsql就可以

    查询语句为:select * from Per where khmc like '%李%',其实是字符集的问题. 解决方案:在连接字符串加一个“Unicode=True;”

  6. 【MYSQL】创建虚表来辅助数据库查询

    在进行数据库查询时,有时需要用到对既有的数据表进行多表查询得出的临时条件的数据表,就可以暂时创建成为虚表,并赋予简单明了的字段名以及临时表名. 例题a:查询出每门课程低于平均成绩的学生姓名.课程名称. ...

  7. MySQL中查询时"Lost connection to MySQL server during query"报错的解决方案

    一.问题描述: mysql数据库查询时,遇到下面的报错信息: 二.原因分析: dw_user 表数据量比较大,直接查询速度慢,容易"卡死",导致数据库自动连接超时.... 三.解决 ...

  8. mysql千万级数据库插入速度和读取速度的调整记录

    一般情况下mysql上百万数据读取和插入更新是没什么问题了,但到了上千万级就会出现很慢,下面我们来看mysql千万级数据库插入速度和读取速度的调整记录吧. 1)提高数据库插入性能中心思想:尽量将数据一 ...

  9. 160304-01、mysql数据库插入速度和读取速度的调整记录

    需求:由于项目变态,需要在一个比较短时间段急剧增加数据库记录(两三天内,由于0增加至5亿).在整个过程调优过程非常艰辛 思路: (1)提高数据库插入性能中心思想:尽量将数据一次性写入到Data Fil ...

  10. mysql千万级数据库插入速度和读取速度的调整

    mysql上百万数据读取和插入更新一般没什么问题,但上千万后速度会很慢,如何调整配置,提高效率.如下: 1.尽量将数据一次性写入DataFile和减少数据库的checkpoint操作,调整如下参数: ...

随机推荐

  1. CTF实验吧认真一点 SQL盲注

    实验吧地址 http://ctf5.shiyanbar.com/web/earnest/index.php 很明显的返回两个不同得页面,判断为SQL盲注 并且 过滤了敏感字符 测试的时候还发现过滤了s ...

  2. 操作系统综合题之“请填写信号量值并说明操作结果(正常、阻塞或唤醒。如阻塞或者唤醒,需说明阻塞或者被唤醒的是P1还是P2)(信号量操作流程-代码补充)”

    1.问题:题36表是两个同步进程的模拟执行,生产者将物品放入共享缓冲区供消费者使用,缓冲区可放2件物品,使用2个信号量,并置初值为S1 = 2,S2=0.现已知操作情况,请填写信号量值并说明操作结果( ...

  3. vue3 基础-Mixin

    本篇开始来学习一波 vue 中的一些复用性代码的基础操作, 首先来介绍关于代码 "混入" mixin 的写法. 直观理解这个 mixin 就是一个 js 对象去 "混入& ...

  4. Seata源码—2.seata-samples项目介绍

    大纲 1.seata-samples的配置文件和启动类 2.seata-samples业务服务启动时的核心工作 3.seata-samples库存服务的连接池配置 4.Seata对数据库连接池代理配置 ...

  5. c++单例模式总结

    分类 懒汉式:实例对象在第一次被使用时才进行初始化. 饿汉式:实例在定义时就被初始化. 特点 1.构造函数和析构函数私有化,不允许外部创建实例对象. 2.拷贝构造函数和复制运算符重载被delete,不 ...

  6. Advanced pandas

    Advanced pandas import numpy as np import pandas as pd Categorical Data This section introduces the ...

  7. qt动画类学习

    Qt动画类 QPropertyAnimation 显示动画 QPropertyAnimation *animation = new QPropertyAnimation(window(), " ...

  8. 第2讲、Transformer架构图详解

    1. Transformer架构运行机制 Transformer架构是一种强大的神经网络结构,主要用于自然语言处理任务.它摒弃了传统的循环神经网络(RNN)和卷积神经网络(CNN),完全基于注意力机制 ...

  9. javax.validation @Valid注解实现参数校验

      在 RESTful 的接口服务中,存在各种各样的请求参数.在跳入业务处理环节之前,通常会有一个基础的数据验证的机制,待验证通过,结果无误后,请求参数才会传递到正式的业务处理中. maven 依赖引 ...

  10. 如何在FastAPI中构建一个既安全又灵活的多层级权限系统?

    title: 如何在FastAPI中构建一个既安全又灵活的多层级权限系统? date: 2025/06/14 12:43:05 updated: 2025/06/14 12:43:05 author: ...