FreeSql icon indicating copy to clipboard operation
FreeSql copied to clipboard

条件聚合函数Count,翻译错误

Open hjkl950217 opened this issue 1 year ago • 7 comments

问题描述及重现代码:

使用count()带条件时,返回的sql中 少了 or null,导致统计出来的值全部一样。

            List<SubmiterData> submiterDataList = this.edoc2FreeSql.Select<ProductSuggestion>()
                .Where(t => t.SubmitTime >= queryDto.StartTime)
                .Where(t => t.SubmitTime <= queryDto.EndTime)
                .GroupBy(t => new { t.CreateId })
                .ToList(t => new SubmiterData
                {
                    SubmiterName = t.Min(t.Value.Submiter),
                    Count_Submit = t.Count(t.Value.Status),
                    Count_Planed = t.Count(t.Value.Status == "已规划"),
                    Count_Adopt = t.Count(t.Value.Status == "已采纳"),
                    Count_Reject = t.Count(t.Value.Status == "已拒绝"),
                    Count_WaitProcess = t.Count(t.Value.Status == "待处理")
                })
                .OrderByDescending(t => t.Count_Submit)
                .ToList();

翻译出来的SQL:

SELECT 
min(a.`Submiter`) as1, 
count(a.`Status`) as2, 
count(a.`Status` = '已规划') as3, 
count(a.`Status` = '已采纳') as4, 
count(a.`Status` = '已拒绝') as5, 
count(a.`Status` = '待处理') as6
FROM `eform_PSM_ProductSuggestion` a
WHERE (a.`SubmitTime` >= '2024-04-01 00:00:00.000') AND (a.`SubmitTime` <= '2024-04-30 00:00:00.000')
GROUP BY a.`createId`

期望 or 正确的SQL是:

SELECT 
min(a.`Submiter`) as1, 
count(a.`Status`) as2, 
count(a.`Status` = '已规划' or null) as3, 
count(a.`Status` = '已采纳'or null) as4, 
count(a.`Status` = '已拒绝'or null) as5, 
count(a.`Status` = '待处理'or null) as6
FROM `eform_PSM_ProductSuggestion` a
WHERE (a.`SubmitTime` >= '2024-04-01 00:00:00.000') AND (a.`SubmitTime` <= '2024-04-30 00:00:00.000')
GROUP BY a.`createId`

缺少`or null``导致统计正确,也可以使用其它写法,我查到的2种其它写法:

select count(if(age <  19, 1, null)) from sys_user // 返回 1条数据
select count(case when age <  19 then 1 end) from sys_user // 返回 1条数据

数据库版本

mysql 8.0

安装的Nuget包

freeSql.Provider.Mysql 3.2.825

.net framework/. net core? 及具体版本

.net core 3.1

hjkl950217 avatar Jun 28 '24 06:06 hjkl950217

翻了下源代码,没找到在那个文件改。。sum count的都没有找到对应位置QAQ。(FreeSql.Provider.MySql这个库里) 我暂时用t.Sum(t.Value.Status == "已规划")取代t.Count(t.Value.Status == "已规划")的位置可行,我的任务先完成了着

hjkl950217 avatar Jun 28 '24 06:06 hjkl950217

t.Sum(t.Value.Status == "已规划" ? 1 : 0),

正常应该这样写

2881099 avatar Jun 28 '24 06:06 2881099

t.Sum(t.Value.Status == "已规划" ? 1 : 0),

正常应该这样写

主要是要用Count,改用你说的这种写法就OK了。。 那这个issue可以改成建议吗?毕竟count本来就是计数的意思,用t.Count(t.Value.Status == "已规划")是最合适的

写成t.Count(t.Value.Status == "已规划" ? 1 : 0)去查询也会错误。 而用Sum表达含义时会错误 另一个就是, Sum返回的类型是decimal,这点设计上OK的,实际count的情况用sum,会导致类型转换,再写一次代码。 用Sum去干Count的会更麻烦一点(我这里就得先tolist后 再select 再tolist)

hjkl950217 avatar Jun 28 '24 07:06 hjkl950217

@hjkl950217 我在最初使用FreeSql时也提过类似的issue哈哈哈,API设计跟EF Core不太一样,适应就好了。

至于类型的问题,常用的整型可以直接在表达式内转换:

                t => new SubmiterData
                {
                    Count_Planed = (int)t.Sum(t.Value.Status == "已规划" ? 1 : 0),
                })

double等类型若不支持(运行时会类型转换异常)的话,FreeSQL支持使用Convert函数:

                t => new SubmiterData
                {
                    Count_Planed = Convert.ToDouble(t.Sum(t.Value.Status == "已规划" ? 1 : 0)),
                })

LeaFrock avatar Jun 28 '24 07:06 LeaFrock

@LeaFrock 问题现在解决了,我就再套一层就行。 API设计跟ef core不一样也OK的,我也用freesql好久了(一直简单用而已) 只是说从表达含义上面, 用Sum去干Count的事很奇怪的。 t.Count(t.Value.Status == "已采纳" ? 1 : 0)也会翻译成count(case when a.Status = '待处理' then 1 else 0 end) 还是会查询错误。 那不如直接修改 t.Count(t.Value.Status == "已采纳") 这种bool。 又没歧义,又好用

hjkl950217 avatar Jun 28 '24 08:06 hjkl950217

只是说从表达含义上面, 用Sum去干Count的事很奇怪的。

这个要看SQL习惯,按条件数数,本质上是按条件求和问题的”子集',所以使用Sum其实也没啥问题,只是大部分人用ORM都是从EF Core及LINQ to Sql开始的,习惯了Count语法优先。

t.Sum(t.Value.Status == "已规划" ? 1 : 0) 翻译成 sum(case when a.Status = '已规划' then 1 else 0 end),C#代码上看有点不够自然,但从SQL上看不违和的。

LeaFrock avatar Jun 28 '24 08:06 LeaFrock

只是说从表达含义上面, 用Sum去干Count的事很奇怪的。

这个要看SQL习惯,按条件数数,本质上是按条件求和问题的”子集',所以使用Sum其实也没啥问题,只是大部分人用ORM都是从EF Core及LINQ to Sql开始的,习惯了Count语法优先。

t.Sum(t.Value.Status == "已规划" ? 1 : 0) 翻译成 sum(case when a.Status = '已规划' then 1 else 0 end),C#代码上看有点不够自然,但从SQL上看不违和的。

恩,这样看也能解释得通。 那剩余的就是类型转换问题了。 用Count计数的习惯源自于linq to object, 它一直是Count计数,Sum求和,大部分人应该是先学linq to object,再学linq to sql的。 计数的类型肯定是int, 和Sum(返回值decimal)求和的情况不同

等等看叶老板咋说哇。。 用Sum有可能的歧义+类型转换 @2881099

hjkl950217 avatar Jun 28 '24 08:06 hjkl950217

这实际上是 mysql 使用上 和 sql 理解上的问题... 其他 db 好像都还不能 在 聚合 和 正常 Count 中 用条件表达式

讨论的是orm的API设计,不是mysql。

hjkl950217 avatar Jul 02 '24 02:07 hjkl950217

不过 查下来 在 MSSQL 中 下面两个等价,但是 直接使用条件判断 貌似不行

COUNT(CASE WHEN xxx = 1 THEN 1 ELSE NULL END)
SUM(CASE WHEN xxx = 1 THEN 1 ELSE 0 END)

恩 ,这里得 else null才行。。但直接翻译Count(t=>t.xxx?1:0) 会翻译成 then 1 else 0,按mysql的用法是无效的。

所以我就想建议直接支持成t.Count(t.Value.Status == "已采纳")翻译COUNT(CASE WHEN xxx = 1 THEN 1 ELSE NULL END), 从ORM这边统一用法,也符合C#中linq的使用习惯

hjkl950217 avatar Jul 02 '24 02:07 hjkl950217

            List<SubmiterData> submiterDataList = this.edoc2FreeSql.Select<ProductSuggestion>()
                .Where(t => t.SubmitTime >= queryDto.StartTime)
                .Where(t => t.SubmitTime <= queryDto.EndTime)
                .GroupBy(t => new { t.CreateId })
                .ToList(t => new
                {
                    SubmiterName = t.Min(t.Value.Submiter),
                    Count_Submit = t.Count(t.Value.Status),
                    Count_Planed = t.Sum(t.Value.Status == "已规划" ? 1 : 0) //这里
                })
                .Select(t => new SubmiterData()
                {
                    SubmiterName = t.SubmiterName,
                    Count_Submit = t.Count_Submit,
                    Count_Planed = (int)t.Count_Planed  //这里得强转
                    Count_WaitProcess = (int)t.Count_WaitProcess
                })
                .OrderByDescending(t => t.Count_Submit)
                .ToList();

@2881099 老板儿最终决定下哇, 我们早点把issue关闭。 现在我只能这样用,其它用法均有问题,现在这种用法就有点别扭。 我还是维持我原来的建议t.Count(t.Value.Status == "已采纳")翻译COUNT(CASE WHEN xxx = 1 THEN 1 ELSE NULL END), 这样符合C#的使用习惯,也可能符合数据库中的使用习惯吧。

原来的翻译不用调,应该只需要调整t.Count(t.Value.Status == "已采纳")这种情况。

当然,最终决定权肯定在你那里,我只能建议下

hjkl950217 avatar Jul 02 '24 02:07 hjkl950217

好的,我以前都是用 sum case when编写sql

只是mysql可以考虑,等我有空测试一下。

2881099 avatar Jul 02 '24 02:07 2881099

@2881099 其它的也可以考虑下,不然换数据库可能有问题QAQ。。 我们最近就在适配国产的各个数据库, 准备换成freesql, 可能连接的是 mssql 达梦 mysql等等。。 我觉得内部可以把count()的情况转换成sum的, 或是内部套一层类型强转,这样API使用上统一, 数据库执行层面功能也OK的

hjkl950217 avatar Jul 02 '24 02:07 hjkl950217

.GroupBy(a => a.GroupId)
.ToSql(g => new
{
    cou1 = g.Count(),
    cou2 = g.Count(g.Value.Nickname),
    cou3 = g.Count(g.Value.Nickname == "xx"),
    cou4 = g.Count(g.Value.Sort > 50),
    cou5 = g.Count(g.Value.Sort > 50 || g.Value.Username == "xx"),
});

如上查询已优化 .Count 解析成 sum(case when ... then 1 else 0 end)

2881099 avatar Jul 02 '24 15:07 2881099

卧槽,这么晚,叶老板主意身体阿

hjkl950217 avatar Jul 03 '24 01:07 hjkl950217

这次改动的 commit

hjkl950217 avatar Jul 04 '24 03:07 hjkl950217