笔记来源:MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!
#1. 数据准备
CREATE DATABASE atguigudb2;
USE atguigudb2;
#建表
CREATE TABLE `class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`className` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
`monitor` INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `student` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`stuno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`classId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
SET GLOBAL log_bin_trust_function_creators=1;
#随机产生字符串
DELIMITER //
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN
DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
WHILE i < n DO
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
SET i = i + 1;
END WHILE;
RETURN return_str;
END //
DELIMITER ;
#用于随机产生多少到多少的编号
DELIMITER //
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN
DECLARE i INT DEFAULT 0;
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;
RETURN i;
END //
DELIMITER ;
#创建往stu表中插入数据的存储过程
DELIMITER //
CREATE PROCEDURE insert_stu( START INT , max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0; #设置手动提交事务
REPEAT #循环
SET i = i + 1; #赋值
INSERT INTO student (stuno, NAME ,age ,classId ) VALUES ((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000));
UNTIL i = max_num
END REPEAT;
COMMIT; #提交事务
END //
DELIMITER ;
#执行存储过程,往class表添加随机数据
DELIMITER //
CREATE PROCEDURE `insert_class`( max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO class ( classname,address,monitor ) VALUES (rand_string(8),rand_string(10),rand_num(1,100000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END //
DELIMITER ;
#执行存储过程,往class表添加1万条数据
CALL insert_class(10000);
#执行存储过程,往stu表添加50万条数据
CALL insert_stu(100000,500000);
SELECT COUNT(*) FROM class;
SELECT COUNT(*) FROM student;
# 删除某表上的索引 存储过程
DELIMITER //
CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE ct INT DEFAULT 0;
DECLARE _index VARCHAR(200) DEFAULT '';
DECLARE _cur CURSOR FOR SELECT index_name FROM information_schema.STATISTICS WHERE table_schema=dbname AND table_name=tablename AND seq_in_index=1 AND index_name <>'PRIMARY' ;
#每个游标必须使用不同的declare continue handler for not found set done=1来控制游标的结束
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=2 ;
#若没有数据返回,程序继续,并将变量done设为2
OPEN _cur;
FETCH _cur INTO _index;
WHILE _index<>'' DO
SET @str = CONCAT("drop index " , _index , " on " , tablename );
PREPARE sql_str FROM @str ;
EXECUTE sql_str;
DEALLOCATE PREPARE sql_str;
SET _index='';
FETCH _cur INTO _index;
END WHILE;
CLOSE _cur;
END //
DELIMITER ;
# 执行存储过程
CALL proc_drop_index("dbname","tablename");
索引建立
SQL优化
调整my.cnf
分库分表
物理查询优化
和逻辑查询优化
两大块。
物理查询优化
是通过索引
和表连接方式
等技术来进行优化,这里重点需要掌握索引的使用。逻辑查询优化
就是通过SQL等价变换
提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。最左前列开始
并且不跳过索引中的列
。实例:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name ='abcd' ;
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student.name =' abcd ' ;
跳过age字段,则无法使用索引
拓展:Alibaba《Java开发手册》
索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
对于一个使用InnoDB存储引擎的表来说,在我们没有显式的创建索引时,表中的数据实际上都是存储在聚簇索引的叶子节点
的。而记录又是存储在数据页
中的,数据页和记录又是按照记录主键值从小到大
的顺序进行排序。所以如果我们插入的记录的主键值是依次增大的话,那我们每插满一个数据页就换到下一个数据页继续插;而如果我们插入的主键值忽大忽小的话,就比较麻烦了,假设某个数据页存储的记录已经满了,它存储的主键值在1~100之间:
如果此时再插入一条主键值为 9 的记录,那它插入的位置就如下图:
由于数据页已满,我们需要把当前页面分裂
成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着:性能损耗
!所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的主键值依次递增
,这样就不会发生这样的性能损耗了。所以我们建议:让主键具有AUTO_INCREMENT
,让存储引擎自己为表生成主键,而不是我们手动插入。
utf8mb4
( 5.5.3版本以上支持)兼容性更好
,统一字符集可以避免由于字符集转换产生的乱码。不同的字符集进行比较前需要进行转换会造成索引失效。怎么样开销小就怎么来
。另外,SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。# 使用模糊查询
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc% ';
# 使用函数
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name, 3) = 'abc ';
CREATE INDEX idx_name ON student(NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
可以看到,使用模糊查询时,索引生效;使用函数,type为ALL,表示没有用到索引,索引失效
。
此外,在索引列上使用各种计算也会使索引失效,例如:
CREATE INDEX idx_sno ON student(stuno);
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = '123';
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = 123;
EXPLAIN SELECT SQL_NO_CACHE * FROM student
WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ;
范围条件右边的条件无法使用索引
。create index idx_age_name_classid on student(age,name,classid)
EXPLAIN SELECT SQL_NO_CACHE * FROM student
WHERE student.age=30 AND student.name = 'abc' AND student.classId>20 ;
应用开发中范围查询,例如:金额查询,日期查询往往都是范围查询。应将查询条件放置where语句最后。(创建的联合索引中,务必把范围涉及到的字段写在最后)│
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> 'abc' ;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name != 'abc' ;
# 用到了索引
EXPLAIN SELECT SQL_NO_CACHE NAME FROM student WHERE student.name != 'abc' ;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;
结论:
最好在设计数据表的时候就将字段设置为NOT NULL约束
,比如你可以将INT类型的字段,默认值设置为0。将字符类型的默认值设置为空字符串(‘’)。
拓展:同理,在查询中使用not like
也无法使用索引,导致全表扫描。
# 可以使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE 'ab%';
# 无法使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE '%ab%';
拓展:Alibaba《Java开发手册》
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
OR前后的两个条件中的列都是索引时,查询中才使用索引
。避免了全表扫描
。例如:EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR NAME = 'Abel';
Where语句 | 索引是否被使用 |
---|---|
where a= 3 | Y,使用到a |
where a = 3 and b=5 | Y,使用到a,b |
where a = 3 and b= 5 and c= 4 | Y,使用到a, b, c |
where b= 3 或者 where b = 3 and c = 4 或者 where c = 4 | N |
where a= 3 and c= 5 | 使用到a,但是c不可以,b中间断了 |
where a = 3 and b> 4 and c= 5 | 使用到a和b,c不能用在范围之后,b断了 |
where a is null and b is not null | is null 支持索引但是is not null不支持。所以a可以使用索引,但是b不可以使用 |
where a <> 3 | 不能使用索引 |
where abs(a) =3 | 不能使用索引 |
where a = 3 and b like ‘kk%’ and c = 4 | Y,使用到a,b,c |
where a = 3 and b like ‘%kk’ and c = 4 | Y,只用到a |
where a = 3 and b like ‘%kk%’ and c = 4 | Y,只用到a |
where a = 3 and b like 'k%kk% 'and c = 4 | Y,使用到a,b,c |
query过滤性更好
的索引位置越靠前越好
。更多字段
的索引。范围查询
时,尽量把这个字段放在索引次序的最后面
。总之,书写SQL语句时,尽量避免造成索引失效的情况。#分类
CREATE TABLE IF NOT EXISTS `type` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
#图书
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);
#向分类表中添加20条记录
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
...
#向图书表中添加20条记录
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
...
# 情况1:左外连接
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
#添加索引
CREATE INDEX Y ON book(card);
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
CREATE INDEX X ON `type`(card);
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
DROP INDEX Y ON book;
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
# 情况2:内连接
DROP INDEX X ON `type`;
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;
#添加索引
CREATE INDEX Y ON book(card);
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;
CREATE INDEX X ON `type`(card);
#结论:对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;
#删除索引
DROP INDEX Y ON book;
#结论:对于内连接来讲,如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表出现。
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;
CREATE INDEX Y ON book(card);
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;
#向type表中添加数据(20条数据)
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
...
#结论:对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择小表作为驱动表。“小表驱动大表”
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;
嵌套循环
(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入BNLJ算法
来优化嵌套执行。SELECT * FROM A JOIN B ON ...
A一定是驱动表吗?不一定,优化器
会根据你查询语句做优化
,决定先查哪张表。先查询的那张表就是驱动表
,反之就是被驱动表。通过explain关键字可以查看。
对于外连接来说:
SELECT * FROM A LEFT JOIN B ON ...
#或
SELECT * FROM B RIGHT JOIN A ON ...
内连接
了所以谁是驱动表都是有可能的
。算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result.以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断。如图所示:
可以看到这种方式效率是非常低的,以上述表A数据10o条,表B数据10oo条计算,则A*B=10万次。开销统计如下:
开销统计 | SNLJ |
---|---|
外表扫描次数: | 1 |
内表扫描次数: | A |
读取记录数: | A+B*A |
JOIN比较次数: | B*A |
回表读取记录次数: | 0 |
Nested-Loop Join优化算法
。Index Nested-Loop Join其优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引
才行。通过外层表匹配条件直接与内层表索引进行匹配
,避免和内层表的每条记录去进行比较
,这样极大的减少了对内层表的匹配次数。
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的
,故mysql优化器都倾向于使用记录数少的表作为驱动表
(外表)。
开销统计 | SNLJ | INLJ |
---|---|---|
外表扫描次数: | 1 | 1 |
内表扫描次数: | A | 0 |
读取记录数: | A+B*A | A+B(match) |
JOIN比较次数: | B*A | A*Index(Height) |
回表读取记录次数: | 0 | B(match)(if possible) |
如果索引不是主键索引
,所以还得进行一次回表
查询。相比,被驱动表的索引是主键索引,效率会更高
。join buffer
缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次
,降低了被驱动表的访问频率
。join_buffer_size=256k
。注意:
这里缓存的不只是关联表的列,select后面的列也会缓存起来。
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量械少不必要的字段,可以让joinbuffer中可以存放更多的列。
被连接的数据子集较小
的情况,Nested Loop是个较好的选择。大数据集连接
时的常用方式,优化器使用两个表中较小(相对较小
)的表利用Join Key
在内存中建立散列表,然后扫描较大的表并探测散列表,找出与Hash表匹配的行。
它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能
。大多数人都说它是Join的重型升降机。Hash Join只能应用于等值连接
(如WHERE A.COL1= B.COL2),这是由Hash的特点决定的。用小结果集驱动大结果集
(其本质就是减少外层循环的数据数量) (小的度量单位指的是表行数*每行大小)│MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作
。
子查询是MySQL的一项重要的功能,可以帮助我们通过一个SQL语句实现比较复杂的查询
。但是,子查询的执行效率不高
。原因:
为内层查询语句的查询结果建立一个临时表
,然后外层查询语句从临时表中查询记录。查询完毕
后,再撤销这些临时表
。这样会消耗过多的CPU和IO资源,产生大量的慢查询。临时表
,不论是内存临时表还是磁盘临时表都不会存在索引
,所以查询性能会受到一定的影响
。在MySQL中,可以使用连接(JOIN)查询来替代子查询
。连接查询不需要建立临时表
,其速度比子查询要快
,如果查询中使用索引的话,性能就会更好
。
结论:尽量不要使用NOT IN或者NOT EXISTS,用LEFT JOIN xxx ON x WHERE xx IS NULL替代
FileSort
和Index
排序。
避免全表扫描
,在ORDER BY子句避免FileSort排序
。当然,某些情况下全表扫描,或者FileSort排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。尽量使用Index完成 ORDER BY排序
。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同就使用联合索引。实例:
INDEX a_b_c(a,b,c)
order by 能使用索引最左前缀
- ORDER BY a
- ORDER BY a,b
- ORDER BY a,b,c
- ORDER BY a DESC,b DESC,c DESC
如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b > const ORDER BY b,c
不能使用索引进行排序
- ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
- WHERE g = const ORDER BY b,c /*丢失a索引*/
- WHERE a = const ORDER BY c /*丢失b索引*/
- WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
- WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/
双路排序
和单路排序
所有列
,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。EXPLAIN SELECT * FROM student LIMIT 2000000,10;
EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a WHERE t.id = a.id ;
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
一个索引包含了满足查询结果的数据就叫做覆盖索引
。好处:
弊端:
为了支持覆盖索引,需要建立冗余索引。
索引字段的维护总是有代价的。因此,在建立冗余索引
来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。
索引下推要求where条件列中有索引列
,可以先利用where条件中的索引列筛选数据SELECT * FROM A WHERE cc IN ( SELECT cc FRo B)I
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)
EXISTS:先遍历外表,再遍历内表
IN:先遍历内表,再遍历外表
MySQL总是优先与小表驱动大表,因为这样可以减少磁盘IO的次数。所以,选用EXISTS还是IN主要看外表和内表的大小:
当A小于B时,用EXISTS。因为EXISTS的实现,相当于外表循环,实现的逻辑类似于:
for i in A
for j in B
if j.cc == i.cc then ...
当A大于B时,用IN。因为实现的逻辑类似于:
for i in B
for j in A
if j.cc == i.cc then ...
# 执行结果0.011sec
SELECT COUNT(*) FROM student;
# 执行结果0.011sec
SELECT COUNT(1) FROM student;
# 执行结果0.011sec
SELECT COUNT(id) FROM student;
# 执行结果0.086
SELECT COUNT(classId) FROM student;
包括为null的记录
不包括null
。故而要遍历整个表进行判断,若不使用索引,则效率自然会差一些。如果你可以确定结果集只有一条
,那么加上LIMIT 1
的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度
。唯一索引
,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上LIMIT 1
了。错
!用自增做主键,架构设计上可能连及格都拿不到
。自增ID除了简单,其他都是缺点
。为了能够唯一地标识一个会员的信息,需要为会员信息表设置一个主键。那么,怎么为这个表设置主键,才能达到我们理想的目标呢?这里我们考虑业务字段做主键。
表数据如下
在这个表里,哪个字段比较合适呢?
卡号:会员卡号(cardno)看起来比较合适,因为会员卡号不能为空,而且有唯一性,可以用来标识一条会员记录。不同的会员卡号对应不同的会员,字段“cardno"唯一地标识某一个会员。如果都是这样,会员卡号与会员一一对应,系统是可以正常运行的。但实际情况
是,会员卡号可能存在重复使用的情况
。比如,张三因为工作变动搬离了原来的地址,不再到商家的i店消费了(退还了会员卡),于是张三就不再是这个商家门店的会员了。但是,商家不想让这个会员卡空着,就把卡号是“10000001”的会员卡发给了王五。
从系统设计的角度看,这个变化只是修改了会员信息表中的卡号是“10000001"这个会员信息,并不会影响到数据一致性。也就是说,修改会员卡号是“10000001”的会员信息,系统的各个模块,都会获取到修改后的会员信息,不会出现“有的模块获取到修改之前的会员信息,有的模块获取到修改后的会员信息,而导致系统内部数据不一致"的情况。因此,从信息系统层面上看是没问题的。
但是从使用系统的业务层面来看,就有很大的问题了,会对商家造成影响。
比如,我们有一个销售流水表(trans),记录了所有的销售流水明细。2020年12月01日,张三在门店购买了一本书,消费了89元。那么,系统中就有了张三买书的流水记录。
接着,我们查询一下2020年12月01日的会员销售记录:
如果会员卡“10000001”又发给了王五,我们会更改会员信息表。导致查询时:
这次得到的结果是:王五在2020年12月01日,买了一本书,消费89元。显然是错误的!结论:千万不能把会员卡号当做主键
。
会员卡号 or 身份证号:选择会员电话或身份证号,会员电话可以做主键吗?不行的。
在实际操作中,手机号也存在被运营商收回
,重新发给别人用的情况。
身份证号属于个人隐私
,顾客不一定愿意给你。
在淘宝的电商业务中,订单服务是一个核心业务。请问,订单表的主键 淘宝是如何设计的呢?是自增ID吗?
从上图可以发现,订单号不是自增ID
!我们详细看下上述4个订单号:
1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113
订单ID = 时间 + 去重字段 + 用户ID后6位尾号
全局唯一
且是单调递增
。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。36
字节,数据无序UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)
为什么UUID是随机无序的呢?因为UUID的设计中,将时间低位放在最前面,而这部分的数据是一直在变化的,并且是无序
。
改造UUID:
SET @uuid = UUID();
SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);
全局唯一
+单调递增
!有序UUID性能测试:16字节的有序UUID,相比之前8字节的自增ID,性能和存储空间对比究竟如何呢?我们来做一个测试,插入1亿条数据,每条数据占用500字节,含有3个二级索引,最终的结果如下所示:
从上图可以看到插入1亿条数据有序UUID是最快的,而且在实际业务使用中有序UIlD在业务端就可以生成。还可以进一步减少SQL的交互次数。另外,虽然有序UUID相比自增ID多了8个字节,但实际只增大了3G的存储空间,还可以接受。
在当今的互联网环境中,非常不推荐自增ID作为主键的数据库设计。更推荐类似有序UUID的全局唯一的实现。
另外在真实的业务系统中,主键还可以加入业务和系统属性,如用户的尾号,机房的信息等。这样的主键设计就更为考验架构师的水平了。