MySQL的架构可在多种不同场景中应用并发挥好的作用,足够灵活,能够适应高要求的环境。
概要地描述MySQL的服务器架构、各种存储引擎之间的主要区别。
三层组件(★):
只要多个查询在同一时刻修改数据,就会产生并发控制的问题。本节讨论MySQL在两个层面的并发控制:服务器层与存储引擎层。
处理并发读或者写时,通过实现一个由两种类型的锁组成的锁系统来解决问题:
(理论上写锁应该能阻塞读锁,但实际中因为事务隔离级别的不同以及快照的存在,写锁并不能阻塞读。)
在给定的资源上,锁定的数据量越小(即锁粒度越小),则系统的并发程度越高。但锁粒度越小,要管理的锁越多,消耗的资源越大。
所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡。
表锁:表锁是MySQL中最基本的锁策略,锁粒度最大,但开销最小。会锁定整张表,一个用户在对表进行写操作前,需要先获得写锁,并阻塞其他用户对该表的所有读写操作。
行锁:最大程度地支持并发处理,锁粒度最小,但开销最大。只锁一行数据。
其他锁: 折中方案,锁粒度一般,资源开源一般,如分页锁、分段锁。
(写锁和读锁同时竞争时,写锁优先,保证后续读到的都是最新的数据)
事务就是一组原子性的SQL查询,或者说一个独立的工作单元。事务内的语句,要么全部执行成功,要么全部执行失败。
事务四个特性(ACID):
一个兼容ACID的数据库系统,需要做很多复杂但可能用户并没有觉察到的工作,才能确保ACID的实现。
针对事务的隔离性,定义了四种隔离级别:
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。
-- 事务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;
死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁,所以应用程序在设计时必须考虑如何处理死锁。
使用事务日志,存储引擎只修改内存中的数据,并追加记录磁盘中的事务日志。(追加事务日志比修改磁盘数据快。)
磁盘中的数据由事务日志后续慢慢追加执行。以此提高效率。
(即响应时间减小,但总花费时间变长)
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也支持通过特定的语句进行显式锁定:
MySQL也支持LOCK TABLES和UNLOCK TABLES语句。
MySQL的多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,一般都实现了多版本并发控制(MVCC)。可认为MVCC是行级锁的一个变种,在多数情况避免了加锁操作,开销更低。
MVCC的实现,是通过保存在某个时间点的数据快照实现的,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的:一个保存行的创建时间,一个保存行的过期时间(或删除时间)。(不是实际的时间值,而是系统版本号(system version number))。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(READ UNCOMMITTED总是读最新的数据行,而SERIALIZABLE会对所有的行都加锁)
不同存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL服务层统一处理的。可使用SHOW TABLE STATUS命令(5.0以后版本也可查INFORMATION_SCHEMA中对应的表)显示表信息。
InnoDB是MySQL的默认事务型引擎,是最重要、使用最广泛的存储引擎。处理短期事务、性能高、自动崩溃恢复特性。
数据存储在表空间(tablespace)中,表空间是由InnoDB管理的一个黑盒子,由一系列的数据文件组成。InnoDB可以将每个表的数据和索引存放在单独的文件中。
采用MVCC来支持高并发,并实现了四个标准的隔离级别。默认级别是REPEATABLE READ(可重复读),通过间隙锁(next-key locking)策略防止幻读的出现。
表是基于聚簇索引建立的,聚簇索引对主键查询有很高的性能。不过它的二级索引(secondary index,非主键索引)中必须包含主键列,所以如果主键列很大的话,其他的所有索引都会很大。
存储格式是平台独立的,可以将数据和索引文件从Intel平台复制到PowerPC或者Sun SPARC平台。
内部做了很多优化:可预测性预读、自适应哈希索引、插入缓冲区(insert buffer)等
支持真正的热备份。
MySQL 5.1及之前的版本,MyISAM是默认的存储引擎。不支持事务和行级锁、崩溃后无法安全恢复。
存储:MyISAM会将表存储在两个文件中:数据文件和索引文件,分别以.MYD和.MYI为扩展名。MyISAM表可以存储的行记录数。
特性:
Archive引擎:只支持INSERT和SELECT操作,磁盘I/O更少,对高速插入和压缩做了优化
Blackhole引擎:Blackhole引擎没有实现存储机制,它不做任何保存。但服务器会记录Blackhole表的日志,所以可用于复制数据到备库,或者只是简单地记录到日志。
CSV引擎:可将普通的CSV文件作为MySQL的表来处理,可在数据库运行时拷入或者拷出文件。可以将Excel等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在MySQL中打开使用,可以作为一种数据交换的机制.
...
OLTP(在线事务处理系统)类引擎:支持ACID事务和MVCC。如XtraDB、PBXT等。
面向列的存储引擎:大数据量处理时,面向列的方式可能效率更高。如不需要整行的数据,面向列的方式可以传输更少的数据。如果每一列都单独存储,那么压缩的效率也会更高。有Infobright、InfiniDB等。
社区存储引擎:...
无特定需求,使用默认InnoDB.。有特殊需求,如只涉及insert/select,考虑使用MyISAM。其他场景结合存储引擎特性考虑。
三种方式:
时间点 | 更新内容 |
版本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在功能和性能方面有显著提升。
开发版本用于获取测试和反馈,GA版本才是正式可用的。
MySQL依然遵循GPL开源协议,全部的源代码(除了一些商业版本的插件)都会开放给社区。
将更多的特性做成插件的开发模式,既是收费的方式,也是保证内部代码质量的方式。
MySQL拥有分层的架构,上层是服务器层的服务和查询执行引擎,下层则是存储引擎。服务器层通过API调用存储引擎层,不同存储引擎对数据的存储方式,以及API的实现方式不同。
存储引擎发展简述:ISAM -> MyISAM -> InnoDB
基准测试(benchmark),指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。
针对系统测试、验证、预估、预测。
两大策略:
开始执行或设计基准测试之前,要先明确测试的目标。
如何设计和执行基准测试
提出问题并明确目标。然后决定是采用标准的基准测试,还是设计专用的测试。
基准测试应该运行足够长的时间,持续观察直到确认系统已经稳定。
在执行基准测试时,需要尽可能多地收集被测试系统的信息。最好为基准测试建立一个目录,并且每执行一轮测试都创建单独的子目录,将测试结果、配置文件、测试指标、脚本和其他相关说明都保存在其中。
获得准确测试结果的最好办法,是回答一些关于基准测试的基本问题:是否选择了正确的基准测试?是否为问题收集了相关的数据?是否采用了错误的测试标准?
(基于MySQL的默认配置的测试没有什么意义,因为默认配置是基于消耗很少内存的极小应用的,所以MySQL之前一直被认为是小型应用专用数据库)
(最好采用自动化,易执行、易记录、易复现)
(QPS:数据库每秒查询次数; TPS: 数据库每秒执行事务数)
图形化能清晰的展现数据,也更容易看出问题
同基准策略,也分为两类
如上
如上
略
首先正确地描述问题,之后选择合适的基准测试来回答问题。设置基准测试的持续时间和参数,运行测试,收集数据,分析结果数据,
性能剖析是一项技术,专注于测量服务器的时间花费在哪里。
性能剖析是种方法,用于测量和分析时间花费在哪里。
性能剖析一般有两个步骤:
性能剖析工具的工作方式基本相同:在任务开始时启动计时器,在任务结束时停止计时器,两者相减得到响应时间。
讨论两种类型的性能剖析:基于执行时间的分析和基于等待的分析。
没显示出来的信息也很重要:
对系统进行性能剖析还是建议自上而下地进行,可以追踪自用户发起到服务器响应的整个流程。虽然性能问题大多数情况下都和数据库有关,但应用导致的性能问题也不少。
建议在所有的新项目中都考虑包含性能剖析的代码。(性能剖析收益 >> 成本)
(New Relic)
MySQL查询性能剖析两种方式:剖析整个数据库服务器、对这些查询进行单独的剖析。
捕获MySQL的查询到日志文件中
慢查询日志是开销最低、精度最高的测量查询时间的工具:效率无需担心,需关注日志可能消耗大量的磁盘空间。
Percona Server的慢查询日志比MySQL官方版本记录了更多细节且有价值的信息,如查询执行计划、锁、I/O活动等。
若无法使用慢查,两种替代的技术:
分析查询日志
从慢查询日志中生成剖析报告,建议使用pt-query-digest,它是分析MySQL查询日志最有力的工具。(作者团力荐)
针对需要优化的单条查询,确认花费的时间去处,及如何优化。
三种方法:SHOW STATUS、SHOW PROFILE、检查慢查询日志的条目(要求是Percona Server,官方MySQL版本的慢查询日志缺失附加信息)
使用SHOW PROFILE
这个工具最有用的作用是在语句执行期间剖析服务器的具体工作:当一条查询提交给服务器时,此工具会记录剖析信息到一张临时表,并且给查询赋予一个从1开始的整数标识符。
剖析报告给出了查询执行的每个步骤及其花费的时间
使用SHOW STATUS
SHOW STATUS命令返回了一些计数器。既有服务器级别的全局计数器,也有基于某个连接的会话级别的计数器。
使用慢查询日志
结合pt-query-digest使用
使用Performance Schema
记录了一些裸数据,可供后期剖析
好的剖析报告能够将潜在的问题显示出来,但最终的解决方案还需要用户来决定(尽管报告可能会给出建议)。
优化查询时,用户需要对服务器如何执行查询有较深的了解。剖析报告能够尽可能多地收集需要的信息、给出诊断问题的正确方向,以及为其他诸如EXPLAIN等工具提供基础信息。
间歇性的问题比如系统偶尔停顿或者慢查询,很难诊断。只有在问题发生的地方通过观察资源的使用情况,并尽可能地测量出数据,才能避免在没有问题的地方耗费精力。
那么如何判断是单条查询问题还是服务器问题呢?可以通过三种技术:
使用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,并结合绘图展示。
当出现间歇性问题时,需要尽可能多地收集所有数据,而不只是问题出现时的数据。需要搞清楚两件事:
诊断触发器
选择一个参数的适当阀值,做为收集数据的开始时间点。
选择一个合适的阈值很重要,既要足够高,以确保在正常时不会被触发;又不能太高,要确保问题发生时不会错。
需要收集什么样的数据
现在需要收集什么样的数据呢?尽可能收集所有能收集的数据,包括系统的状态、CPU利用率、磁盘使用率和可用空间、ps的输出采样、内存利用率,以及可以从MySQL获得的信息,如SHOW STATUS、SHOW PROCESSLIST和SHOW INNODB STATUS...
解释结果数据
如果已经正确地设置好触发条件,并且长时间运行pt-stalk,则只需要等待足够长的时间来捕获几次问题,就能够得到大量的数据来进行筛选。
存在即合理,稍微了解下
Percona Server和MariaDB都引入了INFORMATION_SCHEMA表。可以对数据库活动进行测量和审计,强制执行使用策略。可以帮找出数据库中花费最多时间的地方,使用最(不)频繁的表或索引。
Performance Schema中也有很多类似上面这些功能的表。
strace工具可以调查系统调用的情况。有好几种可以使用的方法,其中一种是计算系统调用的时间并打印出来:
基本知识点:
解决性能问题的方法,首先是要澄清问题,然后选择合适的技术来答问题。(pt-query-digest工具)
良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema。本章关注的是MySQL数据库的设计,主要介绍的是MySQL数据库设计与其他关系型数据库管理系统的区别。
本章内容是为接下来的两个章节做铺垫。在这三章中,我们将讨论逻辑设计、物理设计和查询执行,以及它们之间的相互作用。
几个简单的原则:
有两种类型的数字:整数(whole number)和实数(real number)
整数:TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT。分别使用8,16,24,32,64位存储空间。可选的UNSIGNED属性,表示不允许负值,可以使正数的上限提高一倍。有符号和无符号类型使用相同的存储空间,并具有相同的性能。
实数:FLOAT、DOUBLE、DECIMAL,FLOAT4个字节,DOUBLE8个字节。DECIMAL字节可变,最多65个数字。
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');
这三行数据实际存储为整数,而不是字符串:
枚举字段是按照内部存储的整数而不是定义的字符串进行排序的:
MySQL可以使用许多类型来保存日期和时间值,例如YEAR和DATE。MySQL能存储的最小时间粒度为秒。
DATETIME:能保存大范围的值,从1001年到9999年,精度为秒。它格式为YYYYMMDDHHMMSS的整数中,与时区无关。8个字节。(与时区无关、只与文本表示一致)
TIMESTAMP:保存了从1970年1月1日午夜(格林尼治标准时间)以来的秒数,它和UNIX时间戳相同。只能表示从1970年到2038年。4个字节。(与时区有关)
MySQL有少数几种存储类型使用紧凑的位存储数据。所有这些位类型,不管底层存储格式和处理方式如何,从技术上来说都是字符串类型:
物理主键,最好使用整数, 可使用AUTO_INCREMEN
IPv4地址,经常被VARCHAR(15)来存储IP地址,但它们实际上是32位无符号整数,不是字符串。MySQL提供INET_ATON()和INET_NTOA()函数在这两种表示方法之间转换。
有一些问题是由MySQL的实现机制导致的:
范式:
范式优点:数据简洁、无冗余,修改快
范式缺点:关联查询效率低下。
完全的范式化和完全的反范式化schema都是实验室里才有的东西:在真实世界中很少会这么极端地使用
即将数据量大的表按某些条件汇总数据至小表。
当重建汇总表和缓存表时,要保证数据在操作时依然可用。通过使用“影子表”来实现,“影子表”指的是一张在真实表“背后”创建的表。当完成了建表操作后,通过一个原子的重命名操作切换影子表和原表。例如,如果需要重建my_summary,则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换:temp = a , a = b , b =temp
物化视图实际上是预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新。
有一个计数器表,只有一行数据,记录网站的点击次数:
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;
MySQL的ALTER TABLE操作的的方法是用新的结构创建一个空表,从旧表中查出所有数据插入新表,然后删除旧表。(效率贼慢)
大部分ALTER TABLE操作将导致MySQL服务中断,若不中断,能使用的技巧只有两种:
两种更新table字段的方式:
修改表的.frm文件是很快的,但MySQL有时候会在没有必要的时候也重建表。如果愿意冒一些风险,可以让MySQL做一些其他类型的修改而不用重建表:
建完新表后,直接交换新表与旧表的.frm文件
为了高效地载入数据到MyISAM表中,有一个常用的技巧是先禁用索引、载入数据,然后重新启用索引:
ALTER TABLE test.load_data DISABLE KEYS;
-- load data
ALTER TABLE test.load_data ENABLE KEYS;
这个办法对唯一索引无效,因为DISABLE KEYS只对非唯一索引有效。MyISAM会在内存中构造唯一索引,并且为载入的每一行检查唯一性。一旦索引的大小超过了有效内存大小,载入操作就会变得越来越慢。
索引是存储引擎用于快速找到记录的一种数据结构。索引优化应该是对查询性能优化最有效的手段了。
索引可以包含一个或多个列的值。如果索引包含多个列,那么要满足最左匹配原则。
如索引为(A,B,C)那么查询时也要保证必须先有A的值,再有B的值。例子:select * from table where A = 'xiao' and B = 'csdn'
索引有很多种类型,最常用的是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...
如何高效地使用索引
“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。
此语句中不会使用索引actor_id:
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5
索引的选择性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T)的比值,范围从1/#T到1之间。索引的选择性越高则查询效率越高,选择性高的索引可以在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
前缀索引是指将列截取开头合适长度后最为索引,是一种能使索引更小、更快的有效办法,但另一方面也有其缺点:MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。
在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。MySQL 5.0和更新版本引入了一种叫“索引合并”(index merge)的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。
查询能够同时使用这两个单列索引进行扫描,并将结果进行合并。这种算法有三个变种:OR条件的联合(union),AND条件的相交(intersection),组合前两种情况的联合及相交。
关于选择性和基数的经验法则值得肯定,但不能忘记排序、分组和范围条件等其他因素造成的影响。
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。InnoDB的聚簇索引是在同一个结构中保存了B-Tree索引和数据行。当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中。术语“聚簇”表示数据行和相邻的键值紧凑地存储在一起,所以一个表只能有一个聚簇索引.
选择哪个索引作为聚簇索引?
聚簇索引优点:
聚簇索引缺点:
二级索引的叶子节点存储的是引用行的聚簇索引值,二级索引访问需要两次索引查找。第一次查聚簇索引值,第二次根据聚簇索引获取数据。
聚簇表和非聚簇表区别:
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。
避免根据索引得到的地址后,再回表查询。
举例:
-- 索引为(A)
select A from table where A >= 6;
-- 索引为(A, B, C)
select A, C from table where A = 6 and B =7;
Using index 说明索引覆盖了查询结果,避免了回表查询
MySQL有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描。
最好能直接使用索引的排序,避免额外排序操作。
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。
不要有重复索引,根据需求可适当创建冗余索引。
删除无用索引。
索引可以减少不必要数据读取,从一个角度较少了锁竞争。
如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE子句,Using where 说明服务器层还需额外过滤数据。
略
维护表有三个主要的目的:找到并修复损坏的表,维护准确的索引统计信息,减少碎片。
表损坏(corruption)是很糟糕的事情。会导致查询返回错误的结果或者莫须有的主键冲突等问题,严重时甚至还会导致数据库的崩溃。
CHECK TABLE检查表是否损坏,通常能够找出大多数的表和索引的错误。
REPAIR TABLE修复损坏的表。也可通过一个不做任何操作(no-op)的ALTER操作来重建表:ALTER TABLE innodb_tbl ENGINE=INNODB;
InnoDB一般不会出现损坏,如果出现了损坏,那么一定是发生了严重的错误,需要立刻调查一下原因。
MySQL的查询优化器会通过两个API来了解存储引擎的索引值的分布信息,以决定如何使用索引。
如果表没有统计信息,或者统计信息不准确,可以通过运行ANALYZE TABLE来重新生成统计信息。
InnoDB会在几个场景触发索引统计信息的更新。可以通过innodb_stats_on_metadata关闭自动生成统计信息。
一旦关闭索引统计信息的自动更新,那么就需要周期性地使用ANALYZE TABLE来手动更新。否则,索引统计信息就会永远不变。
B-Tree索引可能会碎片化,这会降低查询的效率。有三种类型的数据碎片。
对于MyISAM表,这三类碎片化都可能发生。但InnoDB不会出现短小的行碎片,InnoDB会移动短小的行并重写到一个片段中。
可以通过执行OPTIMIZE TABLE或者导出再导入的方式来重新整理数据。
MySQL中多数情况会使用B-Tree索引。回顾一下B-Tree索引。
在选择索引和编写利用索引的查询时有三个原则要记住:
总的来说,编写查询语句时应该尽可能选择合适的索引以避免单行查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。
查询优化、索引优化、库表结构优化需要齐头并进,一个不落。
性能≈时间=任务数*次数*单次任务时间, 控制三个因素即可提高性能
对于低效的查询,通过下面两个步骤来分析:
最好每次访问数据库时只请求需要的数据,但多数场景希望接口复用,考虑二者代价。
为了返回结果是否扫描了过多的数据。对于MySQL最简单的衡量查询开销的三个指标:
单个复杂-> 多个简单、大数据量->分批次小量
将一个复杂的查询分成多个简单的查询
对于一个大查询我们需要“分而治之”,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。
对关联查询进行分解,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。
当向MySQL发送一个请求的时候,MySQL到底做了些什么:
max_allowed_packet参数决定客户端请求长度。
SHOW FULL PROCESSLIST命令可查看连接状态:
解析一个查询语句之前,如果查询缓存是打开的,MySQL会优先检查查询是否命中查询缓存。检查是通过一个对大小写敏感的哈希查找实现的。
语法解析器和预处理:语法校验&预处理器权限校验
查询优化器: 优化器会将语法树转换为执行计划,一个查询会有多种查询计划,优化器会选择最好的那个。以下是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)
两次传输排序(旧版本使用):读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。
单次传输排序(新版本使用):先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。
相对于查询优化阶段,查询执行阶段不是那么复杂:MySQL只是简单地根据执行计划给出的指令逐步执行。(服务层按步骤调用存储引擎接口获取需要的数据)
查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数。
如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放到查询缓存中。
MySQL将结果集返回客户端是一个增量、逐步返回的过程。
MySQL查询优化器对少部分查询不适用,而我们可以通过改写查询让MySQL高效地完成工作。
关联子查询会被优化器转换成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;
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;。
在5.0和更新的版本中,当WHERE子句中包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行
某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个非常大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个列表的值和另一个表的某个列相关联。那么优化器会将IN()列表都复制应用到关联的各个表中,但如果这个列表非常大,则会导致优化和执行都会变慢。
MySQL无法利用多核特性来并行执行查询。很多其他的关系型数据库能够提供这个特性,但是MySQL做不到,不要花时间去尝试寻找并行执行查询的方法。
MySQL并不支持哈希关联——MySQL的所有关联都是嵌套循环关联。
MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。
对于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;
MySQL不允许对同一张表同时进行查询和更新。
可以通过使用生成表的形式来绕过上面的限制,因为MySQL只会把这个表当作一个临时表来处理。
如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。通过在查询中加入相应的提示,就可以控制该查询的执行计划:
在MySQL 5.0和更新版本中,新增了一些参数用来控制优化器的行为:
介绍如何优化特定类型的查询
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;
需要特别提醒:
尽可能使用关联查询代替子查询。
MySQL优化器会在内部处理的时候相互转化这两类查询,它们都可以使用索引来优化,这也是最有效的优化办法。
当无法使用索引的时候,GROUP BY使用两种策略来完成:使用临时表或者文件排序来做分组。可以通过使用提示SQL_BIG_RESULT和SQL_SMALL_RESULT来让优化器按照你希望的方式运行。、
优化GROUP BY WITH ROLLUP:最好的办法是尽可能的将WITH ROLLUP功能转移到应用程序中处理。
在偏移量非常大的时候,例如可能是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;
分页的时候,一个常用的技巧是在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快得多。
MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将WHERE、LIMIT、ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器可以充分利用这些条件进行优化(例如,直接将这些子句冗余地写一份到各个子查询)。
除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致对整个临时表的数据做唯一性检查。这样做的代价非常高。即使有ALL关键字,MySQL仍然会使用临时表存储结果。事实上,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。
Percona Toolkit中的pt-query-advisor能够解析查询日志、分析查询模式,然后给出所有可能存在潜在问题的查询,并给出足够详细的建议。这像是给MySQL所有的查询做一次全面的健康检查,它能检测出许多常见的问题。
用户自定义变量如果能够用好,发挥其潜力,可以写出非常高效的查询语句。
用户自定义变量是一个用来存储内容的临时容器,在连接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;
在了解自定义变量的强大之前,我们再看看它自身的一些属性和限制,看看在哪些场景下我们不能使用用户自定义变量:
优化排名语句
用户自定义变量一个重要特性是你可以在给一个变量赋值的同时使用这个变量。换句话说,用户自定义变量的赋值具有“左值”特性。
来看一个更复杂的用法:先编写一个查询获取演过最多电影的前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;
用户自定义变量的其他用处
用户自定义变量能够做的有趣的事情:
略
理解查询是如何被执行的以及时间都消耗在哪些地方,这依然是前面我们介绍的响应时间的一部分。
优化通常都需要三管齐下:不做、少做、快速地做。
除了这些基础的手段,包括查询、表结构、索引等,MySQL还有一些高级的特性可以帮助你优化应用,例如分区,分区和索引有些类似但是原理不同。
MySQL还支持查询缓存,它可以帮你缓存查询结果,当完全相同的查询再次执行时,直接使用缓存结果。
分区表是一个独立的逻辑表,但是底层由多个物理子表组成。索引也是按照分区的子表定义的,而没有全局索引。
从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎无须知道这是一个普通表还是一个分区表的一部分。
分区表上的操作按照下面的操作逻辑进行:
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分区子句中可以使用各种函数。但有一个要求,表达式返回的值要是一个确定的整数,且不能是一个常数。
为了保证大数据量的可扩展性,一般有下面两个策略:
全量扫描数据,不要任何索引:可以使用简单的分区方式存放表,不要任何索引,根据分区的规则大致定位需要的数据位置。
索引数据,并分离热点:如果数据有明显的“热点”,而且除了这部分数据,其他数据很少被访问到,那么可以将这部分热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在内存中。
上面的两个分区策略都基于两个非常重要的假设:查询都能够过滤掉很多额外的分区、分区本身并不会带来很多额外的代价。这两个假设在某些场景下会有问题,介绍一些可能会遇到的问题:
分区最大的优点就是优化器可以根据分区函数来过滤一些分区。
使用EXPLAIN PARTITION可以观察优化器是否执行了分区过滤,下面是一个示例:
合并表(Merge table)是一种早期的、简单的分区实现,和分区表相比有一些不同的限制,并且缺乏优化。分区表严格来说是一个逻辑上的概念,用户无法访问底层的各个分区,对用户来说分区是透明的。但是合并表允许用户单独访问各个子表。分区表和优化器的结合更紧密,这也是未来发展的趋势,而合并表则是一种将被淘汰的技术,在未来的版本中可能被删除。
视图本身是一个虚拟表,不存放任何数据。在使用SQL语句访问视图的时候,它返回的数据是MySQL从其他表中生成的。
MySQL可以使用两种办法中的任何一种来处理视图。这
如果是采用临时表算法实现的视图,EXPLAIN中会显示为派生表(DERIVED)。
可更新视图(updatable view)是指可以通过更新这个视图来增删改视图涉及的相关表。
合理利用视图可提高性能:使用视图实现基于列的权限控制...
但视图也会增加一些看不到的服务器负担,视图背后逻辑可能比较复杂。
MySQL还不支持物化视图(物化视图是指将视图结果数据存放在一个可以查看的表中,并定期从原始表中刷新数据到这个表中)。但可以使用构建缓存表或者汇总表的办法来模拟物化视图和索引。
如需同步更新则可使用外键;
如需数值约束,可使用触发器;
如果只是简单约束,应用内约束即可
MySQL允许通过触发器、存储过程、函数、事件的形式来存储代码。
这四种存储代码都使用特殊的SQL语句扩展,它包含了很多过程处理语法,例如循环和条件分支等,存储过程和存储函数都可以接收参数然后返回值,但是触发器和事件却不行。
存储过程:
-- 定义存储过程
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);
CREATE TRIGGER `myInsert` AFTER INSERT ON `teacher` FOR EACH ROW BEGIN
update student SET name = 'triggerName' WHERE id = 1;
END;
CREATE EVENT eventDemo ON SCHEDULE EVERY 1 WEEK DO CALL getInfo(1);
游标是个啥?
当创建一个绑定变量SQL时,客户端向服务器发送了一个SQL语句的原型。服务器端收到这个SQL语句框架后,解析并存储这个SQL语句的部分执行计划,返回给客户端一个SQL语句处理句柄。以后每次执行这类查询,客户端都指定使用这个句柄。
绑定变量的SQL,使用问号标记可以接收参数的位置,当真正需要执行具体查询的时候,则使用具体值代替这些问号。例如,下面是一个绑定变量的SQL语句:
INSERT INTO tbl(col1, col2, col3) VALUES (?, ?, ?);
因为如下的原因,MySQL在使用绑定变量的时候可以更高效地执行大量的重复语句:
绑定变量相对也更安全。无须在应用程序中处理转义,一则更简单了,二则也大大减少了SQL注入和攻击的风险。
存储过程只能使用SQL来编写,而UDF没有这个限制,可以使用支持C语言调用约定的任何编程语言来实现。
UDF必须事先编译好并动态链接到服务器上,这种平台相关性使得UDF在很多方面都很强大。UDF速度非常快,而且可以访问大量操作系统的功能,还可以使用大量库函数。使用SQL实现的存储函数在实现一些简单操作上很有优势,诸如计算球体上两点之间的距离,但是如果操作涉及到网络交互,那么只能使用UDF了。同样地,如果需要一个MySQL不支持的统计聚合函数,而且无法使用SQL编写的存储函数来实现的话,通常使用UDF是很容易实现的。
能力越大,责任越大。所以在UDF中的一个错误很可能会让服务器直接崩溃,甚至扰乱服务器的内存或者数据,另外,所有C语言具有的潜在风险,UDF也都有。
除了UDF,MySQL还支持各种各样的插件。这些插件可以在MySQL中新增启动选项和状态值,还可以新增INFORMATION_SCHEMA表,或者在MySQL的后台执行任务,等等。简单的插件列表:
字符集是指一种从二进制编码到某类字符符号的映射,“校对”是指一组用于某个字符集的排序规则。
每种字符集都可能有多种校对规则,并且都有一个默认的校对规则。每个校对规则都是针对某个特定的字符集的,和其他的字符集没有关系。校对规则和字符集总是一起使用的,所以后面我们将这样的组合也统称为一个字符集。
MySQL的设置可以分为两类:创建对象时的默认值、在服务器和客户端通信时的设置。
MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会立刻返回结果,跳过了解析、优化和执行阶段。
MySQL判断缓存命中的方法很简单:缓存存放在一个引用表中,通过一个哈希值引用,这个哈希值包括了如下因素,即查询本身、当前要查询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。
查询缓存是完全存储在内存中的。
除了查询结果之外,需要缓存的还有维护相关的数据,这些管理维护数据结构大概需要40KB的内存资源。
除此之外,MySQL用于查询缓存的内存被分成一个个的数据块,数据块是变长的。每一个数据块中,存储了自己的类型、大小和存储的数据本身,还外加指向前一个和后一个数据块的指针。数
当服务器启动的时候,它先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。这个空闲块的大小就是你所配置的查询缓存大小再减去用于维护元数据的数据结构所消耗的空间。
当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_res_unit的配置,即使查询结果远远小于此,仍需要至少申请query_cache_min_res_unit空间。
因为需要先锁住空间块,然后找到合适大小数据块,所以相对来说,分配内存块是一个非常慢的操作。MySQL尽量避免这个操作的次数。当需要缓存一个查询结果的时候,它先选择一个尽可能小的内存块,然后将结果存入其中。如果数据块全部用完,但仍有剩余数据需要存储,那么MySQL会申请一块新数据块继续存储结果数据。当查询完成时,如果申请的内存空间还有剩余,MySQL会将其释放,并放入空闲内存部分。
只有当缓存带来的资源节约大于其本身的资源消耗时才会给系统带来性能提升。
一旦理解查询缓存工作的原理,配置起来就很容易了。只有很少的参数可供配置:
因为InnoDB有自己的MVCC机制,所以相比其他存储引擎,InnoDB和查询缓存的交互要更加复杂。
事务是否可以访问查询缓存取决于当前事务ID,以及对应的数据表上是否有锁。每一个InnoDB表的内存数据字典都保存了一个事物ID号,如果当前事务ID小于该事务ID,则无法访问查询缓存。
原则上,在InnoDB的MVCC架构下,当某些修改不影响其他事务读取一致的数据时,是可以使用查询缓存的。InnoDB做了一个简化,让所有有加锁操作的事务都不使用任何查询缓存。
库表结构的设计、查询语句、应用程序设计都可能会影响到查询缓存的效率。除了前文介绍的之外,这里还有一些要点需要注意:
如果不想所有的查询都进入查询缓存,但是又希望某些查询走查询缓存,那么可以将query_cache_type设置成DEMAND,然后在希望缓存的查询中加上SQL_CACHE。这虽然需要在查询中加入一些额外的语法,但是可以让你非常自由地控制哪些查询需要被缓存。相反,如果希望缓存多数查询,而少数查询又不希望缓存,那么你可以使用关键字SQL_NO_CACHE。
MySQL查询缓存工作的原则是:执行查询最快的方式就是不去执行,但是查询仍然需要发送到服务器端,服务器也还需要做一点点工作。如果对于某些查询完全不需要与服务器通信效果会如何呢?这时客户端的缓存可以很大程度上帮你分担MySQL服务器的压力。我们将在第14章详细介绍更多关于缓存的内容。
回顾一下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或者其他类似的解决方案。
EXPLAIN命令是查看查询优化器如何决定执行查询的主要方法
在要执行的查询语句前添加explain关键字即可
EXPLAIN SELECT 1
id列:这一列总是包含一个编号,标识SELECT所属的行。一般来说,id越大越早被执行。
select_type列:显示了对应行是简单还是复杂SELECT(如果是后者,那么是三种复杂类型中的哪一种)。SIMPLE值意味着查询不包括子查询和UNION。如果查询有任何复杂的子部分,则最外层部分标记为PRIMARY,其他部分标记如下:
SUBQUERY:包含在SELECT列表中的子查询中的SELECT(换句话说,不在FROM子句中)标记为SUBQUERY。
DERIVED:DERIVED值用来表示包含在FROM子句的子查询中的SELECT。
UNION:在UNION中的第二个和随后的SELECT被标记为UNION。
UNION RESULT:用来从UNION的匿名临时表检索结果的SELECT被标记为UNION RESULT。
table列:显示了对应行正在访问哪个表,正常情况下就是table名,下面两种情况特殊
派生表(临时表):
联合表:
type列:显示了“关联类型”,更准确的说法是访问类型——MySQL决定如何查找表中的行。下面是最重要的访问方法,依次从最差到最优。
ALL:全表扫描,通常意味着MySQL必须扫描整张表,从头到尾,去找到需要的行
index: 跟全表扫描一样,只是MySQL扫描表时按索引次序进行而不是行。优点是避免了排序;缺点是若按索引次序读取整个表则需随机I/O访问行,开销将会非常大。
range: 范围扫描就是一个有限制的索引扫描。它开始于索引里的某一点,返回匹配这个值域的行。
ref: 这是一种索引访问(有时也叫做索引查找),它返回所有匹配某个单个值的行。把它叫做ref是因为索引要跟某个参考值相比较(这个参考值或者是一个常数)。
eq_ref: 使用这种索引查找,MySQL知道最多只返回一条符合条件的记录。这种访问方法可以在MySQL使用主键或者唯一性索引查找时看到,它会将它们与某个参考值做比较。
const, system: 当MySQL能对查询的某部分进行优化并将其转换成一个常量时,它就会使用这些访问类型。
NULL: 这种访问方式意味着MySQL能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。
possibIe_keys列:查询可以使用哪些索引,这是基于查询访问的列和使用的比较操作符来判断的。
key列:MySQL 决定采用的索引来优化对该表的访问。如果该索引没有出现在possible_keys列中,那么MySQL选用它是出于另外的原因。
key_len列:该列显示了MySQL在索引里使用的字节数。如果MySQL正在使用的只是索引里的某些列,那么就可以用这个值来算出具体是哪些列。
ref列: 显示了之前的表在key列记录的索引中查找值所用的列或常量。
rows列: 这一列是MySQL估计为了找到所需的行而要读取的行数。
fiItered列: 这一列在使用EXPLAIN EXTENDED时出现。它显示的是针对表里符合某个条件(WHERE子句或联接条件)的记录数的百分比所做的一个悲观估算。
Extra列: 这一列包含的是不适合在其他列显示的额外信息。
Using index :此值表示MySQL将使用覆盖索引,以避免访问表。
Using where:这意味着MySQL服务器将在存储引擎检索行后再进行过滤。
Using temporary :这意味着MySQL在对查询结果排序时会使用一个临时表。
Using filesort:这意味着MySQL会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。
SQL 5.6中将包括一个对EXPLAIN的重要改进:能对类似UPDATE、INSERT等的查询进行解释。
MySQL 5.6还将包括对查询优化和执行引擎的一系列改进,允许匿名的临时表尽可能晚地被具体化,而不总是在优化和执行使用到此临时表的部分查询时创建并填充它们。
MySQL 5.6将通过在服务器中增加优化跟踪功能的方式改进优化器的相关部分。