一 序
本文属于极客时间MySQL45讲读书笔记系列。对于开发同学,本文属于了解。
之前的问题:两个group by 语句都用了order by null,为什么使用内存临时表得到的语句结果里,0这个值在最后一行;而使用磁盘临时表得到的结果里,0这个值在第一行?
二 内存表的数据组织结构
为了便于分析,我来把这个问题简化一下,假设有以下的两张表t1 和 t2,其中表t1使用Memory 引擎, 表t2使用InnoDB引擎。
create table t1(id int primary key, c int) engine=Memory;
create table t2(id int primary key, c int) engine=innodb;
insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
然后,我分别执行select * from t1和select * from t2。
图1 两个查询结果-0的位置
可以看到,内存表t1的返回结果里面0在最后一行,而InnoDB表t2的返回结果里0在第一行。
出现这个区别的原因,要从这两个引擎的主键索引的组织方式说起。
表t2用的是InnoDB引擎,它的主键索引id的组织方式,你已经很熟悉了:InnoDB表的数据就放在主键索引树上,主键索引是B+树。所以表t2的数据组织方式如下图所示:
图2 表t2的数据组织
主键索引上的值是有序存储的。在执行select *的时候,就会按照叶子节点从左到右扫描,所以得到的结果里,0就出现在第一行。
与InnoDB引擎不同,Memory引擎的数据和索引是分开的。我们来看一下表t1中的数据内容。
图3 表t1 的数据组织
可以看到,内存表的数据部分以数组的方式单独存放,而主键id索引里,存的是每个数据的位置。主键id是hash索引,可以看到索引上的key并不是有序的。
在内存表t1中,当我执行select *的时候,走的是全表扫描,也就是顺序扫描这个数组。因此,0就是最后一个被读到,并放入结果集的数据。
可见,InnoDB和Memory引擎的数据组织方式是不同的:
- InnoDB引擎把数据放在主键索引上,其他索引上保存的是主键id。这种方式,我们称之为索引组织表(Index Organizied Table)。
- 而Memory引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表(Heap Organizied Table)。
从中我们可以看出,这两个引擎的一些典型不同:
- InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的;
- 当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
- 数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引;
- InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的。
- InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。
实际上,内存表也是支B-Tree索引的。
其实,一般在我们的印象中,内存表的优势是速度快,其中的一个原因就是Memory引擎支持hash索引。当然,更重要的原因是,内存表的所有数据都保存在内存,而内存的读写速度总是比磁盘快。
但是,接下来我要跟你说明,为什么我不建议你在生产环境上使用内存表。这里的原因主要包括两个方面:
-
锁粒度问题;
-
数据持久化问题。
内存表的锁
我们先来说说内存表的锁粒度问题。
内存表不支持行锁,只支持表锁。因此,一张表只要有更新,就会堵住其他所有在这个表上的读写操作。
数据持久性问题
接下来,我们再看看数据持久性的问题。
数据放在内存中,是内存表的优势,但也是一个劣势。因为,数据库重启的时候,所有的内存表都会被清空。
所以,我建议你把普通内存表都用InnoDB表来代替。但是,有一个场景却是例外的。
这个场景就是,我们在第35和36篇说到的用户临时表。在数据量可控,不会耗费过多内存的情况下,你可以考虑使用内存表。
内存临时表刚好可以无视内存表的两个不足,主要是下面的三个原因:
-
临时表不会被其他线程访问,没有并发性的问题;
-
临时表重启后也是需要删除的,清空数据这个问题不存在;
-
备库的临时表也不会影响主库的用户线程。
现在,我们回过头再看一下第35篇join语句优化的例子,当时我建议的是创建一个InnoDB临时表,使用的语句序列是:
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
了解了内存表的特性,你就知道了, 其实这里使用内存临时表的效果更好,原因有三个:
-
相比于InnoDB表,使用内存表不需要写磁盘,往表temp_t的写数据的速度更快;
-
索引b使用hash索引,查找的速度比B-Tree索引快;
-
临时表数据只有2000行,占用的内存有限。
另外:
- 内存表表在所有客户端之间共享
- 在数据库复制时,如果主机当掉,则会在binLog中自动加入delete from [内存表],将slave的数据也删除掉,以保证两边的数据一致性,老师是画了主从结构的图,我就不贴了。
应用场景
因为内存表有两个主要的特性
- 多线程共享,对所有的用户连接是可见的,这一点和临时表完全不同。
- 数据存储在内存中,有很快的访问速度。
所以内存表很适合作为缓存,存储中间结果,和需要频繁访问的数据。
缺点也很明显,数据存在内存中, 服务器重启后数据会丢失。
小结
今天这篇文章,我从“要不要使用内存表”这个问题展开,和你介绍了Memory引擎的几个特性。
可以看到,由于重启会丢数据,如果一个备库重启,会导致主备同步线程停止;如果主库跟这个备库是双M架构,还可能导致主库的内存表数据被删掉。因此,在生产上,我不建议你使用普通内存表。
如果你是DBA,可以在建表的审核系统中增加这类规则,要求业务改用InnoDB表。我们在文中也分析了,其实InnoDB表性能还不错,而且数据安全也有保障。而内存表由于不支持行锁,更新语句会阻塞查询,性能也未必就如想象中那么好。
关于内存表,贴一下官网的5.7版本的特性介绍,供参考:
The
MEMORY
storage engine (formerly known asHEAP
) creates special-purpose tables with contents that are stored in memory. Because the data is vulnerable to crashes, hardware issues, or power outages, only use these tables as temporary work areas or read-only caches for data pulled from other tables.Table 15.4 MEMORY Storage Engine Features
Feature Support B-tree indexes Yes Backup/point-in-time recovery (Implemented in the server, rather than in the storage engine.) Yes Cluster database support No Clustered indexes No Compressed data No Data caches N/A Encrypted data Yes (Implemented in the server via encryption functions.) Foreign key support No Full-text search indexes No Geospatial data type support No Geospatial indexing support No Hash indexes Yes Index caches N/A Locking granularity Table MVCC No Replication support (Implemented in the server, rather than in the storage engine.) Limited (See the discussion later in this section.) Storage limits RAM T-tree indexes No Transactions No Update statistics for data dictionary Yes