关于 EF Core 使用 SplitQuery 后由于不稳定排序字段对子表查询结果的影响
引子
假设在博客系统中的表设计为:
假设一条Blog数据,有10条Post数据、10条Contributor数据;
在EF Core5.0之前,当我们查询多条Blog数据的时候,生成的SQL语句是单一的 left join 语句,会将三张表的数据组合成一张大表统一返回,这样一条Blog数据就会返回100条的数据,一页假设展示100条Blog数据,数据库将返回10000条数据,产生所谓的笛卡尔爆炸
。
在EF Core5.0的时候引入Split Query解决这个问题。
全局使用拆分查询代码如下:
optionsBuilder.UseSqlServer(
"连接字符串",
// 全局设置使用拆分查询
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
单独使用代码如下:
var blogs = context.Blogs
.Include(blog => blog.Posts)
// 单独引入拆分查询
.AsSplitQuery()
.ToList();
问题
这么棒的功能那必须全局引入,一顿操作猛如虎,当我沉浸在查询速度飙升而带来的喜悦中时,新的问题正如狂风般凶猛来袭;
依旧用上面Blog的例子来描述当时的问题:
- 假设有个Blog的管理列表,采用分页查询,列表中的某一列显示了所有的Contributor的名称;
- 首开页面时确实能感受到查询速度质的提升,操作翻页:首页、第二页、末页都很快;
- 当无意中点到第N页的时候,发现Contributor为空,在翻前一页Contributor依旧为空,后一页亦如此,喜悦之情一扫而光;
- 开启调试模式,开启SQL语句监控,拿到SQL语句去数据库中执行,Contributor的数据能正确查询出来,但是Blog对象里面就是没有,替换成SingleQuery,数据跟Blog对象就都正常了,讲真这个时候人是麻的。
解决
开启百度模式,起初找到几个相关问题的链接,出现了相应的关键字(不稳定、排序),但当时不理解是什么意思;
无奈只能请教群中大佬,无意中一位大佬的截图让我幡然醒悟:
⚠️警告
将拆分查询与 Skip/Take 配合使用时,请特别注意使查询排序完全唯一;不这样做可能会导致返回不正确的数据。
例如,如果结果仅按日期排序,但可能有多个具有相同日期的结果,则每个拆分查询都可以从数据库获取不同的结果。
按日期和 ID(或任何其他唯一属性或属性组合进行排序)使排序完全唯一,并避免此问题。 请注意,关系数据库默认不应用任何排序,即使在主键上也是如此。
上述文字为官方文档中复制,在官方文档中已经做了提示,由于查询Blog的时候用到了排序,使用Order作为排序字段,但是由于Order不是唯一,存在多个完全相同的顺序,就造成了排序不稳定,从而导致子查询中得到的结果不稳定。
要解决这个问题也简单,只需要在原Order排序字段后添加根据Id的排序即可。
// before
context.Blogs.Where(predicate).OrderBy(b => b.Order);
// after
context.Blogs.Where(predicate).OrderBy(b => b.Order).ThenBy(b => b.Id);
若有所悟
万年Java 8确实不是没有道理的,稳定真的很重要,就如同上例中的bug,测试不一定能复现,假如这个bug出现在一个很重要的地方那企业的损失可就大了。
这么一个小功能尚且如此,一个大版本的升级确实需要耗费很大的人力与财力。
但是迟早也要迈出那一步,不是吗?如何取舍?对于个人激流勇进,对企业循序渐进。
相关链接
EF Core 新增功能之拆分查询
EF Core 单一查询与拆分查询的对比
https://github.com/dotnet/efcore/issues/19571
https://github.com/dotnet/efcore/issues/24964
https://github.com/dotnet/EntityFramework.Docs/issues/3242