MySQL实战——表、索引创建与优化

前言

就目前中国市场,作为每一个开发人员都需要去会的技能 MySQL ,大家工作中都会需要 建表建索引,有的同学可能要说了:“这玩意谁不会建呢?太简单了!”。若是如此那真是太好了,我带团队也有几年了前前后后经历了一些人,超过85%的人建表、建索引 真的只是停留在会创建。创建出来的表、索引 在我看来也是问题一大堆。
所以,咱们今天就来好好聊聊如何创建一张漂亮的表,同时给他上一个 高效的有逼格的索引。

MySQL表、索引 优化实战

咱们简单拿一个订单信息表来做基础优化(假设没有别的业务影响),当然实际业务上会比这种案例场景复杂一点,希望大家可以用这次优化思路,持续延伸、扩展。同时也希望大家可以先看看下面的表结构自己判断下哪些地方是可以优化的,对比一下下面的优化思路。

优化前 - 表、索引结构

-- 用户订单表
CREATE TABLE `user_order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id自增长',
  `order_number` varchar(32) DEFAULT NULL COMMENT '订单编号',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `origin` varchar(64) DEFAULT 'google play' COMMENT '订单来源(运营市场分析)',
  `type` int(2) DEFAULT '0' COMMENT '订单类型,0-普通订单;1-xxxxx;2-xxxxxx',
  `source_id` varchar(32) DEFAULT NULL COMMENT '下单设备类型: Android, H5,IOS',
  `amount` bigint(20) DEFAULT NULL COMMENT '申请金额',
  `promotion_code` varchar(32) DEFAULT NULL COMMENT '优惠码',
  `state` varchar(32) DEFAULT NULL COMMENT '状态',
  `applied_at` datetime DEFAULT NULL COMMENT '申请时间',
  `cancelled_at` datetime DEFAULT NULL COMMENT '取消时间',
  `closed_at` datetime DEFAULT NULL COMMENT '关闭时间',
  `created_at` datetime NOT NULL COMMENT '创建时间',
  `updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_on_user_id` (`user_id`) USING BTREE,
  KEY `idx_on_order_number` (`order_number`) USING BTREE,
  KEY `idx_on_applied_at_and_source_id_and_state` (`applied_at`,`source_id`,`state`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户订单表';

表优化思路

  • id:id 主键字段,自增长,带主键索引
    • 这个字段没有问题,希望大家建任何MySQL的表都能加上一个 自增长的主键ID ,且这个主键ID 不要用UUID不要不创建主键
  • order_number:varchar(32) DEFAULT NULL
    • 首先我们一个系统生成订单号一般长度都是 固定 的,不可能一会长度5、一会长度30。 所以这里建议根据实际长度 使用 char(len) 类型。因为varchar是一个 可变长 字段,MySQL需要用 额外的两个字节 去保存长度。 使用 char类型对存储空间、查询效率均有提升。
    • 其次这个order_number,在业务逻辑上应该是 不可能为NULL 的,所以我们应该对order_number 设置 NOT NULL ,第一保证的业务的正确性,第二节省了一个字节,因为如果一个字段允许为NULL,MySQL需要额外的 一个字节标记 该字段是否为NULL。
  • user_id:bigint(20) DEFAULT NULL
    • 同理根据业务可确定该字段需要设置 NOT NULL
  • origin :varchar(64) DEFAULT ‘google play’
    • 既然都设置默认值了 逻辑上也不可能为NULL了 所以 需要设置 NOT NULL
    • 运营的渠道统计分析、这个字段长度要更具实际场景考虑64是否太长了?
    • 如业务允许可以用关联表管理渠道Name,这里直接写入渠道的ID即可,因为大多数渠道应该都是 重复 的,这个字段建索引的概率也不大,用int类型ID,节省存储空间、同时 查询效率提升 。因为内存中 数值类型的判断等于的效率远高于字符类型
  • type:int(2) DEFAULT ‘0’
    • 字段名使用到了MySQL的 关键字 type,建议改为 order_type
    • 既然都设置默认值了 逻辑上也不可能为NULL了 所以 需要设置 NOT NULL
  • source_id:varchar(32) DEFAULT NULL
    • 根据描述这个字段完全可以用int类型1、2、3代替Android、H5、IOS
    • 如果实在不想使用int代替,也可以修改字段长度,因为实际场景中(Android、H5、IOS)这三个字段的长度完全是已知的 所以可以改成 char 类型。
    • 逻辑上也不可能为NULL了 所以 需要设置 NOT NULL
  • amount:bigint(20) DEFAULT NULL
    • 这个字段类型要不要替换成 decimal 咱们就不聊了,也许人家就是整数的,这个根据实际业务确定
    • 逻辑上金额需要设置一个 默认 0,然后设置 NOT NULL
  • promotion_code:varchar(32) DEFAULT NULL
    • 这个字段没啥好说的,根据实际业务来吧
  • state:varchar(32) DEFAULT NULL
    • 逻辑上状态也不可能为NULL,需要设置一个 默认的初始状态 ,同时需要设置 NOT NULL
    • 也可以考虑替换为int类型的码表代替。(自行判断)
  • applied_at: datetime DEFAULT NULL
    • 逻辑上这个时间应该不可能为 NULL ,所以 需要设置 NOT NULL
  • cancelled_at:datetime DEFAULT NULL
    • 这个根据实际业务来,没啥好优化的
  • closed_at:datetime DEFAULT NULL
    • 这个根据实际业务来,没啥好优化的
  • created_at:datetime NOT NULL
    • 这个字段没啥问题,建议大家的数据库表都加上创建时间字段
  • updated_at:datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT
    • 这个字段没啥问题,建议大家的数据库表都加上修改时间字段,且自动根据当前时间更新

非聚簇索引优化思路

  • KEY idx_on_user_id (user_id) USING BTREE
    • user_id索引,没啥问题,正常的业务中一定有根据user_id查询的需求
  • KEY idx_on_order_number (order_number) USING BTREE
    • order_number索引,逻辑上订单号是不能重复的,所以这里的索引应该修改为 唯一索引,数据安全性、查询效率均有提升
  • KEY idx_on_applied_at_and_source_id_and_state (applied_at,source_id,state) USING BTREE
    • 到了这次分享的关键点了,这个联合索引最左侧的是下单时间(applied_at),而这个字段查询多半是会使用范围查询 “>”、"<" 之类的,那问题就来了,如果我把这三个字段都用到查询条件中,可能会出现的场景就是联合索引没有使用全,因为applied_at的 “>”、"<" 查询,导致索引只使用了applied_at字段,并没有去使用后面的source_idstate,所以我的优化思路是: KEY idx_on_source_id_and_state_and_applied_at (applied_at,source_id,state) USING BTREE。

当然这样优化我想大家可能有很多疑问,那么接下来的篇幅中将会针对如上疑问进行一一讲解

组合索引优化思路

         注:以下案例无特殊说明均使用优化前的 user_order 表的字段和索引

认识key_len
  • key_len:数值提示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。 举例来说,user_order的联合索引 idx_on_applied_at_and_source_id_and_state 由 applied_at 和 source_id 以及 state 三个列组成,如果在索引中所有列都用到了则 key_len = 三个字段的长度总和,如只用到了applied_at 则 key_len = datetime类型在数据库中所占用的字节数。

key_len Demo 如下 ↓↓↓

依据最左匹配选择 只使用一列也能走联合索引
EXPLAIN SELECT * from user_order WHERE applied_at = '2021-10-31' ;

在这里插入图片描述


依据最左匹配选择 使用前面两列
EXPLAIN SELECT * from user_order WHERE applied_at = '2021-10-31' AND source_id = 'android';

在这里插入图片描述

上面这个长度可能有同学会问了 为什么是 105 呢? 已知 source_id 是 varchar(32) 且允许字段为NULL,使用的是utf8编码,一个中文在索引中占用字节数为3 ,(32 * 3 ) + 2(varchar类型需要存储长度) + 1(是否为null标识) = 99。
已知第一个字段 applied_at 索引长度为 5 , 5 + 99 = 104


使用联合索引所有列查询
EXPLAIN SELECT * from user_order WHERE applied_at = '2021-10-31' AND source_id = 'android' and state in('canceled','closed');

在这里插入图片描述


范围查询对组合索引的影响
-- 将等于修改为大于
EXPLAIN SELECT * from user_order WHERE applied_at > '2021-10-31' AND source_id = 'android';

EXPLAIN SELECT * from user_order WHERE applied_at > '2021-10-31'  AND source_id = 'android' and state in('canceled','closed');

在这里插入图片描述

根据如上案例得知,如果我们使用 “>”、"<" 范围查询时 有可能 导致组合索引无法完全生效。
当然这里如果将 > ‘2021-10-31’ 修改为 >= ‘2021-11-01’ 是可能会去走后续索引的,但是也并不是一定的。


将查询范围扩大 直接导致全表扫描
EXPLAIN SELECT * from user_order WHERE applied_at > '2021-06-31' AND source_id = 'android' and state in('canceled');

在这里插入图片描述

如上SQL 直接不走索引选择了全表扫描,可是 canceled 这种状态,在业务数据中一般是比较少的,如果可以走索引再加上时间 我们依然可以过滤掉95%以上的数据,但 结果是优化器选择了全表扫描。

接下来我们按照上面对表和索引进行优化 然后看看执行情况。

优化后 - 表、索引结构

CREATE TABLE `user_order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id自增长',
  `order_number` char(32) NOT NULL COMMENT '订单编号',
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `origin` varchar(32) NOT NULL DEFAULT 'google play' COMMENT '订单来源(运营市场分析)',
  `order_type` int(2) NOT NULL DEFAULT '0' COMMENT '订单类型,0-普通订单;1-xxxxx;2-xxxxxx',
  `source_id` char(8) NOT NULL COMMENT '下单设备类型: Android, H5,IOS',
  `amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '申请金额',
  `promotion_code` varchar(32) DEFAULT NULL COMMENT '优惠码',
  `state` varchar(20) NOT NULL DEFAULT 'init' COMMENT '状态',
  `applied_at` datetime NOT NULL COMMENT '申请时间',
  `cancelled_at` datetime DEFAULT NULL COMMENT '取消时间',
  `closed_at` datetime DEFAULT NULL COMMENT '关闭时间',
  `created_at` datetime NOT NULL COMMENT '创建时间',
  `updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `idx_on_order_number` (`order_number`) USING BTREE,
  KEY `idx_on_user_id` (`user_id`) USING BTREE,
  KEY `idx_on_applied_at_and_source_id_and_state` (`source_id`,`state`,`applied_at`) USING BTREE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='借款信息表';

表结构咱们就不看了 直接进入主题看看修改后的组合索引,调整了顺序,废话不多说咱们来看看实际效果

优化后组合索引全字段查询

EXPLAIN SELECT * from user_order WHERE source_id = 'android' and state in('closed') AND applied_at > '2021-10-31' ;

在这里插入图片描述

(8 * 3) + (20 * 3 + 2) + 5 = 91 索引长度大大减少的同时,还能把整个联合索引走全了。

    当然这个时候肯定就有人提出问题了,我当初把 applied_at 放到最左边 是因为之后可能有场景会直接根据时间查询 或者,根据 applied_at 和 source_id 两个条件去查询 这种场景你怎么办呢?

    答:依据业务分析 source_id 和 state 的类型都不多,且业务在查询前就是已知的(可能代码里面有枚举已经提前写好了可能存在的值),那么当咱们只需要根据 applied_at 和 source_id 进行查询时 SQL使用如下写法。

优化后组合索引无法满足最左匹配下SQL写法

EXPLAIN SELECT * from user_order WHERE source_id in ('android','ios','h5') and state in('canceled') and applied_at > '2021-10-31';

在这里插入图片描述

把 source_id 所有可能出现的类型均加入查询条件即可,如果 也需要只根据 applied_at 查询 则将state 所有可能的值写入查询条件中即可。 如果这里选择用int类型来代替 source_id 与 state 效率更高,索引长度更短。


优化后扩大查询范围依然能走索引

EXPLAIN SELECT * from user_order WHERE source_id = 'android' and state = 'canceled' AND applied_at > '2021-06-31' ;

在这里插入图片描述

这条SQL在之前的查询中并未使用索引,因为查询时间范围太大了,但是优化后效果明显!!

总结

注:以上优化仅是提供思路,实际业务中咱们需要根据实际业务场景带入 来建表、建索引,切忌眼中无业务,只知道莽;
或者只知道建单列的索引,不懂组合索引的优势, 这里带入MySQL 索引合并 大家可以查一下 如果不使用组合索引而去用多个单列索引来完成查询字段的覆盖,实际对MySQL的性能还是会造成很大的影响的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zhibo_lv

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值