就目前中国市场,作为每一个开发人员都需要去会的技能 MySQL ,大家工作中都会需要 建表 、建索引,有的同学可能要说了:“这玩意谁不会建呢?太简单了!”。若是如此那真是太好了,我带团队也有几年了前前后后经历了一些人,超过85%的人建表、建索引 真的只是停留在会创建。创建出来的表、索引 在我看来也是问题一大堆。
所以,咱们今天就来好好聊聊如何创建一张漂亮的表,同时给他上一个 高效的 、有逼格的索引。
咱们简单拿一个订单信息表来做基础优化(假设没有别的业务影响),当然实际业务上会比这种案例场景复杂一点,希望大家可以用这次优化思路,持续延伸、扩展。同时也希望大家可以先看看下面的表结构自己判断下哪些地方是可以优化的,对比一下下面的优化思路。
-- 用户订单表
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='用户订单表';
idx_on_user_id
(user_id
) USING BTREE
idx_on_order_number
(order_number
) USING BTREE
idx_on_applied_at_and_source_id_and_state
(applied_at
,source_id
,state
) USING BTREE
idx_on_source_id_and_state_and_applied_at
(applied_at
,source_id
,state
) USING BTREE。当然这样优化我想大家可能有很多疑问,那么接下来的篇幅中将会针对如上疑问进行一一讲解
注:以下案例无特殊说明均使用优化前的 user_order 表的字段和索引
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使用如下写法。
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的性能还是会造成很大的影响的