分区表
对用户来说,分区表是一个独立的逻辑表,但是底层是由多个物理子表组成。
MySQL实现分区表的方式:对底层表的封装。
意味着索引也是按照分区的子表定义的,没有全局索引。
这和Oracle不同,Oracle可以更加灵活的定义索引和表是否分区。
MySQL在创建表时使用 PARTITION BY 子句定义每个分区存放的数据。
执行查询时,优化器会根据分区定义过滤掉不需要查询的分区,这样就无需扫描所有分区了,只需要扫描包含数据的分区。
分区的一个主要目的:将数据按照一个粗的粒度分散到多个表中。
分区的作用:
- 表非常大以至于无法全部加载到内存中,或者只在表的最后部分有热点数据,其他均为历史数据。
- 分区表的数据更容易维护。例如要批量删除大量数据可以直接清除整个分区,还可以对单个分区进行优化。
- 分区表的数据可以分布在不同的物理设备上。
- 可以使用分区表来避免某些特殊的瓶颈,例如ext3文件系统的InnoDB锁竞争等。
- 可以备份和恢复单独的区,在超大数据量下效果很好。
分区表的限制:
- 一个表最多只能有1024个分区。
- MySQL5.1中,分区表达式必须是整数或返回整数的函数。MySQL5.5中,可以使用列来分区。
- 如果分区字段中有主键或唯一索引的列,那么所有的主键列和唯一索引列必须包含进来。
- 分区表无法使用外键约束。
分区表的原理
分区表由多个相关的底层表实现,这些底层表也是由句柄对象(Handler Object)表示,所以我们可以直接访问各个分区。
存储引擎管理分区表的底层表和管理普通表一样,分区表的索引也只是在底层表各自加上完全相同的索引。从存储引擎的角度看,底层表和普通表没什么不同,存储引擎也无需关心是底层表还是普通表。
分区表的操作逻辑:
- SELECT:查询一个分区表时,分区层先打开并锁住底层表,优化器过滤掉无需扫描的分区,然后再调用存储引擎API去访问各个分区的数据。
- INSERT:写入时,分区层先打开并锁住底层表,然后判断由哪个分区接收数据,再将记录写入到分区。
- DELETE:删除记录时,分区层先打开并锁住底层表,然后判断数据在哪个分区,再去对应的分区删除数据。
- UPDATE:更新时,分区层先打开并锁住底层表,然后判断数据在哪个分区,去对应的分区修改数据。
虽然每个操作都会“打开底层表并锁住表”,但是不意味着分区层在处理时会锁住全表。
如果存储引擎实现了行锁,如InnoDB,加锁和释放锁的过程和普通查询一样。
分区表的类型
- RANGE 范围分区
- LIST 列表分区
- HASH 哈希分区
- KEY 键值分区
一些其他的分区技术:
- 根据键值分区,减少InnoDB的互斥量竞争。
- 使用数学模函数来分区,对列进行取模运算。
如何使用分区表
在超大数据量下,B-Tree索引已经无法起作用了。
除非是索引覆盖扫描,否则一旦发生回表,查询的数据量大的情况下将产生大量的随机I/O。
另外,超大数据量下,索引维护(磁盘空间,I/O)的代价也非常高。
有些系统如Infobright意识到这一点后,干脆直接放弃使用B-Tree索引。
分区的目的就是用代价非常小的方式快速定位到数据在哪一块区域。
然后在这块区域中,可以做顺序扫描、建索引、甚至缓存到内存中。
为了保存大数据量的可扩展性,一般有如下策略:
-
全量扫描数据,不要任何索引
使用简单的分区方式存放表,不要索引,根据分区规则大致定位到需要的位置。
只要能够使用WHERE条件将数据限制在很少的分区中,则效率是很高的。 -
索引数据,并分离热点
如果数据有明显的“热点”,除了热点数据,其他数据很少被访问到,那么可以将热点数据单独存放到一个分区。
什么情况下会出问题
使用分区表提升性能时,有两个要求:
- 查询时能过滤掉大多数分区
- 分区本身不会带来很多额外代价
某些场景可能出问题:
-
NULL值使分区过滤无效
分区表达式的值允许为NULL,第一个分区是特殊分区,当分区表达式的值为NULL或非法值时,记录就会被放在第一个分区,查询时会额外查询第一个分区,如果第一个分区数据量很大,而且不使用索引的话,性能也会很糟糕。 -
分区列和索引列不匹配
例如列A定义了索引,却使用列B进行分区,会导致查询无法进行分区过滤,因为每个分区都有其独立索引。 -
选择分区的成本可能很高
分区有很多类型,不同类型的实现方式不同,导致性能各不相同。
尤其是范围分区,当数据量太大,分区过多时,确定数据在哪个分区的成本会越来越高。
对于大多数系统来说,100个分区应该没什么问题。
键值分区,哈希分区没有这个问题。
-
打开并锁住底层表的成本可能很高
访问分区表时,MySQL需要打开并锁住底层表,这是分区表的另一个开销。
这个操作发生在过滤分区之前,会影响所有查询。
所以对于增删改应该尽可能使用批量处理的方式进行。 -
维护分区的成本可能很高
重组分区或者类似ALTER TABLE的语句的操作可能会消耗非常大的资源,特别是当数据量很大的情况下。
除此之外,目前分区实现中还有一些其他限制:
- 所有分区必须使用相同的存储引擎。
- 分区函数中可以使用的函数和表达式也有一些限制。
- 某些存储引擎不支持分区。
- MyISAM分区表,不能使用LOAD INDEX INTO CACHE。
查询优化
通过分区过滤可以让查询扫描更少的数据。
对于分区表来说,在WHERE条件中加上分区列显得很重要,即使多余也要加上,这样可以让优化器过滤多余的分区。
合并表
合并表是一种早期的,简单实现的分区表。
存在很多问题并缺乏优化,属于将被淘汰的技术。
这里不做记录。
视图
MySQL5.0之后引入了视图。
视图本身是个虚拟的表,不存放任何数据。
访问视图时,数据都是MySQL从其他表中查询出来的。
视图和表在同一个命名空间,MySQL在很多地方对待视图和对代表是一样的。
不能对视图创建触发器,也不能DROP TABLE删除视图。
创建视图
CREATE VIEW 关键字
CREATE VIEW person_view AS
SELECT * from person WHERE age = 22;
使用视图
视图和表使用方式差不多。
SELECT * FROM person_view;
MySQL可以使用两种算法的任一种来处理视图:合并算法和临时表算法。
如果可能,会尽量用合并算法。
如果视图中包含GROUP BY、DISTINCT、聚合函数、UNION、子查询等,只要无法在原表记录和视图记录中建立一一映射关系的,MySQL都会使用 临时表算法来实现视图。
如果想确定MySQL使用哪种算法,可以使用EXPLAIN查看执行计划。
可更新视图
可更新视图(updateable view)是指可以通过更新视图来更新视图涉及到的具体表。
只要指定了合适的条件,就可以对视图进行增删改操作。
UPDATE person_view SET name = 'wangwu' WHERE name = 'lisi';
如果视图定义中包含了GROUP BY、UNION、聚合函数等就无法被更新。
使用临时表算法的视图都无法被更新。
视图对性能的影响
多数人认为视图不能提升性能,在MySQL中某些情况下视图可以帮助提升性能。
例如:在重构schema的时候可以使用视图,使得在修改视图底层表结构的时候,应用代码可以不报错继续运行。
可以使用视图实现基于列的权限控制。
如果打算使用视图来提升性能,必须做详细的测试。
即使是合并算法实现的视图也会带来额外的开销,而且视图的性能很难预测。
视图的限制
MySQL还不支持物化视图,也不支持在视图中创建索引。
MySQL也并不会保存创建视图的原始SQL语句,无法通过SHOW CREATE VIEW查看。
外键约束
使用外键是有成本的,比如:每次修改数据时,都需要在另一张表中多执行一次查找操作。
虽然InnoDB强制外键使用索引,但还是无法消除这种约束检查的开销。
某些场景下,外键会提升一些性能。
如果希望确保两个表始终有一致的数据,使用外键比在应用程序中检查一致性要好。
外键约束使得查询需要访问一些别的表,需要额外的开销。
如果向字表写入一条记录,外键约束会让InnoDB检查父表的记录,需要对父表的记录加锁,导致额外的锁等待。
如果只是使用外键做约束,通常在应用程序中实现更好。
外键会带来很大的额外消耗。
在MySQL内部存储代码
MySQL通过触发器、存储过程、函数的形式来存储代码。
存储代码是一种很好的共享和复用代码的方法,不过由于不同数据库的语法规则不同,所以不同的数据库之间代码不能移植。
优点:
- 它在服务器内部运行,离数据最近,没有网络带宽和延迟的问题。
- 代码重用。
- 可以简化代码的维护和版本更新。
- 可以提升安全,实现更细粒度的权限控制。
- 服务器端可以缓存存储过程的执行计划,对于反复调用的过程,可以大大降低消耗。
- 因为是在服务器端部署,所以备份、维护都可以在服务器端完成,没什么外部依赖。
缺点:
- MySQL没有提供开发和调试工具,编写代码比较困难。
- 存储代码的效率相较于应用程序代码会差一些。
- 存储代码会给部署带来额外的复杂性。
- 可能有安全隐患。
- 不好扩展,存储过程会给服务器带来额外的压力,相较于应用程序的扩展,数据库的扩展要难的多。
- MySQL没有控制存储程序的资源消耗,可能存储过程的一个小错误,会让服务器僵死。
- 调试困难。
存储代码是一种帮助应用隐藏复杂性,使得应用开发变得简单的方法。
不过它会让性能更低。
存储过程和函数
MySQL的架构和优化器的特性使得存储代码有一些限制:
- 优化器无法通过关键字DETERMINISTIC来优化单个查询多次调用存储函数的情况。
- 优化器无法评估存储函数的成本。
- 每个连接都有独立的存储函数的执行计划缓存。
通常来说,希望存储程序越小越简单越好,复杂的处理逻辑应该交给应用程序实现。
对于某些操作,存储过程比应用程序实现要快得多。
特别是当存储过程调用很多小查询的时候,相比于查询执行的成本,解析和网络开销就大得多。
例如批量插入数据,存储过程比应用程序实现要快得多。
DROP PROCEDURE insert_person;
DELIMITER //
CREATE PROCEDURE insert_person( IN loops INT )
BEGIN
DECLARE i INT;
SET i = loops;
WHILE i>0 DO
INSERT INTO person VALUES (i,'name',i);
SET i= i-1;
END WHILE;
END;
//
DELIMITER ;
-- 调用存储过程
CALL insert_person(10000);
触发器
触发器可以在你执行INSERT、UPDATE、DELETE时执行一些特定的操作。
可以指定在SQL语句执行前还是后触发。
触发器本身没有返回值,不过它可以读取或者改变触发SQL语句所影响的数据。
所以可以利用触发器进行一些强制限制和一些业务逻辑控制。
注意的点:
- 每个表的每个事件,只能定义一个触发器。
- MySQL只支持“基于行”的触发器。
- 触发器的问题很难排查。
- 触发器可能导致死锁和锁等待。
在不支持事务的存储引擎中,触发器无法保证原子性。
在InnoDB触发器是在同一个事务中完成的。
创建触发器
-- DROP TRIGGER IF EXISTS book_info_trigger;
CREATE TRIGGER【触发器名称】AFTER INSERT ON 【关联表】
FOR EACH ROW
BEGIN
-- 执行sql语句块
-- new:当触发插入和更新事件时可用,new指向的是被操作的记录
-- old: 当触发删除事件时可用,old指向的是被操作的记录
INSERT INTO book_info_his VALUES ( new.book_id, new.book_name );
END;
触发器仅支持表,视图不支持。
事件
事件是MySQL5.1引入的一种新的存储代码的方式。
它类似于Linux的定时任务,可以创建事件,指定MySQL在某个时段执行。
可以在表INFORMATION_SCHEMA.EVENTS中看到各个事件的状态。
通常会把复杂的SQL封装到存储过程中,在事件中通过CALL去调用存储过程即可。
虽然事件调度是一个单独的线程,但是MySQL会创建新的线程用于事件执行。
游标
MySQL在服务器端提供只读的,单向的游标,而且只能在存储过程或更底层的客户端API中使用。
游标会让MySQL执行一些额外的I/O操作,效率可能非常低。
-
游标的创建
DECLARE【游标名称】CURSOR FOR 【检索语句】 -
开启游标
OPEN【游标名称】 -
关闭游标
CLOSE【游标名称】
DROP PROCEDURE IF EXISTS ab;
CREATE PROCEDURE ab()
BEGIN
DECLARE codes VARCHAR(100);
-- 定义游标
DECLARE apply_codes CURSOR
FOR
SELECT apply_code from funds_apply WHERE apply_year = '2018';
-- 开启游标
OPEN apply_codes;
-- 读取游标数据
FETCH apply_codes INTO codes;
-- 关闭游标
CLOSE apply_codes;
SELECT codes;
END;
CALL ab();
用户自定义函数
MySQL支持用户自定义函数(UDF)。存储过程只能使用SQL来编写,UDF没有这个限制,可以使用支持C语言调用约定的任何编程语言来实现。
UDF需要事先编译好并动态链接到服务器上,UDF速度 非常快,可以访问大量操作系统的功能,还可以使用大量的库函数。
UDF无法读取数据表,无法在调用UDF的线程中使用当前事务处理的上下文来读写表。
插件
MySQL支持各种各样的插件,使用插件可以扩展现有的功能。
- 存储过程插件
- 后台插件
- 全文解析插件
- 审计插件
- 认证插件
更多细节参考官方手册。
字符集和校对
字符集:指一种从二进制编码到某类字符符号的映射。
校对:用于某个字符集的排序规则。
MySQL如何使用字符集
每种字符集都有多种校对规则,并且有一个默认的校对规则。
MySQL的设置可以分为两类:创建对象时的默认值、在服务器和客户端通信时的设置。
创建对象时的设置
MySQL服务器有默认的字符集和校对规则,每个数据库也有默认值,每个表也有默认值。
这是一个逐层继承的默认设置。
- 创建数据库时,将根据服务器的character_set_server来设置数据库的默认字符集。
- 创建表时,根据数据库的默认字符集来设置表的字符集。
- 创建列时,根据表的字符集来设置列的字符集。
真正存放数据的是列,所以真正影响到数据的列的字符集。
服务器/客户端通信时的设置
服务器和客户端通信时,可能使用不同的字符集。
这时服务器将进行必要的翻译工作:
- 服务器总是假设客户端按照character_set_client来传输数据。
- 服务端收到SQL语句时,先将其转换成字符集character_set_connection。它还使用这个字符集来决定如何将数据转换成字符串。
- 当服务端返回数据给客户端时,会转换成 character_set_result。
建议MySQL安装时在my.cnf中配置好,不要使用默认的字符集。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7a27CAPZ-1573983783864)(https://i.niupic.com/images/2019/10/05/_90.png)]
MySQL如何比较两个字符串的大小
如果两个字符串的字符集不同,MySQL会先将其转换成同一个字符集再比较,字符集不兼容会报错。
可以使用函数来定位字符集相关的错误。
SELECT charset('');-- utf8
SELECT collation('');-- utf8_general_ci
SELECT coercibility('');-- 4
一些特殊情况
-
诡异的 character_set_database 设置
character_set_database设置的默认值和默认数据库设置的想同。当改变默认数据库时,它也会跟着变。当客户端连接到服务端,但是没指明数据库时,默认值会和character_set_server相同。 -
LOAD DATA INFILE
使用LOAD DATA INFILE时,数据库总是将文件中的字符按照character_set_database字符集来解析。 -
SELECT INTO OUTFILE
MySQL会将查询结果不做任何转码写入文件。如果连接是乱码的,导出文件也是乱码。 -
嵌入式转义序列
MySQL会根据 character_set_client来解析转义序列,即使字符串中包含前缀或COLLATE也一样。因为解析器在处理字符串中的转义字符时,完全不关心校对规则。
选择字符集和校对规则
可以使用SHOW CHARACTER SET和SHOW COLLATION来查看数据库支持的字符集和校对规则。
SHOW CHARACTER SET ;
SHOW COLLATION ;
尽量避免在同一个数据库中使用不同的字符集和排序规则,可能会导致结果和预想的不一致,甚至影响性能。
对于校对规则需要考虑的是:是否以大小写敏感来比较字符串,还是以字符串编码的二进制值来比较大小。
前缀分别是:_cs、_ci、_bin。
- _cs:cs为case sensitive的缩写,即大小写敏感。
- _ci:ci为case insensitive的缩写,即大小写不敏感。
- _bin:字符采用二进制存储,区分大小写。
字符集影响查询
某些字符集和校对规则可能会需要更多的CPU操作、消耗更多的内存和磁盘、甚至影响索引。
不同的字符集和校对规则之间的转换会带来额外的开销。
不同字符集之间的列关联时也会带来很多问题和额外的开销。
一般情况下,建议全部设置为“UTF-8”,可以减少因为字符集带来的很多问题。
但是从性能上考虑,这就不是太好了。
如果应用程序本无需使用“UTF-8”而使用了,只会消耗更多的磁盘空间,影响性能。
全文索引
除了数值比较,范围查询,模糊查询,有时我们可能需要:关键字查询。
例如:电商系统的商品搜索、搜索引擎。
全文索引就是为这种应用场景而设计的。
全文索引有自己独特的语法,没有索引也可以工作,有索引效率会更高。
事实上,很少会用到MySQL的全文索引,一般都使用Lucene、Solr等。
分布式(XA)事务
存储引擎的事务特性能够保证存储引擎级别实现ACID,而分布式事务则让存储引擎级别的ACID可以扩展到数据库层面,甚至是多个数据库之间。
XA事务中需要有一个事务协调器来保证所有的事务参与者都完成了准备工作(第一阶段)。
如果协调器收到所有的参与者准备好的消息,就会告诉所有的事务可以提交了(第二阶段)。
MySQL在XA事务中扮演一个参与者的角色,而不是协调者。
MySQL中有两种XA事务:MySQL可以参与到外部的分布式事务中、也可以通过XA事务来协调存储引擎和二进制日志。
查询缓存
缓存执行计划:对于相同类型的SQL可以跳过SQL解析和生成执行计划阶段。
MySQL还支持缓存完整的查询结果,也就是“查询缓存”。
查询缓存系统会跟踪查询中涉及到的表,如果表发生变化,缓存就会失效。
查询缓存对应用程序是透明的。
MySQL如何判断缓存命中
缓存存放在一个引用表中,通过一个哈希值引用。
哈希值包含的因素:查询本身、要查询的数据库、客户端协议版本等。
对于SQL语句,任何字符上的不同,例如:空格,注释都会导致缓存不命中。
好的编码习惯可以让系统运行的更快。
当查询语句中包含不确定的数据时,不会被缓存。
例如:NOT(),current_date()。
查询中包含自定义函数、存储函数、用户变量、临时表等也都不会缓存。
MySQL的查询缓存很多时候可以提高性能,使用时需要注意,会带来额外消耗:
- 读查询前会检查是否命中缓存。
- 如果结果可以被缓存,且缓存结果中没有,就会将结果存入查询缓存,额外的消耗。
- 写入数据时,会将表的缓存设为失效,也会带来消耗。
查询缓存是一个加 排它锁 的操作。
查询缓存如何利用内存
查询缓存是完全存储在内存中的。
除了缓存查询结果外,还需要缓存其他相关数据。
基本的管理维护数据结构大概需要40KB的内存资源。
什么情况下生效
并不是所有情况下查询缓存都能提升性能。
缓存和失效都会带来额外消耗。
当缓存带来的资源节约大于查询本身的资源消耗时,才会带来性能提升。
这跟服务器的压力模型有关。
理论上可以通过打开或关闭缓存,比较两者的系统效率来判断是否需要开启。
对于需要消耗大量资源的查询非常适合缓存,
例如:汇总计算查询,COUNT()等。
涉及的表进行增删改的操作要相应的少才行,否则缓存会经常失效。
任何没有从查询缓存中返回都称为:“缓存未命中”。
未命中的原因:
- 查询语句无法被缓存。
- MySQL从未处理过这个查询,查询结果从未缓存过。
- 缓存内存耗尽,某些缓存被“逐出”。
如果服务器有大量缓存未命中,但是大多数查询确实已缓存,
那么一定有如下情况发生:
- 查询缓存还没有预热。
- 查询没有重复执行。
- 导致缓存失效的操作太多。
缓存碎片、内存不足、数据修改都会导致缓存失效。
配置足够的缓存空间,query_cache_min_res_unit设置也合理,那么缓存失效应该就是数据修改导致的。
通过Qcache_lowmem_prunes来查看有多少次失效是因为内存不足。
如果缓存结果在失效前没有被任何查询利用,那缓存操作就是浪费时间和内存。
可以通过Com_select和Qcache_inserts的相对值来看看是否一直有这种情况发生。
指标:“命中和写入”的比率,即Qcache_hits 和 Qcache_inserts 的比值。
一般来说,比值大于3:1时,认为查询缓存有效,否则就考虑禁用缓存。
配置和维护查询缓存
-
query_cache_type
是否打开查询缓存,可以设置为:OFF、ON或DEMAND。DEMAND表示只有查询语句写明SQL_CACHE时才会放入缓存。 -
query_cache_size
查询缓存使用的总内存空间,单位是字节,必须是1024的倍数。 -
query_cache_min_res_unit
查询缓存分配内存块的最小单位。 -
query_cache_limit
能缓存的最大结果。 -
query_cache_wlock_invalidate
如果某个数据表被其他表锁住,是否仍然从缓存中返回结果。
减少碎片
配置合适的query_cache_min_res_unit可以减少碎片。
可以根据(query_cache_size-Qcache_free-memory)/Qcache_queries_in_cache计算单个查询的平均缓存大小。
可以通过参数 Qcache_free_blocks 来观察碎片,它反映了查询缓存中空闲块的多少。
如果Qcache_free_blocks达到了Qcache_total_blocks的一半,说明有严重的碎片问题。
可以使用命令 FLUSH QUERY CACHE 完成碎片整理。
使用命令 RESET QUERY CACHE 清空缓存。
提高查询缓存的使用率
查询缓存的内存空间太小也会导致命中率低。
如果MySQL无法为一个新的查询缓存时,就会删除旧的缓存结果。
如果发生这种情况,MySQL会增加状态值:Qcache_lowmem_prunes。
如果这个值增加的很快,可能的原因:
- 如果还有很多空闲块,说明有很多碎片。
- 如果没空闲块,说明缓存内存分配的不够。
InnoDB和查询缓存
InnoDB有自己的MVCC机制,相对于其他存储引擎,InnoDB的查询缓存将更加复杂。
如果表上有任何锁,该表的查询都是不会被缓存的。
通用查询缓存优化
- 用多个小表代替大表。
- 增删改最好批量进行。
- 如果缓存空间太大,过期操作时可能导致服务器僵死,配置合理的缓存大小。
- 对于写密集型应用,禁用缓存。