校招面试题有了!——记录一个有趣的mySql查询

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: ## 前言最近在做一个内部系统的报表统计功能,遇到了一个麻烦的查询场景,因为对sql语句确实不太熟练,在网上查了一些资料,最终找到了一个解法。具体场景和表结构并不复杂,对sql大佬们来说应该也是小case,不过细想确实有趣,特此记录下来。## 问题已知表A有列:主键id、日期date和标签tag,其中tag是逗号分割的多值字符串(值的集合size不超过25个),如下如示例。请统计某段时间(例

前言

最近在做一个内部系统的报表统计功能,遇到了一个麻烦的查询场景,因为对sql语句确实不太熟练,在网上查了一些资料,最终找到了一个解法。具体场景和表结构并不复杂,对sql大佬们来说应该也是小case,不过细想确实有趣,特此记录下来。

问题

已知表A有列:主键id、日期date和标签tag,其中tag是逗号分割的多值字符串(值的集合size不超过25个),如下如示例。请统计某段时间(例如20220101-20220131)内,每个tag的记录数。

id date tag
1 20220101 1
2 20220111 1,2
3 20220121 1,2,3
4 20220131 4
5 20220101 5,8

分析

思路上是把tag中的数据拆分开,然后去统计。比如把上面的行能拆分成如下的多行,然后再进行统计。

id date tag
1 20220101 1
2 20220111 1
2 20220111 2
3 20220121 1
3 20220121 2
3 20220121 3
4 20220131 4
5 20220101 5
5 20220101 8

解法

网上通常解法

作为不怎么直接写mySql、语法都要查字典的同学,当然先去网上扒一下大家有没有遇到相关问题,是怎么解决的。经过多次搜索,找到了一个相关问题——“MySql一行拆多行”,大家可以自行搜索。
适配场景后的sql如下

SELECT
  t.id,
  t.date,
  substring_index(
    substring_index(
      t.tag,
      ',',
    b.help_topic_id + 1 
  ),
  ',',- 1 
) AS tag 
FROM
  test t
  JOIN 
     mysql.help_topic b 
  ON 
     b.help_topic_id < 
     ( length( t.tag ) - length( REPLACE ( t.tag, ',', '' ) ) + 1 )

分析

直接改造后,在idb上试跑了一下,并没有生成预期的拆分表,翻阅了其他资料,基本都是这一解法,那么先理解这一段sql的含义,再看哪里不work
首先看下里面几个函数和一个特殊的表

  • substring_index:按分隔符截取字符串,substring_index(“待截取有用部分的字符串”,“截取数据依据的字符”,截取字符的位置N);N>0,从前向后截取,N<0,从后向前截取
  • length:字符串长度
  • REPLACE:替换字符串中的内容
  • mysql.help_topic:是mysql 'HELP'指令使用的四张表之一,help_topic_id是这张表的连续自增主键,从0开始

那么上面sql拆分理解下:

length( t.tag ) - length( REPLACE ( t.tag, ',', '' ) ) + 1 
  • 前半部分是计算了字符串中分隔符的个数,+1是字符串中分割出来的单值的个数。
substring_index(
    substring_index(
      t.tag,
      ',',
    b.help_topic_id + 1 
  ),
  ',',- 1 
) AS tag
  • 这部分是把多值tag按照分割符,进行两次分割,分割出对应的单值。

问题升级

如果mysql.help_topic可以使用的话,其实问题到上面应该就结束了。奈何idb上无法使用mysql.help_topic表(具体原因不详,可能是DBA把这个表权限给关了)。
又走不通,那么我们再来深入理解一下mysql.help_topic在这里究竟是起到了什么作用。
想知道它有什么作用也比较简单,其实改造一下sql就一目了然了:

SELECT
  t.id,
  t.date,
  t.tag,
  b.help_topic_id
FROM
  test t
  JOIN 
     mysql.help_topic b 
  ON 
     b.help_topic_id < 
     ( length( t.tag ) - length( REPLACE ( t.tag, ',', '' ) ) + 1 )

拿到的数据应该如下:

id date tag help_topic_id
1 20220101 1 0
2 20220111 1,2 0
2 20220111 1,2 1
3 20220121 1,2,3 0
3 20220121 1,2,3 1
3 20220121 1,2,3 2
4 20220131 4 0
5 20220101 5,8 0
5 20220101 5,8 1

这里是用了help_topic_id从0开始连续增长的特性,在join时,匹配出和tag中值个数相等的多条记录,同时作为每条记录中取多值tag中的第几个值的索引。
理解到这里,这个解法的整体思路就很清晰了,mysql.help_topic能不能用并不是关键了,找一个其他的能从0或1连续自增的表来担任这个功能就ok,大部分的主键id自增的表都可以,甚至可以自己创建一个表,只要能保证连续的个数大于tag中多值的总个数即可。

WHAT IF

既然走到了这,那么不妨再加一点约束条件,如果没有其他的表可用,或者没有其他的表能确保是连续自增,且个数大于tag多值的总个数,怎么办呢(毕竟其他的表随时可能会改,不知道这段依赖的话,改出来都是坑)?或者,我们不专门创建一个新的表(创建新表的话,不知道具体原因的同学,看到也是满脸疑惑——为啥会有这个表???),要怎么解决这个问题呢?
继续查资料后,发现可以使用mySql的自定义变量,生成一组连续的数字代替(MySQL生成连续数字)。下面的sql会生成从1-50的连续数字

SELECT @xi:=@xi+1 as xc from 
            (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) xc1, 
            (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10) xc2,  
            (SELECT @xi:=0) xc0 

最后我的sql变成了这样

SELECT `tag`,COUNT(*)
from(
        SELECT info.`id`, substring_index(substring_index(info.`tag`, ",", b.id), ",", -1)  as tag
            FROM(
                SELECT * from test
                where
                      `tag` IS NOT NULL
                        and `date` >= #{startDate}
                        and `date` <= #{endDate}
                )  as t
            join(
            SELECT @xi:= @xi+ 1 as id
                from
                    (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5)  xc1,
                    (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5)  xc2,
                    (SELECT @xi:= 0)  xc0
                ) b
        on b.id <= (length(t.`tag`)  - length( replace(t.`tag`, ",", '')) + 1)
        ) result
        GROUP BY `tag`;

最终的输出应该是:

| tag | count |
| :----:| :----: |
| 1 | 3 |
| 2 | 2 |
| 3 |1 |
| 4 | 1 |
| 5 | 1 |
| 8 |1 |

写在最后

标题起得标题党了,比较少遇到这样的case,解决这个问题过程学到了不少骚操作,感觉挺有意思的。现在的解法并没有考虑性能问题,有更棒的sql欢迎分享。
另外,表设计很重要,不然后面就会遇到这样的奇葩问题,改表结构的话又是非常难受的工作量。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3天前
|
关系型数据库 MySQL
mysql关联查询
mysql关联查询
10 0
|
4天前
|
存储 关系型数据库 MySQL
架构面试题汇总:mysql索引汇总(2024版)
架构面试题汇总:mysql索引汇总(2024版)
|
4天前
|
关系型数据库 MySQL 分布式数据库
PolarDB产品使用问题之mysql迁移后查询不走索引了,该如何解决
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
2天前
|
关系型数据库 MySQL
mysql查询结果时间戳转成日期格式——date、DATE_FORMAT和FROM_UNIXTIME的使用
mysql查询结果时间戳转成日期格式——date、DATE_FORMAT和FROM_UNIXTIME的使用
4 0
|
3天前
|
存储 关系型数据库 MySQL
深入探索MySQL:成本模型解析与查询性能优化
深入探索MySQL:成本模型解析与查询性能优化
|
3天前
|
SQL 关系型数据库 MySQL
【面试高频 time:】关于MYsql性能优化的理解
【面试高频 time:】关于MYsql性能优化的理解
11 0
|
4天前
|
存储 关系型数据库 MySQL
架构面试题汇总:40道题吃透mysql(2024版)
架构面试题汇总:40道题吃透mysql(2024版)
|
2天前
|
存储 关系型数据库 MySQL
|
2天前
|
存储 SQL 关系型数据库
|
3天前
|
存储 关系型数据库 MySQL