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

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. 关于php里怎么把字符串‘false’转成boolean的false

    都知道php里类型转换常用的是settype($str,'boolean')和(bool)$str 但是,他们将字符串'false'和'true'转成boolean后都是true,可能这不是我们需要的 ...

  2. 正点原子ALPHA开发板使用4.3寸触摸屏LCD驱动实验显示不正常

    显示问题 裸机开发时,驱动教程的PDF里给了4.3寸LCD屏幕的设置参数.如下图所示: 但是按照这个设置,编写设备树dts文件,下载到开发板里,却出现了显示异常,具体来说就是帧率不对,图和字都是歪斜的 ...

  3. C# unsafe 快速复制数组

    (1) /// <summary> /// 复制内存 /// </summary> /// <param name="dest">目标指针位置& ...

  4. arcgis创建sqlserver企业级空间数据库过程中出现的问题及解决方案

    在arcgis中创建sqlserver版本的企业空间数据库过程中,出现了多种问题,现把问题的现象.原因和解决方案记录下来,以防遗忘(年纪大了). 1 用sa账号创建空间数据库提示创建失败15456 安 ...

  5. Everyone's Favorite Linear, Direct Access, Homogeneous Data Structure: The Array(英翻中)

    Arrays are one of the simplest and most widely used data structures in computer programs. Arrays in ...

  6. WPF 的Image 控件 设置 Image.Source 的数据源,可能存在跨线程调用的问题。

    相信很多WPF 的开发,应该都很多用到 Image 这个控件来显示图片.这个图片的来源可以来自各种各样的方式获取到. 我们的组内白板.批注的扫码的功能也用到这个去生成二维码,生成后,二维码显示不出来, ...

  7. 基于 Streamlit 和 OpenAI 实现的小红书爆款文案生成器

    项目介绍 在当今自媒体时代,高质量的文案是吸引流量的关键.特别是在小红书这样的平台上,一个吸引人的标题和富有情感的正文可以显著提高内容的曝光率. 本文将介绍一个基于OpenAI API和Streaml ...

  8. 详解鸿蒙Next仓颉开发语言中的动画

    大家上午好,今天来聊一聊仓颉开发语言中的动画开发. 仓颉中的动画通常有两种方式,分别是属性动画和显示动画,我们今天以下面的加载动画为例,使用显示动画和属性动画分别实现一下,看看他们有什么区别. 显示动 ...

  9. Error creating bean with name 'xxx' defined in file异常处理

    SpringBoot整合mybatis 今天在使用mybatis generator时遇到一个坑,出现以下错误 Error creating bean with name 'authorizeCont ...

  10. HyperWorks二维网格划分与单元连续性

    自动网格划分 HyperWorks中为零件定义几何曲面是创建零件壳单元网格的最佳方式.HyperMesh 创建二维网格最有效的方法是使用 Automesh 面板直接在零件的表面创建网格. Autome ...