1、MySQL查询缓存


很多数据库产品都能够缓存查询的执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段。MySQL还有另一种不同的缓存类型:缓存完整的SELECT查询结果,也就是“查询缓存”。


查询缓存系统会跟踪查询中涉及的每个表,如果这些表发生变化,那么和这个表相关的所有的缓存数据都将失效。


查询缓存对应用程序是完全透明的。应用程序无须关心MySQL是通过查询缓存返回的结果还是实际执行返回的结果。


另外,随着现在的通用服务器越来越强大,查询缓存可能是一个影响服务器扩展性的因素。它可能成为整个服务器的资源竞争单点,在多核服务器上还可能导致服务器僵死。



1.1 查询缓存的工作机制


Mysql 判断是否命中缓存的办法很简单,首先会将要缓存的结果放在引用表中,然后使用查询语句,数据库名称,客户端协议的版本等因素算出一个hash值,这个hash值与引用表中的结果相关联。如果在执行查询时,根据一些相关的条件算出的hash值能与引用表中的数据相关联,则表示查询命中。


通过have_query_cache服务器系统变量指示查询缓存是否可用:


mysql> SHOW VARIABLES LIKE ' have_query_cache ';


+------------------+-------+


| Variable_name    | Value |


+------------------+-------+


| have_query_cache | YES   |


+------------------+-------+


为了监视查询缓存性能,使用SHOW STATUS查看缓存状态变量:


mysql> SHOW STATUS LIKE ' Qcache %';


+-------------------------+--------+


|变量名                   |值 |


+-------------------------+--------+


| Qcache_free_blocks      | 36     |


| Qcache_free_memory      | 138488 |


| Qcache_hits             | 79570  |


| Qcache_inserts          | 27087  |


| Qcache_lowmem_prunes    | 3114   |


| Qcache_not_cached       | 22989  |


| Qcache_queries_in_cache | 415    |


| Qcache_total_blocks     | 912    |


+-------------------------+--------+


任何字符的不同,例如空格、注释都会导致缓存的不命中。 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、系统表、或者任何包含列级别权限的表,都不会被缓存。


在检查查询缓存之前,MySQL只做一件事情,就是通过一个大小写不敏感的检查看看SQL语句是不是以SEL开头。


如果查询语句中包含任何不确定函数,那么在查询缓存中不可能找到缓存结果的。



1.2额外的消耗


如果使用查询缓存,在进行读写操作时会带来额外的资源消耗,消耗主要体现在以下几个方面:


·        查询的时候会检查是否命中缓存,这个消耗相对较小。 (检查缓存命中)


·        如果没有命中查询缓存,MYSQL会判断该查询是否可以被缓存,而且系统中还没有对应的缓存,则会将其结果写入查询缓存。 (写查询缓存)


·        如果一个表被更改了,那么使用那个表的所有缓冲查询将不再有效,并且从缓冲区中移出。这包括那些映射到改变了的表的使用MERGE表的查询。一个表可以被许多类型的语句更改,例如INSERT、UPDATE、DELETE、TRUNCATE、ALTER TABLE、DROP TABLE或DROP DATABASE。 (更新/删除查询缓存)


对于InnoDB而言,事务的一些特性还会限制查询缓存的使用。当在事务A中修改了B表时,因为在事务提交之前,对B表的修改对其他的事务而言是不可见的。为了保证缓存结果的正确性,InnoDB采取的措施让所有涉及到该B表的查询在事务A提交之前是不可缓存的。如果A事务长时间运行,会严重影响查询缓存的命中率。


查询缓存的空间不要设置的太大。


因为查询缓存是靠一个全局锁操作保护的,如果查询缓存配置的内存比较大且里面存放了大量的查询结果,当查询缓存失效的时候,会长时间的持有这个全局锁。因为查询缓存的命中检测操作以及缓存失效检测也都依赖这个全局锁,所以可能会导致系统僵死的情况。



1.3查询缓存如何使用内存


查询缓存是完全存储在内存中的,所以在配置和使用它之前,我们需要先了解它是如何使用内存的。 除了查询结果之外,它还需要一些内存专门用来确定哪些内存目前是可用的、哪些是已经用掉的、哪些用来存储数据表和查询结果之间的映射、哪些用来存储查询字符串和查询结果。 这些基本的管理维护数据结构大概需要40KB的内存资源,除些之外,MySQL用于查询缓存的内存被分成一个个的数据块,数据块是变长的。


当服务器启动的时候,它先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。当有查询结果需要缓存的时候,MYSQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_re_unit


因为需要先锁住空间块,然后找到合适大小数据块,所以分配内存是一个非常慢的操作。MYSQL尽量避免这个操作的次数。当需要缓存一个查询结果的时候,它先选择一个尽可能小的内存块,然后将结果存入其中Qcache_inserts。如果数据块全部用完,但仍有剩余数据需要存储,那么MYSQL会申请一个新数据块(仍然是尽可能小的数据块)--继续存储结果数据。当查询完成时,如果申请的内存还有剩余,MYSQL会将其释放,并放入空闲内存部分。(这里的分配内存块,并不是通过malloc函数向操作系统申请内存,而是MYSQL初次创建查询缓存的时候一次性申请下来的,并由自己管理一大块内存)



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


1、查询缓存可以降低查询执行的时间,但是却不能减少查询结果传输的网络消耗,如果这个消耗是整个查询过程的主要瓶颈,那么查询缓存的作用也很小。


2、对于那些需要消耗大量资源的查询通常都是非常适合缓存的,对于复杂的SELECT语句都可以使用查询缓存,不过需要注意的是,涉及表上的UPDATE、DELETE、INSERT操作相比SELECT来说要非常少才行。


3、查询缓存命中率:Qcache_hits/(Qcahce_hits+Com_select),查询缓存命中率多大才是好的命中率,需要具体情况具体分析。只要查询缓存带来的效率提升大于查询缓存带来的额外消耗,即使30%的命中率也是值得。另外,缓存了哪些查询也很重要,如果被缓存的查询本身消耗巨大,那么即使缓存命中率低,对系统性能提升仍然是有好处的。


4、任何SELECT语句没有从查询缓存中返回都称为“缓存未命中”,以如下可能:


  • 查询语句无法被缓存,可能因为查询中包含一个不确定的函数,或者查询结果太大而无法缓存。
  • MySQL从未处理这个查询,所以结果也从不曾被缓存过。
  • 虽然之前缓存了查询结果,但由于查询缓存的内存用完了,MYSQL需要删除某些缓存,或者由于数据表被修改导致缓存失效。

如果服务器上有大量缓存缓存未命中,但是实际上绝大查询都被缓存了,那么一定是有如下情况发生:


查询缓存还没有完成预热,即MYSQL还没有机会将查询结果都缓存起来。


查询语句之前从未执行过。如果应用程序不会重复执行一条查询语句,那么即使完成预热仍然会有很多缓存未命中。


缓存失效操作太多,缓存碎片、内存不足、数据修改都会造成缓存失效。可以通过参数Com_*来查看数据修改的情况(包括Com_update,Com_delete等),还可以通过Qcache_lowmem_prunes来查看有多少次失效是由于内存不足导致的。


5、有一个直观的方法能够反映查询缓存是否对系统有好处, 推荐一个指标:”命中和写入“的比率,即Qcache_hits和Qcache_inserts的比值。根据经验来看,当这个比值大于3:1时通常查询缓存是有效的,如果能达到10:1最好。


6、通常可以通过观察查询缓存内存的实际使用情况Qcache_free_memory,来确定是否需要缩小或者扩大查询缓存。



1.5配置和维护查询缓存


MYSQL提供了一些参数来控制查询缓存的行为,参数如下


·query_cache_limit


MYSQL能够缓存的最大查询结果,查询结果大于该值时不会被缓存。 默认值是1048576(1MB)


如果某个查询的结果超出了这个值,Qcache_not_cached的值会加1,如果某个操作总是超出,可以考虑在SQL中加上 SQL_NO_CACHE 来避免额外的消耗。


· query_cache_min_res_unit


查询缓存分配的最小块的大小(字节)。 默认值是4096(4KB)。


· query_cache_size


为缓存查询结果分配的内存的数量,单位是字节,且数值必须是1024的整数倍。默认值是0,即禁用查询缓存。请注意即使query_cache_type设置为0也将分配此数量的内存。


· query_cache_type


设置查询缓存类型。设置GLOBAL值可以设置后面的所有客户端连接的类型。客户端可以设置SESSION值以影响他们自己对查询缓存的使用。下面的表显示了可能的值:


选项

描述

0或OFF

不要缓存查询结果。请注意这样不会取消分配的查询缓存区。要想取消,你应将query_cache_size设置为0。

1或ON

缓存除了以SELECT SQL_NO_CACHE开头的所有查询结果。

2或DEMAND

只缓存以SELECT SQL_NO_CACHE开头的查询结果。


该变量默认设为ON。


· query_cache_wlock_invalidate


一般情况,当客户端对MyISAM表进行WRITE锁定时,如果查询结果位于查询缓存中,则其它客户端未被锁定,可以对该表进行查询。将该变量设置为1,则可以对表进行WRITE锁定,使查询缓存内所有对该表进行的查询变得非法。这样当锁定生效时,可以强制其它试图访问表的客户端来等待。



1.6减少碎片


没有什么办法能够完全避免碎片,但是选择合适的query_cache_min_res_unit可以帮你减少由碎片导致的内存空间浪费。 这个值太小,则浪费的空间更少,但是会导致频繁的内存块申请操作;如果设置得太大,那么碎片会很多。 调整合适的值其实是在平衡内存浪费和CPU消耗。 可以通过内存实际消耗(query_cache_size_Qcache_free_memory)除以Qcache_queries_in_cahce计算单个查询的平均缓存大小。可以通过Qcahce_free_blocks来观察碎片。


通过FLUSH_QUERY_CAHCE完成碎片整理,这个命令将所有的查询缓存重新排序,并将所有的空闲空间都聚焦到查询缓存的一块区域上。


一个用来分析和配置查询缓存的流程图:







1.7查询缓存的替代方案


MySQL查询缓存工作的原则是:执行查询最快的方式就是不去执行,但是查询仍然需要发送到服务器端,服务器也还需要做一点点工作,如果对于某些查询完全不需要与服务器通信效果会如何呢,这时客户端缓存可以很大程度上分担MySQL服务器的压力。


《高性能MySQL》读书笔记--查询性能优化


对于高性能数据库操作,只靠设计最优的库表结构、建立最好的索引是不够的,还需要合理的设计查询。如果查询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。查询优化、索引优化、库表结构优化需要齐头并进,一个不落。


6.1 为什么查询速度会慢


通常来说,查询的生命周期大致可以按照顺序来看:从客户端>>服务器>>在服务器上进行解析>>生成执行计划>>执行>>返回结果给客户端。其中执行可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。了解查询的生命周期、清楚查询的时间消耗情况对于优化查询有很大的意义。



6.2 优化数据访问


查询性能低下的最基本的原因是访问的数据太多。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。


1.确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。


2.确认MySQL服务器层是否在分析大量超过需要的数据行。



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


请求多余的数据会给MySQL服务器带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU内存和资源。这里有一些典型案例:


1、查询不需要的记录:例如在新闻网站中取出100条记录,但是只是在页面上显示10条。实际上MySQL会查询出全部的结果,客户端的应用程序会接收全部的结果集数据,然后抛弃其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上LIMIT。


2、多表关联时返回全部列,例如:







3、总是取出全部的列:每次看到SELECT *的时候都需要怀疑是不是真的需要返回全部的列?取出全部列,会主优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的IO、内存和CPU的消耗。如果应用程序使用了某种缓存机制,或者有其他考虑,获取超过需要的数据也可能有其好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能就更有好处。


4、重复查询相同的数据:不要不断地重复执行相同的查询,然后每次都返回完全相同的数据。当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能显然更好。



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


对于MySQL,最简单的衡量查询开销的三个指标有:响应时间、扫描的行数、返回的行数。这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法。


响应时间


响应时间是两个部分之和:服务时间和排队时间,一般常见和重要的等待是IO和锁等待。


扫描的行数和返回的行数


分析查询时,查看该查询扫描的行数是非常有帮助的。一定程度上能够说明该查询找到需要的数据的效率高不高。理想的情况下扫描的行数和返回的行数应该是相同的。当然这只是理想情况。一般来说扫描的行数对返回的行数的比率通常很小,一般在1:1到10:1之间。


扫描的行数和访问类型


MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。


在EXPLAIN语句的TYPE列返回了访问类型。如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引。索引让MySQL以最高效、扫描行最少的方式找到需要的记录。


一般MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:


1、在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。


2、使用索引覆盖扫描(在extra列中出现了using index)来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无须回表查询记录。


3、从数据表中返回数据,然后过滤不满足条件的记录(在extra列中出现using where)。这在MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤。



6.3 重构查询的方式



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


MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。在其他条件都相同的时候,使用尽可能少的查询当然是更好的。但是有时候,将一个大查询分解为多个小查询也是很有必要的。



6.3.2 切分查询


有时候对于一个大查询我们需要“分而治之”,对于删除旧数据,如果用一个大的语句一次性完成的话,则可能需要一次性锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小地影响MySQL性能,同时还可以减少MySQL复制的延迟。例如我们需要每个月运行一次下面的查询:




那么可以用类似下面的办法来完成同样的工作:






6.3.3 分解关联查询





乍一看这样做并没有什么好处,但其有如下优势:


1、让缓存的效率更高。对MySQL的查询缓存来说,如果关联中的某个表发生了变化 ,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么该表的查询缓存能重复利用 。


2、将查询后,执行单个查询可以减少锁的竞争。


3、查询性能也有所提升,使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这比随机的关联要更高效。



6.4 查询执行的基础


当希望MySQL能够能更高的性能运行查询时,最好的办法就是弄清楚MySQL是如何优化和执行查询的。





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


MySQL客户端和服务器之间的通信协议是“半双工”的,在任何一个时刻,要么由服务器向客户端向服务端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生。


一旦客户端发送了请求,它能做的事情就只是等待结果了,如果查询太大,服务端会拒绝接收更多的数据并抛出相应错误,所以参数max_allowed_packet就特别重要。相反,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整地接收整个返回结果,而不能简单地只取前面几条结果,然后主服务器停止发送数据。这种情况下,客户端若接收完整的结果,然后取前面几条需要的结果,或者接收完几条结果然后粗暴地断开连接,都不是好主意。这也是必要的时候需要在查询中加上limit限制的原因。


换一种方式解释这种行为:当客户端从服务器取数据时,看起来是一个拉数据的过程,但实际上是MySQL在向客户端推数据的过程。客户端不断地接收从服务器推送的数据,客户端也没法让服务器停下来。


当使用多数连接MySQL的库函数从MySQL获取数据时,其结果看起来都像是从MySQL服务器获取数据,而实际上都是从这个库函数的缓存获取数据。多数情况下这没什么问题,但是如果需要返回一个很大的结果集时,这样做并不好,因为库函数会花很多时间和内存来存储所有的结果集。如果能尽早开始处理这些数据,就能大大减少内在的消耗,这种情况下可以不使用缓存来记录结果而是直接处理。PHP的 mysql_query(),此时数据已经到了PHP的缓存中,而mysql_unbuffered_query()不会缓存结果。


查询状态:可以使用SHOW FULL PROCESSLIST命令查看查询的执行状态。Sleep、Query、Locked、Analyzing and statistics、Copying to tmp table[on disk]、Sorting result、Sending data



6.4.2 查询缓存


在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据。这是检查是通过一个对大小写敏感的哈希查找实现的。如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前MySQL会检查一次用户权限。如果权限没有问题,MySQL会跳过执行阶段,直接从缓存中拿到结果并返回给客户端。



6.4.3 查询优化处理


查询生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。


1、语法解析器和预处理首先MySQL通过关键字将SQL语句进行解析,并生成一棵解析树。MySQL解析器将使用MySQL语法规则验证和解析查询。例如是否使用错误的关键字,或者使用关键字的顺序是否正确,引号是否能前后正确匹配等。


2、预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如检查数据表和数据列是否存在,还会解析名字和别名看它们是否有歧义。


3、一下步预处理会验证权限。



查询优化器 :一条语句 可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到最好的执行计划。MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。成本的最小单位是随机读取一个4K的数据页的成本,并加入一些因子来估算某引动操作的代价。可以通过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本。


这是根据一系列的统计信息计算得来的:每个表或者索引的页面个数、索引的基数(索引中不同值的数量)、索引和数据行的长度、索引分布情况。


当然很多原因会导致MySQL优化器选择错误的执行计划:例如统计信息不准确或执行计划中的成本估算不等同于实际执行的成本。



MySQL如何执行关联查询 :MySQL对任何关联都执行嵌套循环关联操作,即MySQL先在一个表中循环取出单条数据,然后再嵌套循环到一个表中寻找匹配的行,依次下去直到找到的有匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。 (嵌套循环关联)





执行计划 :MySQL生成查询的一棵指令树,然后通过存储引擎执行完成这棵指令树并返回结果。最终的执行计划包含了重构查询的全部信息。如果对某个查询执行EXPLAIN EXTENDED,再执行SHOW WARNINGS,就可以看到重构出的查询。


MySQL的执行计划是一棵左侧深度优先的树。





不过,如果有超过n个表的关联,那么需要检查n的阶乘种关联顺序。我们称之为所有可能的执行计划的“搜索空间”。实际上,当需要关联的表超过optimizer_search_depth的限制的时候,就会选择“贪婪”搜索模式。



排序优化 :无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。如果需要排序的数据量小于排序缓冲区,MySQL使用内存进行“快速排序”操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用“快速排序”进行排序,并将各个块的排序结果存放在磁盘上,然后将各个排序的块进行合并,最手返回排序结果。


MySQL有两种排序方法:


    两次传输排序(旧版),读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。显然是两次传输,特别是读取排序后的数据时(第二次)大量随机I/O,所以两次传输成本高。



    单次传输排序(新版),一次读取出所有需要的或SQL查询指定的列,然后根据排序列,排序,直接返回排序后的结果。顺序I/O,缺点:如果列多,额外占用空间。


MySQL在进行文件排序时需要使用的临时存储空间可能会比想象的要大得多,因为MySQL在排序时,对每一个排序记录都会分配一个足够长的定长空间来存放。这个定长空间必须足够以容纳其中最长的字符串。


在关联查询的时候如果需要排序,MySQL会分两种情况来处理这样的文件排序。如果ORDER BY子句的所有列都来自关联的第一个表,那么MySQL在关联处理第一个表时就进行文件排序。如果是这样那么在MySQL的EXPLAIN结果中可以看到Extra字段会有Using filesort。除此之外的所有情况,MySQL都会将关联的结果存放在一个临时表中,然后在所有的关联都结束后,再进行文件排序。这种情况下Extra字段可以看到Using temporary;Using filesort。如果查询中有LIMIT的话,LIMIT也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。



6.4.4 查询执行引擎


相对于查询优化,查询执行简单些了,MySQL只根据执行计划输出的指令逐步执行。指令都是调用存储引擎的API来完成,一般称为 handler API,实际上,MySQL优化阶段为每个表都创建了一个 handler 实例,用 handler 实例获取表的相关信息(列名、索引统计信息等)。


存储引擎接口有着非常丰富的功能,但是底层接口却只有几十个,这些接口像搭积木一样能够完成查询的大部分操作。例如,有一个查询某个索引的第一行的接口,再有一个查询某个索引条件的下一条目的功能,有了这两个功能就可以完成全索引扫描操作。



6.4.5 返回结果给客户端


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


MySQL将结果集返回客户端是一个增量、逐步返回的过程。一旦服务器处理完最后一个关联表,开始生成第一条结果时,MySQL就可以开始向客户端逐步返回结果集了。


这样处理有两个好处:服务端无须存储太多的结果,也就不会因为要返回太多结果而消耗太多内存。另外,这样的处理也让MySQL客户端第一时间获得返回的结果。



当然,优化器存在其局限性,以及某些特定的优化类型,有兴趣的可以在书中找到答案。


《高性能MySQL》读书笔记--Schema与数据类型优化


1.慢查询


当一个资源变得效率低下的时候,应该了解一下为什么会这样。有如下可能原因:


1.资源被过度使用,余量已经不足以正常工作。


2.资源没有被正确配置


3.资源已经损坏或者失灵


因为慢查询,太多查询的时间过长而导致堆积在逻辑上。


慢查询到底是原因还是结果?在深入调查前是无法知晓的。记住,在正常的时候这个查询也是正常运行的。一个查询需要filesort和创建临时表并不一定意味着就是有问题的。尽管消除filesort和临时表通常来说是“最佳实践”。


2.MySQL数据类型


更小的通常更好:一般情况下,应该尽量使用可以正确存储数据的最小数据类型。更小的数据类型通常更快,因为它们占用更少的磁盘、内存和cpu缓存,并且处理时需要的cpu周期也更少。


简单就好:简单数据类型的操作通常需要更少的cpu周期,例如整型比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂,使用整型存储IP地址。


尽量避免NULL:通常情况下最好制定列为NOT NULL,除非真的需要存储NULL值。如果查询中包含可为NULL的列,对mysql来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。


很多MySQL的数据类型可以存储相同类型的数据,只是存储的长度和范围不一样、允许的精度不同,或者需要的物理空间(磁盘和内存空间)不同。


别名


MYSQL为了兼容性支持很多别名,例如integer、bool、numeric,他们都只是别名,虽然可能令人不解,但是不会影响性能。如果建表的时候采用数据类型的别名,然后用show create table检查,会发现mysql报告的是基本类型,而不是别名。



MySQL可以为整数类型指定宽度,例如int(11),对大多数应用这是没有意义的:它不会限制值的合法范围,只是规定了mysql的一些交互工具(例如mysql命令行客户端)用来显示字符的个数。对于存储和计算来说,int(1)和int(20)是相同的。


字符类型


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



VARCHAR:


varchar类型选用于存储可变长字符串,需要使用1或者2个额外字节记录字符串的长度,比定长类型更节省空间,因为它仅使用必要的空间(例如越短的字符串使用越少的空间)。由于VARCHAR行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。


varchar的使用场景:


字符串列的最大长度比平均长度大很多;列的更新很少,所以碎片不是问题;使用了像utf-8这样复杂的字符集,每个字符都使用不同的字节数进行存储。


CHAR:


CHAR类型是定长的,MySQL总是根据定义的字符串长度分配足够的空间。char适合于存储很短的字符串,或者所有值都接近同一个长度。例如char非常适合存储密码MD5值,因为这是一个定长的值。对于经常变更的数据,char也比varchar更好,因为定长的char类型不容易产生碎片。


大字段字符类型


blob和text都是为了存储很大的数据而设计的字符串数据类型,分别采用二进制和字符串方式存储。MySQL对blob和text列进行排序于其他类型是不同的:它只对每个列的最前max_sort_length字节而不是整个字符串排序。


尽量避免使用text和blob类型,如果实在无法避免,有一个技巧实在所有用到blob字段的地方都是用substring(column,length)将列值转换为字符串(在order by 子句中也适用),这样就可以使用内存临时表了。但是要确保截取的子字符串足够短,不会使临时表的大小超过max_heap_table_size或tmp_table_size,超过以后Mysql会将内存临时表转换为MyISAM磁盘临时表。


时间类型


timestamp类型保存了从1970年1月1日午夜以来的秒数,timestamp只使用4个字节的存储空间,因此它的范围比datatime小得多:只能表示从1970年到2038年。提供了from_unixtime函数把unix时间戳转换为日期、unix_timestamp函数把日期转换为时间戳。


有时候人们会将unix时间戳存储为整数值,这不会带来任何收益。用整数保存时间戳的格式通常不方便处理,所以我们不推荐这么做。


标识符(identifier)


整数通常是标识列最好的选择,因为它们很快并且可以使用auto_increment;千万不要使用enum和set类型作为标识列;尽量避免使用字符串类型作为标识列,因为他们很消耗空间,并且通常比数字类型慢。尤其是在MyISAM表里使用字符串作为标识列时要特别小心,因为MyISAM默认对字符串使用压缩索引,这会导致查询慢得多。


特殊类型数据:IP地址字段(IPv4)


人们经常使用varchar(15)来存储ip地址,然而,它们实际上是32位无符号整数,不是字符串。MySQL提供 INET_ATON ()和 INET_NTOA ()函数将ip地址在整数和四段表示形式之间进行转换。


3.数据库中的范式和反范式


在范式化的数据库中,每个事实数据会出现并且只出现一次;相反,在反范式化的数据库中,信息是冗余的,可能会存储在多个地方。


范式化的优点:


1)范式化的更新操作通常比反范式化要快。


2)当数据较好地范式化时,就只有很少或者没有重复数据,所以只需要修改更少的数据。


3)范式化的表通常更小,可以更好地放在内存里,所以执行操作会更快。


4)很少有多余的数据意味着检索列表数据时更少需要DISTINCT或者GROUP BY语句。


范式化设计的schema的缺点是通常需要关联,较多的关联可能使得一些索引策略无效,例如,范式化可能将列存放在不同的表中,而这些列如果在一个表中本可以属于同一个索引。



反范式的schema因为所有数据都在一张表中,可以很好地避免关联。缺点是update操作的代价高,需要更新多个表,至于这会不会是一个问题,需要考虑更新的频率以及更新的时长,并和执行select查询的频率进行比较。


从另一个父表冗余一些数据到子表的理由通常是排序的需要。


缓存衍生值也是有用的。如果需要显示每个用户发了多少消息(像很多论坛做的),可以每次执行一个昂贵的子查询来计算并显示它;也可以在user表中建一个num_messages列,每当用户发新消息时更新这个值。


4.缓存表和汇总表


缓存表和汇总表,实时计算统计值是非常昂贵的操作,因为要么需要扫描表中的大部分数据,要么查询语句只能在某些特定的索引上才能有效运行,而这类特定索引一般会对update操作有影响,所以一般不希望创建这样的索引。


使用缓存表和汇总表时,必须决定是实时维护数据还是定期重建,哪个更好依赖于应用程序,但是定期重建并不只是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引(这会更加高效)。


5.物化视图


物化视图实际上是预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新。MySQL并不原生支持物化视图。使用开源工具Flexviews可以自己实现物化视图。它由下面这些部分组成:


1)变更数据抓取功能,可以读取服务器的二进制日志并且解析相关行的变更。


2)一系列可以帮助创建和管理视图的定义的存储过程


3)一些可以应用变更到数据库中的物化视图工具


6.计数器表


如果应用在表中保存计数器,在更新计数器时可能会碰到并发问题。有一个技巧:将计数器保存在多行中,更新计数+1的操作改为随机选择一行进行更新,求计数值的时候,做一个sum求和。


7.加快ALTER TABLE操作的速度(表结构更改)


一般而言,大部分alter table操作将导致MySQL服务中断(锁表并重建表)。MySQL执行大部分修改表结构的操作方法是用新的结构创建一个空表,从旧表中查出所有数据插入新表,然后删除旧表。这个操作可能需要花费很长时间。


不是所有的alter table操作都会引起表重建。理论上,MySQL可以跳过创建新表的步骤。列的默认值实际上存在表的.frm文件中,所以可以直接修改这个文件而不需要改动表本身。比如使用 alter comlum改变列的默认值:


alert table tablename


alert column  col1 set default 5;


这个语句会直接修改.frm文件而不涉及表数据,所以这个操作是非常快的


《高性能MySQL》读书笔记--优化服务器设置


MySQL有大量可以修改的参数--但不应该随便去修改。通常只需要把基本的项配置正确(大部分情况下只有很少一些参数是真正重要的),应该将更多的时间花在schema的优化、索引,以及查询设计上。在正确地配置了MySQL的基本配置项之后,再花力气去修改其它配置项的收益通常就比较小了。



1.创建MySQL配置文件


建议不要使用操作系统的安装包自带的配置文件,最好从头开始创建一个配置文件。(首先要确定MySQL使用了哪个配置文件!)






2.InnoDB缓冲池(Buffer Pool)


有一个流行的经验法则说,应该把缓冲池大小设置为服务器内存的约75~80%,但并不总是正确的。有一个更好的办法来设置缓冲池大小,大致如下:


1、从服务器内存总量开始


2、减去操作系统的内存占用


3、减去一些MySQL自身需要的内存


4、减去足够让操作系统缓存InnoDB日志文件的内存,至少是足够缓存最近经常访问的部分。


5、减去其他配置的MySQL缓冲和缓存需要的内存,例如查询缓存,MYISAM键缓存


6、除以105%,把结果向下取一个合理的数值。



如果大部分都是InnoDB表,InnoDB缓冲池或许比其他任何东西更需要内存。InnoDB缓冲池并不仅仅缓存索引:它还缓存行的数据、自适应哈希索引、插入缓存(Insert buffer)、锁、以及其他内部数据结构。InnoDB还使用缓冲池来帮助延迟写入,这样就能合并多个写入操作,然后一起顺序地写回。总之InnoDB严重依赖缓冲池,你必须确认为它分配了足够的内存。


如果数据量不大,并且不会快速增长,就没必要为缓冲池分配过多的内存。把缓冲池配置得比需要缓存的表和索引还要大很多实际上没有什么意义。当然,对一个迅速增长的数据库做超前的规划没有问题,但有时我们也会看到一个巨大的缓冲池只缓存了一点点数据,这就没有必要了。


很大的缓冲池也会带一些挑战,例如,预热和关闭都会花费很长的时间。如果有很多脏页在缓冲池里,InnoDB关闭时可能会花费较长的时间把脏页写回数据文件。当然也可以强制快速关闭,但是重启时就必须做更多的恢复工作。


当脏页的百分比超过了innodb_max_dirty_pages_pct阈值,InnoDB将快速地刷写脏页,尝试让脏页的数量更低。当事务日志没有足够的空间剩余时,InnoDB将进入“激烈刷写”模式,这就是大日志可以提升性能的一个原因。


如果不能快速预热,可以在重启后立即进行全表扫描或者索引扫描,把索引载入缓冲池。也可以使用init_file设置,把SQL放在一个文件里,然后当MySQL启动的时候来执行。



3.线程缓存


线程缓存保存那些当前没有与连接关联但是为后面新的连接服务的线程。当一个新的连接创建时,如果缓存中有线程存在,MySQL从缓存中删除一个线程,并且把它分配给这个新的连接。当连接关闭时,如果线程缓存还有空间的话,MySQL又会把线程放回缓存。如果没有空间的话,MySQL会销毁这个线程。只要MySQL在缓存里还有空闲的线程,它就可以迅速地响应连接请求,因为这样就不用为每个连接创建新的线程。


一个好的办法是观察Threads_connected变量并且尝试设置thread_cache_size足够大以便能处理业务压力正常的波动。每个在线程缓存中的线程或者休眠状态的线程,通常使用256K左右的内存。相对于正在处理查询的线程来说,这个内存不算很大。



4.表缓存


表缓存可以重用资源。当一个查询请求访问一张MYISAM表,MySQL可以从缓存的对象中获取到文件描述符,避免打开一个文件描述符的开销。对MYISAM表来说,表缓存的真正好处是可以让服务器避免修改MYISAM文件头来标记表“正在使用中”,表缓存对于InnoDB重要性就小得多,因为InnoDB不依赖它来做那么多事,例如持有文件描述符等。



5.InnoDB I/O配置(事务日志)




InnoDB使用日志来减少提交事务时的开销。因为日志中已经记录了事务,就无须在每个事务提交时把缓冲池的脏块刷新(flush)到磁盘中。事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机IO。InnoDB假设使用常规磁盘,随机IO比顺序IO昂贵得多,因为一个IO请求需要时间把磁头移到正确的位置,然后等待磁盘上读出需要的部分,再转到开始位置。



InnoDB用日志把随机IO变成顺序IO。一旦日志安全写到磁盘,事务就持久化了,即使断电了,InnoDB可以重放日志并且恢复已经提交的事务。



InnoDB使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。



整体的日志文件大小受控于innodb_log_file_size和innodb_log_files_in_group两个参数,这对写性能非常重要。日志文件的总大小是每个文件的大小之和。



InnoDB使用多个文件作为一组循环日志。通常不需要修改默认的日志数量,只修改每个日志文件的大小即可。要修改日志文件大小,需要完全关闭MySQL,将旧的日志文件移到其他地方保存,重新配置参数。



要确定理想的日志文件大小,必须权衡正常数据变更的开销和崩溃恢复需要的时间,如果日志太小,InnoDB必然将做更多的检查点,导致更多的日志写。如果日志太大,在崩溃恢复时InnoDB可能不得不做大量的工作。



当InnoDB变更任何数据时,会写一条变更记录到内存日志缓冲区中。 在缓冲满的时候,事务提交的时候,或者每一秒钟,这三个条件无论哪个先达到,InnoDB都会刷新缓冲区的内容到磁盘日志文件。 变量 innodb_log_buffer_size 可以控制日志缓冲区的大小,默认为1M。通常不需要把日志缓冲区设置得非常大。推荐的范围是1~8M。作为一个经验法则,日志文件的全部大小,应该足够容纳服务器一个小时的活动内容。


InnoDB怎么刷新日志缓冲?当InnoDB把日志缓冲刷新到磁盘日志文件时,会先使用一个Mutex锁住缓冲区,刷新到所需要的位置,然后移动剩下的条目到缓冲区的前面。日志缓冲必须被刷新到持久化存储,以确保提交的事务完全被持久化了。如果和持久相比更在乎性能,可以修改innodb_flush_log_at_trx_commit变量来控制日志缓冲刷新的频繁程度。可能的设置如下:


0:每秒一次把日志缓冲写到日志文件,但是事务提交时不做任何时。


1:将日志缓冲写到日志文件,然后每次事务提交都刷新到持久化存储(默认并且最安全的设置),该设置保证不会丢失任何已提交的事务。


2:每秒钟做一次刷新,但每次提交时把日志缓冲写到日志文件,但是不刷新到持久化存储。


“把日志缓冲写到日志文件”和“把日志刷新到持久化存储”是不同的。 在大部分操作系统中,把缓冲写到日志只是简单地把数据从InnoDB的内存缓冲转移到了操作系统的缓存,也是在内存里,并没有真正把数据写到持久化存储。


如果MySQL崩溃了或者断电了,设置0和2通常会导致最多1秒的数据丢失,因为数据可能存在于操作系统的缓存中。


相反,把日志刷新到持久化存储意味着InnoDB请求操作系统把数据刷出缓存,并且确认写到磁盘了,这是一个阻塞IO的调用,直到数据被完全写回才会完成,当写数据到磁盘比较慢,而该配置项设置为1时,可能明显地降低InnoDB每秒可以提交的事务数。



6.InnoDB并发配置


InnoDB有自己的“线程调度器”控制线程怎么进入内核访问数据,以及它们在内核中一次可以做哪些事。最基本的限制并发方式是使用innodb_thread_concurrency变量,它会限制一次性可以有多少线程进入内核,0表示不限制。


理论上,可以参考下面的公式:并发值=CPU数量*磁盘数量*2


另外,也可以通过线程池(Thread Pool)来限制并发。



7.优化排序(Filesorts)


如果查询中所有需要的列和order by的列总大小超过max_length_for_sort_data字节,则采用two-pass算法,否则采用single-pass算法。



8.其他配置项


1. max_connections :这个设置的作用就像一个紧急刹车,以保证服务器不会因应用程序激增的连接而不堪重负。如果应用程序有问题,或者服务器遇到如连接延迟的问题,会创建很多新连接,但是如果不能执行查询,那打开一个连接没有好处,所以被“太多的连接”的错误拒绝是一种快速而代价小的失败方式。


要时时小心可能遇到连接限制的突然袭击。例如,若重启应用服务器,可能没有把它的连接关闭干净,同时MySQL可能没有意识到它们已经被关闭了。当应用服务器重新开始运行,并试图打开到数据库的连接,就可能由于挂起的连接还没有超时,导致新连接被拒绝。观察max_used_connections状态变量随着时间的变化,如果这个值达到了max_connections,说明客户端至少被拒绝了一次。



2. thread_cahce_size :观察Threads_connected状态变量并且找到它在一般情况下的最大值和最小值。可以设置为波动范围2~3倍大小。但是也不用设置得非常大,因为保持大量等待连接的空闲线程并没有什么真正的用处。


也可以观察threads_created状态随时间的变化,如果这个值很大或者一直增长,这可能在告诉你,需要调大thread_cahce_size变量。查看threads_cached来看有多少线程已经在缓存中了。


另一个相关的状态变量是slow_launch_threads。这个状态如果很大,那么意味着某些情况延迟了连接分配新线程。一般来说可能是系统过载了,导致操作系统不能为新创建的线程调度CPU。



总之,如果使用的是InnoDB,最重要的配置项是以下两个: innodb_buffer_pool_size 和 innodb_log_file_size