《高性能Mysql》(整理与备注)

第1章 MySQL架构与历史

MySQL的架构可在多种不同场景中应用并发挥好的作用,足够灵活,能够适应高要求的环境。

概要地描述MySQL的服务器架构、各种存储引擎之间的主要区别。

1.1 MySQL逻辑架构

三层组件(★)

  1. 最上层处理连接&安全&认证 :校验服务器是否可连接、客服端认证是否通过、能够访问的资源。
  2. 第二层是核心服务功能(服务器层):查询解析、分析、优化、缓存以及所有的内置函数;跨存储引擎的功能也在这层:存储过程、触发器、视图等。
  3. 第三层是存储引擎层:负责数据的存储与提取。 服务器层通过API与不同存储引擎通信。

1.2 并发架构

只要多个查询在同一时刻修改数据,就会产生并发控制的问题。本节讨论MySQL在两个层面的并发控制:服务器层与存储引擎层。

1.2.1 读写锁

处理并发读或者写时,通过实现一个由两种类型的锁组成的锁系统来解决问题:

  1. 共享锁(shared lock):也叫读锁(read lock),是共享的,相互不阻塞的。多个客户在同一时刻可以同时读取同一个资源,而互不干扰。
  2. 排他锁(exclusive lock):也叫写锁(write lock),是排他的,会阻塞其他的写锁和读锁。确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。

(理论上写锁应该能阻塞读锁,但实际中因为事务隔离级别的不同以及快照的存在,写锁并不能阻塞读。)

1.2.2 锁粒度

在给定的资源上,锁定的数据量越小(即锁粒度越小),则系统的并发程度越高。但锁粒度越小,要管理的锁越多,消耗的资源越大。

所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡。

表锁:表锁是MySQL中最基本的锁策略,锁粒度最大,但开销最小。会锁定整张表,一个用户在对表进行写操作前,需要先获得写锁,并阻塞其他用户对该表的所有读写操作。

行锁:最大程度地支持并发处理,锁粒度最小,但开销最大。只锁一行数据。

其他锁: 折中方案,锁粒度一般,资源开源一般,如分页锁、分段锁。

(写锁和读锁同时竞争时,写锁优先,保证后续读到的都是最新的数据)

1.3 事务

事务就是一组原子性的SQL查询,或者说一个独立的工作单元。事务内的语句,要么全部执行成功,要么全部执行失败。

事务四个特性(ACID):

  • 原子性(atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。
  • 一致性(consistency):数据库总是从一个一致性的状态转换到另外一个一致性的状态。
  • 隔离性(isolation):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。(read-commit 隔离级别除外)
  • 持久性(durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。

一个兼容ACID的数据库系统,需要做很多复杂但可能用户并没有觉察到的工作,才能确保ACID的实现。

1.3.1 隔离级别

针对事务的隔离性,定义了四种隔离级别:

  • READ-UNCOMMITTED:事务中的修改即使没有提交,对其他事务也都是可见的。事务B可以读取事务A未提交的数据,被称为脏读(Dirty Read)。
  • READ-COMMITTED :  多数DBS的默认隔离级别。一个事务从开始到提交之前,所做的修改对其他事务是不可见的。也叫不可重复读(nonrepeatable read),事务B中两次同样的查询,可能会因事务A的提交,得到不一样的结果。
  • REPEATABLE-READ:MySQL的默认事务隔离级别。解决了脏读的问题,保证了在同一个事务中多次读取同样记录的结果是一致的。无法解决另外一个幻读的问题。
  • SERIALIZABLE:最高的隔离级别。强制事务串行执行,避免了前面说的幻读的问题。

1.3.1 死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。

-- 事务1

START TRANSACTION; 
UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2002-05-01'; 
UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2002-05-02'; 
COMMIT;
-- 事务2

START TRANSACTION; 
UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 and date = '2002-05-02'; 
UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = '2002-05-01'; 
COMMIT;

死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁,所以应用程序在设计时必须考虑如何处理死锁。

1.3.1 事务日志

使用事务日志,存储引擎只修改内存中的数据,并追加记录磁盘中的事务日志。(追加事务日志比修改磁盘数据快。)

磁盘中的数据由事务日志后续慢慢追加执行。以此提高效率。

(即响应时间减小,但总花费时间变长)

1.3.1 MySQL中的事务

MySQL只有两种存储引擎支持事务:InnoDB、NDB Cluster。(一些第三方存储引擎也支持事务)

自动提交(AUTOCOMMIT)

MySQL默认采用自动提交(AUTOCOMMIT)模式。

--  查看是否自动提交
show VARIABLES like 'autocommit';

-- 若不是自动提交,则需开启事务后手动提交
start TRANSACTION;
update student set name = 'wang1' where id = 1;
commit;

-- 设置为自动提交(Mysql默认)
set global autocommit = 0;
-- 查看事务隔离级别
select @@tx_isolation;

-- 设置当前会话隔离级别
set tx_isolation = 'read-committed';

-- 设置全局默认会话隔离级别(两种方式)
set global tx_isolation = 'read-committed';
set global transaction isolation level read committed;

在事务中混合使用存储引擎

MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态。
隐式和显式锁定

在事务执行过程中,如有插入修改操作,InnoDB会根据隔离级别在需要的时候自动加锁,为隐式锁定。

InnoDB也支持通过特定的语句进行显式锁定:

  • SELECT ... LOCK IN SHARE MODE
  • SELECT ... FOR UPDATE

MySQL也支持LOCK TABLES和UNLOCK TABLES语句。

1.4 MySQL多版本并发控制

MySQL的多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,一般都实现了多版本并发控制(MVCC)。可认为MVCC是行级锁的一个变种,在多数情况避免了加锁操作,开销更低。

MVCC的实现,是通过保存在某个时间点的数据快照实现的,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的:一个保存行的创建时间,一个保存行的过期时间(或删除时间)。(不是实际的时间值,而是系统版本号(system version number))。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(READ UNCOMMITTED总是读最新的数据行,而SERIALIZABLE会对所有的行都加锁)

1.5 MySQL的存储引擎

不同存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL服务层统一处理的。可使用SHOW TABLE STATUS命令(5.0以后版本也可查INFORMATION_SCHEMA中对应的表)显示表信息。

1.5.1 InnoDB存储引擎

InnoDB是MySQL的默认事务型引擎,是最重要、使用最广泛的存储引擎。处理短期事务、性能高、自动崩溃恢复特性。

数据存储在表空间(tablespace)中,表空间是由InnoDB管理的一个黑盒子,由一系列的数据文件组成。InnoDB可以将每个表的数据和索引存放在单独的文件中。

采用MVCC来支持高并发,并实现了四个标准的隔离级别。默认级别是REPEATABLE READ(可重复读),通过间隙锁(next-key locking)策略防止幻读的出现。

表是基于聚簇索引建立的,聚簇索引对主键查询有很高的性能。不过它的二级索引(secondary index,非主键索引)中必须包含主键列,所以如果主键列很大的话,其他的所有索引都会很大。

存储格式是平台独立的,可以将数据和索引文件从Intel平台复制到PowerPC或者Sun SPARC平台。

内部做了很多优化:可预测性预读、自适应哈希索引、插入缓冲区(insert buffer)等

支持真正的热备份。

1.5.2 MyISAM存储引擎

MySQL 5.1及之前的版本,MyISAM是默认的存储引擎。不支持事务和行级锁、崩溃后无法安全恢复。

存储:MyISAM会将表存储在两个文件中:数据文件和索引文件,分别以.MYD和.MYI为扩展名。MyISAM表可以存储的行记录数。

特性:

  • 加锁与并发:对整张表加锁。读时会对需读到的所有表加共享锁,写入时则对表加排他锁。
  • 修复:对于MyISAM表,MySQL可以手工或者自动执行检查和修复操作,(非崩溃恢复)
  • 索引特性:对BLOB和TEXT等长字段,也可基于其前500个字符创建索引;支持全文索引,支持复杂的查询
  • 延迟更新索引键(Delayed Key Write):不立刻将修改的索引数据写入磁盘,而是写到内存中的键缓冲区,只在清理键缓冲区或者关闭表的时候将对应的索引块写入到磁盘。

1.5.3 MySQL内建的其他存储引擎

Archive引擎:只支持INSERT和SELECT操作,磁盘I/O更少,对高速插入和压缩做了优化

Blackhole引擎:Blackhole引擎没有实现存储机制,它不做任何保存。但服务器会记录Blackhole表的日志,所以可用于复制数据到备库,或者只是简单地记录到日志。

CSV引擎:可将普通的CSV文件作为MySQL的表来处理,可在数据库运行时拷入或者拷出文件。可以将Excel等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在MySQL中打开使用,可以作为一种数据交换的机制.

...

1.5.4 第三方存储引擎

OLTP(在线事务处理系统)类引擎:支持ACID事务和MVCC。如XtraDB、PBXT等。

面向列的存储引擎:大数据量处理时,面向列的方式可能效率更高。如不需要整行的数据,面向列的方式可以传输更少的数据。如果每一列都单独存储,那么压缩的效率也会更高。有Infobright、InfiniDB等。

社区存储引擎:...

1.5.5 选择合适的引擎

无特定需求,使用默认InnoDB.。有特殊需求,如只涉及insert/select,考虑使用MyISAM。其他场景结合存储引擎特性考虑。

1.5.6 转换表的引擎

三种方式:

  1. 直接ALTER TABLE: 语法简单,但消耗系统的所有I/O能力
  2. 导出与导入:先导出结构和数据,改表名和存储引擎,再执行。(相当于新建表)
  3. 创建与查询(CREATE & SELECT): 先创建新表结构,再用INSERT SELECT语法复制数据。(还是新建表方法)

1.6 MySQL时间线

时间点更新内容
版本3.23(2001)引入MyISAM代替ISAM引擎
版本4.0(2003)支持新的语法,重写了复制,InnoDB成为标配,查询缓存
版本4.1(2005)引入了更多新的语法
版本5.0(2006)一些“企业级”特性:视图、触发器、存储过程和存储函数
版本5.1(2008)引入了分区、基于行的复制;事务存储引擎
版本5.5(2010)改善:性能、扩展性、复制、分区、对Windows系统的支持
版本5.6查询优化器的改进
.... 

早期MySQL的特性支持和较低的使用成本,使快速增长。在5.x版本早期,MySQL引入了视图和存储过程等特性,期望成为“企业级”数据库,并未成功,BUG不少。MySQL 5.5可以说是MySQL历史上质量最高的版本。MySQL 5.6在功能和性能方面有显著提升。

1.7 MySQL的开发模式

开发版本用于获取测试和反馈,GA版本才是正式可用的。

MySQL依然遵循GPL开源协议,全部的源代码(除了一些商业版本的插件)都会开放给社区。

将更多的特性做成插件的开发模式,既是收费的方式,也是保证内部代码质量的方式。

1.8 总结

MySQL拥有分层的架构,上层是服务器层的服务和查询执行引擎,下层则是存储引擎。服务器层通过API调用存储引擎层,不同存储引擎对数据的存储方式,以及API的实现方式不同。

存储引擎发展简述:ISAM -> MyISAM -> InnoDB

第2章 MySQL基准测试

基准测试(benchmark),指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

2.1 为什么需要基准测试

针对系统测试、验证、预估、预测。

2.2 基准测试的策略

两大策略:

  • 针对整体(服务器&应用代码&)测试,集成式(full-stack)基准测试
  • 单独测试MySQL,单组件式(single-component)基准测试

2.2.1 测试何种指标

开始执行或设计基准测试之前,要先明确测试的目标。

  • 吞吐率:单位时间内的事务处理数
  • 响应时间:测试任务所需的整体时间
  • 并发性:关注的是正在工作中的并发操作,或者是同时工作中的线程数或者连接数
  • 可扩展性:系统扩展资源后的工作能力
  • ....

2.3 基准测试方法

如何设计和执行基准测试

2.3.1 设计和规划基准测试

提出问题并明确目标。然后决定是采用标准的基准测试,还是设计专用的测试。

2.3.2 基准测试应该运行多长时间

基准测试应该运行足够长的时间,持续观察直到确认系统已经稳定。

2.3.3 获取系统性能和状态

在执行基准测试时,需要尽可能多地收集被测试系统的信息。最好为基准测试建立一个目录,并且每执行一轮测试都创建单独的子目录,将测试结果、配置文件、测试指标、脚本和其他相关说明都保存在其中。

2.3.4 获得准确的测试结果

获得准确测试结果的最好办法,是回答一些关于基准测试的基本问题:是否选择了正确的基准测试?是否为问题收集了相关的数据?是否采用了错误的测试标准?

(基于MySQL的默认配置的测试没有什么意义,因为默认配置是基于消耗很少内存的极小应用的,所以MySQL之前一直被认为是小型应用专用数据库)

2.3.5 运行基准测试并分析结果

(最好采用自动化,易执行、易记录、易复现)

(QPS:数据库每秒查询次数; TPS: 数据库每秒执行事务数)

2.3.6 绘图的重要性

图形化能清晰的展现数据,也更容易看出问题

2.4 基准测试工具

同基准策略,也分为两类

  • 集成式测试工具 (针对整个应用进行测试)
    • ab: 测试Http服务器每秒可处理请求数
    • http_load:同为Http服务器,比ab更灵活
    • JMeter:Java应用程序,加载其他应用并测试其性能
  • 单组件式测试工具(针对MySQL和基于MySQL的系统进行测试)
    • mysqlslap:模拟服务器负载,输出计时信息
    • MySQL Benchmark Suite:在不同数据库服务器上进行比较测试
    • Database Test Suite:工业标准测试
    • Percona's TPCC-MySQL Tool:评估大压力时MySQL的一些行为
    • sysbench: (本书作者自家产品)多线程系统压测工具,评估系统性能

2.4.1 集成式测试工具

如上

2.4.2 单组件式测试工具

如上

2.5 基准测试案例

2.5.1 http_load

2.5.2 MySQL基准测试套件

2.5.3 sysbench

2.5.4 数据库测试套件中的dbt2 TPC-C测试

2.5.5 Percona的TPCC-MySQL测试工具

2.6 总结

首先正确地描述问题,之后选择合适的基准测试来回答问题。设置基准测试的持续时间和参数,运行测试,收集数据,分析结果数据,

第3章 服务器性能剖析

性能剖析是一项技术,专注于测量服务器的时间花费在哪里。

3.1 性能优化简介

  • 性能即响应时间
  • 无法测量就无法优化(优化要基于测量)
  • 性能优化 = 测量 + 修改
  • 任务时间 = 执行时间 + 等待时间

3.1.1 通过性能剖析进行优化

性能剖析是种方法,用于测量和分析时间花费在哪里。

性能剖析一般有两个步骤:

  • 测量任务所花费的时间;
  • 将结果进行统计和按重要性排序

性能剖析工具的工作方式基本相同:在任务开始时启动计时器,在任务结束时停止计时器,两者相减得到响应时间。

讨论两种类型的性能剖析:基于执行时间的分析和基于等待的分析。

3.1.2 理解性能剖析

没显示出来的信息也很重要:

  • 值得优化的查询(worthwhile query):如果优化的成本大于收益,就应当停止优化。
  • 异常情况:执行次数很少,但每次执行都非常慢,严重影响用户体验的某些任务。因为其执行频率低,所以总的响应时间占比并不突出。
  • 未知的未知:“丢失的时间”,任务的总时间和实际测量到的时间之间的差。
  • 被掩藏的细节:性能剖析会隐藏很多信息,而且无法表达全部情况:直方图、百分比、标准差、偏差指数等(较大波动因取平均值会被隐藏)。

3.2 对应用程序进行性能剖析

对系统进行性能剖析还是建议自上而下地进行,可以追踪自用户发起到服务器响应的整个流程。虽然性能问题大多数情况下都和数据库有关,但应用导致的性能问题也不少。

建议在所有的新项目中都考虑包含性能剖析的代码。(性能剖析收益 >> 成本)

New Relic

3.3 剖析MySQL查询

MySQL查询性能剖析两种方式:剖析整个数据库服务器、对这些查询进行单独的剖析。

3.3.1 剖析服务器负载

捕获MySQL的查询到日志文件中

慢查询日志是开销最低、精度最高的测量查询时间的工具:效率无需担心,需关注日志可能消耗大量的磁盘空间。

Percona Server的慢查询日志比MySQL官方版本记录了更多细节且有价值的信息,如查询执行计划、锁、I/O活动等。

若无法使用慢查,两种替代的技术:

  • 通过--processlist选项不断查看SHOW FULL PROCESSLIST的输出,记录查询第一次出现的时间和消失的时间。
  • 通过抓取TCP网络包,然后根据MySQL的客户端/服务端通信协议进行解析。(tcpdump & pt-query-digest的--type=tcpdump)

分析查询日志

从慢查询日志中生成剖析报告,建议使用pt-query-digest,它是分析MySQL查询日志最有力的工具。(作者团力荐)

3.3.2 剖析单条查询

针对需要优化的单条查询,确认花费的时间去处,及如何优化。

三种方法:SHOW STATUS、SHOW PROFILE、检查慢查询日志的条目(要求是Percona Server,官方MySQL版本的慢查询日志缺失附加信息)

使用SHOW PROFILE

这个工具最有用的作用是在语句执行期间剖析服务器的具体工作:当一条查询提交给服务器时,此工具会记录剖析信息到一张临时表,并且给查询赋予一个从1开始的整数标识符。

剖析报告给出了查询执行的每个步骤及其花费的时间

使用SHOW STATUS

SHOW STATUS命令返回了一些计数器。既有服务器级别的全局计数器,也有基于某个连接的会话级别的计数器。

使用慢查询日志

结合pt-query-digest使用

使用Performance Schema

记录了一些裸数据,可供后期剖析

3.3.3 使用性能剖析

好的剖析报告能够将潜在的问题显示出来,但最终的解决方案还需要用户来决定(尽管报告可能会给出建议)。

优化查询时,用户需要对服务器如何执行查询有较深的了解。剖析报告能够尽可能多地收集需要的信息、给出诊断问题的正确方向,以及为其他诸如EXPLAIN等工具提供基础信息。

3.4 诊断间歇性问题

间歇性的问题比如系统偶尔停顿或者慢查询,很难诊断。只有在问题发生的地方通过观察资源的使用情况,并尽可能地测量出数据,才能避免在没有问题的地方耗费精力。

3.4.1 单条查询问题还是服务器问题

那么如何判断是单条查询问题还是服务器问题呢?可以通过三种技术:

使用SHOW GLOBAL STATUS

以较高的频率执行SHOW GLOBAL STATUS命令捕获数据,问题出现时,则可以通过某些计数器(比如Threads_running、Threads_connected、Questions和Queries)的“尖刺”或者“凹陷”来发现.

示例命令及其输出:

$ mysqladmin ext -i1 | awk ' /Queries/{q=$4-qp;qp=$4} /Threads_connected/{tc=$4} /Threads_running/{printf "%5d %5d %5d\n", q, tc, $4}'

这个命令每秒捕获一次SHOW GLOBAL STATUS的数据,输出给awk计算并输出:每秒的查询数、Threads_connected、Threads_running。可以长时间运行后将结果绘制成图形,更明确地定位问题。

使用SHOW PROCESSLIST

通过不停地捕获SHOW PROCESSLIST的输出,来观察是否有大量线程处于不正常的状态或者有其他不正常的特征。

$ mysql -e 'SHOW PROCESSLIST\G' | grep State: | sort | uniq -c | sort -rn

使用查询日志

如果要通过查询日志发现问题,需要开启慢查询日志并在全局级别设置long_query_time为0。

理解发现的问题(Making sense of the findings)

可视化的数据最具有说服力。诊断问题时先使用前两种开销低的方法:SHOW STATUS和SHOW PROCESSLIST,并结合绘图展示。

3.4.2 捕获诊断数据

当出现间歇性问题时,需要尽可能多地收集所有数据,而不只是问题出现时的数据。需要搞清楚两件事:

诊断触发器

选择一个参数的适当阀值,做为收集数据的开始时间点。

选择一个合适的阈值很重要,既要足够高,以确保在正常时不会被触发;又不能太高,要确保问题发生时不会错。

需要收集什么样的数据

现在需要收集什么样的数据呢?尽可能收集所有能收集的数据,包括系统的状态、CPU利用率、磁盘使用率和可用空间、ps的输出采样、内存利用率,以及可以从MySQL获得的信息,如SHOW STATUS、SHOW PROCESSLIST和SHOW INNODB STATUS...

解释结果数据

如果已经正确地设置好触发条件,并且长时间运行pt-stalk,则只需要等待足够长的时间来捕获几次问题,就能够得到大量的数据来进行筛选。

3.4.3 一个诊断案例

3.5 其他剖析工具

存在即合理,稍微了解下

3.5.1 使用USER_STATISTICS表

Percona Server和MariaDB都引入了INFORMATION_SCHEMA表。可以对数据库活动进行测量和审计,强制执行使用策略。可以帮找出数据库中花费最多时间的地方,使用最(不)频繁的表或索引。

Performance Schema中也有很多类似上面这些功能的表。

3.5.2 使用strace

strace工具可以调查系统调用的情况。有好几种可以使用的方法,其中一种是计算系统调用的时间并打印出来:

3.6 总结

基本知识点:

  • 定义性能最有效的方法是响应时间。
  • 无法测量就无法有效地优化,性能优化工作基于高质量、全方位及完整的响应时间测量。
  • 测量的最佳开始点是应用程序,而不是数据库。
  • 完整的测量会产生大量需要分析的数据,所以需要用到剖析器。
  • 剖析报告是一种汇总信息。
  • 有两种消耗时间的操作:工作或者等待。
  • 当优化成本超过收益的时候,要停止优化。
  • 决策应当尽量基于数据而不是感觉。

解决性能问题的方法,首先是要澄清问题,然后选择合适的技术来答问题。(pt-query-digest工具)

第4章 Schema与数据类型优化

良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema。本章关注的是MySQL数据库的设计,主要介绍的是MySQL数据库设计与其他关系型数据库管理系统的区别。

本章内容是为接下来的两个章节做铺垫。在这三章中,我们将讨论逻辑设计、物理设计和查询执行,以及它们之间的相互作用。

4.1 选择优化的数据类型

几个简单的原则:

  • 更小的通常更好:最小数据类型,占用资源少,CPU处理快
  • 简单就好:类型准确,避免类型转换
  • 尽量避免NULL: 可为NULL的列,难以优化查询,因为可为NULL的列使得索引、索引统计和值复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。

4.1.1 整数类型

有两种类型的数字:整数(whole number)和实数(real number)

整数:TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT。分别使用8,16,24,32,64位存储空间。可选的UNSIGNED属性,表示不允许负值,可以使正数的上限提高一倍。有符号和无符号类型使用相同的存储空间,并具有相同的性能。

4.1.2 实数类型

实数:FLOAT、DOUBLE、DECIMAL,FLOAT4个字节,DOUBLE8个字节。DECIMAL字节可变,最多65个数字。

4.1.3 字符串类型

VARCHAR和CHAR是两种最主要的字符串类型

VARCHAR:长度可变,节省空间,但UPDATE时可能会导致行空间变大,导致额外工作。VARCHAR的长度字段最大为2字节,故VARCHAR最大长度为65536.()

CHAR: 长度固定,空间可能浪费,但不易产生碎片,效率也高。

VARCHAR(5)和VARCHAR(200)在存储时效果一样,但在查询时由于内存会按长列分配固定长度内存,所以查询效率不一致。

BLOB和TEXT类型

BLOB和TEXT都是为存储很大的数据而设计的字符串数据类型,分别采用二进制和字符方式存储。

使用枚举(ENUM)代替字符串类型

可使用枚举列代替常用字符串类型。MySQL在存储枚举时非常紧凑,根据列表值的数量压缩到一个或者两个字节中。MySQL在内部会将每个值的位置保存为整数,并在表的.frm文件中保存“数字-字符串”映射关系。举个例子:

CREATE TABLE enum_test( -> e ENUM ('fish', 'apple', 'dog') NOT NULL -> );
INSERT INTO enum_test(e) VALUES('fish'), ('dog'), ('apple');

这三行数据实际存储为整数,而不是字符串:

枚举字段是按照内部存储的整数而不是定义的字符串进行排序的:

4.1.4 日期和时间类型

MySQL可以使用许多类型来保存日期和时间值,例如YEAR和DATE。MySQL能存储的最小时间粒度为秒。

DATETIME:能保存大范围的值,从1001年到9999年,精度为秒。它格式为YYYYMMDDHHMMSS的整数中,与时区无关。8个字节。(与时区无关、只与文本表示一致)

TIMESTAMP:保存了从1970年1月1日午夜(格林尼治标准时间)以来的秒数,它和UNIX时间戳相同。只能表示从1970年到2038年。4个字节。(与时区有关)

4.1.5 位数据类型

MySQL有少数几种存储类型使用紧凑的位存储数据。所有这些位类型,不管底层存储格式和处理方式如何,从技术上来说都是字符串类型:

  • BIT
  • SET

4.1.6 选择标识符(identifier)

物理主键,最好使用整数, 可使用AUTO_INCREMEN

4.1.7 特殊类型数据

IPv4地址,经常被VARCHAR(15)来存储IP地址,但它们实际上是32位无符号整数,不是字符串。MySQL提供INET_ATON()和INET_NTOA()函数在这两种表示方法之间转换。

4.2 MySQL schema设计中的陷阱

有一些问题是由MySQL的实现机制导致的:

  • 太多的列
  • 太多的关联
  • 全能的枚举
  • 变相的枚举
  • 非此发明(Not Invent Here)的NULL

4.3 范式和反范式

范式:

  1. 第一范式:列不可再分
  2. 第二范式:有主键,且所有非主键属性都依赖主键,且不能依赖主键部分(针对联合主键)
  3. 第三范式:属性不依赖其他非主键属性,即无传递性

范式优点:数据简洁、无冗余,修改快

范式缺点:关联查询效率低下。

完全的范式化和完全的反范式化schema都是实验室里才有的东西:在真实世界中很少会这么极端地使用

4.4 缓存表和汇总表

即将数据量大的表按某些条件汇总数据至小表。

当重建汇总表和缓存表时,要保证数据在操作时依然可用。通过使用“影子表”来实现,“影子表”指的是一张在真实表“背后”创建的表。当完成了建表操作后,通过一个原子的重命名操作切换影子表和原表。例如,如果需要重建my_summary,则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换:temp = a , a = b , b =temp

4.4.1 物化视图

物化视图实际上是预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新。

4.4.2 计数器表

有一个计数器表,只有一行数据,记录网站的点击次数:

CREATE TABLE hit_counter (
 cnt int unsigned not null
) ENGINE=InnoDB;
UPDATE hit_counter SET cnt = cnt + 1;

要获得更高的并发更新性能,也可以将计数器保存在多行中,每次随机选择一行进行更新。然后预先在这张表增加100行数据。现在选择一个随机的槽(slot)进行更新:

CREATE TABLE hit_counter ( 
    slot tinyint unsigned not null primary key, cnt int unsigned not null
) ENGINE=InnoDB;
UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;
SELECT SUM(cnt) FROM hit_counter;

一个常见的需求是每隔一段时间开始一个新的计数器(例如,每天一个),可以再简单地修改一下表设计,不用像前面的例子那样预先生成行,而用ON DUPLICATE KEY UPDATE代替:

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;
INSERT INTO daily_hit_counter(day, slot, cnt) 
VALUES(CURRENT_DATE, RAND() * 100, 1) 
ON DUPLICATE KEY UPDATE cnt = cnt + 1;

如果希望减少表的行数,以避免表变得太大,可以写一个周期执行的任务,合并所有结果到0号槽,并且删除所有其他的槽:

UPDATE daily_hit_counter 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;

4.5 加快ALTER TABLE操作的速度

MySQL的ALTER TABLE操作的的方法是用新的结构创建一个空表,从旧表中查出所有数据插入新表,然后删除旧表。(效率贼慢)

大部分ALTER TABLE操作将导致MySQL服务中断,若不中断,能使用的技巧只有两种:

  • 先在一台不提供服务的机器上执行ALTER TABLE操作,然后和提供服务的主库进行切换;
  • “影子拷贝”。影子拷贝的技巧是用要求的表结构创建一张和源表无关的新表,然后通过重命名和删表操作交换两张表。

两种更新table字段的方式:

  • 慢:ALTER TABLE sakila.filmMODIFY COLUMN rental_duration TINYINT(3) NOT NULL DEFAULT 5;
  • 快:ALTER TABLE sakila.filmALTER COLUMN rental_duration SET DEFAULT 5;

4.5.1 只修改.frm文件

修改表的.frm文件是很快的,但MySQL有时候会在没有必要的时候也重建表。如果愿意冒一些风险,可以让MySQL做一些其他类型的修改而不用重建表:

建完新表后,直接交换新表与旧表的.frm文件

4.5.2 快速创建MyISAM索引

为了高效地载入数据到MyISAM表中,有一个常用的技巧是先禁用索引、载入数据,然后重新启用索引:

ALTER TABLE test.load_data DISABLE KEYS;
-- load data
ALTER TABLE test.load_data ENABLE KEYS;

这个办法对唯一索引无效,因为DISABLE KEYS只对非唯一索引有效。MyISAM会在内存中构造唯一索引,并且为载入的每一行检查唯一性。一旦索引的大小超过了有效内存大小,载入操作就会变得越来越慢。

4.6 总结

  • 尽量避免过度设计
  • 使用小而简单的合适数据类型,避免使用NULL值。
  • 尽量使用相同的数据类型存储相似或相关的值,尤其是要在关联条件中使用的列。
  • 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存。
  • 尽量使用整型定义标识列。
  • 小心使用ENUM和SET。最好避免使用BIT。
  • 范式是好的,但是反范式有时也是必需的,并且能带来好处。

第5章 创建高性能的索引

索引是存储引擎用于快速找到记录的一种数据结构。索引优化应该是对查询性能优化最有效的手段了。

5.1 索引基础

索引可以包含一个或多个列的值。如果索引包含多个列,那么要满足最左匹配原则。

如索引为(A,B,C)那么查询时也要保证必须先有A的值,再有B的值。例子:select * from table where A = 'xiao' and B = 'csdn'

5.1.1 索引的类型

索引有很多种类型,最常用的是B-Tree.

(常见的面试题索引失效的情况,需要结合具体索引类型分析。哈希索引,只要不是索引全列统统失效;B-Tree不满最左匹配、或对索引列使用函数都会失效)

B-Tree索引

B-Tree是从索引的根节点开始进行搜索,根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。

叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页,树的深度和表的大小直接相关。

B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。

假设有如下数据表:

CREATE TABLE People (
	last_name VARCHAR (50) NOT NULL,
	first_name VARCHAR (50) NOT NULL,
	dob date NOT NULL,
	gender enum ('m', 'f') NOT NULL,
	KEY (last_name, first_name, dob)
);

对于表中的每一行数据,索引中包含了last_name、frst_name和dob列的值,图5-2显示了该索引是如何组织数据的存储的。

哈希索引

哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。

空间数据索引(R-Tree)

MyISAM表支持空间索引,可以用作地理数据存储。和B-Tree索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效地使用任意维度来组合查询。必须使用MySQL的GIS相关函数如MBRCONTAINS()等来维护数据。(了解就好...)

全文索引

全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。全文搜索和其他几类索引的匹配方式完全不一样。它有许多需要注意的细节,如停用词、词干和复数、布尔搜索等。全文索引更类似于搜索引擎做的事情,而不是简单的WHERE条件匹配。(下章节介绍...)

其他索引类别

TokuDB使用分形树索引(fractal tree index)

ScaleDB使用Patricia tries...

5.2 索引的优点

  • 索引大大减少了服务器需要扫描的数据量。
  • 索引可以帮助服务器避免排序和临时表。
  • 索引可以将随机I/O变为顺序I/O。

5.3 高性能的索引策略

如何高效地使用索引

5.3.1 独立的列

“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。

此语句中不会使用索引actor_id:

SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5

5.3.2 前缀索引和索引选择性

索引的选择性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T)的比值,范围从1/#T到1之间。索引的选择性越高则查询效率越高,选择性高的索引可以在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

前缀索引是指将列截取开头合适长度后最为索引,是一种能使索引更小、更快的有效办法,但另一方面也有其缺点:MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。

5.3.3 多列索引

在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。MySQL 5.0和更新版本引入了一种叫“索引合并”(index merge)的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。

查询能够同时使用这两个单列索引进行扫描,并将结果进行合并。这种算法有三个变种:OR条件的联合(union),AND条件的相交(intersection),组合前两种情况的联合及相交。

5.3.4 选择合适的索引列顺序

关于选择性和基数的经验法则值得肯定,但不能忘记排序、分组和范围条件等其他因素造成的影响。

5.3.5 聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。InnoDB的聚簇索引是在同一个结构中保存了B-Tree索引和数据行。当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中。术语“聚簇”表示数据行和相邻的键值紧凑地存储在一起,所以一个表只能有一个聚簇索引.

选择哪个索引作为聚簇索引?

  1. InnoDB将通过主键聚集数据
  2. 如果没有定义主键,InnoDB会选择一个唯一的非空索引代替
  3. 如何没有唯一非空索引,InnoDB会隐式定义一个主键来作为聚簇索引

聚簇索引优点:

  • 相关数据保存在一起,根据主键聚集在一起
  • 数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。
  • 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。

聚簇索引缺点:

  • 插入速度严重依赖于插入顺序。如果不是按照主键顺序加载数据,还要额外排序。
  • 更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。
  • 主键被更新需要移动行的时候,可能面临“页分裂(page split)”的问题。
  • 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。

二级索引的叶子节点存储的是引用行的聚簇索引值,二级索引访问需要两次索引查找。第一次查聚簇索引值,第二次根据聚簇索引获取数据。

聚簇表和非聚簇表区别:

5.3.6 覆盖索引

如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。

避免根据索引得到的地址后,再回表查询。

举例:

-- 索引为(A)
select A from table where A >= 6;

-- 索引为(A, B, C)
select A, C from table where A = 6 and B =7;

Using index 说明索引覆盖了查询结果,避免了回表查询

5.3.7 使用索引扫描来做排序

MySQL有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描。

最好能直接使用索引的排序,避免额外排序操作。

5.3.8 压缩(前缀压缩)索引

MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。

5.3.9 冗余和重复索引

不要有重复索引,根据需求可适当创建冗余索引。

5.3.10 未使用的索引

删除无用索引。

5.3.11 索引和锁

索引可以减少不必要数据读取,从一个角度较少了锁竞争。

如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE子句,Using where 说明服务器层还需额外过滤数据。

5.4 索引案例学习

5.4.1 支持多种过滤条件

5.4.2 避免多个范围条件

5.4.3 优化排序

5.5 维护索引和表

维护表有三个主要的目的:找到并修复损坏的表,维护准确的索引统计信息,减少碎片。

5.5.1 找到并修复损坏的表

表损坏(corruption)是很糟糕的事情。会导致查询返回错误的结果或者莫须有的主键冲突等问题,严重时甚至还会导致数据库的崩溃。

CHECK TABLE检查表是否损坏,通常能够找出大多数的表和索引的错误。

REPAIR TABLE修复损坏的表。也可通过一个不做任何操作(no-op)的ALTER操作来重建表:ALTER TABLE innodb_tbl ENGINE=INNODB;

InnoDB一般不会出现损坏,如果出现了损坏,那么一定是发生了严重的错误,需要立刻调查一下原因。

5.5.2 更新索引统计信息

MySQL的查询优化器会通过两个API来了解存储引擎的索引值的分布信息,以决定如何使用索引。

  • records_in_range(),通过向存储引擎传入两个边界值获取在这个范围大概有多少条记录。MyISAM返回精确值,InnoDB返回估算值
  • info(),该接口返回各种类型的数据,包括索引的基数(每个键值有多少条记录)。

如果表没有统计信息,或者统计信息不准确,可以通过运行ANALYZE TABLE来重新生成统计信息。

InnoDB会在几个场景触发索引统计信息的更新。可以通过innodb_stats_on_metadata关闭自动生成统计信息。

一旦关闭索引统计信息的自动更新,那么就需要周期性地使用ANALYZE TABLE来手动更新。否则,索引统计信息就会永远不变。

5.5.3 减少索引和数据的碎片

B-Tree索引可能会碎片化,这会降低查询的效率。有三种类型的数据碎片。

  • 行碎片(Row fragmentation):指的是数据行被存储为多个地方的多个片段中。即使查询只从索引中访问一行记录,行碎片也会导致性能下降。
  • 行间碎片(Intra-row fragmentation):行间碎片是指逻辑上顺序的页,或者行在磁盘上不是顺序存储的。对诸如全表扫描和聚簇索引扫描之类的操作有很大的影响
  • 剩余空间碎片(Free space fragmentation):剩余空间碎片是指数据页中有大量的空余空间。这会导致服务器读取大量不需要的数据,从而造成浪费。

对于MyISAM表,这三类碎片化都可能发生。但InnoDB不会出现短小的行碎片,InnoDB会移动短小的行并重写到一个片段中。

可以通过执行OPTIMIZE TABLE或者导出再导入的方式来重新整理数据。

5.6 总结

MySQL中多数情况会使用B-Tree索引。回顾一下B-Tree索引。

在选择索引和编写利用索引的查询时有三个原则要记住:

  • 单行访问是很慢的。使用索引可以创建位置引用以提升效率。
  • 按顺序访问范围数据是很快的。(无磁盘I/O, 也没有排序和分组)
  • 索引覆盖查询是很快的。(直接从索引获取结果,无需再回表查询)

总的来说,编写查询语句时应该尽可能选择合适的索引以避免单行查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。

第6章 查询性能优化

查询优化、索引优化、库表结构优化需要齐头并进,一个不落。

6.1 为什么查询速度会慢

性能≈时间=任务数*次数*单次任务时间性能≈时间=任务数*次数*单次任务时间, 控制三个因素即可提高性能

6.2 慢查询基础:优化数据访问

对于低效的查询,通过下面两个步骤来分析:

  • 应用程序:是否检索了不需要的数据
  • MySQL服务器层:是否分析大量超过需要的数据行。

6.2.1 是否向数据库请求了不需要的数据

最好每次访问数据库时只请求需要的数据,但多数场景希望接口复用,考虑二者代价。

6.2.2 MySQL是否在扫描额外的记录

为了返回结果是否扫描了过多的数据。对于MySQL最简单的衡量查询开销的三个指标:

  • 响应时间:可以使用“快速上限估计”法来估算查询的响应时间(执行计划大概需要的I/O次数 * 硬件单词I/O时间 计算大概时间)
  • 扫描的行数:执行计划中扫描的行数。(理想情况下扫描的行数和返回的行数应该是相同的)
  • 返回的行数:实际查询返回的行数。

6.3 重构查询的方式

单个复杂-> 多个简单、大数据量->分批次小量

6.3.1 一个复杂查询还是多个简单查询

将一个复杂的查询分成多个简单的查询

6.3.2 切分查询

对于一个大查询我们需要“分而治之”,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。

6.3.3 分解关联查询

对关联查询进行分解,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。

6.4 查询执行的基础

当向MySQL发送一个请求的时候,MySQL到底做了些什么:

  • 客户端发送一条查询给服务器。
  • 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
  • 服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。
  • MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
  • 将结果返回给客户端。

6.4.1 MySQL客户端/服务器通信协议

max_allowed_packet参数决定客户端请求长度。

SHOW FULL PROCESSLIST命令可查看连接状态:

  • Sleep:线程正在等待客户端发送新的请求。
  • Query:线程正在执行查询或者正在将结果发送给客户端。
  • Locked:在MySQL服务器层,该线程正在等待表锁。
  • Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划。
  • Copying to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中
  • Sorting result:线程正在对结果集进行排序。
  • Sending data::线程可能在多个状态之间传送数据、或者在生成结果集、或者在向客户端返回数据。

6.4.2 查询缓存

解析一个查询语句之前,如果查询缓存是打开的,MySQL会优先检查查询是否命中查询缓存。检查是通过一个对大小写敏感的哈希查找实现的。

6.4.3 查询优化处理

  • 语法解析器和预处理:语法校验&预处理器权限校验

  • 查询优化器: 优化器会将语法树转换为执行计划,一个查询会有多种查询计划,优化器会选择最好的那个。以下是Mysql能处理的优化类型:

    • 重新定义关联表的顺序:数据表的关联并不总是按照在查询中指定的顺序进行,优化器会按优化方式重新排序

    • 将外连接转化成内连接:MySQL能够识别并重写查询OUTER JOIN语句,可以调整关联顺序,让外连接等价于一个内连接。

    • 使用等价变换规则:MySQL可以使用一些等价变换来简化并规范表达式,合并和减少一些比较,移除一些恒成立和一些恒不成立的判断。

    • 优化COUNT()、MIN()和MAX():索引和列是否可为空通常可以帮助MySQL优化这类表达式。

    • 预估并转化为常数表达式:当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行优化处理。

    • 覆盖索引扫描:当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无须查询对应的数据行

    • 子查询优化:MySQL在某些情况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问。

    • 提前终止查询:在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。

    • 等值传播:如果两个列的值通过等式关联,那么MySQL能够把其中一个列的WHERE条件传递到另一列上。

    • 列表IN()的比较:在很多数据库系统中,IN()完全等同于多个OR条件的子句,因为这两者是完全等价的。

  • 数据和索引的统计信息:MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息(页面、索引基数、数据行、索引长度等),优化器根据这些信息来选择一个最优的执行计划。

  • MySQL如何执行关联查询:并不仅仅是一个查询需要到两个表匹配才叫关联,每一个查询,每一个片段(包括子查询,甚至基于单表的SELECT)都可能是关联。

  • 执行计划:MySQL的执行计划是一棵左测深度优先的树

  • 关联查询优化器:MySQL优化器最重要的一部分就是关联查询优化。多表关联的时候有多种不同的关联顺序来获得执行结果。关联查询优化器会选择一个代价最小的关联顺序。

  • 排序优化:应避免排序或者尽可能避免对大量数据进行排序(filesort)。数据量小于“排序缓冲区”则使用内存,如果内存不够排序则分块后排序,将各块的排序结果存放在磁盘上然后合并(merge)

    • 两次传输排序(旧版本使用):读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。

    • 单次传输排序(新版本使用):先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。

6.4.4 查询执行引擎

相对于查询优化阶段,查询执行阶段不是那么复杂:MySQL只是简单地根据执行计划给出的指令逐步执行。(服务层按步骤调用存储引擎接口获取需要的数据)

6.4.5 返回结果给客户端

查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数。

如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放到查询缓存中。

MySQL将结果集返回客户端是一个增量、逐步返回的过程。

6.5 MySQL查询优化器的局限性

MySQL查询优化器对少部分查询不适用,而我们可以通过改写查询让MySQL高效地完成工作。

6.5.1 关联子查询

关联子查询会被优化器转换成exist语句或连接语句,但转换后的语句执行效率反而可能会变低

-- 原sql
SELECT * FROM sakila.film WHERE film_id IN (SELECT film_id FROM sakila.film_actor WHERE actor_id = 1;

-- 转换后等效exist语句
SELECT * FROM sakila.film WHERE EXISTS (SELECT * FROM sakila.film_actor WHERE actor_id = 1AND film_actor.film_id = film.film_id);

-- 转换后的内连接语句
SELECT film.* FROM sakila.film INNER JOIN sakila.film_actor USING(film_id) WHERE actor_id = 1;

6.5.2 UNION的限制

MySQL无法将限制条件从外层“下推”到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。

需要我们改写sql:

-- 原sql,查询量大但返回结果却很少,优化器无法处理
(SELECT first_name, last_name FROM sakila.actor ORDER BY last_name) UNION ALL (SELECT first_name, last_name FROM sakila.customer ORDER BY last_name) LIMIT 20;

-- 手动改写后sql
(SELECT first_name, last_name FROM sakila.actor ORDER BY last_name LIMIT 20) UNION ALL (SELECT first_name, last_name FROM sakila.customer ORDER BY last_name LIMIT 20) LIMIT 20;。

6.5.3 索引合并优化

在5.0和更新的版本中,当WHERE子句中包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行

6.5.4 等值传递

某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个非常大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个列表的值和另一个表的某个列相关联。那么优化器会将IN()列表都复制应用到关联的各个表中,但如果这个列表非常大,则会导致优化和执行都会变慢。

6.5.5 并行执行

MySQL无法利用多核特性来并行执行查询。很多其他的关系型数据库能够提供这个特性,但是MySQL做不到,不要花时间去尝试寻找并行执行查询的方法。

6.5.6 哈希关联

MySQL并不支持哈希关联——MySQL的所有关联都是嵌套循环关联。

6.5.7 松散索引扫描

MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。

6.5.8 最大值和最小值优化

对于MIN()和MAX()查询,MySQL的优化做得并不好。如果该字段上并没有索引,MySQL将会进行一次全表扫描,然后再排序。

不过可以尝试改写sql

-- 获取名字为PENELOP记录对应的最下actor_id

-- 原sql, 全表扫描后,再分组获取最小值
SELECT MIN(actor_id) FROM sakila.actor WHERE first_name='PENELOPE';

-- 使用索引项actor_id,获取的第一条即满足条件
SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENELOPE' LIMIT 1;

6.5.9 在同一个表上查询和更新

MySQL不允许对同一张表同时进行查询和更新。

可以通过使用生成表的形式来绕过上面的限制,因为MySQL只会把这个表当作一个临时表来处理。

6.6 查询优化器的提示(hint)

如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。通过在查询中加入相应的提示,就可以控制该查询的执行计划:

  • HIGH_PRIORITY和LOW_PRIORITY:告诉MySQL,当多个语句同时访问某一个表的时候,哪些语句的优先级相对高些、哪些语句的优先级相对低些。
  • DELAYED:对INSERT和REPLACE有效。MySQL会将使用该提示的语句立即返回给客户端,并将插入的行数据放入到缓冲区,然后在表空闲时批量将数据写入。
  • STRAIGHT_JOIN:可放在SELECT语句的SELECT关键字后,也可放在两个关联表的之间。第一个用法让查询中所有的表按照语句中出现的顺序进行关联。第二个用法是固定其前后两个表的关联顺序。
  • SQL_SMALL_RESULT和SQL_BIG_RESULT:只对SELECT语句有效,告诉优化器对GROUP BY或者DISTINCT查询如何使用临时表及排序。
  • SQL_BUFFER_RESULT:告诉优化器将查询结果放入到一个临时表,然后尽可能快地释放表锁。
  • SQL_CACHE和SQL_NO_CACHE :告诉MySQL这个结果集是否应该缓存在查询缓存中。
  • SQL_CALC_FOUND_ROWS:这并不是一个优化器提示,它不会告诉优化器任何关于执行计划的东西,它会让MySQL返回的结果集包含更多的信息。
  • FOR UPDATE和LOCK IN SHARE MODE:这也不是优化器提示,主要控制SELECT语句的锁机制,会对符合查询条件的数据行加锁。(只对实现了行级锁的存储引擎有效)
  • USE INDEX、IGNORE INDEX和FORCE INDEX:告诉优化器使用或者不使用哪些索引来查询记录

在MySQL 5.0和更新版本中,新增了一些参数用来控制优化器的行为:

  • optimizer_search_depth:控制优化器在穷举执行计划时的限度。如果查询长时间处于“Statistics”状态,那么可以考虑调低此参数。
  • optimizer_prune_level:默认是打开的,让优化器会根据需要扫描的行数来决定是否跳过某些执行计划。
  • optimizer_switch:包含了一些开启/关闭优化器特性的标志位。例如在MySQL 5.1中可以通过这个参数来控制禁用索引合并的特性。

6.7 优化特定类型的查询

介绍如何优化特定类型的查询

6.7.1 优化COUNT()查询

count(*) = count(1): 获取所有行数

count(column): 获取该列有值的数量(排除NULL)

MyISAM会将总行数存在常量中,故没有任何条件的count(*)在该引擎中非常快,可以利用这个特性做点优化:


-- 原语句,即使ID字段为索引项,也会全表扫描
SELECT COUNT(*) FROM world.City WHERE ID > 5;

-- 优化后,只扫描5行
SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*) FROM world.City WHERE ID <= 5;

6.7.2 优化关联查询

需要特别提醒:

  • 确保ON或者USING子句中的列上有索引。当表A和表B用列c关联的时候,如果优化器的关联顺序是A、B,那么就不需要在A表的对应列上建上索引,但B上对应列上要加索引。
  • 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程。
  • 当升级MySQL的时候需要注意:关联语法、运算符优先级等其他可能会发生变化的地方。因为以前是普通关联的地方可能会变成笛卡儿积,不同类型的关联可能会生成不同的结果等。

6.7.3 优化子查询

尽可能使用关联查询代替子查询。

6.7.4 优化GROUP BY和DISTINCT

MySQL优化器会在内部处理的时候相互转化这两类查询,它们都可以使用索引来优化,这也是最有效的优化办法。

当无法使用索引的时候,GROUP BY使用两种策略来完成:使用临时表或者文件排序来做分组。可以通过使用提示SQL_BIG_RESULT和SQL_SMALL_RESULT来让优化器按照你希望的方式运行。、

优化GROUP BY WITH ROLLUP:最好的办法是尽可能的将WITH ROLLUP功能转移到应用程序中处理。

6.7.5 优化LIMIT分页

在偏移量非常大的时候,例如可能是LIMIT 1000,20这样的查询,这时MySQL需要查询10 020条记录然后只返回最后20条,前面10000条记录都将被抛弃,这样的代价非常高。

优化此类分页查询的一个最简单的办法就是尽可能地使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。

-- 原sql
SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;

-- 优化后,先根据索引项film_id筛选数据,再做关联获取需要的数据
SELECT film.film_id, film.descriptionFROM sakila.filmINNER JOIN (SELECT film_id FROM sakila.film ORDER BY title LIMIT 50, 5) AS lim USING(film_id);

有时候也可以将LIMIT查询转换为已知位置的查询,让MySQL通过范围扫描获得到对应的结果。例如,如果在一个位置列上有索引,并且预先计算出了边界值,上面的查询就可以改写为:

-- 也可直接将limit语句转换为范围查询,前提是该字段是索引项
SELECT film_id, description FROM sakila.film WHERE position BETWEEN 50 AND 54 ORDER BY position;

6.7.6 优化SQL_CALC_FOUND_ROWS

分页的时候,一个常用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示(hint),这样就可以获得去掉LIMIT以后满足条件的行数,因此可以作为分页的总数。MySQL只有在扫描了所有满足条件的行以后,才会知道行数,所以加上这个提示以后,不管是否需要,MySQL都会扫描所有满足条件的行,然后再抛弃掉不需要的行,而不是在满足LIMIT的行数后就终止扫描。所以该提示的代价可能非常高。

一个更好的设计是将具体的页数换成“下一页”按钮,假设每页显示20条记录,那么我们每次查询时都是用LIMIT返回21条记录并只显示20条,如果第21条存在,那么我们就显示“下一页”按钮,否则就说明没有更多的数据,也就无须显示“下一页”按钮了。

另一种做法是先获取并缓存较多的数据——例如,缓存1000条——然后每次分页都从这个缓存中获取。这样做可以让应用程序根据结果集的大小采取不同的策略,如果结果集少于1000,就可以在页面上显示所有的分页链接,因为数据都在缓存中,所以这样做性能不会有问题。如果结果集大于1000,则可以在页面上设计一个额外的“找到的结果多于1000条”之类的按钮。这两种策略都比每次生成全部结果集再抛弃掉不需要的数据的效率要高很多。

有时候也可以考虑使用EXPLAIN的结果中的rows列的值来作为结果集总数的近似值(实际上Google的搜索结果总数也是个近似值)。当需要精确结果的时候,再单独使用COUNT(*)来满足需求,这时如果能够使用索引覆盖扫描则通常也会比SQL_CALC_FOUND_ROWS快得多。

6.7.7 优化UNION查询

MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将WHERE、LIMIT、ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器可以充分利用这些条件进行优化(例如,直接将这些子句冗余地写一份到各个子查询)。

除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致对整个临时表的数据做唯一性检查。这样做的代价非常高。即使有ALL关键字,MySQL仍然会使用临时表存储结果。事实上,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。

6.7.8 静态查询分析

Percona Toolkit中的pt-query-advisor能够解析查询日志、分析查询模式,然后给出所有可能存在潜在问题的查询,并给出足够详细的建议。这像是给MySQL所有的查询做一次全面的健康检查,它能检测出许多常见的问题。

6.7.9 使用用户自定义变量

用户自定义变量如果能够用好,发挥其潜力,可以写出非常高效的查询语句。

用户自定义变量是一个用来存储内容的临时容器,在连接MySQL的整个过程中都存在。可以使用下面的SET和SELECT语句来定义它们:

SET @one := 1;
SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
SET @last_week := CURRENT_DATE-INTERVAL 1 WEEK;

然后可以在任何可以使用表达式的地方使用这些自定义变量:

SELECT ... WHERE col<=@last_week;

在了解自定义变量的强大之前,我们再看看它自身的一些属性和限制,看看在哪些场景下我们不能使用用户自定义变量:

  • 使用自定义变量的查询,无法使用查询缓存。
  • 不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名和LIMIT子句中。
  • 用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信。
  • 如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交互(如果是这样,通常是代码bug或者连接池bug,这类情况确实可能发生)。
  • 在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题。
  • 不能显式地声明自定义变量的类型。。MySQL的用户自定义变量是一个动态类型。
  • MySQL优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想的方式运行。
  • 赋值的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定。实际情况可能很让人困惑,后面我们将看到这一点。
  • 赋值符号:=的优先级非常低,所以需要注意,赋值表达式应该使用明确的括号。使用未定义变量不会产生任何语法错误,如果没有意识到这一点,非常容易犯错。

优化排名语句

用户自定义变量一个重要特性是你可以在给一个变量赋值的同时使用这个变量。换句话说,用户自定义变量的赋值具有“左值”特性。

来看一个更复杂的用法:先编写一个查询获取演过最多电影的前10位演员,然后根据他们的出演电影次数做一个排名,如果出演的电影数量一样,则排名相同。我们先编写一个查询,返回每个演员参演电影的数量:

再把排名加上去,这里看到有四名演员都参演了35部电影,所以他们的排名应该是相同的。

使用三个变量来实现:一个用来记录当前演员参演的电影数量,一个用来记录前一个演员的电影数量,一个用来记录当前的排名。只有当前演员参演的电影的数量和前一个演员不同时,排名才变化。

避免重复查询刚刚更新的数据

如果在更新行的同时又希望获得该行的信息,要怎么做才能避免重复的查询呢?

例如,我们的一个客户希望能够更高效地更新一条记录的时间戳,同时希望查询当前记录中存放的时间戳是什么。简单地,可以用下面的代码来实现:

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1; 
SELECT lastUpdated FROM t1 WHERE id = 1;

使用变量,我们可以按如下方式重写查询:

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW(); 
SELECT @now;

上面看起来仍然需要两个查询,但这里的第二个查询无须访问任何数据表,所以会快非常多。

统计更新和插入的数量

当使用了INSERT ON DUPLICATE KEY UPDATE的时候,如果想知道到底插入了多少行数据,到底有多少数据是因为冲突而改写成更新操作的?实现办法的本质如下:

INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1) ON DUPLICATE KEY UPDATE c1 = VALUES(c1) + ( 0 * ( @x := @x +1 ) );

当每次由于冲突导致更新时对变量@x自增一次。然后通过对这个表达式乘以0来让其不影响要更新的内容。另外,MySQL的协议会返回被更改的总行数,所以不需要单独统计这个值。

确定取值的顺序

使用用户自定义变量的一个最常见的问题就是没有注意到在赋值和读取变量的时候可能是在查询的不同阶段。

编写偷懒的UNION

假设需要编写一个UNION查询,其第一个子查询作为分支条件先执行,如果找到了匹配的行,则跳过第二个分支。在某些业务场景中确实会有这样的需求,比如先在一个频繁访问的表中查找“热”数据,找不到再去另外一个较少访问的表中查找“冷”数据。(区分热数据和冷数据是一个很好的提高缓存命中率的办法)。

下面的查询会在两个地方查找一个用户——一个主用户表、一个长时间不活跃的用户表,不活跃用户表的目的是为了实现更高效的归档:

SELECT id FROM users WHERE id=123 UNION ALL SELECT id FROM users_archived WHERE id=123;

上面这个查询是可以正常工作的,但是即使在users表中已经找到了记录,上面的查询还是会去归档表users_archived中再查找一次。我们可以用一个偷懒的UNION查询来抑制这样的数据返回,而且只有当第一个表中没有数据时,我们才在第二个表中查询。一旦在第一个表中找到记录,我们就定义一个变量@found。我们通过在结果列中做一次赋值来实现,然后将赋值放在函数GREATEST中来避免返回额外的数据。为了明确我们的结果到底来自哪个表,我们新增了一个包含表名的列。最后我们需要在查询的末尾将变量重置为NULL,这样保证遍历时不干扰后面的结果。完成的查询如下:

SELECT GREATEST(@found := −1, id) AS id, 'users' AS which_tbl FROM users WHERE id = 1 UNION ALL SELECT id, 'users_archived' FROM users_archived WHERE id = 1 AND @found IS NULL UNION ALL SELECT 1, 'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;

用户自定义变量的其他用处

用户自定义变量能够做的有趣的事情:

  • 查询运行时计算总数和平均值。
  • 模拟GROUP语句中的函数FIRST()和LAST()。
  • 对大量数据做一些数据计算。
  • 计算一个大表的MD5散列值。
  • 编写一个样本处理函数,当样本中的数值超过某个边界值的时候将其变成0。
  • 模拟读/写游标。

6.8 案例学习

6.9 总结

理解查询是如何被执行的以及时间都消耗在哪些地方,这依然是前面我们介绍的响应时间的一部分。

优化通常都需要三管齐下:不做、少做、快速地做。

除了这些基础的手段,包括查询、表结构、索引等,MySQL还有一些高级的特性可以帮助你优化应用,例如分区,分区和索引有些类似但是原理不同。

MySQL还支持查询缓存,它可以帮你缓存查询结果,当完全相同的查询再次执行时,直接使用缓存结果。

第7章 MySQL高级特性

7.1 分区表

分区表是一个独立的逻辑表,但是底层由多个物理子表组成。索引也是按照分区的子表定义的,而没有全局索引。

7.1.1 分区表的原理

从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎无须知道这是一个普通表还是一个分区表的一部分。

分区表上的操作按照下面的操作逻辑进行:

  • SELECT查询:当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据。
  • INSERT操作:当写入一条记录时,分区层先打开并锁住所有的底层表,然后确定哪个分区接收这条记录,再将记录写入对应底层表。
  • DELETE操作:当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作。
  • UPDATE操作:当更新一条记录时,分区层先打开并锁住所有的底层表,MySQL先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区,最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作。

7.1.2 分区表的类型

MySQL支持多种分区表。我们看到最多的是根据范围进行分区,每个分区存储落在某个范围的记录,分区表达式可以是列,也可以是包含列的表达式。

例如,下表就可以将每一年的销售额存放在不同的分区里:

CREATE TABLE sales ( 
    order_date DATETIME NOT NULL, 
    -- Other columns omitted 
) ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date)) ( 
    PARTITION p_2010 VALUES LESS THAN (2010), 
    PARTITION p_2011 VALUES LESS THAN (2011), 
    PARTITION p_2012 VALUES LESS THAN (2012), 
    PARTITION p_catchall VALUES LESS THAN MAXVALUE );

PARTITION分区子句中可以使用各种函数。但有一个要求,表达式返回的值要是一个确定的整数,且不能是一个常数。

7.1.3 如何使用分区表

为了保证大数据量的可扩展性,一般有下面两个策略:

全量扫描数据,不要任何索引:可以使用简单的分区方式存放表,不要任何索引,根据分区的规则大致定位需要的数据位置。

索引数据,并分离热点:如果数据有明显的“热点”,而且除了这部分数据,其他数据很少被访问到,那么可以将这部分热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在内存中。

7.1.4 什么情况下会出问题

上面的两个分区策略都基于两个非常重要的假设:查询都能够过滤掉很多额外的分区、分区本身并不会带来很多额外的代价。这两个假设在某些场景下会有问题,介绍一些可能会遇到的问题:

  • NULL值会使分区过滤无效
  • 分区列和索引列不匹配
  • 选择分区的成本可能很高
  • 打开并锁住所有底层表的成本可能很高
  • 维护分区的成本可能很高

7.1.5 查询优化

分区最大的优点就是优化器可以根据分区函数来过滤一些分区。

使用EXPLAIN PARTITION可以观察优化器是否执行了分区过滤,下面是一个示例:

7.1.6 合并表

合并表(Merge table)是一种早期的、简单的分区实现,和分区表相比有一些不同的限制,并且缺乏优化。分区表严格来说是一个逻辑上的概念,用户无法访问底层的各个分区,对用户来说分区是透明的。但是合并表允许用户单独访问各个子表。分区表和优化器的结合更紧密,这也是未来发展的趋势,而合并表则是一种将被淘汰的技术,在未来的版本中可能被删除。

7.2 视图

视图本身是一个虚拟表,不存放任何数据。在使用SQL语句访问视图的时候,它返回的数据是MySQL从其他表中生成的。

MySQL可以使用两种办法中的任何一种来处理视图。这

  • 临时表算法(TEMPTABLE):将SELECT语句的结果存放到临时表中。当需要访问视图的时候,直接访问这个临时表就可以了。
  • 合并算法(MERGE(会尽可能地使用合并算法):重写含有视图的查询,将视图的定义SQL直接包含进查询的SQL中。

如果是采用临时表算法实现的视图,EXPLAIN中会显示为派生表(DERIVED)。

7.2.1 可更新视图

可更新视图(updatable view)是指可以通过更新这个视图来增删改视图涉及的相关表。

7.2.2 视图对性能的影响

合理利用视图可提高性能:使用视图实现基于列的权限控制...

但视图也会增加一些看不到的服务器负担,视图背后逻辑可能比较复杂。

7.2.3 视图的限制

MySQL还不支持物化视图(物化视图是指将视图结果数据存放在一个可以查看的表中,并定期从原始表中刷新数据到这个表中)。但可以使用构建缓存表或者汇总表的办法来模拟物化视图和索引。

7.3 外键约束

如需同步更新则可使用外键;

如需数值约束,可使用触发器;

如果只是简单约束,应用内约束即可

7.4 在MySQL内部存储代码

MySQL允许通过触发器、存储过程、函数、事件的形式来存储代码。

这四种存储代码都使用特殊的SQL语句扩展,它包含了很多过程处理语法,例如循环和条件分支等,存储过程和存储函数都可以接收参数然后返回值,但是触发器和事件却不行。

7.4.1 存储过程和函数

存储过程:

-- 定义存储过程
CREATE PROCEDURE getInfo (num INT)
BEGIN
	SELECT * FROM student WHERE	id = num;
END;

-- 调用存储过程
call getInfo(1);

存储函数:

-- 创建存储函数
CREATE FUNCTION getCount (num INT) RETURNS INT
BEGIN
	DECLARE count INT;
	SELECT count(*) INTO count FROM	student;
	RETURN count;
END;

-- 调用存储函数
SELECT getCount (1);

7.4.2 触发器

CREATE TRIGGER `myInsert` AFTER INSERT ON `teacher` FOR EACH ROW BEGIN
update student SET name = 'triggerName' WHERE	id = 1;
END;

7.4.3 事件

CREATE EVENT eventDemo ON SCHEDULE EVERY 1 WEEK DO CALL getInfo(1);

7.4.4 在存储程序中保留注释

7.5 游标

游标是个啥?

7.6 绑定变量

当创建一个绑定变量SQL时,客户端向服务器发送了一个SQL语句的原型。服务器端收到这个SQL语句框架后,解析并存储这个SQL语句的部分执行计划,返回给客户端一个SQL语句处理句柄。以后每次执行这类查询,客户端都指定使用这个句柄。

绑定变量的SQL,使用问号标记可以接收参数的位置,当真正需要执行具体查询的时候,则使用具体值代替这些问号。例如,下面是一个绑定变量的SQL语句:

INSERT INTO tbl(col1, col2, col3) VALUES (?, ?, ?);

因为如下的原因,MySQL在使用绑定变量的时候可以更高效地执行大量的重复语句:

  • 在服务器端只需要解析一次SQL语句。
  • 在服务器端某些优化器的工作只需要执行一次,因为它会缓存一部分的执行计划。
  • 以二进制的方式只发送参数和句柄,比起每次都发送ASCII码文本效率更高,一个二进制的日期字段只需要三个字节,但如果是ASCII码则需要十个字节。不过最大的节省还是来自于BLOB和TEXT字段,绑定变量的形式可以分块传输,而无须一次性传输。二进制协议在客户端也可能节省很多内存,减少了网络开销,另外,还节省了将数据从存储原始格式转换成文本格式的开销。
  • 仅仅是参数——而不是整个查询语句——需要发送到服务器端,所以网络开销会更小。
  • MySQL在存储参数的时候,直接将其存放到缓存中,不再需要在内存中多次复制。

绑定变量相对也更安全。无须在应用程序中处理转义,一则更简单了,二则也大大减少了SQL注入和攻击的风险。

7.7 用户自定义函数

存储过程只能使用SQL来编写,而UDF没有这个限制,可以使用支持C语言调用约定的任何编程语言来实现。

UDF必须事先编译好并动态链接到服务器上,这种平台相关性使得UDF在很多方面都很强大。UDF速度非常快,而且可以访问大量操作系统的功能,还可以使用大量库函数。使用SQL实现的存储函数在实现一些简单操作上很有优势,诸如计算球体上两点之间的距离,但是如果操作涉及到网络交互,那么只能使用UDF了。同样地,如果需要一个MySQL不支持的统计聚合函数,而且无法使用SQL编写的存储函数来实现的话,通常使用UDF是很容易实现的。

能力越大,责任越大。所以在UDF中的一个错误很可能会让服务器直接崩溃,甚至扰乱服务器的内存或者数据,另外,所有C语言具有的潜在风险,UDF也都有。

7.8 插件

除了UDF,MySQL还支持各种各样的插件。这些插件可以在MySQL中新增启动选项和状态值,还可以新增INFORMATION_SCHEMA表,或者在MySQL的后台执行任务,等等。简单的插件列表:

  • 存储过程插件:存储过程插件可以帮你在存储过程运行后再处理一次运行结果。这
  • 后台插件:后台插件可以让你的程序在MySQL中运行,可以实现自己的网络监听、执行自己的定期任务。
  • INFORMATION_SCHEMA插件:这个插件可以提供一个新的内存INFORMATION_SCHEMA表。
  • 全文解析插件:这个插件提供一种处理文本的功能,可以根据自己的需求来对一个文档进行分词,所以如果给定一个PDF文档目录,可以使用这个插件对这个文档进行分词处理。也
  • 审计插件:审计插件在查询执行的过程中的某些固定点被调用,所以它可以用作(例如)记录MySQL的事件日志。
  • 认证插件:认证插件既可以在MySQL客户端也可在它的服务器端,可以使用这类插件来扩展MySQL的认证功能,例如可以实现PAM和LDAP认证。

7.9 字符集和校对

字符集是指一种从二进制编码到某类字符符号的映射,“校对”是指一组用于某个字符集的排序规则。

每种字符集都可能有多种校对规则,并且都有一个默认的校对规则。每个校对规则都是针对某个特定的字符集的,和其他的字符集没有关系。校对规则和字符集总是一起使用的,所以后面我们将这样的组合也统称为一个字符集。

MySQL的设置可以分为两类:创建对象时的默认值、在服务器和客户端通信时的设置。

7.10 全文索引

7.11 分布式(XA)事务

7.12 查询缓存

MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会立刻返回结果,跳过了解析、优化和执行阶段。

7.12.1 MySQL如何判断缓存命中

MySQL判断缓存命中的方法很简单:缓存存放在一个引用表中,通过一个哈希值引用,这个哈希值包括了如下因素,即查询本身、当前要查询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。

7.12.2 查询缓存如何使用内存

查询缓存是完全存储在内存中的。

除了查询结果之外,需要缓存的还有维护相关的数据,这些管理维护数据结构大概需要40KB的内存资源。

除此之外,MySQL用于查询缓存的内存被分成一个个的数据块,数据块是变长的。每一个数据块中,存储了自己的类型、大小和存储的数据本身,还外加指向前一个和后一个数据块的指针。数

当服务器启动的时候,它先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。这个空闲块的大小就是你所配置的查询缓存大小再减去用于维护元数据的数据结构所消耗的空间。

当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_res_unit的配置,即使查询结果远远小于此,仍需要至少申请query_cache_min_res_unit空间。

因为需要先锁住空间块,然后找到合适大小数据块,所以相对来说,分配内存块是一个非常慢的操作。MySQL尽量避免这个操作的次数。当需要缓存一个查询结果的时候,它先选择一个尽可能小的内存块,然后将结果存入其中。如果数据块全部用完,但仍有剩余数据需要存储,那么MySQL会申请一块新数据块继续存储结果数据。当查询完成时,如果申请的内存空间还有剩余,MySQL会将其释放,并放入空闲内存部分。

 

7.12.3 什么情况下查询缓存能发挥作用

只有当缓存带来的资源节约大于其本身的资源消耗时才会给系统带来性能提升。

7.12.4 如何配置和维护查询缓存

一旦理解查询缓存工作的原理,配置起来就很容易了。只有很少的参数可供配置:

  • query_cache_type:是否打开查询缓存。可以设置成OFF、ON或DEMAND。
  • query_cache_size:查询缓存使用的总内存空间,单位是字节。这个值必须是1 024的整数倍。
  • query_cache_min_res_unit:在查询缓存中分配内存块时的最小单位。
  • query_cache_limit:MySQL能够缓存的最大查询结果。如果查询结果大于这个值,则不会被缓存。
  • query_cache_wlock_invalidate:如果某个数据表被其他的连接锁住,是否仍然从查询缓存中返回结果。这个参数默认是OFF。

7.12.5 InnoDB和查询缓存

因为InnoDB有自己的MVCC机制,所以相比其他存储引擎,InnoDB和查询缓存的交互要更加复杂。

事务是否可以访问查询缓存取决于当前事务ID,以及对应的数据表上是否有锁。每一个InnoDB表的内存数据字典都保存了一个事物ID号,如果当前事务ID小于该事务ID,则无法访问查询缓存。

原则上,在InnoDB的MVCC架构下,当某些修改不影响其他事务读取一致的数据时,是可以使用查询缓存的。InnoDB做了一个简化,让所有有加锁操作的事务都不使用任何查询缓存。

7.12.6 通用查询缓存优化

库表结构的设计、查询语句、应用程序设计都可能会影响到查询缓存的效率。除了前文介绍的之外,这里还有一些要点需要注意:

  • 用多个小表代替一个大表对查询缓存有好处。
  • 批量写入时只需要做一次缓存失效,所以相比单条写入效率更好。
  • 因为缓存空间太大,在过期操作的时候可能会导致服务器僵死。
  • 无法在数据库或者表级别控制查询缓存,但是可以通过SQL_CACHE和SQL_NO_CACHE来控制某个SELECT语句是否需要进行缓存。你
  • 对于写密集型的应用来说,直接禁用查询缓存可能会提高系统的性能。
  • 因为对互斥信号量的竞争,有时直接关闭查询缓存对读密集型的应用也会有好处。

如果不想所有的查询都进入查询缓存,但是又希望某些查询走查询缓存,那么可以将query_cache_type设置成DEMAND,然后在希望缓存的查询中加上SQL_CACHE。这虽然需要在查询中加入一些额外的语法,但是可以让你非常自由地控制哪些查询需要被缓存。相反,如果希望缓存多数查询,而少数查询又不希望缓存,那么你可以使用关键字SQL_NO_CACHE。

7.12.7 查询缓存的替代方案

MySQL查询缓存工作的原则是:执行查询最快的方式就是不去执行,但是查询仍然需要发送到服务器端,服务器也还需要做一点点工作。如果对于某些查询完全不需要与服务器通信效果会如何呢?这时客户端的缓存可以很大程度上帮你分担MySQL服务器的压力。我们将在第14章详细介绍更多关于缓存的内容。

7.13 总结

回顾一下MySQL高级特性。

分区表

分区表是一种粗粒度的、简易的索引策略,适用于大数据量的过滤场景。最适合的场景是,在没有合适的索引时,对其中几个分区进行全表扫描,或者是只有一个分区和索引是热点,而且这个分区和索引能够都在内存中;限制单表分区数不要超过150个,并且注意某些导致无法做分区过滤的细节,分区表对于单条记录的查询并没有什么优势,需要注意这类查询的性能。

视图

对好几个表的复杂查询,使用视图有时候会大大简化问题。当视图使用临时表时,无法将WHERE条件下推到各个具体的表,也不能使用任何索引,需要特别注意这类查询的性能。如果为了便利,使用视图是很合适的。

外键

外键限制会将约束放到MySQL中,这对于必须维护外键的场景,性能会更高。不过这也会带来额外的复杂性和额外的索引消耗,还会增加多表之间的交互,会导致系统中更多的锁和竞争。外键可以被看作是一个确保系统完整性的额外的特性,但是如果设计的是一个高性能的系统,那么外键就显得很臃肿了。很多人在更在意系统的性能的时候都不会使用外键,而是通过应用程序来维护。

存储过程

MySQL本身实现了存储过程、触发器、存储函数和事件,老实说,这些特性并没什么特别的。而且对于基于语句的复制还有很多问题。通常,使用这些特性可以帮你节省很多的网络开销——很多情况下,减少网络开销可以大大提升系统的性能。在某些经典的场景下你可以使用这些特性(例如中心化业务逻辑、绕过权限系统,等等),但需要注意在MySQL中,这些特性并没有别的数据库系统那么成熟和全面。

绑定变量

当查询语句的解析和执行计划生成消耗了主要的时间,那么绑定变量可以在一定程度上解决问题。因为只需要解析一次,对于大量重复类型的查询语句,性能会有很大的提高。另外,执行计划的缓存和传输使用的二进制协议,这都使得绑定变量的方式比普通SQL语句执行的方式要更快。

插件

使用C或者C++编写的插件可以让你最大程度地扩展MySQL功能。插件功能非常强大,我们已经编写了很多UDF和插件,在MySQL中解决了很多问题。

字符集

字符集是一种字节到字符之间的映射,而校对规则是指一个字符集的排序方法。很多人都使用Latin1(默认字符集,对英语和某些欧洲语言有效)或者UTF-8。如果使用的是UTF-8,那么在使用临时表和缓冲区的时候需要注意:MySQL会按照每个字符三个字节的最大占用空间来分配存储空间,这可能消耗更多的内存或者磁盘空间。注意让字符集和MySQL字符集配置相符,否则可能会由于字符集转换让某些索引无法正常使用。

全文索引

在本书编写的时候只有MyISAM支持全文索引,不过据说从MySQL 5.6开始, InnoDB也将支持全文索引。MyISAM因为在锁粒度和崩溃恢复上的缺点,使得在大型全文索引场景中基本无法使用。这时,我们通常帮助客户构建和使用Sphinx来解决全文索引的问题。

XA事务

很少有人用MySQL的XA事务特性。除非真正明白参数innodb_support_xa的意义,否则不要修改这个参数的值,并不是只有显式使用XA事务时才需要设置这个参数。InnoDB和二进制日志也是需要使用XA事务来做协调的,从而确保在系统崩溃的时候,数据能够一致地恢复。

查询缓存

完全相同的查询在重复执行的时候,查询缓存可以立即返回结果,而无须在数据库中重新执行一次。根据我们的经验,在高并发压力环境中查询缓存会导致系统性能的下降,甚至僵死。如果一定要使用查询缓存,那么不要设置太大内存,而且只有在明确收益的时候才使用。那该如何判断是否应该使用查询缓存呢?建议使用Percona Server,观察更细致的日志,并做一些简单的计算。还可以查看缓存命中率(并不总是有用)、“INSERTS和SELECT比率”(这个参数也并不直观)、或者“命中和写入比率”(这个参考意义较大)。查询缓存是一个非常方便的缓存,对应用程序完全透明,无须任何额外的编码,但是,如果希望有更高的缓存效率,我们建议使用memcached或者其他类似的解决方案。

第8章 优化服务器设置

 

第9章 操作系统和硬件优化

第10章 复制

第11章 可扩展的MySQL

第12章 高可用性

第13章 云端的MySQL

第14章 应用层优化

第15章 备份与恢复

第16章 MySQL用户工具

附录A

附录B

附录C

附录D EXPLAIN

EXPLAIN命令是查看查询优化器如何决定执行查询的主要方法

调用EXPLAIN

在要执行的查询语句前添加explain关键字即可

EXPLAIN SELECT 1

EXPLAIN中的列

  1. id列:这一列总是包含一个编号,标识SELECT所属的行。一般来说,id越大越早被执行。

  2. select_type列:显示了对应行是简单还是复杂SELECT(如果是后者,那么是三种复杂类型中的哪一种)。SIMPLE值意味着查询不包括子查询和UNION。如果查询有任何复杂的子部分,则最外层部分标记为PRIMARY,其他部分标记如下:

    1. SUBQUERY:包含在SELECT列表中的子查询中的SELECT(换句话说,不在FROM子句中)标记为SUBQUERY。

    2. DERIVED:DERIVED值用来表示包含在FROM子句的子查询中的SELECT。

    3. UNION:在UNION中的第二个和随后的SELECT被标记为UNION。

    4. UNION RESULT:用来从UNION的匿名临时表检索结果的SELECT被标记为UNION RESULT。

  3. table列:显示了对应行正在访问哪个表,正常情况下就是table名,下面两种情况特殊

    1. 派生表(临时表):<derivedN>的形式

    2. 联合表:<union N1, N2>的形式

  4. type列:显示了“关联类型”,更准确的说法是访问类型——MySQL决定如何查找表中的行。下面是最重要的访问方法,依次从最差到最优。

    1. ALL:全表扫描,通常意味着MySQL必须扫描整张表,从头到尾,去找到需要的行

    2. index: 跟全表扫描一样,只是MySQL扫描表时按索引次序进行而不是行。优点是避免了排序;缺点是若按索引次序读取整个表则需随机I/O访问行,开销将会非常大。

    3. range: 范围扫描就是一个有限制的索引扫描。它开始于索引里的某一点,返回匹配这个值域的行。

    4. ref: 这是一种索引访问(有时也叫做索引查找),它返回所有匹配某个单个值的行。把它叫做ref是因为索引要跟某个参考值相比较(这个参考值或者是一个常数)。

    5. eq_ref: 使用这种索引查找,MySQL知道最多只返回一条符合条件的记录。这种访问方法可以在MySQL使用主键或者唯一性索引查找时看到,它会将它们与某个参考值做比较。

    6. const, system: 当MySQL能对查询的某部分进行优化并将其转换成一个常量时,它就会使用这些访问类型。

    7. NULL: 这种访问方式意味着MySQL能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。

  5. possibIe_keys列:查询可以使用哪些索引,这是基于查询访问的列和使用的比较操作符来判断的。

  6. key列:MySQL 决定采用的索引来优化对该表的访问。如果该索引没有出现在possible_keys列中,那么MySQL选用它是出于另外的原因。

  7. key_len列:该列显示了MySQL在索引里使用的字节数。如果MySQL正在使用的只是索引里的某些列,那么就可以用这个值来算出具体是哪些列。

  8. ref列: 显示了之前的表在key列记录的索引中查找值所用的列或常量。

  9. rows列: 这一列是MySQL估计为了找到所需的行而要读取的行数。

  10. fiItered列: 这一列在使用EXPLAIN EXTENDED时出现。它显示的是针对表里符合某个条件(WHERE子句或联接条件)的记录数的百分比所做的一个悲观估算。

  11. Extra列: 这一列包含的是不适合在其他列显示的额外信息。

    1. Using index :此值表示MySQL将使用覆盖索引,以避免访问表。

    2. Using where:这意味着MySQL服务器将在存储引擎检索行后再进行过滤。

    3. Using temporary :这意味着MySQL在对查询结果排序时会使用一个临时表。

    4. Using filesort:这意味着MySQL会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。 

MySQL 5.6中的改进

SQL 5.6中将包括一个对EXPLAIN的重要改进:能对类似UPDATE、INSERT等的查询进行解释。

MySQL 5.6还将包括对查询优化和执行引擎的一系列改进,允许匿名的临时表尽可能晚地被具体化,而不总是在优化和执行使用到此临时表的部分查询时创建并填充它们。

MySQL 5.6将通过在服务器中增加优化跟踪功能的方式改进优化器的相关部分。

附录E

附录F

附录G

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页