[TOC]
MySQL各种优化
查询优化
查询优化器模块
查询优化器的任务是发现执行 SQL 查询的最佳方案。大多数查询优化器,要么基于规则、要么基于成本。
大多数查询优化器,包含 MySQL 的查询优化器,总或多或少地在所有可能的查询评估方案中搜索最佳方案
MySQL 中 MySQL Query Optimizer 是优化器的核心,当 MySQL 数据拿到一个 Query 语句之后会交给 Query Optimizer 去解析,并产生一个最优的执行计划(这个是 Optimizer 认为是最优的,但不一定是真正最优的,就跟 Oracle 数据库会估算错 rows 一样)。
然后数据库按照这个执行计划去执行查询语句。
在 SQL 语句整个执行过程中,Optimizer 是最耗时的,但是也有第三方工具为了提高性能绕开 MySQL 的 Query Optimizer 模块,比如:handlerSocket。
对于多表关联查询,MySQL 优化器所查询的可能方案数随查询中引用的表的数目成指数增长。
对于小数量的表,这不是一个问题。
然而,当提交的查询需要的结果集很大时,查询优化所花的时间会很容易地成为服务器性能的瓶颈。
查询优化的一个更加灵活的方案时容许用户控制优化器详细地搜索最佳查询评估方案。一般思想是调查的方案越少,它编译一个查询所花费的时间越少。
optimizer_prune_level
变量告诉优化器根据对每个表访问的行数的估计跳过一些方案。我们的试验显示该类 “有根据的猜测” 很少错过最佳方案,并且可以大大降低查询编辑次数。
这就是为什么默认情况该选项为on(optimizer_prune_level=1)
。
然而,如果你认为优化器错过了一个更好的查询方案,则该选项可以关闭(optimizer_prune_level=0),风险是查询编辑花费的时间更长。
请注意即使使用该启发,优化器仍然可以探测呈指数数目的方案。
timizer_search_depth
变量告诉优化器对于每个未完成的 “未来的” 方案,应查看多深,以评估是否应对它进一步扩大。
optimizer_search_depth
值较小会使查询编辑次数大大减小。
例如,如果 optimizer_search_depth
接近于查询中表的数量,对 12、13 或更多表的查询很可能需要几小时甚至几天的时间来编译。
同时,如果用 optimizer_search_depth
等于 3 或 4 编辑,对于同一个查询,编译器编译时间可以少于 1 分钟。
如果不能确定合理的 optimizer_search_depth
值,该变量可以设置为 0,告诉优化器自动确定该值。
查询优化的基本思路
1. 优化更需要优化的 Query 语句
应该优化并发高的 Query 语句,不至于高并发下,由于 SQL 导致应用程序卡死,比如 php-fpm 的大量等待,而且一个高并发的 Query 语句,如果走错执行计划,本来只需要扫描几百行,结果扫描了几百万行,可能会有灾难性的后果,更加会导致业务卡顿,尤其是核心业务下出现的高并发 Query 语句。
2. 查看执行计划调整 Query 语句
根据 explain extended SQL 分析查询语句,就能查看执行计划,这个时候需要关注执行计划中的一些要素:
- id:查询的序列化
select type
- depent subquery:说明该查询是子查询中的第一个 Select, 依赖与外部查询的结果集
- PRIMARY:子查询的最外层查询,注意不是主键查询
- simple:除子查询或者 UNION 之外的其它查询
- table:访问数据表的名称,书写 SQL 的人,需要明确此表是否是核心表、是否是大数据量表等
type 扫描方式
- all:全表扫描
- const:读常量,且最多只有一条记录匹配。由于是常量只需要读一次
- index:全索引扫描
- eq_ref:最多只有一条匹配结果 通过主键和唯一索引来访问的
- range:索引范围扫描
- possible_keys:该查询可以利用到的索引有哪些
- key:优化器模块选择用了哪个索引,有索引不一定就会用到,看执行计划才知道用了哪个。
- key_len:索引长度
- rows:返回的行数
- extra:附加信息,比如 using filesort---> 说明用了排序算法
- filtered:列给出了一个百分比的值,这个百分比值和 rows 列的值一起使用,可以估计出那些将要和 QEP 中的前一个表进行连接的行的数目。前一个表就是指 id 列的值比当前表的 id 小的表。这一列只有在 EXPLAIN EXTENDED 语句中才会出现。
3. 学会查看性能损耗(cpu 消耗、io 消耗)
当发现有慢 Query 语句时,需要定位到底是哪里慢,CPU 还是 IO 等:
mysql>set profiling=1;
mysql>show profiles;
mysql>show profile cpu,block io for query n;
查询的基本原则
1. 永远用小结果集驱动大结果集
做 join 查询时,驱动表,一定是条件限定后记录较少的表。
MySQL 的 join 只有一种算法 nested loop 也就是程序中的 for 循环,通过嵌套循环实现,驱动结果集越大,所需要循环的次数越多,访问被驱动表的次数也越多。
降低 IO 同时降低 CPU。
2. 只查询需要的列
只查询需要的列,可以让 IO 降低,列和排序算法也有关系。
3. 仅仅使用最有效的过滤条件
前提是用 a 条件 查询出结果 用 b 条件查询出结果,a、b 都用查询出结果,这三次结果都一样。
到底是用 a 条件还是 b 条件,还是两个条件都限定,只能看执行计划。
4. 尽量避免复杂的 join 和子查询
在 MySQL 中(仅限于 MySQL) CROSS JOIN 与 INNER JOIN 的表现是一样的,在不指定 ON 条件得到的结果都是笛卡尔积,反之取得两个表完全匹配的结果。 INNER JOIN 与 CROSS JOIN 可以省略 INNER 或 CROSS 关键字
5. 尽量在索引列上完成排序和查询
- 在索引列上排序:索引列上是排好序的,不需要启动额外的排序的算法降低了 CPU 的损耗。
- 在索引列上查询:降低了 IO 的损耗
- 创建索引,优化器模块并不一定会用,但可以 SQL 中加上 force index(强制走那个索引).
索引利弊及索引分类
万事万物都有利弊,一个东西的出现,比如会在不同场景下有好好坏,就看如何权衡。
好处:
- 通过索引列查询数据,能够提高数据检索的效率,降低数据库的 IO 成本。
- 通过索引列对数据进行排序,降低数据排序的成本,降低了 CPU 的消耗。
坏处:
- 假设表 a 其中有列 column ca 给其创建索引 indxaca:
- 每次更新 ca 的操作,都会调整因为更新所带来的键值变化后的索引信息,这样就会增加 IO 损耗,索引列也是要占用空间的,a 列数据的增多,indxaca 索引占用的空间也会不断增长。所以索引还会带来存储空间资源的消耗。
索引分类
- b-tree 索引:根据平衡二叉树演变来的
- hash 索引:
- hash 索引只能满足 "="、"in" <> 查询,不能支持范围查询
- hash 索引无法被利用进行排序操作
- hash 索引不能利用部分索引键查询
- hash 索引不能避免表扫描
- full-text 索引:只有 myisam 存储引擎支持 ---> 只有 char 、varchar、text 支持,但是在 MySQL 5.7,innodb 存储引擎也支持啦。
- R-Tree 索引:主要解决空间数据检索问题,极少使用。
索引相关优化
1. 如何判断是否需要创建索引
- 频繁作为查询条件的字段应该创建索引。
- 唯一性太差的字段不适合单独创建索引。比如该字段重复上千万;即使你创建了索引优化器模块是不会选择使用的;会有极大的性能问题 有很多重复值,会带来大量的随机 IO 甚至是重复 IO。
- 更新非常频繁的字段不适合创建索引:不仅仅更新表中的数据,还需要更新索引数据 IO 访问增大。
- 不会出现在 where 字句中的字段不该创建索引。
- 单键索引还是组合索引。
2. MySQL 中索引的限制
- 是否用到了索引可以查看执行计划
- 在任何索引列上做计算、函数、类型转换(哪怕是自动的)都会使得索引失效而转向全表扫描操作:不要在索引列上做任何操作因为可能为导致索引失效。
- MySQL 在使用不等于 (!= or <>) 的时候无法使用索引会导致全表扫描。
- is null ,is not null 也无法使用索引。
- join 语句中 join 条件字段类型不一致的时候 MySQL 无法使用索引。
- 模糊查询的时候 (like 操作) 如果以通配符开头 ('%abc...')MySQL 索引失效会变成全表扫描的操作。
- 如果使用的是 hash 索引,在做非等值连接时候无法使用索引,会是全表扫描的操作。
- 在 MySQL 中 BLOB 和 Text 类型的列只能创建前缀索引。
- MyISAM 存储引擎的话索引键长度总和不能超过 1000 字节。(好像从 5.7 之后,大多默认 innodb 存储引擎)
- 当有唯一性索引和非唯一性索引都存在时,往往只会选择唯一性索引。
- 组合索引,查询时组合索引第一列出现的时候会使用索引。
3. 使用索引的一些建议
- 对于单键索引,尽量选择针对当前 Query 过滤性更好的索引。
- 在选择组合索引的时候,当前 Query 中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
- 在选择组合索引的时候,尽量选择可以能够包含当前 Query 中的 where 字句中更多字段的索引。
- 尽可能通过分析统计信息和调整 Query 的写法来达到选择合适索引的目的。减少通过使用 Hint 认为控制索引的选择,如果使用 Hint 会使得后期维护成本比较高。
- 综上所述,大致简单明了的阐述了 MySQL 查询优化一些相关的东西,至少对于中小型企业,可以作为研发人员的数据库规范,避免后期迁移或扩容时的一些问题。一切相关问题可以后续读者圈交流,谢谢大家耐心看完。
索引优化
问:为什么索引结构默认使用B-Tree,而不是hash,二叉树,红黑树?
hash:虽然可以快速定位,但是没有顺序,IO复杂度高。
二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。
红黑树:树的高度随着数据量增加而增加,IO代价高。
问:为什么官方建议使用自增长主键作为索引。
结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。
事物隔离机制
事务隔离级别
- 注意: mysql默认的事务隔离级别为repeatable-read(可重复读),可以通过修改配置文件来改变事务隔离级别.
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
何为脏读
比如:
- 进程1 开启事务, 修改了数据A.这时候还未提交.
- 进程2 读取了数据B,此时数据B显然为进程1修改后的数据.
- 进程1 回滚了事务,数据B会回滚到修改前的状态.
- 此时, 进程2读到的就是脏数据.
读取未提交的数据,也被称之为脏读(Dirty Read)。
注意:
新手比较容易搞混淆这一块.
上面的例子,在不同事务隔离级别下的结果是不一样的,这里仅仅描述何为脏读.具体可以参考上面的表格.
如果是用同一个进程的话,肯定是可以读的到的.
比如,用同一个cli开启事务,修改数据,再查询,会发现读到的就是修改后的数据.
如果想要测试的话,可以使用两个cli来做测试.
何为不可重复读
比如:
- 进程1 开启事务,同时多次进行读取 数据A.
- 进程2 开启事务,对数据A进行修改并提交.
- 进程1 多次读取数据A的的结果前后不一样.
何为幻读
比如:
- 进程1 开启事务,将表中所有的数据全部进行删除.
- 进程2 开启事务,新插入一条数据.
- 进程1 再查查询表数据,发现居然还有一条数据,就像发生了幻觉一样.
这就是幻读.
何为串行化
这是事务的最高隔离级别,在每个读的数据上加锁进而强制事务排序,使之不可能相互冲突,因此,在这个级别尚,可能导致大量的超时现象和锁竞争。
小结 (不可重复读和幻读的区别)
不可重复读的和幻读有点像,平时很容易混淆,不可重复主要读侧重于修改,幻读主要侧重于新增和删除。
解决不可重复读的问题只需锁行即可,解决幻读则需要锁表.
事务的基本要素(ACID,ACID为四个单词的首字母)
原子性(Atomicity)
事务必须是原子工作单元;对于其数据修改,要么全都执行commit,要么全都不执行rollback,不可能停滞在中间某个环节。
事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency)
一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
也就是说:如果事务是并发多个,系统也必须如同串行事务一样操作。
比如,人们之间相互转账,无论多少个操作并发执行,都应该当做串行操作,即人们的总金额在转账过程中应该是不变的。
在事务开始之前和事务结束以后,数据库的完整性约束必须保证没有被破坏,即数据是正确的。
隔离性(Isolation)
隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。
隔离性是指多个用户并发访问数据库时,多个并发事务之间各自独立,不会相互干扰。隔离是通过用锁机制实现的。
持久性(Durability
事务完成之后,事务对数据库的影响是永久性的。该修改即使出现问题也将一直保持,不能回滚。
索引原理
实际上就是下面<高性能MySQL第三版里面的索引基础这一片的内容>后面整理一下,弄过来就行了
存储引擎的原理与区别
高性能MySQL第三版 度过的部分篇幅
第四章 Schema与数据类型优化
Schema: 模式
本章内容时为接下来的两个章节做铺垫.
在这三章中,我们将讨论逻辑设计,物理设计,查询执行,以及他们之间的相互作用.
如果再回头来看这一张,会发现本章很有种,很多讨论的议题不能孤立的考虑.
本章只讨论,基本的数据类型.MySQL为了兼容支持很多别名,例如INTEGER,BOOL,以及NUMERIC.
他们都只是别名,这些别名可能会令我们不解,但是不会影响性能.
如果建表时采用数据类型的别名, 我们使用SHOW CREATE TABLE检查,会发现MySQL报告的时基本类型,而不是别名.
选择优化的数据类型
良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema,这往往要权衡各种因素.
例如,反范式的设计可以加快某些类型的查询,但同时可能使另一种类型的查询变慢.
比如添加计数表和汇总表是一种很好的优化查询的方式,但这些表的维护成本可能会很好.
MySQL独有的特性和实现细节对性能的影响也很大.
选择优化的数据类型
MySQL支持的数据类型非常多,选择正确的数据类型对于获得高性能至关重要.
不管储存那种类型的数据,记得遵循以下原则,可以做出更好的选择:
更小的通常更好
个人总结:
简而言之就是要,在满足我们存储范围时,选择尽可能小的,小到恰到好处的存储范围.
一般情况下, 应该尽量使用可以正确存储数据的最小数据类型(泪如存储0-200,选择tinyint unsigned更好).
更小的数据类型通常更快,因为他们占用的磁盘,内存和CPU缓存,更少.并且处理时需要的CPU周期也更少.
但是要保证期没有低估需要存储的值的范围,因为在schema中的多个地方增加数据类型的范围是一个非常耗时的痛苦操作.
如果无法确定那个数据累心最好,就选择你认为不会超过范围的最小类型.(如果系统不是很忙,或者存储的数据量不多,或者是在可以轻易修改设计的早期阶段,那之后修改数据类型也会比较容易)
简单就好
个人总结:
简而言之,就是能用整型的用整型,整型比字符串的查询速度更快.
因为整型的排序规则比字符串的更加简单.这里这个不再深究,知道就好.
简单数据类型的操作通常需要更少的CPU周期.
比如,整型比字符串操作带价更低,因为字符串集和校对规则(排序规则)使字符串比较,比整型比较更加更加的复杂.
这里举两个例子:
一个是应该使用MySQL内建的类型(date,time,datatime等等),而不是字符串来存储日期和时间,另外一个是应该用整型来存储IP地址.
尽量避免NULL
个人总结:
在设计表时,除非必须,否则不要用NULL值.
使用NULL值会使MySQL的索引列,索引统计和值比较,更加的复杂.
在非InnoDB中,NULL值会占用更多的磁盘空间,因为,InnoDB使用的单独的位(bit)存储的NULL值.
NULL值可能会让MyISAM的的固定大小的索引编程,变成可变大小的索引
很多表都包含可为NULL(空值)的列,即使应用程序并不需要保存NULL也是如此.
这是因为NULL是列的默认属性(如果定义表结构时没有指定列为NOT NULL,默认都是允许为NULL的).
通常情况下最好指定列为NOT NULL, 除非真的需要存储NULL值.
如果查询中包含可为NULL的列,对MySQL来说会更加难以优化,因为NULL的列索引,索引统计和值比较,都更加复杂.
可为NULL的列会使用更多的储存空间,在MySQL里也需要特殊处理.
当可为NULL的列被索引时,每个索引记录需要一个额外的字节.
在MyISAM里甚至可能会导致固定大小的索引(例如只有一个整数列的索引),变成可变大小的索引
通常把可为NULL的列改为NOT NULL带来性能提升比较小,所以在调优时没有必要首先在现有的schema中查找并修改掉这种情况,除非确定这回导致问题.
但是,如果计划在列上建立索引,就应该避免设计成可以为NULL的列.
当然也有例外,例如值的一提的时,InnoDB使用单例的位(bit) 存储NULL值,所以对于稀疏数据(很多值为NULL,只有少数行的列有非NULL值)有很好的空间效率.但是这一点不适用于MyISAM.
在为列选择数据类型时,首先要确定合适的大类型: 数字,字符串,时间等.
然后选择具体类型,很多MySQL的数据类型可以存储相同类型的数据,只是存储的长度和范围不一样,允许的精度不同,或者需要的物理空间不同.
相同的大类型的不同子类型数据有有时也有一些特殊的行为和属性.
比如:DATATIME和TIMESTAMP列都可以存储相同类型的数据: 时间和日期,精确到秒.
然而TIMESAMP只使用DATATIME一般的存储空间,并且会根据时区变化,具有特殊的自动更新能力.
另一方面,TIMESTAMP允许的时间范围要小得多,有时候他的特殊能力又会成为了障碍.
整数类型
个人总结:
int(n)中的n指的是存储空间的位即bit位又即4个字节,不是可以存多长,比如int(11)不是指可以存11位.
所以说int(11) 和int(1)是没有区别的,区别在于限制MySQL-cli显示时的长度
有两种类型的数字, 整数(whole number) 和实数(real number).
如果存储整合素,可以使用集中整合素类型: tinyint,smallint,mediumint,int,bigint. 分别使用8,16,24,32,64位存储空间.
他们可以存储的值的范围从-1(n-1)次方到2(n-1)-1次方, 其中N是存储空间的位数即bit位.
整数类型有可选的UNSIGNED属性,标识不允许负值,这大致可以使整数的上限提高一倍.
例如TINYINT,UNSIGNED可以存储的范围是0~255,而TINYINT的存储范围是-128~127.
有符号和无符号类型使用相同的存储空间,并具有相同的性能, 因此可以根据实际情况选择合适的类型.
你的选择决定MySQL是怎么在内存和磁盘中保存数据的, 然而整数计算一般使用64位的bigint整数,及时在32位环境中也是如此.(一些聚合函数是例外,他们使用DECIMAL或者DOUBLE进行计算).
MySQL可以为整数类型指定宽度, 例如int(11),对大多数应用这是没有意义的: 他不回限制值的合法范围,只是规定了MySQL的一些交互工具(例如MySQL命令行工具)用来显示字符的个数.
对于存储和计算来说,int(1)和int(20)是相同的.
(对于书上这两句话, 这么说,我们在用的时候确实,int(3)可以存储11位数, 但是有一回我遇到int11不能存储11位的手机号,哪怕是11个1也不行, 百思不得其解,按说应该是没问题的.)
实数类型
个人总结
普通的浮点运算是使用的CPU的标准浮点运算,所以不能保持精度.且浮点运算如:float(11)也只是指定存储的bit位的长度而已.
普通的浮点运算是直接使用CPU的标准运算,而DECIMAL是MySQL自己在5.0+自己实现的,所以普通浮点运算的效率要快很多.
浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间.
非必要,不要使用DECIMAL, 因为DECIMAL可以保持高精度计算,但是会占用非常多的磁盘空间.数据量特别大的时候,可以使用BIGINT类型.然后把小数位相应的位数即可.
实数是带有小数部分的数字,然而他们不只是为了存储小数部分;也可以使用DECIMAL存储比DECIMAL还打的整数.MySQL即支持精确类型,也支持不精确类型.
FLOAT和DOUBLE类型支持使用标准的浮点运算进行近似计算.如果需要知道浮点运算是怎么计算的,则需要计算所使用的平台的浮点数的具体实现.
DECIMAL类型用户存储精确的小数, 在MySQL5.0+的版本中,DECIMAL类型支持精确计算.而MySQL4.1-则使用浮点运算来实现DECIMAL的计算,这样会因为精度损失导致一些奇怪的结果.
在这些版本的MySQL中,DECIMAL只是一个存储类型.
因为CPU不支持对DECIMAL的直接计算,所以在MySQL5.0以及更高版本中,MySQL服务器自身实现了DECIMAL的高精度计算.
相对而言,CPU直接支持原生浮点运算,所以浮点运算明显更快.
浮点和DECIMAL类型都可以指定精度.对于DECIMAL列,可以指定小数位前后所允许的最大位数.这回影响到空间消耗.
MySQL5.0+将数字打包保存到一个2进制的字符串中(每4个字节存9个数字).
例如,DECIMAL(18.9)小数点两边个存储9个数字,一共使用9个字节:小数前的数字使用4个字节,小数位后面使用4个字节,小数点本身占一个字节.
MySQL5.0+中的DECIMAL类型允许最多65个数字. 而早期的MySQL版本中这个限制是254个数字.并且保存为未压缩的字符串,即每个数字一个 字节.
然而,这些早期版本实际上并不能在计算中使用这么大的数字,因为DECIMAL只是一种存储格式;在计算中DECIMAL中会转为DOUBLE类型.
有多种方法可以指定浮点列所需要的精度, 这回让MySQL悄悄的选择不同的数据类型,或者在存储时对值进行取舍.
这些精度定义时非标准的, 所以我们建议只指定数据类型,不指定精度.
浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间.
FLOAT使用4个字节存储.DOUBLE占用8个字节,相比FLOAT有更高的精度和更大的范围. 和整数类型一样,能选择的知识存储格式,MySQL使用DOUBLE作为内部浮点运算的类型.
因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL.例如存储的财务数据.
但在数据量比较大的时候,可以考虑使用BIGINT代替DECIMAL,将需要存储的货币单位,根据小数位乘以相应的倍数即可.
假设要存储财务精确到万分之一分,则可以把所有金额乘以一百万,然后将结果存储到BIGINT中.
这样可以同时避免浮点存储计算不精确和DECIMAL精确计算的代价高的问题.
字符串类型
MySQL支持多种字符串类型,每种类型还有很多编程. 这些数据类型在4.1和5.0版本发生了很大的变化.
从4.1版本开始,每个字符串列可以定义自己的字符串集和排序规则,或者说校对规则(collation),这些东西会很大程度上影响性能.
VARCHAR和CHAR类型
VARCHAR和CHAR是两种最主要的字符串类型,不过很难精确的说明这些值是怎么存储在磁盘和内存中的,因为这和存储引擎的具体实现有关.
下面所有的描述都是假设,使用的存储引擎是InnoDB或MyISAM.如果不是这种的,到时候就要单独看文档了.
先看看VARCHAR和CHAR的值是怎么存储在磁盘上的,注意,存储引擎存储CHAR或者VARCHAR值的方式在内存中和磁盘上的可能不一样,
所以MySQL服务器从存储引擎读取的值可能需要转换为另一种存储格式. 下面是两种类型的比较:
VARCHAR
VARCHAR类型用于存储可变长字符串,是最常见的字符串数据类型.他比定长类型更节省空间,因为它仅使用必要的空间(例如,越短的字符串使用越少的空间).
有一种情况例如,就是MySQL表中使用ROW_FORMAT=FIXED创建的话,每一行都会使用订场存储这样很浪费空间.
VARCHAR需要使用1或2个额外字节记录字符串的长度: 如果列的最大长度小于或等于255字节,则使用1个字节标识,否则使用2个字节标识.
假设采用latin1字符集,一个VARCHAR(10)的列需要11个字节的存储空间.VARCHAR(1000)的列则需要1002个字节,因为需要2个字节存储长度信息.
VARCHAR节省了存储空间,所以对性能上也有帮助.但是由于行是编程的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作.
如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的.
例如,MyISAM会将行拆成不同的片段存储,InnoDB则要分裂页来使行可以放进页内.其他一些存储引擎也许从不在原数据位置更新数据.
下面这些情况下使用VARCHAR是合适的:字符串列的最大长度币平均长度大很多;
列的更新很少,所以碎片不是问题;使用了像UTF-8这样复杂的字符集,每个字符都使用不同的字符数进行存储.
在5.0或者更高的版本,MySQL在存储和索引时会保留末尾空格.但在4.1或更老的版本,MySQL会提出末尾空格.
InnoDB则更加灵活,他可以把过长的VARCHAR存储为BLOB,我们稍后讨论这个问题.
CHAR
CHAR类型是定长的: MySQL总是根据定义的字符串长度分配足够的空间.
存储CHAR值时,MySQL会删除所有的末尾空格(在MySQL4.1和更老的版本中VARCHAR也是这样实现的,也就是说这些版本中的CHAR和VARCHAR在逻辑上是一样的,区别只是存储格式上).
CHAR值会根据需要采用空格进行天空以方便比较.
CHAR适合存储很短的字符串,或者所有的值都接近同一个长度.
例如:CHAR非常适合存储密码的MD5只,因为是一个定长的值.对于经常编程的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型不容易产生碎片.
对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率.
例如:用CHAR(1)来存储只有Y和N的值,如果采用单字节字符集只需要一个字节,但是VARCHAR(1)却需要两个字节,因为还有一个记录长度的额外字节.
CHAR 类型的这些行为可能感觉比较难理解,这里举一个例子:
首先我们创建一张只有一个CHAR(10)字段的表并且插入一些值:
CREATE TABLE char_test(char_col CHAR(10));
INSERT INTO char_test(char_col) VALUES ('string1'),(' string2'),('string3 ');
当索引这些值的时候,会发现string3末尾空格被过滤了
SELECT CONCAT("'",char_col,"'") FROM char_test;
mysql> SELECT CONCAT("'",char_col,"'") FROM char_test;
+--------------------------+
| CONCAT("'",char_col,"'") |
+--------------------------+
| 'string1' |
| 'string2' |
| 'string3' |
+--------------------------+
3 rows in set (0.01 sec)
如果使用VARCHAR(10)字段存储相同的值,可以得到如下结果
SELECT CONCAT("'",char_col,"'") FROM char_test;
mysql> SELECT CONCAT("'",varchar_col,"'") FROM varchar_test;
+--------------------------+
| CONCAT("'",varchar_col,"'") |
+--------------------------+
| 'string1' |
| ' string2' |
| 'string3 ' |
+--------------------------+
3 rows in set (0.01 sec)
数据如何存储取决于存储引擎,并非所有的存储引擎都会按照形同的方式处理定长和编程字符串.
Memory引擎只支持定长的行,即使有变长字段也会根据最大长度进行分配最大空间.
不过,填充和截取空格的行为在不同存储引擎中都是一样的,因为这是在MySQL服务器层进行处理的.
与CHAR和VARCHAR类似的类型还有BINARY和VARBINARY,他们存储的时二进制字符串.
二进制字符串根常规字符串非常想死,但是二进制字符串存储的时字节码而不是字符.
填充也不一样,MySQL填充BINARY采用的时\0(0字节)而不是空格, 在检索时也不会去掉填充值.
当需要存储二进制数据,并且希望MySQL使用字节码而不是字符进行比较时,这些类型是很有用的.
二进制比较的优势并不仅仅提现大小写敏感上.
MySQL比较BINARY字符时,每次按一个字节,并且根据该字节的数值进行比较.
因此,二进制比较比字符比较简单很多,所以也就更快.
慷慨是不明智的,需要多少分配多少
VARCHAR(5)和VARCHAR(200)存储'hello'的空间开销是一样的, 那么使用更短的列有什么优势呢?
事实证明有很大的优势,更长的列会消耗更多的内存,因为MySQL通常会分配固定大小的内存块来保存内部值.
尤其是使用内存临时表进行排序或操作时会特别糟糕,在利用磁盘临时表进行排序时也同样糟糕.
所以最好的策略是只分配真正需要的空间.
BLOB和TEXT类型
BLOB和TEXT都是为了存储很大的数据而设计的字符串数据类型,分别采用二进制和字符方式存储.
实际上,他们分别数据两组不同的数据类型: 字符类型是TINYTEXT,SMALLTEXT,TEXT,MEDIUMTEXT,LONGTEXT;
对应的二进制类型是TINYBLOB,SMALLBLOB,BLOB,MEDIUMBLOB,LONGBLOB.BLOB是SMALLBLOB的同义词,TEXT是SMALLTEXT的同义词.
与其他类型不同,MySQL把每个BLOB和TEXT的值当做一个独立的对象处理.
存储引擎在存储时通常会做特殊处理. 当BLOB和TEXT值太大时,InnoDB会使用专门的'外部'存储区域来进行存储,
此时每个值在行内需要1~4个字节存储一个指针,然后在外部存储区域存储实际的值.
BLOB和TEXT家族之间仅有的不同是BLOB存储类型的时二进制数据,没有排序规则或字符集,而TEXT类型有字符集和排序规则.
MySQL对BLOB和TEXT列进行排序与其他类型是不同的:它只对每个列的最前max_sort_length字节而不是整个字符串做排序.
如果只需要排序前面一小部分字符,则可以减小max_sort_length的配置,或者使用ORDER BY SUSTRING(column,length).
MySQL不能将BLOB和TEXT列全部长度的字符串进行索引,也不能使用这些索引消除排序.
磁盘临时表和文件排序
因为Memory引擎不支持BLOB和TEXT类型,所以,如果查询使用了BLOB或TEXT列并且需要使用隐式临时表,
将不得不使用MyISAM磁盘临时表,即使只有几行数据也是如此(Percona Server的Memory引擎支持BLOB和TEXT类型,但值到我看书的时候,通常的场景下,还是需要使用磁盘临时表.
这回导致严重的性能开销,即使配置MySQL将临时表存储在内存块设备上,依然需要许多昂贵的系统调用.
最好的解决方案是尽量避免使用BLOB和TEXT类型,如果实在无法避免,有一个技巧
是在所有用到BLOB字段的地方都使用SUBSTRING(column,length)将列值转换为字符串(在ORDER BY子句中也适用),这样就可以使用内存临时表了.
但是要确保截取的子字符串足够短,不会使临时表的大小超过max_heap_table_size或者tmp_table_size,超过以后MySQL会将内存临时表转换为MyISAM磁盘临时表.
最坏情况下的长度分配对于子排序的时候也是一样的,所以这一招对于内存中创建大临时表和文件排序,以及在磁盘创建大临时表和文件排序这两种情况都很有帮助.
如果假设有一个1000万行的表,占用几个GB的磁盘空间. 其中有一个utf8字符集的VARCHAR(100)列,每个字符最懂使用3个字符,最坏的情况下需要3000字节的空间.
如果ORDER BY中用到这个列,并且扫描整个表,为了排序就需要超过20GB的做临时表.
如果使用EXPLAIN执行计划的Extra列包含'Using temporary',则说明这个查询使用了隐式临时表.
使用枚举(ENUM)代替字符串类型
有时候可以使用美剧列代替常用的字符串类型.枚举列可以把一些不重复的字符串存储成一个预定义的集合.
MySQL在存储枚举时非常紧凑,会根据列表值的数量压缩到一个或者两个字节中. MySQL在内部会将每个值在列表中国的位置保存为整数,并且在表的.frm文件中保存'数字-字符串'映射关系的查找表.
这里有一个例子:
CREATE TABLE enum_test(
e ENUM('fish','apple','dog') not null
);
INSERT INTO enum_test(e) VALUES('fish'),('dog'),('apple');
这三行数据实际存储为整数,而不是字符串. 可以通过在数字上下文环境检索看到这个双重属性:
select e+0 from enum_test;
mysql> select e+0 from enum_test;
+-----+
| e+0 |
+-----+
| 1 |
| 3 |
| 2 |
+-----+
3 rows in set (0.00 sec)
如果使用数字作为ENUM枚举常量,这种双重性很容易导致混乱, 例如:ENUM('1','2','3').所以最好不要这么做.
枚举字段时按照内部存储的整数而不是定义的字符串进行排序的:
select e from enum_test order by e;
mysql> select e from enum_test order by e;
+-------+
| e |
+-------+
| fish |
| apple |
| dog |
+-------+
3 rows in set (0.00 sec)
一种绕过这种限制的方式是按照需要的排序来定义枚举列. 另外也可也在排序中使用FIELD()函数显示的指定排序顺序,但是这回导致MySQL无法利用索引消除排序.
select e from enum_test order by field(e,'apple','dog','fish');
mysql> select e from enum_test order by field(e,'apple','dog','fish');
+-------+
| e |
+-------+
| apple |
| dog |
| fish |
+-------+
3 rows in set (0.00 sec)
如果在定义时就是按照字母的顺序,就没有必要这么做了.
枚举最不好的地方是,字符串列表时固定的, 添加或删除字符串必须使用ALTER TABLE.
因此对于一系列未来可能会改变的字符串, 使用枚举不是一个好主意, 除非能接受只在列表末尾添加元素, 这样在MySQL5.1中就可以不用重建整个表来完成修改.
由于MySQL把每个枚举值保存为整数,并且必须进行查找才能转换为字符串,所以枚举列有一些开销.
通常枚举的列表都比较小,所以开销还可以控制,但也不能保证一直如此.
在特定情况下,把CHAR/VARCHAR列与枚举列进行关联可能会比直接关联CHAR/VARCHAR更慢.
为了说明这个情况,我们队一个应用中的一张表进行了基准测试, 看看在MySQL中执行上面说的关联速度如何.
该表有一个很大的主键:
CREATE TABLE webservicecalls (
day date NOT NULL,
account smallint NOT NULL,
service varchar(10) NOT NULL,
method varchar(50) NOT NULL,
calls int NOT NULL,
items int NOT NULL,
time float NOT NULL,
const decimal(9,5) NOT NULL,
updated datetime,
PRIMARY KEY (day,account,service,method)
) ENGINE=InnoDB;
这表有11万行的数据,只有10MB大小,所以可以完全载入内存. service 列包含了5个不同的值,平均长度为4个字符, method列包含了71个值,怕暖个就长度为20个字符.
我们把上面那个表改成下面这个样子,把service和method字段改成枚举类型:
CREATE TABLE webservicecalls (
...omitted...
service ENUM(...values omitted...) NOT NULL,
method ENUM(...values onmitted...) NOT NULL,
...omitted...
) ENGINE=InnoDB;
然后我们用主键关联这两个表,下面是锁使用的查询语句:
select sql_no_cache count(*)
from webservicecalls
join webservicecalls USING(day,account,service,method);
然后我们用VARCHAR和ENUM分别测试了这个语句,结果如下.
测试 QPS
VARCHAR 关联 VARCHAR 2.6
VARCHAR 关联 ENUM 1.7
ENUM 关联 VARCHAR 1.8
ENUM 关联 ENUM 3.5
从上面的节骨偶可以看出,当把列都转换成ENUM之后,关联变得很快.但是当VARCHAR列和ENUM列进行关联时则慢很多.
在本例中,如果不是必须和VARCHAR进行不啊两年,那么转换这些列为ENUM就是一个好主意.
这是一个通用的设计实践,在查找表时用整数主键,避免采用基于字符串的值进行关联.
然后转换为枚举类型还有另一个好处,根据SHOW TABLE STATUS命令输出结果中的DATA_LENGTh,
把这两个转换为ENUM可以让表的大小缩小1/3.
在某些情况下,即使可能出现ENUM和VARCHAR进行挂了年的情况,这也是值得的(可以节省很多IO).
同样,转换后主键也只有原来的一半大小了.因为这是InnoDB表,如果表上有其他suoyin,减小主键大小会是非主键索引也变小.
日期和时间类型
MySQL可以使用许多类型来保存日期和时间值,例如YEAR和DATE.
MySQL能存储的最小时间颗粒为妙(MariaDB支持微妙级别的时间类型).
但是MySQL也可以使用微妙级的颗粒进行临时运算,我们会展示怎么绕开这种存储限制.
大部分时间类型都没有替代品,因此没有什么最佳选择的问题.
唯一的问题是保存日期和时间的时候需要做什么.MySQL提供两种想死的日期时间:DATETIME和TIMESTAMP.
对于很多应用程序,他们都能工作, 但是某些场景,一个比另一个工作的好.
DATETIME
这个类型能保存大范围的值, 从1001年到9999年,精度为妙.
他把日期和时间封装到各市为YYYYMMDDHHMS的整数中,与时区什么的无关, 使用8个字节的存储空间.
默认情况下MySQL以一种可排序的,无歧义的格式显示DATETIME值,例如:"2008-01-16 22:37:08".这是ANSI标准定义的日期和时间表示方法.
TIMESTAMP
就像他的名字一样,TIMESTAMP类型保存了从1970年1月1日午夜(格林尼治标准时间)以来的妙数,他和UNIX时间戳相同.
TIMESTAMP只使用4个字节的存储空间.
因为他的范围比DATETIME小的多:只能表示从1970年到2038年.
MySQL提供了FROM_UNIXTIME()函数把Unix时间戳转换为日期格式,兵提供了UNIX_TIMESTAMP()函数把日期转换为UNIX时间戳.
MySQL4.1+按照DATETIME的方式格式化TIMESTAMP的值,但是MySQL4.0-不会再各个部分之间显示任何标点符号.
这仅仅是显示格式上的区别, TIMESTAMP的存储格式在各个版本都是一样的.
TIMESTAMP显示的值也以来于时区.MySQL服务器,操作系统,以及客户端连接都有时区设置.
因此存储值为0的TIMESTAMP在美国东部时区显示为'1969-12-31 19:00:00',和格林尼治时间差了5个小时.
有必要强调一下这个区别: 如果在多个时区存储或访问数据,TIMESTAMP和DATETIME的行为将很不一样.
前者提供的值与时区有关系,后者则保留文本表示的日期和时间.
TIMESTAMP也有DATETIME没有的特殊属性.
默认情况下,如果插入时没有指定第一个TIMESTAMP列的值,MySQL则设置这个列的值为当前时间(TIMESTAMP的行为规则比较负责,并且不同版本也不一样,
必要的时候修改完TIMESTAMP后用 SHOW CREATE TABLE命令检查输出).
在插入一行记录时,MySQL默认也会更新第一个TIMESTAMP列的值(除非在update语句中明确制定了值).
我们可以配置任何TIMESTAMP列的插入和更新行为.最后,timest列默认为NOT NULL,这也和其他数据类型不一样.
除了特殊行为之外,通常也应该尽量使用TIMESTAMP,因为他比DATETIME空间效率更高.
有时候人民会将UNIX时间戳存储为整数值,但是这不会带来任何收益.
用整数保存时间戳的格式通常不方便处理,所以我们不推荐这么做.
如果要存储比妙更小颗粒的日期时间值怎么办?MySQL目前没有提供合适的时间类型,但是可以使用自己的存储格式:
可以使用BIGINT类型存储微妙级别的时间戳,或者使用DOUBLE存储妙之后的小树部分.这两种方法都可以.
或者也可以使用MariaDB代替MySQL.
位数据类型
MySQL有少数几种存储类型使用紧凑的位存储数据.所以这些位类型,不管底层存储格式和处理方式如何,从技术上来说都是字符串类型.
BIT
在MySQL5.0之前,BIT是TINYINT的同义词. 但是在MySQL5.0以上版本这是一个特性完全不同的数据类型.
可以使用BIT列在一列中存储一个或者多个true/false值. BIT(1)定义一个包含单个位的字段,BIT(2)存储2个位,以此类推.
BIT列最大的长度是64个位.
BIT列只需要17个位存储(假设没有可为NULL的列),这样MyISAMySQL只使用3个字节就能存储着17个BIT列.
其他存储引擎例如Memory和InnoDB,为每个BIT列使用一个足够存储的最小整数类型来存放,所以不能节省存储空间.
MySQL把BIT当做字符串类型,而不是数字类型,.
当检索BIT(1)的值时,结果是一个包含二进制0或1值得字符串, 而不是ASCII码的0或1.
然而,在数字上下文的场景中检索时,结果将是位字符串转换成数字.
如果需要和另外的值比较结果,一定要记得这一点.
例如,存储一个值b'00111001'(二进制值等于57)到BIT(8)的列并且检索它,得到的内容是字符吗位57的字符串.
也就是说得到ASCII码位57的字符9.但是在数字上下文场景中, 得到的事数字57.
CREATE TABLE bittest(a bit(8));
INSERT INTO bittest VALUES(b'00111001');
select a,a+0 FROM bittest;
+------+------+
| a | a+0 |
+------+------+
| 9 | 57 |
+------+------+
1 row in set (0.00 sec)
这是相当令人费解的,所以我们认为应该谨慎使用BIT类型,对于大部分应用,最好避免使用这种类型.
如果想在一个BIT的存储空间中存储一个true/false值,另一个方法是创建一个可以为空的CHAR(0)列. 该列可以保存空值(NULL)或者长度为0的字符串(空字符串).
set类型
如果要保存很多的true/false值,可以考虑合并这些列到一个SET数据类型,他在MySQL内部是以一系列打包的位的集合来表示的.
这样就有效的利用了存储空间.并且MySQL有像FIND_IN_SET()和FIELD()这样的函数,方便在查询中使用.
他的主要缺点是改变列的定义的带价较高:需要ALTER TABLE ,这对大表来说是非常昂贵的操作.一般来说,也无法在SET列上通过索引查找.
在整数列上进行按位操作
一个替代SET的方式是使用一个整数包装一系列的位;
例如,可以把8个位包装到一个TINYINT中,并且按位操作来使用.可以在应用中为每个位定义名称常量来简化这个操作使用.
可以在应用中为每个位定义名称常量来简化这个工作.
比起SET,这种话办法主要的好处在于可以不适用ALTER TABLE改变字段代表的"枚举"值,缺点是查询语句更加难写,并且更加难以理解(当第五个BIT位被设置时是什么意思?);
一些人非常适应这种方式,一些人不适应,所以是否采用这种技术取决于个人的偏好.
一个包装位的应用的列子是保存权限的访问控制列表(ACL).
每个位或者SET元素代表一个值,例如CAN_READ,CAN_WRITE,或者CAN_DELETE.如果使用SET列,可以让MySQL在列定义里存储位到值的映射关系;
如果使用整数列,则可以在应用代码里存储这个对应关系,这是使用SET列时的查询:
CREATE TABLE acl(
perms SET('CAN_READ','CAN_WRITE','CAN_DELETE') NOT NULL
);
INSERT INTO acl(perms) VALUES('CAN_READ,CAN_DELETE');
SELECT perms FROM acl WHERE FIND_IN_SET('CAN_READ',perms);
+---------------------+
| perms |
+---------------------+
| CAN_READ,CAN_DELETE |
+---------------------+
1 row in set (0.00 sec)
如果使用整数来存储,则可以参考下面的例子:
SET @CAN_READ :=1 << 0,
@CAN_WRITE :=1 << 1,
@CAN_DELETE :=1 << 2;
CREATE TABLE acl (
perms TINYINT UNSIGNED NOT NULL DEFAULT 0
);
INSERT INTO acl(perms) VALUES(@CAN_READ + @CAN_DELETE);
SELECT perms FROM acl WHERE perms & @CAN_READ;
+-------+
| perms |
+-------+
| 5 |
+-------+
1 row in set (0.00 sec)
我们这里使用MySQL变量来定义,但是也可以在代码里使用常量来代替.
选择标识符(identifier)
为标识列(identifier column)选择合适的数据类型非常重要.
一般来说更有可能用标识符列与其他值进行比较(例如,在关联操作中),或者通过标识列寻找其他列.
标识列也可能在另外的表中作为外键使用,所以为标识列选择数据类型时,应该选择和关联表中的对应列一样的类型.
在相关表中,使用相同的数据类型是个好主意, 因为这些列很可能在关联中使用.
当选择标识列的类型时,不仅仅需要考虑存储类型,还需要考虑MySQL对这种类型怎么执行计算和比较.
例如,MySQL在内部使用整数类型存储ENUM和SET类型,然后再做比较操作时转换为字符串
一旦选定某种类型,要确保在所有关联表中都使用同样的类型.
类型之间需要精确匹配,包括像UNSIGNED这样的属性,因为,如果使用的时InnoDB存储引擎,将不能再数据库类型不是完全匹配的情况下创建外键.
否则会有报错信息:"ERROR 1005(HY000): Can't create talbe", 但是,在不同长度的VARCHAR列上创建外键又是可以的.
混用不同数据类型可能会导致性能问题,即使没有性能问题,在比较操作时隐式类型转换也可能导致很难发现的错误.
这种错误可能会很久以后才突然出现,那时候可能都已经晚了是在比较不同的数据类型.
在可以满足值的范围的需求,并且预留未来增长空间的前提下,应该选择最小的数据类型.
流入有一个state_id列存储美国各州的名字(这是关联到另一张存储名字的表的ID),就不需要几千或几百万个值,所以不需要用INT.
TINYINT足够存储,而且比INT少了3个字节.如果用这个值作为其他表的外键,3个字节可能导致很大的性能差异.
下面是一些使用的小技巧:
整数类型
整数通常是标识列最好的选择,因为他们很快而且可以使用AUTO_INCREMENT.
ENUM和SET类型
对于标识列来说,ENUM和SET类型通常是一个糟糕的选择,尽管对某些只包含固定状态或者类型的静态定义表来说可能是没有问题的.
ENUM和SET列适合存储固定信息,例如有序的状态,产品类型,人的性别.
举个例子,如果使用枚举字段类定义差皮类型,也许会设计一张以这个枚举字段为主键的查找表(可以在查找表中增加一些列来保存描述性质的文本,这样就能够生成一个属于表
或者未网站的下拉菜单提供有意义的标签). 这时,使用枚举类型作为标识列时可行的,但是大部分情况下都要避免这么做.
字符串类型
如果可能,应该避免使用字符串类型作为标识列,因为他们很消耗空间,并且通常比数字类型慢.
尤其是在MyISAM表里使用字符串作为标识列时要特别小心.MyISAM默认为字符串使用压缩索引,这回导致查询慢的多.
在我们的测试中,我们注意到最最多有6倍的性能下降.
对于完全随机的字符串也需要多加注意,例如MD5(),SHA1()或者UUID()产生的字符串.
这些函数生成的新值会任意分布在很大的空间内,这回导致INSERT以及一些select语句变得很慢,另一方面,对一些有很多写的特别大的表,这种随机值实际上可以帮助消除热点.
- 因为插入会随机的写到索引的不同位置,所以使得INSERT语句更慢.这回导致页分裂,磁盘随机访问,以及对于聚簇存储引擎产生的聚簇索引随便.
- select语句会变得更慢,因为逻辑上相邻的行会分布在磁盘和内存不同的地方.
- 随机值导致缓存对所有类型的查询语句效果都很差,因为使得缓存赖以工作的访问局部性原理失效,如果整个数据集都一样的"热",那么缓存任何一部分特定数据到内存都没有好处,如果内存集币内存大,缓存将会有很多刷新和不命中
如果存储UUID值,则应该溢出"-"符号,或者更好的做法是,用UNHEX()函数转换UUID值为16字节的数字,并且存储在一个BINARY(16)列中.
索引时可以通过HEX()函数来格式化为十六进制格式.
UUID()生成的值与加密散列函数例如SHA1()生成的值有不同的特征:UUID值虽然分布也不均匀,但是还是有一定的顺序,但是还是不如递增的整数好用.
当新增动生成的schema
上面已经介绍了大部分重要数据类型的考虑(有些会严重影响性能,有些则影响较小),但是我们还没有提到自动生成的schema设计有多糟糕.
写的很慢的schema迁移程序,或者自动生成的schema的程序,都会导致严重的性能问题.
有些程序存储任何东西都会使用很大的VARCHAR列,或者对需要再关联时表的列使用不同的数据类型.
如果schema是自动生成的,一定要反复检查确认没有问题.
对象关系映射(ORM)系统(以及使用它们的"框架")是另一种常见的性能噩梦.
一些ORM系统会存储任意类型的数据到任意类型的后端数据库存储中,这通常意为着其没有设计使用更加优秀的数据类型来存储.
有时会为每个对象的每个属性使用单独的行,甚至使用基于时间戳的版本控制,导致单个属性会有多个版本存在.
这种设计对开发者很有吸引力,因为这使得他们可以用面向对象的方式工作,不需要考虑数据时怎么存储的.
然而,"对开发者隐藏复杂性"的应用通常不能很好的扩展,我们建议在用性能交换开发人员的效率之前仔细考虑,并且总是在真实大小的数据集上做测试.
这样就不会太晚发现问题.
特殊类型数据
某些类型的数据并不直接与内置类型一致.低于秒级精度的时间戳就是一个例子:本章前面部分也演示过存储此类数据的一些选项.
另一个例子是一个IPV4地址,人们经常使用VARCHAR(15)来存储IP地址.
然而,他们实际上是一个32位无符号的证书,不是字符串.用小数点将地址分成四段的方法,只是为了方便阅读.
所以应该用无符号整数存储IP地址. MySQL提供INET_ATON()和INET_NTOA()函数在这两种表示方法之间转换.
MySQL schema 设计中的陷阱
虽然有一些普通的好或坏的设计原则,但也有一些问题时由MySQL的实现机制导致的.
这意味着有可能会犯一些只在MySQL下发生的特定错误.这里讨论一下设计MySQL的schema的问题.
这也许会帮助你避免这些错误,并且选择在MySQL特定实现下工作的更好地替代方案.并选择在MySQL特定实现下工作的更好地替代方案.
太多的列
MySQL的存储引擎API工作时需要再服务器层和存储引擎之间通过缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列.
从行缓冲中奖编码过的列转换成行数据结构的操作带价是非常高的.
MyISAM的定长行结构实际上与服务器层的行结构正好匹配,所以不需要转换.然而,MyISAM的变长行结构和Innodb的行结构则总是需要转换.
转换的带价依赖于列的数量.当我们研究一个CPU占用非常高的案例时,发现客户使用了非常宽的表(数千个字段),然而只有一小部分的列会实际用到,
这时候转换的带价就非常的高.如果计划使用数千个字段,必须意识到服务器的性能运行特征会有一些不同.
太多的关联
所谓的"实体-属性-值"(EVA) 设计模式是一个常见的糟糕设计模式,尤其是在MySQL下不能考不的工作.
MySQL限制了吗诶个关联操作最多只能有61个表的情况下,解析和优化查询的带价也会成为MySQL的问题.
一个粗略的经验法则,如果希望查询执行的快速且并发性好,单个查询最好在12个表以内做关联.
全能的枚举
注意防止过度使用枚举(ENUM).下面这里举一个例子:
CREATE TABLE ...(
country enum('','0','1','2',...,'31')
这种模式的schema设计非常凌乱,那么使用枚举值类型也许在任何支持枚举类型的数据库都是一个有问题的设计方案.
这里应该用整数作为外键关联到字段表或者查找表来查找具体值. 但是在MySQL中,当需要再枚举列表中增加一个新的国家时就要做一次ALTER TABLE操作.
在MySQL5.0以及更早的版本中ALTER TABLE 是一中欧冠阻塞操作; 即使在5.1和更新版本中,如果不是在列表的末尾增加值也会一样需要ALTER TABLE
我们将展示一些骇客式的方法来避免阻塞操作,但是这只是骇客的玩法,别轻易用在生产环境.
变相的枚举
枚举(ENUM)列允许在列中存储一组定义值中的单个值,集合(SET)列则允许在列中存储一组定义值中的一个或多个值.但是有时候会显得很混乱.
下面是一个例子:
CREATE TABLE ...(
is_default set('Y','N') NOT NULL default 'N'
如果这李真核假梁中秋情况不会同时出现,那么毫无疑问应该使用枚举列代替集合列.
非此发明(Not Invent Here)的NULL
我们之前写了避免使用NULL的好处,并且建议尽可能的考虑替代方案.
即使需要存出一个实时上的"空值"到表中时,也不一定非得使用NULL.需要可以使用0,某个特殊值,或者空字符串作为替换.
但是遵循这个原则也不要走极端,当确实需要标识为知值时也不要害怕使用NULL.
在一些场景中,使用NULL可能会比某个神器常数更好.从特定类型的值域中选择一个不可能的值.
例如用-1代表一个未知的整数,可能导致代码复杂很多,并容易引入bug,还可能会让事情变得一团糟.处理NULL切实不容易,但有时候会比它的替代方案更好.
下面是一个常看到的例子
CREATE TABLE ...(
dt DETETIME NOT NULL DEFAULT '0000-00-00 00:00:00'
伪造的全0值可能导致很多问题(可以配置MySQL的SQL_MODE来禁止不可能的日期,对于新应用这个是非常好的实践经验,它不会让创建的数据库里充满不可能的值).
值得一提的是,MySQL会在索引中存储NULL值,而Oracle则不会.
范式和反范式
个人总结
范式化设计常见的是第二范式,该方式是将数据分表.
比如:对一个部门的管理,将表设计为部门表和人员表.部门表,仅仅用来记录部门和部门领导,人员表仅仅用来记录部门下所含有的人员.
其实我们一直在用第二范式,只不过之前不知道在用而已.比如我们常见的RBAC.
范式化的设计对性能有很大的好处,尤其是对插入操作.但是一般范式化设计都会有多个表.
这时候就需要连表查询了,但是实际项目中,一般我们都不会连表查.
为了性能, 一般都是查多次.之后admin做统计才会用连表查
反范式化其实我们用的是比较少的,因为一般不建议,一种数据存多分,搞得到处都是.
常见的反范式化, 应该就是无限分类的.
反范式话,对性能尤其是插入性能来说,应该是一个噩梦.一种数据要同时修改多个地方,如果是新人维护的话,直接就懵逼了.
但是反范式,因为有冗余数据,可以放在同一个表中,这时候就可以设置为同一个索引,查起来肯定要比范式的连表查询要快很多.
单纯的范式化和反范式话,只针对某个表或者说是某个业务的模块.
在整体的项目中,一般我们都会进行范式化和反范式话的混用.
对于任何给定的数据通常都会有很多表示方法,从完全的范式化到完全的反范式化,以及两者的折中方案.
在范式化的数据库中,每个事实数据会出现并且只出现一次. 相反,在反范式话的数据库中,信息室冗余的,可能会存储在多个地方.
范式化和反范式化很多种方式,这里只举一个简单的例子: 下面以经典的,"雇员,部门,部门领导"的例子开始
EMPLOYEE | DEPARTMENT | HEAD |
---|---|---|
Jones | Accounting | Jones |
Smith | Engineering | Smith |
Brown | Accounting | Jones |
Green | Engineering | Smith |
这个schema的问题时修改数据时可能发生不一致.
假如Say Brown接人Accounting部门的领导就需要修改多行数据来反应这个情况.
如果Jones这一行显示部门的领导跟 Brown这一行的不一样,就没办法知道哪个是对的.
这就像有一句老话说的:一个人有两块表就永远不知道时间.
此外,这个设计在没有雇员信息的情况下,就无法表示一个部门.
如果我们删除了所有Accounting部门的雇员,我们就永远时区这个部门本身的所有记录.
要避免这个问题,我们需要对这个表进行范式化设计,方式是拆分雇员和部门项.
拆分以后可以使用下面两张表分别来存储雇员表:
EMPLOYEE_NAME | DEPARTMENT |
---|---|
Jones | Accounting |
Smith | Engineering |
Brown | Accounting |
Green | Engineering |
和部门表: DEPARTMENT|HEAD ---|--- Accounting|Jones Accounting|Jones Engineering|Smith
这样设计的两张表符合第二范式,在很多情况下做到这一步已经足够好了.
然而,第二范式只是需要可能的范式中的一种
小结:
这个例子中我们使用姓氏作为主键,因为这是数据的自然标识.
从实践来看,无论如何都不改这么做, 因为这不能保证唯一,而且用一个很长的字符串做主键是一个很糟糕的注意.
范式的优点和缺点
当为性能为题而寻求帮助时,经常会被建议对schema进行范式化设计,尤其是写密集的场景. 这通常是一个比较好的建议,范式化能够带来的好处:
- 范式化的更新操作通常比反范式化要快.
- 当数据较好的范式化时,就只有很少或者没有重复数据,所以只需要修改更少的数据.
- 范式化的表通常小很多, 可以更好的放在内存中,所以执行操作会更快.
- 很少有多余的数据意味着检索列表时更少需要DISTINCT或者GROUP BY 语句.
- 还是前面的例子,在非范式化的结构中必须使用DISTINCT或者GROUP BY 才能获得一份唯一的部门列表,但是如果部门(DEPARTMENT)是一张独立的表,则只需要简单查这一份表就可以了.
范式化设计的schema的缺点是通常需要关联查询.
稍微复杂一些的查询语句在符合范式的schema上都可能需要至少一次关联,也许更多.这不但带价昂贵,也可能使一些索引策略无效.
比如:范式化可能将列存放在不同的表中,而这些如果在一个表汇总,就可也设置为同一个索引.
反范式的优点和缺点
反范式的schema因为所有数据都在一个表中,可以很好地避开关联查询. 如果不需要关联表,则对大部分查询最差的情况,(及时表没有使用搜索引)是全表扫描.
当数据币内存大时,这可能币关联查询要快的多,因为这避免的随机IO(全表扫描基本上是顺序IO,但也不是100%,根引擎的实现有关);
单独表也能使用更有效的索引策略,假如有一个网站,允许用户发送信息,并且一些用户是付费用户.
现在想查看付费用户最新的10条数据.如果是范式结构并且索引了发送日期的字段published,这个查询也许看起来向这样:
SELECT message_text, user_name
FROM message
Inner JOIN user ON message.user_id=user.id
WHERE user.account_type='premiumv'
WHERE user.account_type='premiumv'
ORDER BY message.published DESC LIMIT 10;
要更有效的执行这个查询,MySQL需要扫描message表的published字段的索引.
对于每一行找到的数据,将需要到user表里检查这个用户是不是付费用户.
如果只有一小部分用户是付费账户,那么这是效率低下的做法.
另一种可能的执行计划是从user表开始,选择所有的付费用户,获得他们所有的信息,并且排序,但这样更糟糕.
主要问题时关联,使得需要再一个索引中又排序又过滤.
如果采用反范式话的组织数据,将两个表的字段合并一下.且增加一个索引(account_type,published),这样就可以不通过鼓励年写出这个查询.
SELECT message_type,user_name
FROM user_message
WHERE account_type='premium'
ORDER BY published DESC
LIMIT 10;
混用范式化和反范式化
范式化和反范式话的scheme各有优劣,怎么设计才会比较好.
事实是,完全的范式化和完全的反范式话schema都是理想中才有的.
在真实世界中很少会这么极端的使用,在实际引用中经常需要混用,可能使用部分范式化的schema,缓存表,以及其他技巧.
最常见的反范式话数据的方法是赋值或缓存,在不同的表中存储相同的特定列.在MySQL5.0+的版本中, 可以使用触发器更新缓存值,这使得实现这一的方案变得更加简单.
在我们的网站实例中,可以在user表和message表中都存储account_type字段,而不用完全的反范式话.这避免了完全反范式化的插入和删除问题,因为即使没有消息的时候也绝对不会丢失用户的信息.这也不会吧user_message表搞得太大,有利于搞笑的获取数据.
但是很显然,后面要更新用户的账户类型的操作带价就搞了.因为需要同时更新两个表.
至于这会不会是一个问题,需要考虑更新的频率以及更新的额时长,并和执行SELECT查询的频率进行比较.
另一个从附表冗余一些数据到子表的理由是排序的需要.
例如,在范式化的schema里通过作者的名字对消息做排序的带价将会非常的搞,但是如果在message表中缓存autheor_name字段并且建立好索引,则可以非常高效的完成排序.
缓存衍生值也是有用的,如果需要显示每个用户发了多少信息,可以每次执行一个昂贵的子查询来计算并显示它;也可以在user表汇总建一个num_message列,每当用户发新消息时更新这个值.
缓存表和汇总表
有时候提升性能最好的方法是在同一个表中保存衍生的冗余数据.
然而有时候也需要创建一张完全独立的汇总表或者缓存表(特比是为了满足检索的需要时).如果能容许少量的脏数据,这是一个非常好的方法,但是有时确实没有选择的余地(例如,需要避免复杂昂贵的实时更新操作).
术语:缓存表和汇总表没有标准的含义.
我们用术语缓存表来标识存储那些可以比较简单从schema其他表获取数据的表.
而术语汇总表时,则保存得是使用GROUP BY语句聚合数据的表.也有人用数据累计表称呼这些表,因为这些数据被累积了.
仍然以网站为例,假设需要计算24小时内发送的消息数.
在一个很繁忙的网站不可能维护一个实时精确的计数器,作为替代方案,可以每小时生成一张汇总表.这样也许一条简单的查询就可以做到.并且比实时计数器要高效的多.缺点是计数器并不是100%精确的.
如果必须获得过去24小时准确的消息发送数量,另外有一个选择.
以每小时汇总表为基础,把前23个完整的小时的统计表中的计数全部加起来,最后再加上开始阶段和结束阶段不完整的小时内的计数.
假设统计表叫做msg_per_hr并且这样定义:
CREATE TABLE msg_per_hr(
hr DATETIME NOT NULL,
cnt INT UNSIGNED NOT NULL,
PRIMARY KEY(hr)
)
可以通过把下面的三个语句的家国加起来,得到过去24小时发送消息的总数.
我们用LEFT(NOW(),14)来获得当前的日期和时间最接近的小时.
SELECT SUM(cnt) FROM msg_per_hr
WHERE hr BETWEEn
CONCAT(LEFT(NOW(),14), '00:00')- INTERVAL 23 HOUR
AND CONCAT(LEFT(NOW(),14),'00:00') - INTERVAL 1 HOUR;
SELECT COUNT(*) FROM message
WHERE posted >=NOW()-INTERVAL 24 HOUR
AND posted < CONCAT(LEFT(NOW(),14),'00:00') - INTERVAL 23 HOUR;
SELECT COUNT(*) FROM message
WHERE posted >= CONCAT(LEFT(NOW(),14), '00:00');
不管是哪种方法,不严格的计数或者通过小范围查询填满间隙的严格计数, 逗比计算message表的所有行要有效的多.
这是建立汇总表的最关键原因, 实时计算统计值是很昂贵的操作, 因为要么需要扫描表中的大部分数据,要么查询语句只能在某些特定的索引上才能有效执行,而这类特定索引一般会对UPDATE操作有影响, 所以一般不希望创建这样的索引.
计算最活跃的用户或者最常见的标签是这种操作的经典例子.
缓存表则相反,其对优化搜索和检索查询语句很有效.这些查询语句经常需要特殊的表和索引结构,根普通OLTP操作用的表有些区别.
例如可能会需要很多不同的索引组合来加速各种类型的查询. 这些矛盾的需求有时需要创建一张只包含主表中部分列的缓存表,一个有用的技巧是对缓存表使用不同的存储引擎.
例如: 例如,如果主表使用InnoDB,使用MyISAM作为缓存表的引擎将会得到更小的索引占用空间,并且可以做全文搜索.有时候甚至想把整个表导出MySQL,插入到专门的搜索系统中获得更高的搜索效率,例如Lucene或者Sphinx搜索引擎.
在使用缓存表和汇总表时,必须决定实时维护数据还是定期重建, 哪个更好依赖于应用程序.
但是定期重建并不只是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引(这回更加高效).
当重建汇总表和缓存表时,通常需要保证数据在操作时依然可用.这就需要通过使用影子表来实现.
影子表指的是一张在真实表背后创建的表. 当完成建表操作后,可用通过一个院子的重命名操作切换影子表和原表.
例如如果需要重建my_summary,则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换:
DROP TABLE IF EXISTS my_summary_new,my_summary_old;
CREATE TABLE my_summary_new like my_summary;
RENAME TABLE my_summary TO my_summary_old,my_summary_new TO my_summary;
如果像上面的例子一样,在将my_summary这个名字分配给新建表之前将原始的my_summary表重命名为my_summary_old,就可以在下一次重建之前一直保留旧版版本的数据. 如果新表有问题,则很容易的进行快速的回滚操作.
物化视图
许多数据库管理系统(例如Oracle和微软的SQL Server)都提供了一个被成为物化视图的功能.
无话视图实际上是预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新.MySQL并不原生支持,物化视图,然而使用Justin Swanhart的开远工具Flexviews,也可以自己实现物化视图
感觉没什么...大概看一下,就不记录了...
计数器表
如果应用在表中保存计数器,则在更新计数器时可能碰到并发问题.
计数器表在web应用中很常见.可以用这种表缓存一个用户的朋友数,文件下载次数等.
创建一张独立的表存储计数器,通常是一个好主意,这样可以使计数器表小且快.使用独立的表可以帮助避免查询缓存失败,并且可以使用本节展示的一些更高级的技巧.
应该让事情变得尽可能的简单,假设有一个计数器表只有一行数据,记录网站的点击次数:
CREATE TABLE hit_counter(
cnt int UNSIGNED NOT NULL
)ENGINE=InnoDB;
网站的每次点击都会导致对计数器进行更新;
UPDATE hit_counter SET cnt=cnt+1;
问题在于,对于任何想要更新这一行的事务来说,这条记录上都有一个全局的互斥锁(mutex).这回使得这些事务只能穿行执行.要获得更高的并发更新性能,也可也将计数器保存在多行中,每次随机选择一行进行更新:
CREATE TABLE hit_couter(
slot tinyint unsigned not null primary key,
cnt int unsigned not null
)ENGINE=InnoDB;
然后预先在这个表增加100行数据,现在选择一个随机的槽(slot)进行更新.
UPDATE hit_counter SET cnt=cnt+1 WHERE slot=RAND()*100;
需要获得统计结果,需要使用下面这样的聚合查询:
SELECT SUM(cnt) FROM hit_counter;
一个常见的需求是每隔一段时间开始一个新的计数器(例如每天一个),如果需要这么做,则可以再简单的修改一下表设计:
CREATE TABLE daily_hit_counter(
day date not null,
slot tinyint unsigned not null,
cnt int unsigned not null,
primary key(day,slot)
)ENGINE=InnoDB;
在这个场景中,可以不用像前面的例子那种预先生成行,而用 NO DUPLICATE KEY UPDATE代替:
INSET INTO daily_hit_counter(day,slot,cnt)
VALUES(CURRENT_DATE,RAND()*100,1)
ON DUPLICATE KEYUPDATE cnt=cnt+1;
如果希望减少表的行数,以避免表变得太大,可以写一个周期执行任务,合并所有结果到0号槽,并且删除所有其他槽:
UPDATE daily_hit_conter as c
INNER JOIN(
SELECT day,SUM(cnt) as cnt,MIN(slot) as mslot
FROM daily_hit_counter
GROUP BY day
) AS x USING(day)
SET c.cnt = IF(c.slot=x.mslot,x.cnt,0),
c.slot=IF(c.slot=x.mslot,0,c.slot);
DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt=0;
小结
为了提升读查询的速度,经常会需要建一些额外索引, 增加冗余列.
甚至是创建缓存表和汇总表.这些方法会增加写查询的负担,也需要额外的维护任务,但是在设计高性能数据库时,这些都是常见的技巧.
虽然写操作变得更慢了,但是显著提高了读操作的性能.
然而,写操作变慢并不是读操作变得更快锁付出的唯一带价,还可能同时增加了读操作和写操作的开发难度.
加快ALTER TABLE操作的速度
MySQL的ALTER TABLE操作的性能对达标来说是一个大问题. MySQL执行大部分修改表结构操作的犯法是用新的表结构创建一个空表,从旧表中查出所有数据插入新表,然后删除旧表.
这样操作可能需要话费很长的时间,如果内存不足而表很大,而且还有很多索引的情况下尤其如此.
许多人都有这样的经验,ALTER TABLE 操作需要话费数个小时甚至数天才能完成.
MySQL5.1以及更新版本,包含一些类型的"在线"操作支持.这些功能不需要在整个操作过程汇总锁表.最新版本的In弄DB也支持通过排序来建索引,这使得建索引更快并且有一个紧凑的索引布局.
一般而言,大部分ALTER TABLE操作将导致MySQL服务终端.这里展示一些在DDL操作时游泳的技巧,但这是针对一些特殊的场景而言的.对常见的场景,能使用的技巧只有两种: 一种是先在一台不提供服务的机器上执行ALTER TABLE操作,然后和提供服务的主库进行切换;另外一种技巧是"影子拷贝".影子拷贝的技巧是用要求的的表结构创建一张和源表无关的新表,然后通过重命名和删表操作交换两张表.也有一些工具可以帮助完成影子拷贝工作:例如,facebook数据库运营团队的"online schema change"工具等等.
不是所有的ALTER TABLE操作都会引起表重建.例如:有两种方法可以改变或者删除一个列的默认值(一种方法很快,另外一种弄则很慢).加入要修改电影的默认租赁期限,从三天改到五天.
下面是很慢的方式:
ALTER TABLE sakila.film
MODIFY COLUMN rental_duration TINYINT(3) NOT NULL DEFAULT 5;
SHOW STATUS 显示这个语句做了1000次读和100次插入操作.换句话说,他拷贝了整张表到一张新表,甚至列的类型,大小和可否为NULL属性都没有改变.
理论上,MySQL可以跳过创建新表的步骤.列的默认值实际上存在表的.frm文件中.
所以我们可以直接修改这个文件而不需要改动表本身.然而,MySQL还没有采用这种优化的方法,所有的MODIFY COLUMN操作都将导致表重建.
另外一种方法是通过ALTER COLUMN(ALTER TABLE允许使用ALTER COLUMN,MODIFY COLUMN和CHANGE COLUMN语句修改列,这三种操作都是不一样的.) 操作来改变列的默认值:
ALTER TABLE sakila.film
ALTER COLUMN rental_duration SET DEFAULT 5;
这个语句会直接修改.frm文件而不设计表数据,所有这个操作时非常快的.
只修改.frm文件
从上面的例子我们看到修改表的.frm文件时很快的, 但MySQL有时候会在没有必要的时候也重建表. 如果愿意毛线,可以让MySQL做一些其他类型的修改而不用重新建表.
下面演示的技巧是不收官方支持的.也没有文档记录,并且可能不能正常工作.要用浙西而技术,自担风险.
下面这些操作时有可能不需要重建表的:
- 移除(不是增加)一个列的AUTO_INCEMENT属性.
- 增加,移除,或更改ENUM和SET常量. 如果移除的时已经有行数据用到其值的常量,查询则会返回一个空字符串值.
基本的技巧是为想要的表结构创建一个新的.frm文件,然后用它替换掉已存在的那张表的.frm文件,像下面这样:
- 创建一张有相同结构的空表,并进行所需要的修改(例如增加ENUM常量).
- 执行FLUSH TABLES WITH READ LOCK.这将会关闭所有正在使用的表,并且禁止任何表被打开.
- 交换.frm文件
- 执行UNLOCK TABLES来释放第二步的读锁.
下面以给sakila.film表的rating列增加一个常量为例来说明.
当前表看起来如下:
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
rating | enum('G','PG','PG-13','R','NC-17') | YES | G |
假设我们需要为那些对电影更加谨慎的父母们增加一个PG-14的电影分级:
CREATE TABLE sakila.film_new LIKE sakila.film;
ALTER TABLE sakila.film_new
MODIFY COLUMN rating ENUM('G','PG','PG-13','R','NC-17','PG-14')
DEFAULT 'G';
FLUSH TABLE WITH READ LOCK;
注意,我们是在常量列表的末尾增加一个新的值,如果把新增的值放在中间,例如PG-13之后,则会导致已存在的数据含义被改变:已经存在的R值将变成PG-14,而已经存在的NC-17将变成R,等等.
接下面操作系统的命令交换.frm文件:
/var/lib/mysql/sakila# mv film.frm film_tmp.frm
/var/lib/mysql/sakila# mv film_new.frm film.frm
/var/lib/mysql/sakila# mv film_tmp.frm film_new.frm
再回到MySQL命令行,现在可以解锁表并且看到变更后的效果了:
UNLOCk TABLES;
SHOW COLUMNS FROM sakila.film LIKE 'rating'\G
最后需要做的是删除为完成这个操作而创建的辅助表:
DROP TABLE sakila.film_new;
快速创建MyISAM索引
为了更搞笑的载入数据到MyISAM表中,有一个常用的技巧是先禁用索引,载入数据,然后重新启用索引:
ALTER TABLE test.load_data DISABLE KEYS;
--载入数据操作
ALTER TABLE test.load_data ENABLE KEYS;
这个技巧能够发挥作用,是因为构建索引的工作被延迟到数据完全载入以后,这个时候已经可以通过排序来构建索引了.这样做会快很多,并且使得索引树的碎片更少,更紧凑.
不幸的是这个办法对唯一索引无效,因为DISABLE KEYS支队非唯一索引有效.MyISAM会在内存中构造唯一索引,并且为载入的每一行检查唯一索引.一旦索引的大小超过了有效内存大小,载入操作就会变得越来越慢.
在现代版本的INNODB版本中,有一个类似的技巧,这依赖于In弄DB的快速在线索引创造功能,这个技巧是,先删除所有的唯一索引,然后增加新的列,最后重新创建删除掉的索引.Percona Server可以自动完成这些操作步骤.
也可以使用像前面说的ALTER TABLE的骇客方法来加速这个操作,但需要多做一些工作并且承担一定的风险.这对从备份中载入数据是很有用的,例如,当已经知道所有数据都是有效的并且没有必要做唯一性检查时就可以这么做.
再次说明,这些是没有文档说明并且不收官方支持的技巧.
下面是步骤
- 用需要的表结构创建一张表,但是不包括索引.
- 载入数据到表中以构建.MYD文件
- 按照需要的结构创建另外一张空表,这次要包含索引,这回创建需要的.frm和.MYI文件.
- 获取读锁并刷新表
- 重命名第二张表.frm和.MYI文件,让MySQL认为这是第一张表.
- 释放读锁
- 使用REPAIR TABLE来重建表索引,该操作会通过排序来构建所有的索引,包含唯一索引.
这个操作步骤对达标来说会快很多
本章总结
良好的schema设计原则是普遍适用的,但是MySQL有他自己的实现细节需要注意.概括来说,尽可能保持任何东西小二简单总是好的.MySQL喜欢简单需要使用数据库的人也应该同样喜欢简单的原则:
- 尽量避免过度设计,例如会导致极其复杂查询的schema设计,或者有很多列的表设计(很多的意思是介于有点多和非常多之间)
- 使用小而简单的合适数据类型,除非真实数据模型中有确切的需要,否则应该尽可能的避免使用NULL值.
- 尽量使用相同的数据类型存储相似或相关的值,尤其是要在关联条件中使用的列.
- 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存.
- 尽量使用整型定义标识列.
- 避免使用MySQL已遗弃的特性,例如指定浮点数的精度,或者整数的显示宽度.
- 小心使用ENUM和SET,虽然他们用起来很方便,但是不要滥用,否则有时候会变成陷阱,最好避免使用BIT.
范式是好的,但是反范式(大多数情况下意味着重复数据)有时也是必须的,并且能带来好处.
最后ALTER TABLE 是让人痛苦的操作,因为在大部分情况下,他都会锁表并且重建整张表.我们展示了一些特殊的场景可以使用骇客方法;但是对大部分场景,必须使用其他更常规的方法,例如在备机执行ALTER并在完成后把他切换为主库.
第五章 创建高性能索引
索引(在MySQL中也叫做'键(KEY)')是存储引擎用于快速找到的记录的一种数据结构.这是索引的基本功能,除此之外,本章还将讨论索引其他一些方面有用的属性.
索引对于良好的性能非常关键,尤其是当表汇总的数据量越来越大时,索引对性能的影响越发重要.在数量量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但是当数据量逐渐增大时,性能则会急剧降.
不过,索引却经常被忽略,甚至被误解,所以在实际案例中经常会遇到由于糟糕的索引导致的问题, 这也是我们把索引优化放在了靠前的张杰,甚至比查询优化还靠前的原因.
索引优化应该是对查询性能优化最有效的手断了,索引能够轻易将查询性能提高几个数量级,'最优'的索引有时比一个'好的'索引性能要好两个数量级.创建一个真好'最优'的索引经常需要重写查询,所以,本章和下一张的关系非常紧密.
索引基础
要理解MySQL中个索引是如何工作的,最简单的方法就是去看看一本书的索引部分:如果想在一本书中找到某个特定主体,一般会先看书的索引,找到对应的页码.
在MySQL中存储引擎用类似的方法使用索引,其先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行.
假如要运行下面的查询:
SELECT first_name FROM sakila.actor WHERE actor_id=5;
如果在 actor_id列 上建有索引,则MySQL将使用该索引找到actor_id为5的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行.
索引可以包含一个或多个列的值. 如果索引包含多格列,那么列的顺序也十分重要, 因为MySQL只能搞笑的使用索引的最左前缀列.创建一个包含两个列的索引,和创建两个只包含一列的索引时大不相同的
下面将详细介绍:
如果使用的时ORM,是否还需要关心索引?
简而言之: 是的,仍然需要理解索引,即使是使用对象关系映射(ORM)工具.
ORM工具能够生产符合逻辑的,合法的查询(多数时候),除非只是生成非常基本的查询(例如仅是根据主键查询),否则他河南生成适合索引的查询.无论是多么复杂的ORM工具,在精妙和复杂的索引面前都是浮云.
读完本章后面的内容以后, 你就会同意这个观点的,很多时候即使是查询优化技术专家也很难兼顾到各种情况,更不要说ORM了.
索引的类型
索引有很多类型,可以为不同的场景提供更好的性能.
在MySQL中索引时存储在引擎层而不是服务器层实现的.
所以,并没有统一的索引标准:不同存储引擎的索引的工作方式不同,也不是所有的存储引擎都支持素有类型的索引.
即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同.
下面我们来看看MySQL支持的索引类型以及他们的优点和缺点.
B-Tee索引
当人们讨论索引的时候,如果没有特别指明类型,那么多半说的是B-Tree类型,他使用B-Tree数据结构来存储数据(实际上很多存储引擎使用的时B+Tree,即每个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历.).大多数MySQL引擎都支持这种索引.Archive引擎是个例外: 5.1之前Archive不支持任何索引.直到5.1才开始支持单个自增列索引(AUTO_INCREMENT);
我们使用属于B-Tree,是因为MySQL在CREATE TABLE和其他语句中也是用该关键字.
不过,底层的存储引擎也可能使用不同的存储结构,例如NDB集群存储引擎内部实际上使用了T-Tree结构存储这种索引,即使其名字是BTREE;InnoDB则使用的时B+Tree,各种数据结构和算法的变种不在本书讨论范围之内.
存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣;例如,MyISAM使用前缀压缩技术使得索引更小,但是InnoDB则按照原数据格式进行存储.再比如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行.
B-Tree通常意味着所有的值都是按照顺序存储的,并且每一个叶子到根的举例相同.MyISAM使用的结构有所不同,但是基本思想是类似的.
这里有一个图5-1建立在B-Tree结构(从技术上来说是B+Tree)上的索引
B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。
这里就不在展示了,直接百度得了.下面我列了几个比较好的博客文章,不记得的时候可以翻一下.BTREE理解了原理就行.
这是之前同事LogBird推荐的三个关于MySQL索引的网站
这是我找到的关于MySQL的B+Tree的博客,推荐第二个
B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索哦.
根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找.通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限.最终存储引擎要么是找到对应的值,要么该记录不存在.
叶子节点比较特别,他们的指针指向的是被索引的数据,而不是其他的节点页(不同引擎的指针类型不同).其实在根节点和叶子节点之间可能有很多层节点页.树的深度和表的大小直接相关.
B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据.
例如,在一个基于文本域的索引树上,按字母排序传递顺序的值进行查找是非常合适的,所以像找出所有以I到K开头的名字,这样的查找效率会非常高.
请注意,索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序.
可以使用B-Tree索引的查询类型,B-Tree索引适用于全键值,键值范围或键值范围前缀查找. 其中见前缀查找只适用于根据最前缀的查找.前面所述的索引对如下类型的查询有效.
- 全值匹配
全职匹配指的是和索引中的所有列进行匹配,例如前面提到的索引可用于查找姓名为Cuba Allen生于1960-01-01的人.
- 匹配最左前缀
前面提到的索引可用于查找所有姓为Allen的人,即只使用索引的第一列.
- 匹配列前缀
也可以只匹配某一列的值的开头部分,例如前面提到的索引可用于查找所有以J开头的姓的人,这里页只使用了索引的第一列.
- 匹配范围值
例如前面提到的索引可用于查找姓在Allen和Barrymore之间的人. 这里也只使用了索引的第一列.
- 精确匹配某一列并范围匹配另外一列
前面提到的索引也可用于查找所有姓为Aleen,并且名字是字母K开头(比如Kim,Karl等)的人.即第一列last_name全匹配,第二列first_name范围匹配.
- 只访问索引的查询
B-Tree通常可以支持'只访问索引的查询',即查询只需要访问索引,而无须访问数据行.后面讨论这种覆盖索引的优化.
因为索引树中的节点是有序的,所以除了按值查找之外,索引还可以用于查询中的ORDER BY操作(按顺序查找). 一般来说,如果B-Tree可以按照某种方式查找到值,那么也可以按照这种方式用于排序.所以,如果ORDER BY子句满足前面列出的集中查询类型,则这个索引也可以满足对应的排序需求.
下面是一些关于B-Tree索引的牵制
-
如果不是按照索引的最左列开始查询,则无法使用索引.例如上面例子中的索引无法用于查找名字为Bill的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列.类似的,也无法查找姓氏以某个字母结尾的人.
-
不能跳过索引中的列.也就是说,前面所述的索引无法用于查找姓为Smith并且在某个特定日期出生的人,如果不制定名(first_name),则MySQL只能使用索引的第一列.
-
如果查询中有某个列的范围查询,则最右边所有列都无法使用索引优化查询.例如有查询WHERE last_name='Smith' AND first_name LIKE 'J%' AND dob ='1976-12-23', 这个查询只能使用索引的前两列,因为这个LIKE是一个范围条件(但是服务器可以把其余用于其他目的).如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件.