MySQL · 源码分析 · 临时表与TempTable存储引擎Allocator
本文基于MySQL Community 8.0.25 Version
临时表创建
显式创建临时表
临时表可使用 CREATE TEMPORARY TABLE 语句创建,它仅在当前session中可以使用,session断开时临时表会被自动drop掉。因此开启不同的session时可以使用相同的临时表名。
1. CREATE TEMPORARY TABLE temp_table (id int primary key auto_increment, payload int);
2. INSERT INTO temp_table (payload) VALUES (100), (200), (300);
8.0.13 版本后的MySQL的默认临时表存储引擎是 TempTable。 可以在创建临时表时指定存储引擎,如指定使用 Memory:
1. CREATE TEMPORARY TABLE temp_table_memory (x int) ENGINE=MEMORY;
隐式创建临时表
一些情况下,server在执行语句时会创建内部临时表,用户无法对此进行直接控制。如:
UNION语句- 派生表(即在查询的
FROM子句中生成的表) - 通用表表达式(
WITH子句下的表达式) DISTINCT组合ORDER BYINSERT...SELECT语句。MySQL会创建内部临时表保存SELECT的结果,并将这些rowINSERT到目标表中- 窗口函数
GROUP_CONCAT()、COUNT(DISTINCT)表达式
如何判断SQL语句是否隐式使用了临时表:使用EXPLAIN语句并检查EXTRA 列,若显示Using temporary,则说明使用了临时表。
临时表存储引擎
Memory
在MySQL 8.0.13版本引入TempTable 存储引擎前,使用Memory 存储引擎来创建内存中的临时表。
但是它有不足之处如下:
- 不支持含有BLOB或TEXT类型的表,这种情况在8.0.13版本前只能将临时表建在disk上
- 对于VARCHAR类型的字段,如VARCHAR(200),映射到内存里处理的字段变为CHAR(200),容易造成空间浪费
### InnoDB / MyISAM
MySQL 8.0.16及以后,server使用InnoDB 存储引擎在disk上创建临时表。internal_tmp_disk_storage_engine 变量已被删除,用户无法再自定义选择MyISAM 存储引擎。
### TempTable
MySQL 8.0.13版本引入 TempTable 存储引擎,此后TempTable 成为在内存中创建临时表的默认存储引擎。使用internal_tmp_mem_storage_engine可以指定内存中创建临时表的存储引擎(另一个是MEMORY)。 相较于MEMORY 存储引擎,TempTable 存储引擎可以支持变长数据类型的存储。
TempTable 内存分配策略及源码分析
MySQL 8.0.23版本之后引入temptable_max_mmap 变量,因此本次分析针对8.0.23版本后的策略及其源码。
内存分配策略
- 若临时表分配空间未超过
temptable_max_ram值,则使用TempTable存储引擎在RAM中为临时表分配空间 - 若临时表大小超过了
temptable_max_ram值 - 若
temptable_use_mmap=on且temptable_max_mmap > 0,则从memory-maped file中为临时表分配空间- 在此分配过程中,若临时表大小小于了
temptable_max_ram值,则可以继续从RAM中分配空间 - 若临时表大小超过了
temptable_max_mmap值,则使用InnoDB临时表从disk上分配空间,并将内存中的临时表迁移到disk上
- 在此分配过程中,若临时表大小小于了
- 若
temptable_use_mmap=off或temptable_max_mmap=0,则使用InnoDB存储引擎从disk上分配空间,并将内存中的临时表迁移到disk上
源码分析
Allocator类及RAM、MMAP空间分配
TempTable 存储引擎分配空间由Allocator类完成,位于storage/temptable/include/temptable/allocator.h。
1. template <class T,
2. class AllocationScheme = Exponential_growth_preferring_RAM_over_MMAP>
3. class Allocator {
4. ...
首先可以看到Allocator类有一个模版参数类AllocationScheme
1. template <typename Block_size_policy, typename Block_source_policy>
2. struct Allocation_scheme {
3. static Source block_source(size_t block_size) {
4. return Block_source_policy::block_source(block_size);
5. }
6. static size_t block_size(size_t number_of_blocks, size_t n_bytes_requested) {
7. return Block_size_policy::block_size(number_of_blocks, n_bytes_requested);
8. }
9. };
它用来控制Allocator的分配机制。其中block_size()方法会指定每次分配的Block大小,block_source()方法会指定存储介质使用策略。
Allocator的默认模版参数为Exponential_growth_preferring_RAM_over_MMAP,其具体为:
1. using Exponential_growth_preferring_RAM_over_MMAP =
2. Allocation_scheme<Exponential_policy, Prefer_RAM_over_MMAP_policy>;
可以看到,默认的block_size的分配策略为Exponential_policy、block_source策略为Prefer_RAM_over_MMAP_policy。
1. struct Exponential_policy {
2. static size_t block_size(size_t number_of_blocks, size_t n_bytes_requested) {
3. size_t block_size_hint;
4. if (number_of_blocks < ALLOCATOR_MAX_BLOCK_MB_EXP) {
5. block_size_hint = (1ULL << number_of_blocks) * 1_MiB;
6. } else {
7. block_size_hint = ALLOCATOR_MAX_BLOCK_BYTES;
8. }
9. return std::max(block_size_hint, Block::size_hint(n_bytes_requested));
10. }
11. };
由上述代码可见,Exponential_policy的block_size()方法每次分配的block大小block_size_hint是指数级增加的,直到到了最大值ALLOCATOR_MAX_BLOCK_BYTES,此后每次分配的block的大小都是ALLOCATOR_MAX_BLOCK_BYTES。若函数调用者请求的block大小大于block_size_hint,则返回该请求大小,即使它可能大于ALLOCATOR_MAX_BLOCK_BYTES。
1. struct Prefer_RAM_over_MMAP_policy {
2. static Source block_source(uint32_t block_size) {
3. if (MemoryMonitor::RAM::consumption() < MemoryMonitor::RAM::threshold()) {
4. if (MemoryMonitor::RAM::increase(block_size) <=
5. MemoryMonitor::RAM::threshold()) {
6. return Source::RAM;
7. } else {
8. MemoryMonitor::RAM::decrease(block_size);
9. }
10. }
11. if (MemoryMonitor::MMAP::consumption() < MemoryMonitor::MMAP::threshold()) {
12. if (MemoryMonitor::MMAP::increase(block_size) <=
13. MemoryMonitor::MMAP::threshold()) {
14. return Source::MMAP_FILE;
15. } else {
16. MemoryMonitor::MMAP::decrease(block_size);
17. }
18. }
19. throw Result::RECORD_FILE_FULL;
20. }
21. };
由上述代码可见,Allocator会首先在RAM分配空间,若RAM消耗超过了RAM::threshold(),即temptable_max_ram,则会开始尝试在mmap files上分配空间。 MMAP::threshold()代码为:
1. static size_t threshold() {
2. if (temptable_use_mmap) {
3. return temptable_max_mmap;
4. } else {
5. return 0;
6. }
7. }
若temptable_use_mmap=false,threashold()函数返回0;当temptable_max_mmap=0时,threshold() 函数实际返回的也是0。这两种情况下不会在mmap file上分配空间,而是直接抛出Result::RECORD_FILE_FULL异常。所以可以看到temptable_max_mmap=0实际上是等价于temptable_use_mmap=false的。
当temptable_use_map=true、temptable_max_mmap>0且mmap file分配空间小于temptable_max_mmap时,Allocator会在mmap file上为临时表分配空间。
如果在此过程中,RAM分配使用空间小于了temptable_max_ram,则还会优先从RAM分配空间。
Allocator类的allocate(size_t)方法会调用以上方法为临时表进行空间的分配:
1. else if (m_state->current_block.is_empty() ||
2. !m_state->current_block.can_accommodate(n_bytes_requested)) {
3. const size_t block_size = AllocationScheme::block_size(
4. m_state->number_of_blocks, n_bytes_requested);
5. m_state->current_block =
6. Block(block_size, AllocationScheme::block_source(block_size));
7. block = &m_state->current_block;
8. ++m_state->number_of_blocks;
9. }
当当前block为空时,需要进行空间分配,会调用Block构造函数为current_block分配空间,Block构造函数中会掉用上述的Exponential_policy::block_size()方法进行空间分配大小计算以及调用Prefer_RAM_over_MMAP_policy::block_source()进行存储介质的选择以及空间分配。
disk空间分配
由上述代码可知,RAM空间不够且mmap file不允许使用或mmap file空间不够时,TempTable 存储引擎会抛出Result::RECORD_FILE_FULL异常,即HA_ERR_RECORD_FILE_FULL异常。
此时MySQL会将临时表迁移到disk上,由于使用的是InnoDB 存储引擎,所以在disk上建临时表的代码自然不会在storage/temptable路径下,而是在更上层的server层。
此功能的代码逻辑为:当server层调用ha_write_row向临时表中写入row时,同时会调用create_ondisk_from_heap()函数。
如sql/sql_union.cc的 Query_result_union::send_data()函数中:
1. const int error = table->file->ha_write_row(table->record[0]);
2. if (!error) {
3. m_rows_in_table++;
4. return false;
5. }
6. // create_ondisk_from_heap will generate error if needed
7. if (!table->file->is_ignorable_error(error)) {
8. bool is_duplicate;
9. if (create_ondisk_from_heap(thd, table, error, true, &is_duplicate))
10. return true; /* purecov: inspected */
11. // Table's engine changed, index is not initialized anymore
12. if (table->hash_field) table->file->ha_index_init(0, false);
13. if (!is_duplicate) m_rows_in_table++;
14. }
create_ondisk_from_heap()函数的作用是当接受到HA_ERR_RECORD_FILE_FULL异常时,即内存中的表已满时,会将该表迁移到disk上。
create_ondisk_from_heap()函数中,当接收到的error不是HA_ERR_RECORD_FILE_FULL时,会直接返回:
1. if (error != HA_ERR_RECORD_FILE_FULL) {
2. /*
3. We don't want this error to be converted to a warning, e.g. in case of
4. INSERT IGNORE ... SELECT.
5. */
6. wtable->file->print_error(error, MYF(ME_FATALERROR));
7. return true;
8. }
它会使用InnoDB 存储引擎创建新的表:
1. share.db_plugin = ha_lock_engine(thd, innodb_hton);
2. // ...
3. new_table.s = &share; // New table points to new share
5. new_table.file =
6. get_new_handler(&share, false, old_share->alloc_for_tmp_file_handler,
7. new_table.s->db_type());
并将临时表迁移到该位于disk的表上:
1. /*
2. copy all old rows from heap table to on-disk table
3. This is the only code that uses record[1] to read/write but this
4. is safe as this is a temporary on-disk table without timestamp/
5. autoincrement or partitioning.
6. */
7. while (!table->file->ha_rnd_next(new_table.record[1])) {
8. write_err = new_table.file->ha_write_row(new_table.record[1]);
9. DBUG_EXECUTE_IF("raise_error", write_err = HA_ERR_FOUND_DUPP_KEY;);
10. if (write_err) goto err_after_open;
11. }
12. /* copy row that filled HEAP table */
13. if ((write_err = new_table.file->ha_write_row(table->record[0]))) {
14. if (!new_table.file->is_ignorable_error(write_err) ||
15. !ignore_last_dup)
16. goto err_after_open;
17. if (is_duplicate) *is_duplicate = true;
18. } else {
19. if (is_duplicate) *is_duplicate = false;
20. }
参考资料
Internal Temporary Table Use in MySQL MySQL · 引擎特性 · 临时表改进 MySQL 8.0: Support for BLOBs in TempTable engine Temporary Tables in MySQL mysql-server 8.0.25 Source Code/
