MySQL 能够分为 Server 层和存储引擎层两部分。mysql
Server层包括链接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及全部的内置函数(如日期、时间、数学和加密函数等),全部跨存储引擎的功能都在这一层实现,好比存储过程、触发器、视图等。算法
存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。如今最经常使用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。create table 语句中使用 engine=memory,来指定使用内存引擎建立表。sql
如今最经常使用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。create table 语句中使用 engine=memory, 来指定使用内存引擎建立表。数据库
第一步,链接器链接到数据库,链接器负责跟客户端创建链接、获取权限、维持和管理链接。后端
链接命令通常是这么写的:mysql -h$ip -P$port -u$user -p$password帐号密码错误会报错:Access denied for user数组
链接完成后,若是没有后续的动做,这个链接就处于空闲状态,能够在show processlist命令中看到它。文本中这个图是show processlist的结果,其中的Command列显示为"Sleep"的这一行,就表示如今系统里面有一个空闲链接。缓存
客户端若是太长时间没动静,链接器就会自动将它断开。这个时间是由参数wait timeout控制的,默认值是8小时。安全
断开后再执行sql会报错:Lost connection to MySQL server during query
创建链接的过程一般是比较复杂的,因此建议在使用中要尽可能减小创建链接的动做,也就是尽可能使用长链接。性能优化
可是 MySQL 在执行过程当中临时使用的内存是管理在链接对象里面的。这些资源会在链接断开的时候才释放。因此若是长链接累积下来,可能致使内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启。网络
怎么解决这个问题呢?能够考虑如下两种方案。
第二步,查询语句会先查询缓存,以前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。
可是查询缓存利大于弊,由于查询缓存的失效很是频繁,只要有对一个表的更新,这个表上全部的查询缓存都会被清空。
除非是静态配置表才适合用查询缓存。能够将参数 query_cache_type 设置成DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。SQL_CACHE 显式指定使用查询缓存。
select SQL_CACHE * from T where ID=10;
可是,MySQL 8.0版本完全删除了查询缓存功能。
第三步,分析语句,先是词法分析,找出select,表名,列名等关键字;而后是语法分析,判断语法是否正确。表名列名不对的sql,会在语法分析时报错。
语法错误:ERROR 1064 (42000): You have an error in your SQL syntax;
第四步,决定使用哪一个索引,join的时候决定各个表的链接顺序。
第五步,先判断对当前表是否有权限(若是命中查询缓存,会在返回结果时验证权限)。
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
如:select * from T where ID=10; 执行过程
慢查询日志中有一行 rows_examined 字段,表示这个语句执行过程当中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。可是引擎扫描行数跟 rows_examined 并非彻底相同的。
对一个200G的大表作全表扫描,而内存只有16G,会不会把数据库主机的内存用光了?
实际上,MySQL不是取到所有数据再返回客户端。取数据和发数据的流程是这样的:
MySQL 客户端发送请求后,接收服务端返回结果的方式有两种:
另外一种是不缓存,读一个处理一个。若是用 API 开发,对应的就是 mysql_use_result 方法。
MySQL 客户端默认采用第一种方式,而若是加上–quick 参数,就会使用第二种不缓存的方式。采用不缓存的方式时,若是本地处理得慢,就会致使服务端发送结果被阻塞,所以会让服务端变慢。
MySQL 是“边读边发的”。这就意味着,若是客户端接收得慢,会致使 MySQL 服务端因为结果发不出去,这个事务的执行时间变长。
对于正常的线上业务来讲,若是一个查询的返回结果不会不少的话,都建议使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存。
更新语句一样会走链接器,查询缓存(清空该表缓存),分析器,优化器这一套流程,与查询流程不同的是,更新流程还涉及两个重要的日志模块,redo log(重作日志)和 binlog(归档日志)。
若是每一次的更新操做都须要写进磁盘,而后磁盘也要找到对应的那条记录,而后再更新,整个过程 IO 成本、查找成本都很高。
MySQL采用了WAL技术,全称是 Write-Ahead Logging,的关键点就是先写日志,再写磁盘。
具体来讲,当有一条记录须要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操做记录更新到磁盘里面,而这个更新每每是在系统比较空闲的时候作。
可是若是 InnoDB 的 redo log 写满了。这时候系统会中止全部更新操做,把 checkpoint 往前推动(对应的全部脏页都 flush 到磁盘上),redo log 留出空间能够继续写。
一旦一个查询请求须要在执行过程当中先 flush 掉一个脏页时,这个查询就可能要比平时慢了。因为刷脏页的逻辑会占用 IO 资源并可能影响到了更新语句,要尽可能避免这种状况,就要合理地设置 innodb_io_capacity 的值,而且平时要多关注脏页比例,不要让它常常接近 75%。脏页比例是经过 Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 获得的,具体的命令参考下面代码:
mysql> select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';
select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';
select @a/@b;在 InnoDB 中,innodb_flush_neighbors 参数就是用来控制这个行为的,值为 1 的时候会有“连坐”机制,值为 0 时表示不找邻居,本身刷本身的。固态硬盘建议设置为0。
InnoDB 的 redo log 是能够配置的固定大小,好比能够配置为一组 4 个文件,每一个文件的大小是 1GB,总共就能够记录 4GB 的操做。从头开始写,写到末尾就又回到开头循环写,以下面这个图所示。若是redo log 设置的过小,磁盘压力很小,可是数据库出现间歇性的性能下跌。
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是日后推移而且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是还空着的部分,能够用来记录新的操做。若是 write pos 追上 checkpoint,这时候就得停下来先擦掉一些记录,把 checkpoint 推动一下。
有了 redo log,InnoDB 就能够保证即便数据库发生异常重启,以前提交的记录都不会丢失,这个能力称为crash-safe。
redo log buffer :插入数据的过程当中,生成的日志都得先保存起来,但又不能在还没 commit 的时候就直接写到 redo log 文件里。因此,redo log buffer 就是一块内存,用来先存 redo 日志的。也就是说,在执行第一个 insert 的时候,数据的内存被修改了,在执行 commit 的时候 redo log buffer 才写入了日志。
为了控制 redo log 的写入策略,innodb_flush_log_at_trx_commit 参数,它有三种可能取值:
InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,而后调用 fsync 持久化到磁盘。也就是说,一个没有提交的事务的 redo log,也是可能已经持久化到磁盘的。
还有两种场景也会把没有提交的redo log 写到硬盘。
redo log 是 InnoDB 引擎特有的日志,而 Server 层也有本身的日志,称为 binlog(归档日志)。
binlog 的三种格式对比:
statement:记录到 binlog 里的是语句原文,最后会有 COMMIT;可能会致使主备不一致,由于limit 、等sql 执行时可能主备优化器选择的索引不同,排序也不同。now()执行的结果也不同。
row :记录了操做的事件每一条数据的变化状况,最后会有一个 XID event。缺点是太占空间。
mixed:同时使用两种格式,由数据库判断具体某条sql使用哪一种格式。可是有选择错误的状况。
这两种日志有如下三点不一样。
redo log 和 binlog 是怎么关联起来的?
它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:
处于 prepare 阶段的 redo log 加上完整 binlog,重启也能恢复,由于 binlog 完整了,那么从库就同步过去了,为了保证主从一致,有完整的 binlog 就算成功。
事务执行过程当中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
write 和 fsync 的时机,是由参数 sync_binlog 控制的:
比较常见的是将其设置为 100~1000 中的某个数值。对应的风险是:若是主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。
好比:update T set c=c+1 where ID=2;
这里给出这个 update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。其实就是把redo log 和binlog 作两阶段提交,为了让两份日志之间的逻辑一致。
保存必定时间的binlog,同时系统会按期作整库备份。
当须要恢复到指定的某一秒时,
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1,这样能够保证 MySQL 异常重启以后数据不丢失。
binlog用于备份恢复和从库同步。sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数也建议设置成 1,这样能够保证 MySQL 异常重启以后 binlog 不丢失。
一主一备结构,须要注意主备切换,备库设置只读,避免切换bug形成双写不一致问题(设置 readonly 对超级用户是无效的,同步更新的线程有超级权限,因此还能写入同步数据)。
双主结构,要避免循环更新问题,由于MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的 server id。因此能够规定两个库的 server id 必须不一样,每一个库在收到从本身的主库发过来的日志后,先判断 server id,若是跟本身的相同,表示这个日志是本身生成的,就直接丢弃这个日志。
能够在备库上执行 show slave status 命令,它的返回结果里面会显示 seconds_behind_master,用于表示当前备库延迟了多少秒。每一个事务的 binlog 里面都有一个时间字段,用于记录主库上写入的时间; 备库取出当前正在执行的事务的时间字段的值,计算它与当前系统时间的差值,获得 seconds_behind_master。
主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢。
主备延迟的来源
考虑到主备切换,主备机器通常都同样了,可是还可能备库读的压力太大,
一主多从,或者经过binlog输出到外部系统(好比Hadoop),让外部系统提供部分统计查询能力。
在官方的 5.6 版本以前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。
并行复制策略有按表并行分发策略,按行并行分发策略,可是按行分发在决定线程分发的时候,须要消耗更多的计算资源。这两个方案其实都有一些约束条件:
官方 MySQL5.6 版本,支持了并行复制,只是支持的粒度是按库并行。相比于按表和按行分发,这个策略有两个优点:
MariaDB 的并行复制策略,伪模拟主库并发度,主库 redo log 组提交 (group commit) 优化,同一组提交会记录commit_id,备库把同一个commit_id分发到多个worker执行。
官方的 MySQL5.7 版本,由参数 slave-parallel-type 来控制并行复制策略:
MySQL 5.7.22 版本里,MySQL 增长了一个新的并行复制策略,基于 WRITESET 的并行复制。对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。若是两个事务没有操做相同的行,也就是说它们的 writeset 没有交集,就能够并行。
读写分离有两种方案:
主从延迟的状况下怎么办?
配合 semi-sync 方案;半同步复制:
ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、、隔离性、持久性)。
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
幻读:仍然指的是一个事务中读了两次,结果不一样,可是与不可重复读不一样的是,这里不一样是由于别的事物作了插入操做,而是读的条件是一个范围的条件,这样第二次会多读到一条数据。
不可重复读重点在于update和delete,而幻读的重点在于insert。
即便把全部的记录都加上锁,仍是阻止不了新插入的记录,也就是说行锁解决不了幻读问题,行锁只能锁住行,可是新插入记录这个动做,要更新的是记录之间的“间隙”。所以,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。
当执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了没法再插入新的记录。
间隙锁和行锁合称 next-key lock,每一个 next-key lock 是前开后闭区间。也就是说,表 t 初始化之后,若是用 select * from t for update 要把整个表全部记录锁起来,就造成了 7 个 next-key lock,分别是(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
间隙锁和 next-key lock 的引入,解决了幻读的问题,但同时也带来了一些“困扰”。间隙锁的引入,可能会致使一样的语句锁住更大的范围,这实际上是影响了并发度的。
SQL 标准的事务隔离级别包括:读未提交read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。隔离级别越高,效率越低。
在实现上,数据库里面会建立一个视图,访问的时候以视图的逻辑结果为准。
在 MySQL 里,有两个“视图”的概念:
MySQL 默认隔离级别是可重复读,Oracle 默认隔离级别是“读提交”。
将启动参数 transaction-isolation 的值设置成 READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ 、SERIALIZABLE。
能够用 show variables 来查看当前的值。
每条记录在更新的时候都会同时记录一条回滚操做。同一条记录在系统中能够存在多个版本,这就是数据库的(MVCC)。
MVCC的全称是“多版本并发控制”。为了查询一些正在被另外一个事务更新的行,而且能够看到它们被更新以前的值,不用等待另外一个事务释放锁。
InnoDB会给数据库中的每一行增长三个字段,它们分别是DB_TRX_ID(事务版本号)、DB_ROLL_PTR(建立时间)、DB_ROW_ID(惟一id)。
InnoDB 里面每一个事务有一个惟一的事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
InnoDB 利用了“全部数据都有多个版本”的这个特性,实现了“秒级建立快照”的能力。
B+Tree叶结点上,始终存储的是最新的数据(多是还未提交的数据)。而旧版本数据,经过UNDO记录存储在回滚段(Rollback Segment)里。每一条记录都会维护一个ROW HEADER元信息,存储有建立这条记录的事务ID,一个指向UNDO记录的指针。经过最新记录和UNDO信息,能够还原出旧版本的记录。
假设一个值从 1 被按顺序改为了 二、三、4,在回滚日志里面就会有相似下面的记录。
当前值是 4,可是在查询这条记录的时候,不一样时刻启动的事务会有不一样的 read-view。同一条记录在系统中能够存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要获得 1,就必须将当前值依次执行图中全部的回滚操做获得。这些回滚信息记录在undo log 里。
当系统里没有比这个回滚日志更早的 read-view 的时候会删除老的undo log。
尽可能不要使用长事务,长事务意味着系统里面会存在很老的事务视图。会有很大的undo log日志占用空间。并且长事务还会占据锁资源,也可能拖垮整个库。
能够在 information_schema 库的innodb_trx 这个表中查询长事务,好比下面这个语句,用于查找持续时间超过 60s 的事务。能够监控这个表,设置长事务阈值报警或者直接kill。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
能够经过 SET MAX_EXECUTION_TIME 命令来控制每一个语句执行的最长时间,避免单个语句意外执行太长时间。
确认是否有没必要要的只读事务。
若是使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2或更大的值)。若是真的出现大事务致使undo log过大,这样设置后清理起来更方便。
Hash表 + 链表,查询新增都很快,可是只适用于只有等值查询的场景,不能区间查询, Memcached 及其余一些 NoSQL 引擎在用。
有序数组,等值查询和范围查询场景中的性能就都很是优秀,二分查找O(log(N)),可是更新的效率很低,因此只适用于静态存储引擎。
平衡二叉树,更新和查询都比较快。
还有跳跃表,LSM树等。
为了让一个查询尽可能少地读磁盘,就须要使用多叉树。MySQL采用的是B+树,因为索引不止存在内存中,还要写到磁盘上。二叉树的树高过高,100万数据,就有20层,在机械硬盘时代,从磁盘随机读一个数据块须要 10 ms 左右的寻址时间。就要花费200ms的寻址时间,就太慢了。MySQL B+树 的一层节点数量在1200左右,只须要1-3次磁盘IO就能够了,由于InnoDB存储引擎的最小储存单元页(Page),一个页的大小是16K。通常来讲主键id为bigint类型,长度8字节,指针6字节,那么16284/14 = 1170。因此一次IO最多读取1170个节点。
相对于B树,B+树把全部的数据都放在了叶子节点上,这样虽然每次都须要查询叶子节点,但也不过两三层,若是干节点也放数据,那干节点就变大了,一次就读取不了1200节点了,层高会变大不少。
而且MySQL把B+树的全部叶子节点的数据用指针连起来了,这样作区间查询是很是快的。
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
查询语句,若是走主键索引,会直接获得数据,若是走非主键索引,查到主键后,还须要回主键索引再查一次数据。这个过程称为回表。(覆盖索引不须要回表)
分为聚簇索引和非聚簇索引的缘由:更新数据的时候,因为数据的地址变了,须要更改索引,可是因为数据只跟主键索引绑定,索引只须要更新聚簇索引,固然还有被更新列涉及到的索引也要更新。若是全部全部都跟数据绑定,虽然省掉了回表的过程,可是每次更新,须要更新全部的索引,得不偿失。
B+ 树为了维护索引有序性,在插入新值的时候须要作必要的维护。
好比按顺序插入1-499,501-1000,索引都在一页,再插入一个500,根据 B+ 树的算法,这时候须要申请一个新的数据页,而后挪动部分数据(501到1000的数据)过去。这个过程称为页分裂。在这种状况下,性能天然会受影响。
除了影响性能外,页分裂操做还影响数据页的利用率。本来放在一个页的数据,如今分到两个页中,总体空间利用率下降大约 50%。
固然有分裂就有合并。当相邻两个页因为删除了数据,利用率很低以后,会将数据页作合并。合并的过程,能够认为是分裂过程的逆过程。
因此通常建表规范都要求用自增主键,避免页分裂,固然也有特殊状况,使用别的字段当作主键。
而且索引可能由于删除,或者页分裂等缘由,致使数据页有空洞,重建索引的过程会建立一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。
alter table T drop index k;
alter table T add index(k);
可是不能重建主键索引,不管是删除主键仍是建立主键,都会将整个表重建。可使用 alter table T engine=InnoDB 重建表。
若是执行的语句是 select ID from T where k between 3 and 5,这时只须要查 ID 的值,而 ID 的值已经在 k 索引树上了,所以能够直接提供查询结果,不须要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”查询需求,称为覆盖索引。
因为覆盖索引能够减小树的搜索次数,显著提高查询性能,因此使用覆盖索引是一个经常使用的性能优化手段。
若是有根据身份证号查询市民信息的需求,只要在身份证号字段上创建索引就够了。若是如今有一个高频请求,要根据市民的身份证号查询他的姓名,再创建一个(身份证号、姓名)的联合索引就是覆盖索引,省去了回表环节。
若是为每一种查询都设计一个索引,索引是否是太多了。
B+ 树这种索引结构,能够利用索引的“最左前缀”,来定位记录。
为了直观地说明这个概念,用(name,age)这个联合索引来分析。
能够看到,索引项是按照索引定义里面出现的字段顺序排序的。
当逻辑需求是查到全部名字是“张三”的人时,能够快速定位到 ID4,而后向后遍历获得全部须要的结果。
若是要查的是全部名字第一个字是“张”的人,SQL 语句的条件是"where name like ‘张 %’"。这时,也可以用上这个索引,查找到第一个符合条件的记录是 ID3,而后向后遍历,直到不知足条件为止。
能够看到,不仅是索引的所有定义,只要知足最左前缀,就能够利用索引来加速检索。这个最左前缀能够是联合索引的最左 N 个字段,也能够是字符串索引的最左 M 个字符。
使用前缀索引,定义好长度,就能够作到既节省空间,又不用额外增长太多的查询成本。
在创建索引时关注的是区分度,区分度越高越好。由于区分度越高,意味着重复的键值越少。所以,能够经过统计索引上有多少个不一样的值来判断要使用多长的前缀。
可使用下面这个语句,算出这个列上有多少个不一样的值:
select count(distinct email) as L from SUser;
使用前缀索引就用不上覆盖索引对查询性能的优化了,这是在选择是否使用前缀索引时须要考虑的一个因素。
那么对于身份证号,一共 18 位,其中前 6 位是地址码,因此同一个县的人的身份证号前 6 位通常会是相同的。该怎么存储,怎么设计索引呢?
第一种方式是使用倒序存储。身份证号的最后 6 位没有地址码这样的重复逻辑。
select field_list from t where id_card = reverse('input_id_card_string');select field_list from t where id_card = reverse('input_id_card_string');
第二种方式是使用 hash 字段。在表上再建立一个整数字段,来保存身份证的校验码,同时在这个字段上建立索引。
alter table t add id_card_crc int unsigned, add index(id_card_crc);而后每次插入新记录的时候,都同时用 crc32() 这个函数获得校验码填到这个新字段。因为校验码可能存在冲突,因此查询语句 where 部分要判断 id_card 的值是否精确相同。
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
最左前缀的时候,那些不符合最左前缀的部分,会怎么样呢?
若是如今有一个需求:检索出表中“名字第一个字是张,并且年龄是 10 岁的全部男孩”。那么,SQL 语句是这么写的:
mysql> select * from tuser where name like '张 %' and age=10 and ismale=1;
这个语句在搜索索引树的时候,只能用 “张”,找到第一个知足条件的记录 ID3。
而后须要判断其余条件是否知足。
在 MySQL 5.6 以前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。
而 MySQL 5.6 引入的索引下推优化(index condition pushdown),能够在索引遍历过程当中,对索引中包含的字段先作判断,直接过滤掉不知足条件的记录,减小回表次数。
当须要更新一个数据页时,若是数据页在内存中就直接更新,而若是这个数据页尚未在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操做缓存在 change buffer 中,这样就不须要从磁盘中读入这个数据页了。在下次查询须要访问这个数据页的时候,将数据页读入内存,而后执行 change buffer 中与这个页有关的操做。经过这种方式就能保证这个数据逻辑的正确性。虽然是只更新内存,可是在事务提交的时候,把 change buffer 的操做也记录到 redo log 里了,因此崩溃恢复的时候,change buffer 也能找回来。
须要说明的是,虽然名字叫做 change buffer,实际上它是能够持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。
将 change buffer 中的操做应用到原数据页,获得最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会按期 merge。在数据库正常关闭(shutdown)的过程当中,也会执行 merge 操做。
显然,若是可以将更新操做先记录在 change buffer,减小读磁盘,语句的执行速度会获得明显的提高。并且,数据读入内存是须要占用 buffer pool 的,因此这种方式可以避免占用内存,提升内存利用率。
惟一索引的更新就不能使用 change buffer,实际上也只有普通索引可使用。
change buffer 用的是 buffer pool 里的内存,所以不能无限增大。change buffer 的大小,能够经过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
若是要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。
第一种状况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程以下:
对于普通索引来讲,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
这个判断只会耗费微小的 CPU 时间。不是重点
第二种状况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程以下:
将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操做之一。change buffer 由于减小了随机磁盘访问,因此对更新性能的提高是会很明显的。
change buffer 适用于写多读少的业务,好比帐单类、日志类的系统。由于会记录不少change buffer(写的时候) 才会merge(读的时候)
反过来,读多写少的业务,几乎每次把更新记录在change buffer 后,就会当即出发merge,这样随机访问 IO 的次数不会减小,反而增长了change buffer 的维护代价。
因此,对于身份证号这类字段,若是业务已经保证不会写入重复数据,不须要数据库作约束,加普通索引比加主键索引要好,若是全部的更新后面,都立刻伴随着对这个记录的查询,那么应该关闭 change buffer。而在其余状况下,change buffer 都能提高更新性能。
在实际使用中,能够发现,普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化仍是很明显的,特别是在使用机械硬盘时。
change buffer 和 redo log 对比
insert into t(id,k) values(id1,k1),(id2,k2);
这条更新语句作了以下操做:
后续的更新操做
因此,若是要简单地对比这两个机制在提高更新性能上的收益的话,redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。
优化器结合是否扫描行数、是否使用临时表、是否排序等因素进行综合判断。
MySQL 在真正开始执行语句以前,并不能精确地知道知足条件的记录有多少条,而只能根据统计信息来估算记录数。
这个统计信息就是索引的“区分度”。显然,一个索引上不一样的值越多,这个索引的区分度就越好。而一个索引上不一样的值的个数,称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。
可使用 show index 方法,看到一个索引的基数。
MySQL 采样统计的方法得到基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不一样值,获得一个平均值,而后乘以这个索引的页面数,就获得了这个索引的基数。当变动的数据行数超过 1/M 的时候,会自动触发从新作一次索引统计。analyze table t 命令,能够用来从新统计索引信息。
在 MySQL 中,有两种存储索引统计的方式,能够经过设置参数 innodb_stats_persistent 的值来选择:
其实索引统计只是一个输入,对于一个具体的语句来讲,优化器还要判断,执行这个语句自己要扫描多少行。
rows 这个字段表示的是预计扫描行数。
少数状况下优化器会选错索引,第一种方法能够采用 force index 强行选择一个索引。
但其实使用 force index 最主要的问题仍是变动的及时性。由于选错索引的状况仍是比较少出现的,因此开发的时候一般不会先写上 force index。而是等到线上出现问题的时候,才会再去修改 SQL 语句、加上 force index。可是修改以后还要测试和发布,对于生产系统来讲,这个过程不够敏捷。
因此,数据库的问题最好仍是在数据库内部来解决。既然优化器放弃了使用索引 a,说明 a 还不够合适,因此第二种方法就是,能够考虑修改语句,引导 MySQL 使用指望的索引。好比,在这个例子里,显然把“order by b limit 1” 改为 “order by b,a limit 1” ,语义的逻辑是相同的。
以前优化器选择使用索引 b,是由于它认为使用索引 b 能够避免排序(b 自己是索引,已是有序的了,若是选择索引 b 的话,不须要再作排序,只须要遍历),因此即便扫描行数多,也断定为代价更小。
如今 order by b,a 这种写法,要求按照 b,a 排序,就意味着使用这两个索引都须要排序。所以,扫描行数成了影响决策的主要条件,因而此时优化器选了只须要扫描 1000 行的索引 a。
固然,这种修改并非通用的优化手段,可能修改语义这件事儿不太好,能够用 limit 100 让优化器意识到,使用 b 索引代价是很高的。实际上是根据数据特征诱导了一下优化器,也不具有通用性。
select from (select from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 100)alias limit 1;
第三种方法是:在有些场景下,能够新建一个更合适的索引,来提供给优化器作选择,或删掉误用的索引。
对索引字段作函数操做,可能会破坏索引值的有序性,所以优化器就决定放弃走树搜索功能。
条件字段函数操做
select count(*) from tradelog where month(t_modified)=7;同理 where id+1=1000 也不会用索引,改为 where id =1000 - 1 会用索引。
隐式类型转换
select * from tradelog where tradeid=110717; (tradeid 是varchar)等同于 select * from tradelog where CAST(tradid AS signed int) = 110717;
隐式字符编码转换
select * from trade_detail where tradeid=$L2.tradeid.value;$L2.tradeid.value 的字符集是 utf8mb4。字符集 utf8mb4 是 utf8 的超集,因此当这两个类型的字符串在作比较的时候,MySQL 内部的操做是,先把 utf8 字符串转成 utf8mb4 字符集,再作比较。
至关于 select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;
顾名思义,全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当须要让整个库处于只读状态的时候,可使用可使用这个命令,以后其余线程的如下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,作全库逻辑备份。也就是把整库每一个表都 select 出来存成文本。
经过 FTWRL 确保不会有其余线程对数据库作更新,而后对整个库作备份。在备份过程当中整个库彻底处于只读状,这是很危险的。可是不加锁,备份的数据会有不一致的问题。
能够拿到一个一致性视图来备份,官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据以前就会启动一个事务,来确保拿到一致性视图。而因为 MVCC 的支持,这个过程当中数据是能够正常更新的。
那为何还须要FTWRL呢,由于一致性读是好,但前提是引擎要支持这个隔离级别。对于 MyISAM 这种不支持事务的引擎,就须要使用 FTWRL 命令了。
既然要全库只读,为何不使用 set global readonly=true 的方式呢?确实 readonly 方式也可让全库进入只读状态,但仍是建议用 FTWRL 方式,主要有两个缘由:
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁的语法是 lock tables … read/write。与 FTWRL 相似,能够用 unlock tables 主动释放锁,也能够在客户端断开的时候自动释放。须要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操做对象。
对于 InnoDB 这种支持行锁的引擎,通常不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面仍是太大。
另外一类表级的锁是 MDL(metadata lock)。MDL 不须要显式使用,在访问一个表的时候会被自动加上。MDL 的做用是,保证读写的正确性。能够想象一下,若是一个查询正在遍历一个表中的数据,而执行期间另外一个线程对这个表结构作变动,删了一列,那么查询线程拿到的结果跟表结构对不上,确定是不行的。
所以,在 MySQL 5.5 版本中引入了 MDL,当对一个表作增删改查操做的时候,加 MDL 读锁;当要对表作结构变动操做的时候,加 MDL 写锁。
有几个请求在读写表,会加上MDL读锁,而后修改表字段的请求会被blocked,请求MDL写锁,这个时候,后面的所有读写请求都会被MDL写锁 blocked,若是查询语句频繁,并且客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。
那么如何安全的给表加字段呢?
首先要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,能够查到当前执行中的事务。若是要作 DDL 变动的表恰好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
其次,在 alter table 语句里面设定等待时间,若是在这个指定的等待时间里面可以拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。以后开发人员或者 DBA 再经过重试命令重复这个过程。
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
MyISAM 引擎就不支持行锁。不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任什么时候刻只能有一个更新在执行,这就会影响到业务并发度。InnoDB 是支持行锁的,这也是 MyISAM 被 InnoDB 替代的重要缘由之一。
在 InnoDB 事务中,行锁是在须要的时候才加上的,但并非不须要了就马上释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
若是事务中须要锁多个行,要把最可能形成锁冲突、最可能影响并发度的锁尽可能日后放。
当并发系统中不一样线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会致使这几个线程都进入无限等待的状态,称为死锁。这里用数据库中的行锁举个例子。
这时候,事务 A 在等待事务 B 释放 id=2 的行锁,而事务 B 在等待事务 A 释放 id=1 的行锁。 事务 A 和事务 B 在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁之后,有两种策略:
一种策略是,直接进入等待,直到超时。这个超时时间能够经过参数 innodb_lock_wait_timeout 来设置。
设置时间长,等待时间太长;设置时间短,有的长事务,不是死锁的也会结束。
另外一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其余事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
每一个新来的被堵住的线程,都要判断会不会因为本身的加入致使了死锁,这是一个时间复杂度是 O(n) 的操做。会耗费大量的CPU资源。
使用 show processlist 命令查看 Waiting for table metadata lock 的示意图。
这个状态表示的是,如今有一个线程正在表 t 上请求或者持有 MDL 写锁,把 select 语句堵住了。
经过查询 sys.schema_table_lock_waits 这张表,就能够直接找出形成阻塞的 process id,把这个链接用 kill 命令断开便可。
经过 sys.innodb_lock_waits 查行锁
select * from t sys.innodb_lock_waits where locked_table='test'.'t'
G
![]()
这个信息很全,4 号线程是形成堵塞的罪魁祸首。而干掉这个罪魁祸首的方式,就是 KILL QUERY 4 或 KILL 4。实际上,这里 KILL 4 才有效。
MyISAM 引擎把一个表的总行数存在了磁盘上,所以执行 count(*) 的时候会直接返回这个数,效率很高;
InnoDB 引擎就麻烦了,执行 count(*) 的时候,须要把数据一行一行地从引擎里面读出来,而后累积计数。由于多版本并发控制(MVCC)的缘由,InnoDB 表“应该返回多少行”也是不肯定的。
count() 是一个聚合函数,对于返回的结果集,一行行地判断,若是 count 函数的参数不是 NULL,累计值就加 1,不然不加。最后返回累计值。
因此,count(*)、count(主键 id) 和 count(1) 都表示返回知足条件的结果集的总行数;而 count(字段),则表示返回知足条件的数据行里面,参数“字段”不为 NULL 的总个数。
按照效率排序的话,count(字段) < count(主键id) < count(1) < count(*),因此建议,尽可能使用count(*)。
MySQL 会给每一个线程分配一块内存用于快速排序,称为 sort_buffer。
explain 结果里的 Extra 这个字段中的“Using filesort”表示的就是须要排序。
sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。若是要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但若是排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
创建联合索引,甚至覆盖索引,能够避免排序过程。
直接使用 join 语句,MySQL 优化器可能会选择表 t1 或 t2 做为驱动表,改用 straight_join 让 MySQL 使用固定的链接方式执行查询,这样优化器只会按照指定的方式去 join。
select * from t1 straight_join t2 on (t1.a=t2.a);
在这条语句里,被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引,所以效率是很高的。称之为“Index Nested-Loop Join”,简称 NLJ。
若是被驱动表 t2 的字段 a 上没有索引,那每次到 t2 去匹配的时候,就要作一次全表扫描。这个效率很低。这个算法叫作“Simple Nested-Loop Join”的算法,简称 BNL。
因此在判断要不要使用 join 语句时,就是看 explain 结果里面,Extra 字段里面有没有出现“Block Nested Loop”字样。
在决定哪一个表作驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成以后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该做为驱动表。
Multi-Range Read 优化,这个优化的主要目的是尽可能使用顺序读盘。由于大多数的数据都是按照主键递增顺序插入获得的,因此能够认为,若是按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,可以提高读性能。
select * from t1 where a>=1 and a<=100;
Batched Key Access(BKA) 算法。这个 BKA 算法,其实就是对 NLJ 算法的优化。
NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去作 join。也就是说,对于表 t2 来讲,每次都是匹配一个值。这时,MRR 的优点就用不上了。
既然如此,就把表 t1 的数据取出来一部分,先放到一个临时内存。这个临时内存就是 join_buffer。
表定义里面出现了一个 AUTO_INCREMENT=2,表示下一次插入数据时,若是须要自动生成自增值,会生成 id=2。
实际上,表的结构定义存放在后缀名为.frm 的文件中,可是并不会保存自增值。
InnoDB 引擎的自增值,实际上是保存在了内存里,MySQL 8.0 版本后,才有了“自增值持久化”的能力。
自增值修改机制
若是插入数据时 id 字段指定了具体的值 X ,就直接使用语句里指定的值 Y。
新的自增值生成算法是:从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 X 的值,做为新的自增值。
自增值的修改时机
因此,sql执行报错了,自增值已经改变了,惟一键冲突是致使自增主键 id 不连续的第一种缘由。一样地,事务回滚也会产生相似的现象,这就是第二种缘由。
批量插入的时候,因为系统预先不知道要申请多少个自增 id,因此就先申请一个,而后两个,而后四个,直到够用。这是主键 id 出现自增 id 不连续的第三种缘由。
一、主键id
再申请下一个 id 时,获得的值保持不变。因此到最大值以后,再申请id,因为id不变,因此插入会报主键冲突,若是数据量比较大,主键id应该用 bigint unsigned。默认是无符号整型 (unsigned int) ,4 个字节232-1(4294967295)。
二、系统row_id
若是建立的 InnoDB 表没有指定主键,那么 InnoDB 会建立一个不可见的,长度为 6 个字节的 row_id。InnoDB 维护了一个全局的 dict_sys.row_id 值,全部无主键的 InnoDB 表,每插入一行数据,都把当前的 dict_sys.row_id 值做为要插入数据的 row_id,而后把 dict_sys.row_id 的值加 1。
实际上,在代码实现时 row_id 是一个长度为 8 字节的无符号长整型 (bigint unsigned)。可是,InnoDB 在设计时,给 row_id 留的只是 6 个节的长度,这样写到数据表中时只放了最后 6 个字节,因此 row_id 能写到数据表中的值,就有两个特征:
248-1到 264 之间,row_id 会是0,264 以后会从0开始。
在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中;若是表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。
覆盖数据,就意味着数据丢失,影响的是数据可靠性;报主键冲突,是插入失败,影响的是可用性。而通常状况下,可靠性优先于可用性。
三、Xid
redo log 和 binlog 相配合的时候,提到了有一个共同的字段叫做 Xid。它在 MySQL 中是用来对应事务的。
MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,而后给这个变量加 1。若是当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。
而 global_query_id 是一个纯内存变量,重启以后就清零了。因此就知道了,在同一个数据库实例中,不一样事务的 Xid 也是有可能相同的。
可是 MySQL 重启以后会从新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 必定是唯一的。
可是 global_query_id 定义的长度是 8 个字节,这个自增值的上限是 264-1。理论上也是可能重复的。
四、trx_id
Xid 是由 server 层维护的。InnoDB 内部使用 Xid,就是为了可以在 InnoDB 事务和 server 之间作关联。可是,InnoDB 本身的 trx_id,是另外维护的。
InnoDB 内部维护了一个 max_trx_id 全局变量,每次须要申请一个新的 trx_id 时,就得到 max_trx_id 的当前值,而后并将 max_trx_id 加 1。
InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是经过事务的一致性视图与这行数据的 trx_id 作对比。
对于正在执行的事务,能够从 information_schema.innodb_trx 表中看到事务的 trx_id。
update 和 delete 语句除了事务自己,还涉及到标记删除旧数据,也就是要把数据放到 purge 队列里等待后续物理删除,这个操做也会把 max_trx_id+1, 所以在一个事务中至少加 2; InnoDB 的后台操做,好比表的索引信息统计这类操做,也是会启动内部事务的,所以你可能看到,trx_id 值并非按照加 1 递增的。
只读事务会分配一个特殊的,比较大的id,把当前事务的 trx 变量的指针地址转成整数,再加上 248,使用这个算法,就能够保证如下两点:
加上248是为了保证只读事务显示的 trx_id 值比较大,正常状况下就会区别于读写事务的 id。理论状况下也可能只读事务与读写事务相等,可是没有影响。
max_trx_id 会持久化存储,重启也不会重置为 0,那么从理论上讲,只要一个 MySQL 服务跑得足够久,就可能出现 max_trx_id 达到 248-1 的上限,而后从 0 开始的状况。当达到这个状态后,MySQL 就会持续出现一个脏读的 bug。由于后续的trx_id确定比末尾那些trx_id大,能看到这些数据。
五、thread_id
系统保存了一个全局变量 thread_id_counter,每新建一个链接,就将 thread_id_counter 赋值给这个新链接的线程变量。定义的大小是 4 个字节,所以达到 232-1 后,它就会重置为 0,而后继续增长。可是,在 show processlist 里不会看到两个相同的 thread_id。由于 MySQL 设计了一个惟一数组的逻辑,给新线程分配 thread_id 的时候,逻辑代码是这样的:
do {
new_id= thread_id_counter++;
} while (!thread_ids.insert_unique(new_id).second);
delete 语句误删数据行:Flashback工具过闪回把数据恢复回来。 原理是修改 binlog 的内容,拿回原库重放。而可以使用这个方案的前提是,须要确保 binlog_format=row 和 binlog_row_image=FULL。
如何预防:把 sql_safe_updates 参数设置为 on。,delete 或者 update 语句必须有where条件,不然执行会报错。
误删库 / 表:全量备份,加增量日志,在应用日志的时候,须要跳过 12 点误操做的那个语句的 binlog:
若是实例使用了 GTID 模式,就方便多了。假设误操做命令的 GTID 是 gtid1,那么只须要执行 set gtid_next=gtid1;begin;commit; 先把这个 GTID 加到临时实例的 GTID 集合,以后按顺序执行 binlog 的时候,就会自动跳过误操做的语句。
如何加速恢复:使用 mysqlbinlog 命令时,加上一个–database 参数,用来指定误删表所在的库。
在 start slave 以前,先经过执行 change replication filter replicate_do_table = (tbl_name) 命令,就可让临时库只同步误操做的表;
延迟复制备库,通常的主备复制结构存在的问题是,若是主库上有个表被误删了,这个命令很快也会被发给全部从库,进而致使全部从库的数据表也都一块儿被误删了。延迟复制的备库是一种特殊的备库,经过 CHANGE MASTER TO MASTER_DELAY = N 命令,能够指定这个备库持续保持跟主库有 N 秒的延迟。
好比把 N 设置为 3600,这就表明了若是主库上有数据被误删了,而且在 1 小时内发现了这个误操做命令,这个命令就尚未在这个延迟复制的备库执行。这时候到这个备库上执行 stop slave,再经过以前介绍的方法,跳过误操做命令,就能够恢复出须要的数据。
预防误删库 / 表的方法,制定操做规范。这样作的目的,是避免写错要删除的表名。
delete 命令其实只是把记录的位置,或者数据页标记为了“可复用”,但磁盘文件的大小是不会变的。也就是说,经过 delete 命令是不能回收表空间的。这些能够复用,而没有被使用的空间,看起来就像是“空洞”。
实际上,不止是删除数据会形成空洞,插入数据也会。若是数据是随机插入的,就可能形成索引的数据页分裂。更新索引上的值,能够理解为删除一个旧的值,再插入一个新值。不难理解,这也是会形成空洞的。
也就是说,通过大量增删改的表,都是多是存在空洞的。因此,若是可以把这些空洞去掉,就能达到收缩表空间的目的。而重建表,就能够达到这样的目的。
使用 alter table A engine=InnoDB 命令来重建表。MySQL 会自动完成转存数据、交换表名、删除旧表的操做。
重建表的时候,InnoDB 不会把整张表占满,每一个页留了 1/16 给后续的更新用。也就是说,其实重建表以后不是“最”紧凑的。
一、mysqldump 方法
使用 mysqldump 命令将数据导出成一组 INSERT 语句。你可使用下面的命令:
mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql
而后能够经过下面这条命令,将这些 INSERT 语句放到 db2 库里去执行。
mysql -h127.0.0.1 -P13000 -uroot db2 -e "source /client_tmp/t.sql"
二、导出 CSV 文件
直接将结果导出成.csv 文件。MySQL 提供了下面的语法,用来将查询结果导出到服务端本地目录:
select * from db1.t where a>900 into outfile '/server_tmp/t.csv';
而后用下面的 load data 命令将数据导入到目标表 db2.t 中。
load data infile '/server_tmp/t.csv' into table db2.t;
三、物理拷贝方法
直接拷贝文件是不行的,须要在数据字典中注册。
MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,,能够经过导出 + 导入表空间的方式,实现物理拷贝表的功能。