MySQL子查询、WITH AS、LAG查询统计数据实战

需求

给出一个比较常见的统计类业务需求:统计App(包括iOS和Android两大类)每日新注册用户数、以及累计注册用户数。

数据库采用MySQL,根据上面的需求,不难设计表如下:

create table os_day_count(
    stat_date     varchar(10) not null comment '统计日期',
    os            varchar(7) not null comment '操作系统类型',
    stat_count    int         not null comment '用户数',
    os_stat_count int         null comment 'os类型累计用户数',
    primary key (stat_date, os)
) comment '每日App新装机统计表';

由于面对的是一个日活量非常小的App,经常出现每日新增用户数为0的情况。

insert数据落库逻辑如下:

public void appOsStatisticFromUser(String time) {
	// 远程Feign接口获取新用户数
    Response<List<OsDayCountVO>> resp = remoteUserService.appOsStats(time);
    boolean check = resp != null && resp.getCode() == 0 && CollectionUtils.isNotEmpty(resp.getData());
    // 有新用户数才insert
    if (check) {
        for (OsDayCountVO item : resp.getData()) {
            OsDayCount po = BeanConvertUtils.convert(item, OsDayCount.class);
            osDayCountMapper.insert(po);
            // 前一天 osStatCount = 前一天 statCount + 前两天 osStatCount
            String twoDayAgo = DateUtils.addDay(DateUtils.parse(item.getStatDate(), DateUtils.DATE_SMALL_STR), DateUtils.DATE_SMALL_STR, -1);
            Integer count = osDayCountMapper.osMax(twoDayAgo, item.getOs());
            po.setOsStatCount(count + item.getStatCount());
            // 此处update逻辑一定要注意where条件限制否则报错:SQLIntegrityConstraintViolationException Duplicate entry
            osDayCountMapper.update(po, new LambdaUpdateWrapper<OsDayCount>().eq(OsDayCount::getStatDate, item.getStatDate()).eq(OsDayCount::getOs, item.getOs()));
        }
    }
}

问题

上面的业务逻辑没有问题,运行之后,数据库如下:
在这里插入图片描述
表里的数据不是连续的!!没有某个stat_date日期的数据则表示该天没有新增用户,os_stat_count表示的是累计用户数。

现在想要查询【连续】日期的用户数,即实现

// 没有2023-12-18数据,则取2023-12-17;没有2023-12-17数据,则取2023-12-16;以此类推
select stat_date, os_stat_count from os_day_count where stat_date in ('2023-12-16','2023-12-17','2023-12-18');

最后返回的数据应该有3行,分别是2023-12-16、2023-12-17、2023-12-18,而且因为2023-12-17和2023-12-18没有新增用户。故而查询出来的三行数据结果是一模一样的。

实现方案

全量冗余存储

想要查询某个连续时间段,如最近一个月的累计用户数。很简单,修改insert逻辑即可,每天都落数据,哪怕和前一天数据一模一样。这样查询时直接使用上面的SQL即可实现功能。

但是这样会在数据库里全量存储很多冗余数据。不建议。

应用层实现

保持insert逻辑不变,那就需要在select处花点心思,也很简单。

数据库PO实体类定义如下:

@Data
@TableName(value = "os_day_count")
public class OsDayCount {
    @TableId(value = "stat_date", type = IdType.NONE)
    private String statDate;
    private String os;
    private Integer statCount;
    private Integer osStatCount;

    public OsDayCount(String statDate, String os, Integer statCount) {
        this.statDate = statDate;
        this.os = os;
        this.statCount = statCount;
    }
}

枚举类定义:

@Getter
@AllArgsConstructor
public enum OsEnum {
    IOS("iOS", "iOS"),
    ANDROID("Android", "Android"),
    ALL("ALL", "ALL");

    private final String desc;
    private final String name;

    public static String getNameByDesc(String desc) {
        for (OsEnum osEnum : OsEnum.values()) {
            if (osEnum.desc.equals(desc)) {
                return osEnum.name;
            }
        }
        return null;
    }
}

Mapper接口类定义查询方法:

Integer osMax(@Param("time") String time, @Param("os") String os);

对应的MyBatis mapper.xml文件:

<select id="osMax" resultType="java.lang.Integer">
    SELECT ifnull(max(os_stat_count), 0)
    FROM os_day_count
    WHERE stat_date &lt;= #{time}
      AND os = #{os};
</select>

Service层通过简简单单一个for循环来执行 2 ∗ N 2*N 2N次SQL查询实现,其中2表示枚举类定义的类型个数,N表示查询日期跨度。

List<OsDayCount> osList = Lists.newArrayListWithExpectedSize(dto.getTimeList().size() * 2);
for (String item : dto.getTimeList()) {
    osList.add(new OsDayCount(item, OsEnum.ANDROID.getDesc(), osDayCountMapper.osMax(item, OsEnum.ANDROID.getDesc())));
    osList.add(new OsDayCount(item, OsEnum.IOS.getDesc(), osDayCountMapper.osMax(item, OsEnum.IOS.getDesc())));
}

不管是查询日期跨度增加,还是换一种场景,枚举类型个数增长。上面这种方式都是极不可取的。

SQL

上面这种for循环肯定不可取,因此有必要替换成一个SQL来实现查询取数逻辑。提到MySQL实现,一般都会有MySQL 8和非MySQL 8两种情况。

非MySQL 8

相当多的公司,哪怕他们的业务并不是金融或保险或交易相关等,也不会(不敢)考虑选择(或升级迁移)使用MySQL 8。哪怕MySQL 8于2018年4月份发布,距今已经五年多。原因无外乎慎重起见、因循守旧等。

事实上,这几年工作中,鄙人也仅在一家公司的一个产品中,在生产中用过MySQL 8。

不难分析出来,stat_date是一个非常关键的字段,由于数据库里并没有存储2023-12-17,2023-12-18两天的数据。

因此非常有必要做一个子查询:

SELECT '2023-12-16' AS stat_date
UNION ALL SELECT '2023-12-17'
UNION ALL SELECT '2023-12-18' AS dates

此子查询返回期望的多行日期数据。然后关联另一个子查询:

SELECT os_stat_count FROM os_day_count WHERE stat_date <= dates.stat_date ORDER BY stat_date DESC LIMIT 1;

事实上,这个子查询和上面的应用层实现方案里的查询逻辑一样:

SELECT ifnull(max(os_stat_count), 0) FROM os_day_count WHERE stat_date &lt;= #{time};

注意到一定要使用LIMIT 1来限制只返回一条数据,否则报错:Subquery returns more than 1 rowmaxmin函数只会返回一条数据,所以不用冗余追加limit 1限制。

组合之后,写出如下SQL:

SELECT
    dates.stat_date,
    (SELECT os_stat_count FROM os_day_count WHERE stat_date <= dates.stat_date ORDER BY stat_date DESC LIMIT 1) AS os_stat_count
FROM
    (SELECT '2023-12-16' AS stat_date
    UNION ALL SELECT '2023-12-17'
    UNION ALL SELECT '2023-12-18') AS dates
ORDER BY
    dates.stat_date;

达到效果。

那如何进一步区分os枚举类型信息呢?当然也是join。不过不是使用left joinleft join需要使用on条件关联一下。这里使用cross join

最终的SQL如下:

SELECT
    dates.stat_date,
    oss.os,
    (SELECT os_stat_count FROM os_day_count WHERE stat_date <= dates.stat_date and os = oss.os ORDER BY stat_date DESC limit 1) AS os_stat_count
FROM
    (
    SELECT '2023-12-16' AS stat_date
    UNION ALL SELECT '2023-12-17'
    UNION ALL SELECT '2023-12-18'
) AS dates
cross join (select distinct os from os_day_count) AS oss
ORDER BY
    dates.stat_date;

SQL没有问题,实现期望效果。那如何把SQL转写为MyBatis Mapper.xml文件支持的语法呢?

最关键的部分,还是子查询得到的dates数据。总不可能一一列出来吧,如果要查询最近半年的数据呢?

MyBatis提供的标签符合此场景的貌似只有foreach。经过尝试,MyBatis果然支持以Index方式取集合元素,即:#{timeList[0]}#{timeList[0]}foreachcollection有重复第一个元素,一开始想要改造collection标签元素,没搞定。

咱不就是想去重嘛。去重的话,使用UNION替换UNION ALL

其他就是foreach的几个元素的处理:opencloseseparator,都置为空即可。

Anyway,日期子查询转写成MyBatis语法最终如下:

SELECT #{timeList[0]} AS stat_date
<foreach close="" collection="timeList" item="item" open="" separator="">
    UNION SELECT
    #{item}
</foreach>

最终版MyBatis mapper.xml文件如下:

<select id="osSum" resultType="com.aaaaa.collect.data.dao.entity.OsDayCount">
    SELECT
    dates.stat_date AS statDate,
    oss.os,
    (SELECT os_stat_count FROM os_day_count WHERE stat_date &lt;= dates.stat_date AND os = oss.os ORDER BY stat_date
    DESC limit 1) AS statCount
    FROM
    (SELECT #{timeList[0]} AS stat_date
    <foreach close="" collection="timeList" item="item" open="" separator="">
        UNION SELECT
        #{item}
    </foreach>
    ) AS dates
    CROSS JOIN (SELECT DISTINCT os FROM os_day_count) AS oss
    ORDER BY dates.stat_date;
</select>

MySQL 8

借助于MySQL 8提供的WITH AS及LAG函数,可写出如下SQL:

WITH dates AS (
    SELECT '2023-12-16' AS stat_date
    UNION ALL SELECT '2023-12-17'
    UNION ALL SELECT '2023-12-18'
),
cte AS (
    SELECT
        dates.stat_date,
        IFNULL(os_day_count.os_stat_count, LAG(os_day_count.os_stat_count) OVER (ORDER BY dates.stat_date)) AS os_stat_count
    FROM
        dates
    LEFT JOIN
        os_day_count ON dates.stat_date = os_day_count.stat_date
)
SELECT
    stat_date,
    IFNULL(os_stat_count, (SELECT os_stat_count FROM cte WHERE os_stat_count IS NOT NULL ORDER BY stat_date DESC LIMIT 1)) AS os_stat_count
FROM
    cte
ORDER BY
    stat_date;

如果想要进一步增加OS信息,写出如下SQL:


TODO:cross join os后有重复的数据

最后

在写SQL的过程中,还是相当耗费一些心力的,各种Stackoverflow浏览帖子,各种Google搜索,没有找到解决方案。也体验过CSDN推出的C知道,呵呵。OpenAI的Chat GPT也体验过,虽然比C知道强,但是也没有拿到满意的答案。

最后在CSDN问答里发布帖子MySQL查询不存在的日期数据。不过1~2分钟,就拿到满意的答案。不得不说,GitHub与OpenAI强强联合推出的GitHub Copilot真™强大啊!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/265622.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

原型链污染[JavaScript]

一、原型链污染 此类型一般存在以nodejs编写的后端程序当中&#xff0c;其中Express是一个流行的Node.js Web应用程序框架 1.JavaScript 1.1 原理 引入 解释&#xff1a;直接先读代码来理解原型链污染 // let jack {b:2} console.log(typeof jack) // 它的类型是obejct con…

一文掌握分布式锁:Mysql/Redis/Zookeeper实现

目录 一、项目准备spring项目数据库 二、传统锁演示超卖现象使用JVM锁解决超卖解决方案JVM失效场景 使用一个SQL解决超卖使用mysql悲观锁解决超卖使用mysql乐观锁解决超卖四种锁比较Redis乐观锁集成Redis超卖现象redis乐观锁解决超卖 三、分布式锁概述四、Redis分布式锁实现方案…

React 中的 ref 和 refs:解锁更多可能性(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

安卓13上手势导航失效、手机卡死问题

问题描述&#xff1a;打开我们开发的app后&#xff0c;手势导航无法退回、无法回到桌面、无法切换应用。 使用设备&#xff1a;小米手机、MI14,、安卓13 未适配安卓13安卓x的情况下&#xff0c;检查自己的 AndroidManifest 文件&#xff0c;过滤器是否设置了 <category a…

信息安全等级保护的定义与意义

目录 前言 信息安全等级保护定义 广义上 狭义上 技术和管理 信息安全的基本要素 信息安全等级保护的意义 当前形式 形式严峻 国家安全 三个基本一个根本 预期目标 最终效果 实际意义 前言 信息安全等级保护是对信息和信息载体按照重要性等级分级进行保护的一种…

HTML5+CSS3+Vue小实例:彩带圣诞树

实例:彩带圣诞树 技术栈:HTML+CSS+Vue 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><…

【水文专业词汇】气象水文、水利工程等

水文专业词汇&#xff1a;气象水文、水利工程等 气象水文类水循环过程地区分类 水利工程类跨流域调水工程 参考 气象水文类 水循环过程 中文英文降水/降雨precipitation/rainfall径流runoff/streamflow产汇流runoff generation 地区分类 中文英文雨养作物区rain-fed agricu…

git入门指南:新手快速上手git(Linux环境如何使用git)

目录 前言 1. 什么是git&#xff1f; 2. git版本控制器 3. git在Linux中的使用 安装git 4. git三板斧 第一招&#xff1a;add 第二招&#xff1a;commit 第三招&#xff1a;push 5. 执行状态 6. 删除 总结 前言 Linux的基本开发工具介绍完毕&#xff0c;接下来介绍一…

游戏开发公司需要具备哪些能力?

中懿游游戏软件开发,游戏开发行业的竞争日益激烈&#xff0c;成功的游戏开发公司需要具备多方面的能力&#xff0c;从技术创新到市场推广&#xff0c;再到团队协作。以下是构建成功游戏开发公司所需的关键能力概览&#xff1a; 1. 游戏开发技术&#xff1a; 在技术方面&#…

【flink】状态清理策略(TTL)

flink的keyed state是有有效期(TTL)的&#xff0c;使用和说明在官网描述的篇幅也比较多&#xff0c;对于三种清理策略没有进行横向对比得很清晰。 全量快照清理(FULL_STATE_SCAN_SNAPSHOT)增量清理(INCREMENTAL_CLEANUP)rocksdb压缩清理(ROCKSDB_COMPACTION_FILTER) 注意&…

MySQL8.0聚合函数+over()函数

1、数据表内容为&#xff1a; CREATE TABLE chapter11 (shopname VARCHAR(255) NULL,sales VARCHAR(255) NULL,sale_date VARCHAR(255) NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;INSERT INTO chapter11 (shopname, sales, sale_date) VALUES(A…

Linux 系统调用

系统调用 在现代操作系统中&#xff0c;内核提供了用户进程与内核进行交互的一组接口。 这些接口让应用程序受限地访问硬件设备&#xff0c;提供了创建新进程并与已有进程进行通信的机制&#xff0c;也提供了申请操作系统其他资源的能力。 应用程序发出各种请求&#xff0c;而…

WEB渗透—PHP反序列化(八)

Web渗透—PHP反序列化 课程学习分享&#xff08;课程非本人制作&#xff0c;仅提供学习分享&#xff09; 靶场下载地址&#xff1a;GitHub - mcc0624/php_ser_Class: php反序列化靶场课程&#xff0c;基于课程制作的靶场 课程地址&#xff1a;PHP反序列化漏洞学习_哔哩…

持续集成交付CICD:Linux 部署 Jira 9.12.1

目录 一、实验 1.环境 2.K8S master节点部署Jira 3.Jira 初始化设置 4.Jira 使用 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 主机架构版本IP备注master1K8S master节点1.20.6192.168.204.180 jenkins slave &#xff08;从节点&#xff09; jira9.12.1…

【强化学习】循序渐进讲解Deep Q-Networks(DQN)

文章目录 1 Q-learning与Deep Q-learning2 DQN的结构组成3 DQN创新技术&#xff08;重点&#xff09;3.1 Experience Replay&#xff08;经验回放&#xff09;3.2 Fixed Q-Target&#xff08;固定Q目标&#xff09;3.3 Double Deep Q-Learning&#xff08;双重深度Q学习方法&am…

momentum2靶机

文章妙语 遇事不决&#xff0c;可问春风&#xff1b; 春风不语&#xff0c;遵循己心。 文章目录 文章妙语前言一、信息收集1.IP地址扫描2.端口扫描3.目录扫描 二&#xff0c;漏洞发现分析代码bp爆破1.生成字典2.生成恶意shell.php2.抓包 三&#xff0c;漏洞利用1.反弹shell 四…

pytorch张量的创建

张量的创建 张量&#xff08;Tensors&#xff09;类似于NumPy的ndarrays &#xff0c;但张量可以在GPU上进行计算。从本质上来说&#xff0c;PyTorch是一个处理张量的库。一个张量是一个数字、向量、矩阵或任何n维数组。 import torch import numpy torch.manual_seed(7) # 固…

深圳鼎信|输电线路防山火视频监控预警装置:森林火灾来袭,安全不留白!

受线路走廊制约和环保要求影响&#xff0c;输电线路大多建立在高山上&#xff0c;不仅可以减少地面障碍物和人类活动的干扰&#xff0c;还能提高线路的抗灾能力和可靠性。但同时也会面临其它的难题&#xff0c;例如森林火灾预防。今天&#xff0c;深圳鼎信智慧将从不同角度分析…

随机森林 2(决策树)

通过 随机森林 1 的介绍&#xff0c;相信大家对随机森林都有了一个初步的认知&#xff0c;知道了随机和森林分别指的是什么&#xff0c;以及决策树根据什么选择内部节点。本文将会从森林深入到树&#xff0c;去看一下决策树是如何构建的。网上很多文章都讲了决策树如何构建&…

幺模矩阵-线性规划的整数解特性

百度百科:幺模矩阵 在线性规划问题中&#xff0c;如果A为幺模矩阵&#xff0c;那么该问题具有最优整数解特性。也就是说使用单纯形法进行求解&#xff0c;得到的解即为整数解。无需再特定使用整数规划方法。 m i n c T x s . t . { A x ≥ b x ≥ 0 \begin{align*} min \quad…