Mysql中存储引擎、索引、sql调优、锁、innodb引擎架构、MVCC多版本并发控制总结
一、存储引擎
Innodb是Mysql的默认引擎,因为其可以使用外键、行锁、事务管理。所以也是开发使用的最多的存储引擎。
根据上图,可以明确, InnoDB 索引使用的是 B+Tree,点查和范围查性能都很好,但写入索引时确实要付出页分裂、页合并等维护成本。为了进一步加速热点页的点查,InnoDB 内部会自动启用“自适应哈希索引”(Adaptive Hash Index),对频繁访问的叶子页建立哈希缓存,从而在这些场景下获得类似哈希查找的 O(1) 性能。这个机制是引擎自动触发和维护的,无须也无法由用户直接创建或控制。 (查看后续的索引以及innodb引擎架构)
Memory引擎由于数据结构特性,hash索引无法适用于范围查找,memory引擎存储的数据是在内存中的,所以memory引擎更适用于小规模的无范围查询数据存储。
MyISAM引擎也使用B+树索引,但实际不支持外键、索引、事务管理,其批量插入速度快、耗能少。
为什么MYISAM引擎使用B+树索引,但是批量插入速度比Innodb更快?
答案是MYISAM引擎本身只有表锁, 表级锁粒度更粗、管理开销更小 。其次MYISAM没有redo log,不会存储额外的数据来保证持久性,开销也会更小。
二、索引
我们只研究Innodb的索引。
在上文说到了,Innodb实质上是使用B+树来做为存储结构。
1.那么为什么不使用线性表、B树、哈希索引等数据结构进行存储?
1.1 线性表:
- 顺序表(数组)
- 随机读写都能 O(1),但插入/删除要搬移大量元素,O(n);
- 需要大块连续空间,不方便动态扩缩,碎片化严重。
- 链表
- 插入/删除 O(1)(拿到指针位置后),但查找某个元素或定位到第 k 个节点需要 O(n)。
- 无法支持高效的“按值查找”或“按范围查找”。
两者都不能同时兼顾“快速定位”、“范围扫描”、“高效更新”这三点。
1.2 B树
- B‑Tree
- 每个节点(包括内节点)都存“完整记录”(key + payload),优点是查到某个 key 时数据就拿到了;
- 缺点是节点体积大、度数(fan‑out)小,树高更高;范围扫描要在叶子和内节点间跳来跳去,效率不够集中。
- B+Tree
- 内节点只存 key+child pointer,不存实际行数据,度数很高 → 树更“矮胖”,一次磁盘读能扫更多键 → 查找路径更短;
- 所有行数据都在叶子节点,并且叶子节点通过双向链表串起来,支持非常高效的范围扫描(扫链表即可);
- 写入时内节点分裂、合并也只涉及键和指针,逻辑更简单些。
1.3哈希索引
- 哈希表对等值查找 O(1) 很快,但:
- 不支持范围查询;
- 不支持前缀匹配(只能精确 match);
- 扩容/缩容时要重哈希,成本高;
- 无法利用磁盘顺序做批量读取。
因此在通用关系数据库里,哈希索引只能做点查补充(部分引擎可选),而不能替代 B+Tree 承担主流的 OLTP/OLAP 场景。
总结:为什么使用B+树?
因为B+树查询速度快,因为B+树非叶子节点只存储索引和主键值,导致B+树在几万的存储量级下树高也就在3层左右,那么一次查询只需要比较3次,查询速度非常快。
其次,B+树的所有内容都存储在叶子节点,叶子节点之间通过双向链表连接,所以范围查询轻松。
最后,B+树开辟的空间是非连续的,最小的存储空间是页,所以可以节省空间间隙。
2.索引的应用
2.1查看执行频次、慢查询日志、show profiles、explain
查询频次
可以查看增删改查的使用频次,从而了解一张表中哪个DML语句占主导,根据这个可以选择索引的使用。
慢查询日志
可以设置sql语句执行上限,慢查询日志会记录查过上限的sql操作
show profiles
使用该语句可查看所有sql语句的执行耗时
explain** 用的最多
用于获取sql语句的详细信息,包括索引、查询方式等等,是使用的最多的。
2.2最左前缀法则
在之后的例子中,使用到的联合索引都是:profession,age,status
在实际开发过程中,都是不建议为单个字段添加索引的,单独简历索引开销太大,所以对较为常用的字段,可以将其添加为联合索引,并且在创建索引时,最左侧的字段具有最高优先级
我们在查询的过程中,如果要使用上述创建的联合索引,那么在查询的字段中,必须在最左侧使用该最高优先级的字段,索引才能生效,否则就不会走索引
如图所示:
2.3索引失效
即使我们创建了索引,并且使用了最左前缀法则,索引仍然可能出现失效,具体有如下几种方式:
1.函数运算
2.范围查询
使用闭区间时不会出现失效,而使用开区间可能会搜索全表而不走索引,底层原因是浮点数造成的。
3.模糊匹配
使用模糊匹配时,即使满足最左前缀法则,模糊匹配在前侧添加%就无法查询。
4.索引列运算
-- 举例1:在索引列上调用函数
SELECT * FROM t WHERE UPPER(name) = 'ALICE';-- 举例2:在索引列上做算术
SELECT * FROM t WHERE score + 1 > 80;-- 举例3:在索引列上做日期/字符串运算
SELECT * FROM t WHERE DATE(created_at) = '2025-07-14';
score + 1 > 80;以这部分为例,mysql数据库会先查询全表算出score+1的值,再去判断>80的行。
可以简单的理解,mysql遇到列运算的习惯是先查询全表,将全表按照函数运算式进行运算,再进行对应的判断。
5.字符串不加引号
6.or的连接条件
2.4索引提示
当一个字段同时有联合索引和唯一索引时,默认走联合索引,但是可以使用命令进行设置为使用具体的哪条索引(了解即可)。
2.5索引覆盖与回表
还是按照上述例子创建了一个联合索引profession,age,status
那么如果此时查询的字段在profession,age,status中,就是索引覆盖,如果不在字段中,
比如现在有个字段A,现在需要查询profession,age,status,A,此时由于索引中没有字段A的内容,所以需要回表。
1.如何进行回表?
在上述内容中已经介绍过innodb引擎的数据结构以及对应的存储内容,每一个索引实质上也是一颗B+树,每个非叶子节点包含索引的字段数据、主键值以及指向下个节点的指针。
查询的字段A不在这颗B+树中,所以根据节点对应的主键值回到聚簇索引(主表)进行查询(聚簇索引就是表建立时不添加任何索引的数据结构,叶子节点包含所有行的数据),聚簇索引是根据主键来进行构建的,所以我们再根据字段A对应联合索引的主键值回到聚簇索引,找到对应的节点,拿出对应的字段A,这个过程就是回表。
2.应用
答:建立一个联合索引,包含id,username,password的字段,同时username放在最左侧,符合最左前缀。
2.6索引使用
假设有表 tb_user
中,phone
列建了索引,且目前只插入了尾号从 00
到 20
共 21 条数据,具体如下(省略前面固定位数,只看后两位):
查询phone的尾号>=5的行,那么就不会走索引,因为直接全表查询的命中率是16/21,这个命中率称为选择性,选择性越靠近1,mysql越不会走索引,因为走全表查询更划算;相反,如果选择性越接近0,那么越是可能走索引。
2.7前缀索引
当一个字段中存储的内容过多,并且查询的次数也很多,那么就会建立前缀索引。前缀索引指根据字段中的前缀来建立索引。这里也需要根据索引的选择性来决定索引长度。
索引选择性指:以某长度切割字段作为索引,也就是使用字段的前缀作为索引,导致的非重复度为多少。
比如原本的字段是唯一的,有20个数据内容,此时不使用前缀索引的选择性就是100%,因为没有重复度,所以非重复度就是100%,而此时取前5位作为索引判断依据,此时有2个重复的内容,所以选择性就是18/20=90%。
前缀索引需要找到高选择性和低前缀的平衡。
2.8索引的结构
在innodb中索引都是以B+树作为存储数据结构。聚簇索引就是以主键作为键值的索引结构,也就是一张表的存储结构,所有数据内容都在叶节点。每个非叶子节点会保留键值,而不会保留具体的数据内容(节省空间在页中存储更多的索引)。
对于二级索引,二级索引的非叶子节点会包含自身索引键值以及主键值,主键值的作用是在当二级索引的键值相同时,使用主键值来进行划分存储位置,因为主键是唯一的,所以一定能够找到每个数据的正确位置。在叶子节点只会存储对应索引字段的数值,而不会保存整行的数值。比如usr表,有主键id,age,name,phone字段,现在对name添加了索引。那么在叶子节点只会保存id与name。为什么会有id的值?因为前面说了,二级索引的非叶子节点都会保存主键值,叶子节点自然也会保存主键值,保存主键值的用途:非叶子节点用于判断重复字段的正确位置,叶子节点用于回表。
2.9索引的设计原则
1.对查询频繁的字段建立索引,写多读少的不建议建立索引,因为B+树的插入会涉及页分裂和页合并,增加开销。
2.对于字段内容很长的字符串,如有需要,可以建立前缀索引。
3.字段经常作为查询条件(where)、分组(order by)、排序(group by)的,可以建立索引。因为建立索引会对相应的字段进行排序,where时接的条件语句会更好的通过索引进行查询,同理排序也能直接走叶子节点的链表。
4.建立索引时,尽量建立联合索引与唯一索引以及NotNull索引,可以提升索引效率。联合索引的原因在于更容易进行索引覆盖而省去回标。唯一索引在于不需要再根据主键进行比较。
三、SQL调优
3.1.主键调优
主键自带NotNULL特性,主键创建时,最好是设置为自增,因为乱序插入会涉及页分裂与页合并,增加开销。
3.2.order by调优
order by对应的索引最好选择重复度低的容易区分的字段,比如user字段中选择name或者主键id,而不是选择gender性别。
其次,order by如果涉及多个字段,那么这些字段的索引升序与降序尽可能与查询时一致,如果不一致,很可能造成索引失效。
联合索引的物理结构
索引:INDEX(age ASC, phone ASC)
假设有以下数据:
- id age phone
- 1 20 13800138001
- 2 20 13800138002
- 3 20 13800138000
- 4 25 13900139001
- 5 25 13900139000
- 6 30 13700137001
联合索引的B+树叶子节点结构:
索引页(叶子节点)按顺序存储:
[20, 13800138000, rowid] -> [20, 13800138001, rowid] -> [20, 13800138002, rowid]
-> [25, 13900139000, rowid] -> [25, 13900139001, rowid] -> [30, 13700137001, rowid]
- 排序规则:
- 首先按age升序排列
- age相同时,按phone升序排列
- 每个索引项还包含主键rowid用于回表
- 查询流程详解
- 查询语句:SELECT * FROM table ORDER BY age ASC, phone DESC
第1步:索引扫描
MySQL使用联合索引进行全索引扫描:
读取顺序:
[20, 13800138000] -> [20, 13800138001] -> [20, 13800138002]
-> [25, 13900139000] -> [25, 13900139001] -> [30, 13700137001]
第2步:数据获取
对每个索引项进行回表操作:
通过rowid回表获取完整行数据:
Row1: (1, 20, 13800138000)
Row2: (2, 20, 13800138001)
Row3: (3, 20, 13800138002)
Row4: (4, 25, 13900139000)
Row5: (5, 25, 13900139001)
Row6: (6, 30, 13700137001)
第3步:排序处理(Using filesort)
问题出现:
索引扫描得到的顺序:age=20时,phone是 000, 001, 002(升序)
查询要求的顺序:age=20时,phone应该是 002, 001, 000(降序)
MySQL的处理方式:
将所有行数据加载到排序缓冲区(sort_buffer)
在内存中执行排序操作
按照 age ASC, phone DESC 重新排序
第4步:最终排序结果
期望的结果顺序:
(1, 20, 13800138002) -- age=20, phone最大
(2, 20, 13800138001) -- age=20, phone中间
(3, 20, 13800138000) -- age=20, phone最小
(4, 25, 13900139001) -- age=25, phone最大
(5, 25, 13900139000) -- age=25, phone最小
(6, 30, 13700137001) -- age=30
为什么不能直接使用索引排序
索引顺序 vs 查询要求对比
索引物理存储顺序:
age=20: phone 000 -> 001 -> 002 (升序)
age=25: phone 000 -> 001 (升序)
查询要求的顺序:
age=20: phone 002 -> 001 -> 000 (降序)
age=25: phone 001 -> 000 (降序)
核心问题
在联合索引 (age ASC, phone ASC) 中:
第一级排序(age):符合查询要求(ASC)
第二级排序(phone):不符合查询要求(索引是ASC,查询要求DESC)
因此MySQL无法直接按索引的物理顺序返回结果,必须进行额外的排序操作。
性能优化建议
所以,对于联合索引,并且需要排序的时候,最好按照预定顺序进行简历索引
3.3.group by调优
需要满足最左前缀法则
3.4.count 调优
这部分记住即可,count对count*与count1做了专门的优化。除非是对某个字段有累加的需求,不然尽量走count*
3.5.update调优
update调优就一个,需要根据索引字段进行修改,因为update操作需要上锁。
如果没有索引,那么此时就需要全表扫描,找到对应的name为SpringBoot的行,因为需要对name字段为SpringBoot的行进行上锁,但是需要全表扫描才能找到,为了保证数据的一致性,只能上表锁。那么表锁粗粒度,不利于并发。
而如果修改的字段的查询字段有索引,那么直接根据索引能够快速找到对应的行,查找到后立刻对行进行上锁,此时上的就是表锁。
总结:update的优化就是where语句中的字段需要有索引,有索引时上的是行锁,否则上的是表锁。
3.6.limit调优
对于limit优化,官方给的建议是使用覆盖索引以及子查询。
所以在子查询中,我们不要再select* 而是使用select主键或其他唯一索引,那么这一次的查询不需要进行回表,不需要传递完整内容,只需要传递主键值,再在主查询根据子查询中得到的id或其他唯一键来获得想要查询的内容。
除了这种方法外,如果我们的了解我们的主键结构,就可以使用书签分页方式。
假设我们需要从第38999999条数据开始查询10条数据,那么我们直接根据id来查询即可:
select * from table where id = 38999999 limit 10;即可非常快速的拿到具体内容,因为不需要进行排序(排序走索引走的的是B+树叶子结点的链表O(N)复杂度,而不排序走索引走的是从父节点向下走O(logN)复杂度。)
3.7.批量插入
mysql自带了批处理操作,优化的内容就是提交的次数,因为每次提交都有网络延迟。
四、锁
4.1全局锁
在对数据库做备份时,需要使用到全局锁,保证整个数据库的此时数据不受干扰
4.2表锁
1. MDL元数据锁Meta data lock
元数据锁是一种在MysqlV5.5之后引入的 自动进行加锁与解锁 的表锁,也就是说MDL是由Mysql自行控制的。元数据锁有共享锁和排他锁,基本上所有的DML(Data Manipulation Language(数据操作语言) )操作都是上的元数据共享锁,这些操作即使是在操作时,也能共享同一张表(只要不操作同一行)。
而对于大部分的DDL(Data Definition Language(数据定义语言) )操作,上的都是排他锁。
共享锁与共享锁之间是兼容的,而排他锁与任何锁都是互斥的。
2. 意向锁
意向锁输入表锁的范畴,主要的用途是便于表锁上锁。
如果没有意向锁,表中的某行此时上了行锁,为了表锁不与行锁发生冲突,现在只能逐一遍历行去查看是否上了锁。这就很不方便。
有了意向锁后,每行在上锁时除了添加行锁外,还需要添加一个意向锁,用于表锁的查看,表锁发现当前所有行的所有意向锁与表锁不冲突,那么就上表锁,否则阻塞等待意向锁取消。
意向锁的共享锁与表锁的共享锁兼容,其他排列组合都是互斥。
4.3行锁
1.介绍
行锁是在Mysql中粒度最细的锁,只有innodb引擎支持行级锁,行级锁可以分为行锁、间隙锁、临建锁。
innodb引擎通过索引来构建数据内容,行锁只会锁住对于的索引,而不会对数据行row进行加锁。
间隙锁会对索引之间的间隙进行加锁,也可以看作是对当前加锁索引的行row的间隙进行加锁,防止对行的间隙进行insert来避免幻读。
临建锁是行锁于间隙锁的结合。
2.为什么间隙锁能够防止幻读
假设现在开启了一个事务,事务的任务就是通过范围查询,连续读取两次某两行连续行A、B的数据,并且没有对A、B行加上间隙锁。
第一次读取行A、B的数据为a,b,
某个线程执行insert操作,在A、B行插入了数据C,
第二次读取A、C、B的数据为a,c,b
此时就出现了幻读
如果加上了间隙锁,那么AB之间的缝隙就被锁住了,无法进行插入。
3.排他锁与共享锁
涉及增删改语句,mysql都会自动加上行锁(这里指临建锁),特殊的比如:select ... for update,这也涉及到改的操作,所以加的也是行锁。
而对于select..from..不会添加任何锁;
对于select.. from.. LOCK IN SHARE MODE,这是显式的加上了共享锁。
4.锁升级
1.innodb是根据索引进行存储数据内容的,如果innodb根据索引进行搜索与扫描时,那么此时会自动加上间隙锁用于防止幻读。
2.如果索引是唯一索引,那么就会将间隙锁升级为行锁(这里仍然指临建锁)。
3.如果根据某个没有索引的字段进行扫描,那么此时为了防止幻读,就必须使用表锁。
问:为什么有索引的时候加的是间隙锁或者行锁(临建锁),而没有走索引的时候加的是表锁?
因为mysql中innodb引擎使用索引存储数据内容,当走索引的时候,能够明确下一步的可能选项,索引只需要对下一步的所有指针的间隙进行上锁,就能够避免幻读。
而当不走索引时,走的是索引的叶子节点的链表结构,此时不知道所找行的具体位置,为了避免在查找过程中被捷足先登出现幻读,所以需要将整张表锁住。
五、Innodb引擎
5.1逻辑存储结构
innodb引擎中的存储结构从上到下依次是 :表空间、段、区、页、行。最小的存储单位是页
默认情况下,一个区的大小是1M,一个区有64页,每页16K。
5.2存储结构
在innodb中,为了缓解cpu与磁盘IO操作的速度矛盾,在中间搭建了一层缓存,也称缓冲区。
1.内存结构
内存结构就是缓冲区,主要分为三块:Buffer pool缓冲池、change buffer修改缓冲、log buffer日志缓冲
1)buffer pool,buffer pool会将从磁盘读取到数据内容存储到buffer pool中用作缓存,后续查找命中buffer pool就会提升访问速率。buffer pool中没有被使用的页称为free page空闲页,被使用但没有被修改的页称为clean page,而修改过的页称为dirty page。
buffer pool会以一定的频率区刷新磁盘,来保证内存 与磁盘的 一致性。
2)change buffer,change buffer主要用于存储二级非唯一索引的修改内容。二级索引的存储过程是相对随机的,因为不是根据主键有序存储的,这就导致了二级索引的插入与删除操作会变更表结构(比如页合并和页分裂),浪费大量的磁盘IO。所以修改的页如果没有存储在buffer pool中,那么会存储在change buffer中。
可以将change buffer看作是buffer pool的缓存,最后当buffer pool读取到相应的存储在change buffer中的内容时,此时会将change buffer对应的内容写入到buffer pool中。
总结:读修改的内容buffer pool中没有时,如果这个数据内容属于二级索引,那么会将修改的内容存储在change buffer中。当读取该内容时,会从change buffer写入到buffer pool。最后buffer pool按照自己的频率刷新磁盘。
3)log buffer日志缓冲,这部分内容保存的时redo log和undo log日志信息,默认大小时16M,当达到存储上限时会对日志进行覆盖。
4)Adaptive Hash Index,自适应hash索引,innodb使用的时B+树进行存储的,搜索的时间复杂度为O(LogN),而对于某些热点的数据(innodb自行定义的)会对其构建哈希索引,将其查询速率提升至O(1)。
2.磁盘结构
磁盘结构主要有:文件表空间、双写缓冲空间、临时空间、系统表空间、重做日志Redo Log、通用空间、撤销表空间
1)文件表空间:是最主要部分,存储的是所有数据内容以及索引。
2)双写缓冲空间:用于保证数据持久性的空间,为了缓解内存向磁盘写入故障的矛盾,先将内存内容写入双写缓冲空间,再将内容从内存写入磁盘中的文件表空间。这是因为双写缓冲空间是连续的空间,而文件表空间的存储结构不是连续的,涉及到寻址过程,所以写入双写缓冲的速度是更快的。
双写缓冲空间就是为了避免内存向磁盘写入出错而无法纠错的情况,如果出错,可以读取双写缓冲区来进行纠错。
3)临时空间:用于存储一些临时表。
4)系统表空间:为了对内存的change buffer持久化,系统表空间会存储change buffer的内容。
5)重做日志Redo Log:用于保证数据库的一致性,包含重做日志缓冲(redo logbuffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。用于在内存将buffer pool中的脏页刷新到磁盘出现错误时的恢复操作。
redo log记录了最近对物理页进行修改的操作,用于崩溃时的恢复。
- 更重要的是用于事务的持久性和崩溃恢复
- 遵循WAL(Write-Ahead Logging)原则
6)通用表空间:用户创建通用表。
7)撤销表空间Undo Tablespaces:用于事务提交出错时的回滚。
- 事务回滚
- 多版本并发控制(MVCC)
- 一致性读
磁盘写入出错崩溃恢复的流程:
磁盘写入崩溃,首先会检测是否有损坏页(写入一半),找到之后调用双写缓冲区的对应内容将页覆盖。之后调用redo log,找到检查点check point,之后重做redo原本的操作。
5.4后台线程
有了存储结构,那么线程怎样维护各个存储结构之间的数据传输以及保证数据库事务的ACID?
很显然需要线程来进行操作。
主要有四种线程,分别为核心主线程、IO线程、回收线程、页清理线程
非重点
5.5事务原理
1.什么是事务
事务是指一系列的数据库操作需要按照操作顺序原子性的完成,这个过程称为一个事务。
2.事务的特性
原子性
指整个事务是不可分割的最小单位,要么同时提交要么同时回滚。
一致性
事务在提交前是合法状态,在提交后也是合法状态。
持久性
事务回滚或提交后都会永久改变数据库。
隔离性
事务相当于是在单机运行,不受外界干扰。
3.谁来保证事务的ACID
redo log与undo log保证了一致性、原子性、持久性。
undo log可以用于事务的回滚保证了原子性,redo log可以用于在数据库崩溃时恢复保证数据的一致性,共同维护了持久性。
锁与多版本并发控制保证了不同事务之间的隔离性。
4.redo log:记录了最近的物理页修改操作,并且有相应的check point,用于保证事务的持久性。
5undo log:记录的是逻辑操作,简单来说就是A->B怎么来的,undo->就记录B->A怎么回去,也就是逻辑回滚操作。比如A delete a ->B,那么undo log就会记录 B insert a ->A,这只是一个举例。
5.6MVCC多版本并发控制Multi-Version Concurrency Control
1.基本概念
1)当前读
对于增删改以及select ..for update 都属于当前读,读取的内容是当前最新的内容。会加锁,对于判断字段有索引的加的是行锁,判断字段无索引内容加的是表锁。
2)快照读
对于select 操作,是快照读,读取的是当前可见版本的内容,可能是最新的,也可能是老版本的内容。不进行加锁操作。
3)MVCC
指维护一个数据多个版本,使得读写操作能够共存。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undolog日志、readView。
2.记录中的隐式字段
DB TRXND:最新的事务id
DB ROLL PTR:回滚指针,指向MVCC中的上一个版本。调用Undo log进行回滚
DB ROW ID:隐式主键ID,如果没有设置主键就会有这个内容。
3.事务实现原理
1)readview读视图
readview读视图包含四个字段:
当前活跃事务ID集合->指当前还未提交的事务
创建readview的事务id
最小活跃事务id->用于记录当前未提交的最早事务
预分配事务id->当前活跃事务id+1
2)在Read Commited事务隔离级别下的MVCC运行流程:
访问版本的规则:
如果当前事务id>最大活跃id,那么说明该事务是在readview生成后创建的,那么不可以访问该版本;
如果最小活跃id<=当前事务id<=最大活跃id,并且事务id不属于活跃id,说明当前版本已经提交,可以查看该事务版本对应的数据;
如果当前事务id<最小活跃id,说明当前事务已提交,可以进行访问;
如果当前事务id = 创建事务id,说明事务是自己创建的,当然可以访问。
简单的总结一下:拿到当前读试图,从事务记录中进行比对,找到离当前最近的已提交的事务版本进行使用,不同版本的事务通过undo log以链表形式进行连接,如果当前版本未提交就根据undo log往下找,知道找到已提交能够被访问的版本,读出该版本的数据。