别再使用count distinct了

本文分享了一种优化数仓开发中大数据量去重统计的方法,通过先去重再汇总,避免了count(distinct)带来的性能瓶颈。通过实例展示了如何将SQL中的distinct操作替换为子查询和case when,提升查询效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在数仓开发中经常会对数据去重后统计,而对于大数据量来说,count(distinct )操作明显非常的消耗资源且性能很慢。

下面介绍我平时使用最多的一种优化方式,供大家参考。

原SQL

select 
  group_id,
  app_id,
  count(distinct case when dt>='${7d_before}' then user_id else null end) as 7d_uv, -- 7日内UV
  count(distinct case when dt>='${14d_before}' then user_id else null end) as 14d_uv --14日内UV
from tbl
where dt>='${14d_before}'
group by 
  group_id,
  app_id
;

优化后

先去重,再汇总。

select  group_id
        ,app_id
        ,sum(case when 7d_cnt>0 then 1 else 0 end) AS 7d_uv, -- 7日内UV
        ,sum(case when 14d_cnt>0 then 1 else 0 end) AS 14d_uv --14日内UV
from    (
        select   
            group_id,
            app_id,
            user_id, --按user_id去重
            count(case when dt>='${7d_before}' then user_id else null end) as 7d_cnt, -- 7日内各用户的点击量
            count(case when dt>='${14d_before}' then user_id else null end) as 14d_cnt --14日内各用户的点击量
        from tbl
        where dt>='${14d_before}'
        group by 
            group_id,
            app_id,
            user_id
        ) a
group by group_id,
         app_id
;

希望本文对你有帮助,请点个赞鼓励一下作者吧~ 谢谢!

<think>我们正在使用MyBatis-Plus的LambdaQueryWrapper进行计数查询,但需要去重计数(即COUNT(DISTINCT column))。在LambdaQueryWrapper中,默认的count方法执行的是普通的COUNT(*),所以我们需要自定义计数方式。 在MyBatis-Plus中,可以使用`selectCount`方法,并传入一个自定义的SELECT语句来实现COUNT(DISTINCT ...)。具体步骤如下: 1. 创建一个LambdaQueryWrapper实例。 2. 使用`select`方法指定要查询的列,并传入一个SQL片段,其中包含COUNT(DISTINCT column)。 3. 然后调用`selectCount`方法(注意:这里不能直接使用wrapper的count方法,因为count方法默认是COUNT(1),所以我们需要使用自定义的select加上selectCount)。 但是注意,`selectCount`方法会执行类似`SELECT COUNT(1) FROM ( ... )`的操作,所以如果我们直接使用`select("COUNT(DISTINCT column_name)")`,然后调用`selectCount`,会导致嵌套计数,这不是我们想要的。 因此,正确的方法是使用`select`方法设置我们要的COUNT(DISTINCT)表达式,然后使用`getBaseMapper`的`selectCount`方法,但这样可能会被MyBatis-Plus解析成子查询。所以另一种做法是使用自定义SQL,或者使用`selectOne`方法然后取结果。 实际上,MyBatis-Plus的`selectCount`方法设计用于返回一个数值,即记录数。如果我们想要一个聚合函数的结果(比如COUNT(DISTINCT)),我们可以使用`selectObj`方法,然后获取单个对象。 但是,为了保持使用LambdaQueryWrapper的链式调用和类型安全,我们可以这样: 步骤: 1. 使用`queryWrapper.select("COUNT(DISTINCT column_name) as count")`,然后调用`getBaseMapper().selectOne`或者`selectObjs`,然后取第一个元素(注意:这里需要确保没有其他列,且只有一个结果)。 2. 或者,我们可以使用`select`方法设置聚合函数,然后使用`selectOne`方法,并指定返回类型(如果使用selectObjs,则返回一个Object列表,每个元素是查询结果的一行,而这里我们只有一行一列)。 具体示例: 假设我们有一个User实体,有一个字段为`deptId`,我们想要统计不同的`deptId`的数量。 方法一:使用`selectObjs` ```java LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.select("COUNT(DISTINCT dept_id)"); List<Object> list = userMapper.selectObjs(wrapper); Long count = (Long) list.get(0); ``` 方法二:使用自定义的查询方法(推荐使用更类型安全的方式,但这里LambdaQueryWrapper对于自定义聚合函数支持有限) 注意:在LambdaQueryWrapper中,我们也可以使用SFunction来指定列,但是MyBatis-Plus目前(3.4.3.4版本)并没有直接提供COUNT(DISTINCT)的Lambda方式。所以我们需要用字符串来写列名,或者使用`SqlUtils`来转换。 如果我们想用Lambda方式指定列,可以这样: ```java String column = LambdaUtils.getColumn(User::getDeptId); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.select("COUNT(DISTINCT " + column + ")"); List<Object> list = userMapper.selectObjs(wrapper); Long count = (Long) list.get(0); ``` 但是,`LambdaUtils`是MyBatis-Plus内部工具类,不建议直接使用(在3.4.3.4中,我们可以使用`com.baomidou.mybatisplus.core.conditions.segments.SelectSegmentList`相关?不,实际上我们可以用`Columns`)。 另外,MyBatis-Plus 3.4.0以上版本提供了`select`方法可以传入`SqlSelect`,但LambdaQueryWrapper的select方法不支持直接传入函数式聚合表达式。 因此,目前比较简洁的方式是使用方法一,使用字符串指定列名。 如果我们希望避免字符串(为了类型安全),我们可以自己写一个工具方法来将SFunction转换为列名,例如: ```java public class LambdaUtil { public static <T> String getColumn(SFunction<T, ?> fn) { // 实现从SFunction获取列名,这里可以使用SerializedLambda解析 // 或者使用MyBatis-Plus的TableInfoHelper和LambdaUtils(内部类) // 由于MyBatis-Plus内部有相关工具,但未暴露,我们可以自己实现一个简单的 // 这里不展开,因为需要写解析SerializedLambda的代码 // 或者使用第三方工具如hutool的BeanUtil // 但为了简化,我们直接使用字符串,或者等待MyBatis-Plus提供官方支持 } } ``` 考虑到实现的复杂性,如果项目中有多处使用,建议封装。否则,直接使用字符串列名。 另外,注意:如果我们的表有别名,那么我们需要在列名前加上别名,例如`t.dept_id`。 总结:在LambdaQueryWrapper中实现COUNT(DISTINCT column)的步骤: 1. 构建LambdaQueryWrapper。 2. 使用`wrapper.select("COUNT(DISTINCT {0})", "列名字符串")`,这里{0}是占位符,可以用`String.format`或者直接拼接,但MyBatis-Plus的select方法支持格式化字符串。 3. 调用`userMapper.selectObjs(wrapper)`,获取List<Object>。 4. 取出第一个元素,并转换为Long(注意:不同数据库返回类型可能不同,可能是BigDecimal,需要转换)。 示例代码: ```java LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.select("COUNT(DISTINCT dept_id)"); List<Object> objs = userMapper.selectObjs(wrapper); Long distinctCount = (Long) objs.get(0); ``` 如果返回的是多个值(比如有group by),那么selectObjs会返回多个Object,每个Object数组对应一行。但这里我们只有一个值。 注意:如果查询结果为空,那么selectObjs返回的list可能为空,所以需要判断。 另外,我们也可以使用`selectOne`,但是selectOne要求返回一个实体对象,而我们这里是一个聚合值,所以不适用。 因此,使用`selectObjs`是合适的。 如果要去重多个字段,比如COUNT(DISTINCT col1, col2),在MySQL中我们可以写`COUNT(DISTINCT col1, col2)`,但在其他数据库可能不同。注意数据库兼容性。 最后,我们也可以使用注解或XML写自定义SQL,这样更灵活,但这里问题要求使用LambdaQueryWrapper。 所以,以上就是在LambdaQueryWrapper中使用COUNT(DISTINCT)的方法。 相关问题: 1. 在MyBatis-Plus中,如何使用LambdaQueryWrapper进行分组计数(GROUP BY)? 2. 在MyBatis-Plus中,如何执行复杂的聚合查询(如SUM, AVG)? 3. 在MyBatis-Plus中,如何使用Lambda表达式避免在查询中使用字符串列名?</think>在 MyBatis-Plus 中使用 `LambdaQueryWrapper` 实现 `COUNT(DISTINCT column)` 查询,可通过以下步骤实现: ### 解决方案 ```java // 1. 创建 LambdaQueryWrapper LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); // 2. 使用 select() 指定 DISTINCT 计数表达式 wrapper.select("COUNT(DISTINCT dept_id) AS distinct_count"); // 替换 dept_id 为你的字段 // 3. 执行查询并获取结果 List<Object> results = userMapper.selectObjs(wrapper); // 注意:返回 List<Object> // 4. 解析结果(结果类型取决于数据库驱动,通常为 Long 或 BigDecimal) Long distinctCount = (Long) results.get(0); // 实际类型需根据数据库调整 ``` ### 关键点说明 1. **`select()` 方法** 通过 SQL 片段指定 `COUNT(DISTINCT column)` 表达式,使用 `AS` 定义别名(可选但推荐)。 2. **`selectObjs()` 方法** - 返回 `List<Object>`,其中每个 `Object` 对应查询的一列 - 聚合查询结果在 `results.get(0)` 中 - 结果类型需根据数据库驱动转换(如 MySQL 返回 `Long`) 3. **Lambda 安全写法(MyBatis-Plus 3.4.3+)** 使用函数式引用避免硬编码字段名: ```java wrapper.select("COUNT(DISTINCT {0})", User::getDeptId); // 自动替换 {0} 为字段名 ``` ### 完整示例 ```java // 统计不同部门ID的数量 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.select("COUNT(DISTINCT dept_id)"); List<Object> results = userMapper.selectObjs(wrapper); // 处理结果(MySQL 示例) if (!results.isEmpty()) { Long distinctDeptCount = (Long) results.get(0); System.out.println("Distinct dept count: " + distinctDeptCount); } ``` ### 注意事项 1. **结果类型转换** 不同数据库返回值类型可能不同(如 PostgreSQL 返回 `BigInteger`),需根据实际情况转换。 2. **空结果处理** 添加空结果检查:`if (!results.isEmpty()) {...}` 3. **性能影响** `COUNT(DISTINCT)` 在大数据量下可能较慢,建议在目标列上添加索引优化[^1]。 --- ### 相关问题 1. **如何在 MyBatis-Plus 中实现多字段 DISTINCT 计数?** (例如 `COUNT(DISTINCT col1, col2)`) 2. **`LambdaQueryWrapper` 如何进行分组计数(GROUP BY)?** (结合 `groupBy()` 和聚合函数的使用) 3. **MyBatis-Plus 执行原生 SQL 计数查询的最佳实践是什么?** (对比 `@Select` 注解与 `Wrapper` 的适用场景) 4. **如何优化大数据量下的 DISTINCT COUNT 查询性能?** (索引策略与近似计数方案) [^1]: 数据库索引可显著提升 `COUNT(DISTINCT)` 性能,但需平衡写入开销。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王义凯_Rick

遇见即是缘,路过就给个评论吧~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值