分类: Mysql

Mysql临时表CREATE TEMPORARY TABLE

群友的一个小问题,大意如下。我手中目前已知有若干ID列表,如[1,2,3,4,5]的ID,需要到某张表中查询出表中不存在的ID。

好像有点绕,实际列举数据说明下,比如此刻表中有ID为[2,4,5,6,7]的数据,那么最终查询出来的结果应当为[1,3],因为[1,3]在我们已知的列表[1,2,3,4,5]中,但是不在数据库的表[2,4,5,6,7]中,而[6,7]因为不在[1,2,3,4,5]中,所以也不符合要求。

其实这个问题很好解的,最直观的来说我们先拿[1,2,3,4,5],到表中查询[2,4,5,6,7]用in语句得到[2,4,5]的结果,之后再拿[1,2,3,4,5]到[2,4,5]的结果集中用not in语句就可以得到[1,3]。

但,问题不是这里,而是[1,2,3,4,5]不是一个结果集,而是入参,所以我们这里我们需要用到MYSQL的临时表。而如果[1,2,3,4,5]是在一张表里的结果的话,那么我们就可以换另一个思路了,用临时表[1,2,3,4,5]LEFT JOIN目标表使用id关联,并保留当前的[1,2,3,4,5]的列和目标表的ID列,关联后的结果如果目标ID列为空的数据可以再做一次过滤剔除。

Continue reading

一次Mysql使用order by和limit分页查询的陈年旧坑

  • 起因背景

本次开发是在一个旧功能的增加新的逻辑,其中有这么一个消息重发的模块。在正常的Kafka消息推送过程中,因为异常、数据条件暂时无法确定是否能够下发的情况下,把这条消息暂时存到一张Mysql表中,并使用定时任务每隔一段时间扫一下这张表,把消息重新尝试下发Kafka。如果发送失败,则把这条消息的重试次数加1。如果发送成功,则把这条消息的从重试表中删除。

  • 问题现象

因为这是一个上线了几年的功能了,且这次开发的需求是在下发的时候进行的调整,所以一开始对这块并没有太多在意。直到昨天测试同学给我讲了他发现的一个现象,执行一次重试任务之后,有些数据的重试次数加了不止1次,但是有些数据的重试次数却没有增加。那么疑点就来了,根据测试提供的充实任务入口名称,我找到了这段已经在生产环境跑了好几年的代码,初步看了一遍基本定位到问题所在。

基本逻辑是这样的

  1. 根据分页参数分页从Mysql捞取待重试数据,需要查询已重试次数小于一个设定值的,比如5次
  2. 将待重试数据调用推送Kafka方法尝试进行推送,这之中包括一系列验证逻辑
  3. 推送成功的数据从表中清除
  4. 推送失败的数据在表中更新重试次数加1
Continue reading

小坑+1,Mysql的UNIQUE索引中允许存在Null值

一个小坑,如题。

新建了一张表,假设如下

create table table_c
(
id int auto_increment
primary key,
req_id int null,
req_type int default 0 null,
req_version int default 0 null,
constraint table_c_req_id_req_type_uindex
unique (req_id, req_type)
)
charset = latin1;

表中有个(req_id, req_type)组成的唯一索引,插入数据一些数据后得到如下结果

从上表中插入的结果来看,可以看到有两条(req_id = 124, req_type = null)组成的行数据,可见,unique索引中可以包含null值字段

A UNIQUE index permits multiple NULL values for columns that can contain NULL.

https://dev.mysql.com/doc/refman/5.6/en/create-index.html

官方文档中提到了这么一段,确实可以包含的,且根据上面的实验结果,但凡某个字段为null了,那么唯一性约束就失效了。

相关处理办法

  1. 给unique索引相关字段添加上 not null属性 和 default
  2. 程序代码中相关对象拼装的时候校验相关字段,并赋予默认值

首先,在设置了default值之后,如果插入执行insert语句的时候没有对应字段,mysql会赋予默认的default值。其次,某些封装的JDBC操作的工具如果我们传入的字段没有值,那么工具会自动给这个字段赋值null,那么就导致如果没有设置 not null属性会被插入一个null值,仍然导致唯一索引约束失效。

ON DUPLICATE KEY UPDATE踩入一个小坑

评审了新迭代的需求,翻以前功能的代码,看到了一个ON DUPLICATE KEY UPDATE的语句,有点不对劲。

实际数据库设计业务相关内容,这里不写,单独抽象出来一个场景如下

有一张表table_c,主键id无关紧要,其中有一个唯一键索引req_id,这个索引保证了表中每行的req_id都是唯一的

create table table_c
(
    id          int auto_increment
        primary key,
    req_id      int           null,
    req_type    int           null,
    req_version int default 0 null,
    constraint table_c_req_id_uindex
        unique (req_id)
);

需求场景是这样的,从第三方拉取到数据,对应唯一的req_id,如果表中没有则插入一条新数据。如果表中有,则判断req_version字段,只有当新数据的req_version比原表中的大的时候才更新整行数据。于是根据这个需求场景来原先的代码中的SQL如下

INSERT INTO table_c (req_id, req_type, req_version)
VALUES (124, 4, 1)
ON DUPLICATE KEY UPDATE 
    req_version = IF(VALUES(req_version) > req_version, VALUES(req_version), req_version),
    req_type    = IF(VALUES(req_version) > req_version, VALUES(req_type), req_type)

乍一看起来使用了ON DUPLICATE KEY UPDATE,也使用了IF判断了req_version字段,好像没毛病。但是实际上是有问题的,我们跑一个实际数据看下

表中现有数据

执行上面的SQL,按照预期req_type应该会被更新为4,同时req_version被更新为1,执行一下

可以看到执行成功,而表中现在的结果如下

req_version确实更新了但是req_type却没有更新,那么我们可以推断出req_type后面的IF语句没有获得返回结果true,那么可能的情况就是,在前面先执行

req_version = IF(VALUES(req_version) > req_version, VALUES(req_version), req_version)

这一段的时候req_version的值已经被更新为1了,且VALUES(req_version)的值也是1,那么在后面再执行VALUES(req_version) > req_version判断的时候就会得到false

不妨改变下SQL再试下

在UPDATE语句部分直接填上req_version的值,注意不是VALUES(req_version)的值

表中结果如下

可以确定的看到在执行到req_type这行的时候req_version的值确实变成2了。

根据这样的结论,我们再次调整下SQL语句,执行查看结果

Continue reading

一次DataIntegrityViolationException的生产异常

处理步骤

第一步,检查报异常的方法,发现是一个SQL查询报出来的异常,大意是从MYSQL查询出来的数据转换成数字类型的,内容如下

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [SELECT colum_xxxxx FROM table_xxxxxx where STATUS = 0 order by LAST_MODIFY_TIME limit 300]; '815996050122815896400122' in column '1' is outside valid range for the datatype BIGINT.; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLDataException: '815996050122815896400122' in column '1' is outside valid range for the datatype BIGINT.

从上的内容来看,想要把815996050122815896400122的结果转换成BIGINT类型失败,因为超出了BIGINT的范围

这里提一下,MySQL BIGINT 类型的范围在 -9223372036854775808 和 9223372036854775807 之间,与Java的Long类型范围一致,为一个19位数字。

而给定的值为一个22位数字,这显然会失败。

接下来去查看对应表结构,发现对应字段是varchar(32)的字符串类型的。那么显然A同事在做开发这个功能的时候,这个22位长度的数字可以正常转成字符串存进去,但是另外一个B同事在取数据的时候根据自己的业务场景想要将Mysql查出来的结果直接映射为Long类型,这时候就出现异常了。

到这一步,可能觉得那把取值的时候改为String型不就可以了么?于是尝试着评估下改为String型会带来哪些影响。以下是是处理过程,涉及到具体业务场景的考虑。

首先这个字段需要对12取模,然后把对应数据存入不同的库里。其次这个字段需要请求淘宝的官方API查询数据,相关调用部分都要进行修改。然后对应的数据需开放了内部服务接口给其他系统调用,这块相关字段类型也需要修改,以及还有其他的一些零碎的地方。瞬间感觉好坑。近几个月迭代的需求都会要做一些修改调整。

但是,重新看下需求,这个字段的来源其实是淘宝给的分销采购单的退款单号,淘宝API上的给的定义类型就是Long类型的,再看下目前线上生产环境的数据是11位的数字,所以实际上来说,B同事在一开始做这块查询的时候用Long类型取值是没有问题的。而A同事在做写入功能和数据库设计的时候,没有考虑具体的业务场景,只是考虑觉得这个ID应该预留长度空间,给数据库设计了个varchar(32)的字段存储,写入的时候用String型存入,咱们也不能说他做得不对。本质上来说是两个模块的人对需求理解不一致,沟通不充分产生的结果。

那么对这个异常处理的方式也就很明显了,在写入的时候对该字段进行校验,如果不能转换成Long类型的退款单号ID字符串,给业务人员报错,这显然是不正确的退款单号。

照着这个思路,我们有两种实现方法

第一种,直接Long.parseLong(idString)来尝试将字符串转换为Long类型对象,用try catch将Long.parseLong()转换的代码包含起来,尝试捕获异常,如果转换失败,那么给业务提示不正确的单号。

但是根据一个开发准则“不应使用异常来实现业务逻辑(或流程控制)”,以及最小惊讶原则(Principle of Least Astonishment),这提醒着我,不应当这样来处理。

http://wiki.c2.com/?DontUseExceptionsForFlowControl

于是试着用第二种方式,自己来实现判断一个字符串能否转换成Long类型对象,代码如下

    public boolean validateLongNumStr(String longNumStr) {
        //清除先导0
        String nonePreZeroNum = longNumStr.replaceFirst("^0+", "");
        String longMax = Long.toString(Long.MAX_VALUE);
        //长度比longMax长的必定错误
        if ( nonePreZeroNum.length() > longMax.length()){
            return false;
        }
        //长度相同的比较字典序即可
        if (nonePreZeroNum.length() == longMax.length() && longMax.compareTo(nonePreZeroNum) < 0){
            return false;
        }
        return true;
    }
  1. 清除先导0,目标字符串需要提前处理清除掉非数字字符
  2. 生成一个Long类型的最大值,即9223372036854775807的字符串
  3. 如果目标字符串长度大于这个longMax的字符串,则必定会转换失败
  4. 如果长度相等,则比较下两个字符串的字典序,如果目标字符比longMax的字典序大,那么也会转换失败
  5. 剩下的就是长度比longMax短的字符串,可以顺利转换成功

当然,别忘了写下对应单元测试

MYSQL清除超大表数据

群友聊天,有人求助一个问题,他要清理一张数据量超大的表,大概1.5G(其实也不算大),执行了TRUNCATE命令后10分钟过去都没反应。

以下,给个方案,特地自己造了张表出来

表名`tao_order_sub_info`,我从测试环境扒到本地来的,淘宝订单子表,共计356万行数据,79个字段,表文件大小2.41G

接下来开始正式操作,新建一张表`tao_order_sub_info_bak`,表结构都同原来的表

如果有部分需要保留的数据,可以在这里将需要保留的数据灌入新建的表

insert into tao_order_sub_info_bak (select * from tao_order_sub_info where N_ID < 100)

接下来同时修改新表旧表的表名,用一句SQL执行

rename table tao_order_sub_info to tao_order_sub_info_old,tao_order_sub_info_bak to tao_order_sub_info

将原表改名为一个另外一个名字,同时将刚刚新建的表改名为原来的表名,因为是在一个sql语句中执行的,可以保证本次操作的对两张表的操作是原子的。

接下来,对新的`tao_order_sub_info`表的查询写入就是这张新生成的没有数据的表了。我们可以直接删除旧表(drop table),或者进行相关备份存档操作

实测这2.4G的表执行truncate其实也是很快的,这位网友执行truncate非常慢的原因应该不是数据问题

记一次SQL慢查优化

 

原SQL语句(所有相关敏感信息已做脱敏处理)

 

image-20210106140645055

sql解析顺序分析

1.from条件后的 table_xxx_activity表(只有5万条数据)字段查询条件过滤,根据目前的生产业务情况,最多不超过(小)几百条数据,每个项目下最多只建了几百个活动

没有覆盖索引,走辅助索引后,回表用Using where过滤

2.遍历1中的结果分别执行exist语句的子查询判断当条记录是否保留

exist语句1可以走索引PK_tablexxxregistry_37、38

exist语句2可以走索引table_xxx_registry_store_idx1、table_xxx_activity_areastore,但是没有覆盖索引需要走Using where

已知table_xxx_registry表数据60万,table_xxx_activity_areastore表数据400万

3.执行select部分,选择查询的字段和signUpCount字段的子查询

signUpCount字段的子查询可以走辅助索引PK_tablexxxregistry_38

4.排序用到PUBLISH_TIME字段,重新走Using filesort排序

5.limit截断

【时间估算大致情况】

查询1的时间 + 查询1的结果条数 * (exist语句1的时间 + exist语句2的时间) + exist判断后结果2的条数 *( select字段获取的时间 + signUpCount字段的子查询的时间)+ PUBLISH_TIME字段排序的时间

 

explain解析

 

初步分析

该语句为一个分页查询语句,慢查中报出的count语句为.queryForListPage(方法自动封装的语句

语句中嵌套了3个子查询

1.signUpCount字段

2.exist语句1

3.exist语句2

explain结果中出现了Using filesort排序,因为最终的order by t.PUBLISH_TIME DESC不在索引中出现

 

解决方案

1.原java代码中

在现有sql语句结果集上,添加count(1)自动封装了获取总条数的sql语句,而根据分析,count计算不需要通过order by,且原来的查询字段中所有字段均不参与最终条数计算,包括signUpCount子查询,可以使用新的

代替原来的方法,并去除掉对应的字段signUpCount子查询(对应key_len为390,id为2的那一步查询),排序(对应id为1中的Using filesort排序)等语句作为新的countSqlId进行总数量查询

2.signUpCount字段可以另外写逻辑单独查询,而具体展示数据查询的时候,因为有limit条件的存在,所以相对关联到的子查询数量会比较少

3.exist语句1、2 作为前置查询条件,先查询出结果集,再代入到本sql中作为新的in查询条件,或者做一个join查询,需要预先判断这个结果集的数量多少

方案2 子查询2表table_xxx_activity_areastore构建联合索引。ACTIVITY_CODE & AREA_CODE

5.排序字段order by t.PUBLISH_TIME DESC,可以看业务情况考虑是否使用id倒序排列,减少排序资源消耗

6.目前本表table_xxx_activity建立的索引已经非常多了,不便于重新新增新的联合索引

优化发布后验证结果,查询时间降低到700毫秒以下