百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

MySQL分表时机:100w?300w?500w?都对也都不对

wptr33 2025-06-28 17:12 19 浏览

导读

以交友平台用户中心的user表为例,单表数据规模达到千万级别时,你可能会发现使用用户筛选功能查询用户变得非常非常慢,明明查询命中了索引,但是,部分查询还是很慢,这时候,我们就需要考虑拆分这张user表了。

如果此时,我们才去做分表,可能已经太晚了,为什么呢?

我以最典型的应用场景:用户筛选功能,以查询年龄在18到24岁的100位女性用户为例:

在单表的情况下,我们的SQL是这么写的:

SELECT * FROM user WHERE age >= 18 AND age <= 24 AND sex = 0 LIMIT 100
复制代码

但是,拆分user表后,用户记录分散到了多张表,比如,分散到user_1,user_2,user_3这三张表,此时,要查询满足上面条件的用户,我们的查询过程就变成这样:

  1. 遍历user_1到user_3这三张表
  2. 分别从三张表找出满足条件的用户,即执行上面的SQL
  3. 合并这些用户记录
  4. 从合并结果中过滤出前100名用户记录

通过对比,我们会发现分表后的查询过程跟单表相比,变化是比较大的,这势必导致我们不得不修改代码,如果系统内类似的情况很多,那么,可能引发系统较大规模的业务逻辑改动,所以,在系统真正出现数据库性能瓶颈前,必须提前规划分表方案,预留时间去做系统改造。

那么,问题来了,我们到底在单表数据规模达到多少时,做分表是最合适的呢?

在开头我提到分表的原因是因为单表数据规模太大,导致系统功能使用越来越慢,而影响数据库查询性能的因素很多,有并发连接线程数、磁盘IO,锁等等。但是,一条查询语句如果需要通过磁盘IO来获得查询结果,那么,无论是否存在数据库的并发查询请求,磁盘IO的性能瓶颈都会存在。而连接线程和锁导致的的性能问题,一般只有在高并发的场景下才会出现。所以,减少数据查询的磁盘IO,是我们在优化数据库查询性能时,最先需要考虑的。

那么,MySQL又是通过什么方法来减少数据查询的磁盘IO的呢?我们来看下面这张图:


这是很典型的应用请求MySQL的示意图,从图中,我们很容易发现,MySQL为了避免查询时都从磁盘读取查询结果,所以,在磁盘和应用之间加了一层内存,尽可能将磁盘数据加载到内存,那么,下次查询请求访问MySQL时,可以从内存中获取查询结果,避免了过多的磁盘IO的读取。

所以,通过MySQL对磁盘IO的优化方案,我们可以看出,只要把表中大部分数据缓存在内存中,那么,数据库的查询性能可以大大提升。结合user表来看,只要user表的数据规模可以保证大多数的数据可以加载到内存,那么,就不需要对user表拆分,反之,则需要拆分。

既然MySQL内存的大小决定了表何时拆分,那么,我们就先来看一下MySQL的内存结构吧!

内存管理

MySQL的内存结构:

整个MySQL的内存主要分为3部分:

Thread Memory:这部分内存空间是每个连接线程独享的,也就是说每个连接自身独立拥有自己的内存空间。连接释放时,内存就释放。所以,它是动态的。

Sharing:这部分是所有连接线程共享的内存空间。

InnoDB Buffer Pool:这部分就是InnoDB引擎层维护的一块内存空间,它也是共享给每个连接线程的。它是相对静态的内存,不会随连接的释放而释放。

其中,Thread Memory和Sharing属于MySQL Server层的内存空间,InnoDB Buffer Pool属于MySQL InnoDB层的内存空间

下面我再简单介绍一下上面3部分内存空间具体包含哪些部分:

Thread Memory

thread stack(线程栈):主要用来存放每一个线程自身的标识信息,如线程id,线程运行时基本信息等等。

sort_buffer:MySQL使用该内存区域进行记录排序。排序相关的内容,我会在《如何让我的大表排序更快?》详 细讲解。

join_buffer:在链表查询时,MySQL会使用该内存区来协助完成 Join操作。我会在《Join查询的极致优化》详细 讲解。

read_buffer:当查询无法使用索引时,需要全表扫描或全索引扫描来读取记录,那么,这时候,MySQL按照记录 的存储顺序依次读取数据页,每次读取的数据页首先会暂存在read_buffer中,该buffer写满或记录 读取完,就会将结果返回给上层调用。

read_rnd_buffer:和上面的顺序读取相对应,当 MySQL 进行非顺序读取(随机读取)数据页的时候,会利用这 个缓冲区暂存读取的数据。

net_buffer:这部分用来存放客户端连接线程的连接信息。

bulk_insert_buffer:当我们执行批量插入时,会使用该内存空间收集批量插入的记录,当该内存写满时,将该内 存中的记录写入数据文件。

tmp_table:临时表使用的内存空间。

Sharing

Key Buffer:MyISAM 索引缓存使用的内存空间。

Thread Cache:MySQL 为了减少连接线程的创建,将部分空闲的连接线程缓存在该内存区域,给后续连接使用。

InnoDB Log Buffer:这是 InnoDB 存储引擎的事务日志所使用的缓冲区。

Query Cache:缓存查询结果集的内存空间。

Table Cache:用来缓存表文件的文件句柄信息。

BinLog Buffer:用来缓存binlog的信息。

Table Definition Cache:用来缓存表定义信息。

InnoDB Additional Memory Pool:用来缓存InnoDB存储引擎internal 的共享数据结构信息。

InnoDB Buffer Pool

Index Page/Data Page:用来缓存InnoDB索引树的节点,包括非叶子节点的Index Page和叶子节点的Data Page。

Lock:用来缓存InnoDB索引树锁、AHI锁、数据字典锁等锁信息。

Dictionary:用来缓存InnoDB数据字典信息。

AHI:用来缓存InnoDB AHI结构相关信息,其中,AHI在《有没有比B-Tree更快的查询数据的数据结构?》这篇中 有详细讲解过。

Change Buffer:用来存储change buffer信息。其中,change buffer在《为什么不建议使用唯一索引?》这篇中 有讲解过。

LRU List/Free List/Flush List:InnoDB管理和维护索引树节点使用的几个链表,即使用这3个链表维护节点的增删 改查。

通过上面MySQL内存结构的讲解,我们得出2点:

  1. Thread Memory是连接线程独享的内存空间。
  2. Sharing和InnoDB Buffer Pool是连接线程共享的内存空间。

由于sort_buffer属于Thread Memory,为连接线程独享的内存空间,所以,我们就来看下线程独享的内存空间是如何分配和释放的?

Linux内存结构

由于大多数情况,我们会把MySQL安装在Linux系统下,所以,MySQL连接线程独享的内存空间对Linux而言,就是Linux内存空间,所以,这里,我先讲解一下Linux中的内存结构是怎么样的?然后,再看一下它的分配和释放过程。

上图为Linux系统分别在32位和64位情况下的内存结构。

32位

内核空间:从0xC0000000 ~ 0xFFFFFFF为内核空间,大小为1G,只有Linux系统自身可以访问,用户进程不能访问。

用户空间:从0x0 ~ 0xC0000000,大小为3G,Linux系统自身和用户进程都可以访问。

64位

内核空间:从0xFFF8000000000000 ~ 0xFFFFFFFFFFFF为内核空间,大小为128T,只有Linux系统自身可以访问,用户进程不能访问。

用户空间:从0x0 ~ 0x00007FFFFFFFF000,大小也为128T,Linux系统自身和用户进程都可以访问。

未定义:从0x00007FFFFFFFF000 ~ 0xFFF8000000000000,Linux未定义的空间。

用户空间

由于用户空间是我们进程使用的内存区,对MySQL而言,就是MySQL进程可以访问并控制的内存区域,所以,我们再详细看一下用户空间的内存结构:


上图为Linux用户空间(用户态)的内存结构,叫做虚拟内存,它包括以下几部分:

栈:包括局部变量和函数调用的上下文、调用返回地址等。

文件映射:包括动态库、共享内存等,从高地址开始向下增长。

堆:包括动态分配的内存,从低地址开始向上增长。

数据段:包括全局变量等。

只读段:包括代码和常量等。

内存分配

理解了用户空间内存的概念,我们再结合用户空间的概念,来看一下MySQL进程是如何分配和释放用户空间内存的?

MySQL使用C标准库的malloc()在堆动态分配内存,使用mmap()在文件映射段动态分配内存。详细过程如下图:


上图为MySQL分配内存的过程,主要分Server层和InnoDB层两部分的内存分配。

通过上图,我们发现MySQL在Server层是通过malloc来分配内存的,而InnoDB层是通过mmap来分配内存的。

图中,我们从上往下看:

  1. MySQL Server层调用C语言的malloc函数申请分配内存
  2. malloc调用内存分配器从用户态向Linux内核申请内存,为什么有个内存分配器,这是什么?我们先来看一张图:

这张图是malloc函数直接调用系统函数申请内存的过程,我们发现malloc通过brk和mmap这两个Linux系统函数从用户态向内核申请内存。这两个系统函数是干什么的呢?

brk

当申请内存大小小于MMAP_THRESHOLD这个内核参数配置的大小**(默认128K)**时,Linux系统使用brk来分配内存,上图展示了brk分配内存的过程,从上到下,假设内存总大小为50 + 20 + 20 = 90M:

  1. 进程1申请分配了50M堆内存
  2. 进程1执行结束,释放50M堆内存,如上图,50M内存区域变为虚线
  3. 进程2申请分配了20M堆内存,如上图,在50M堆内存右边又分配了20M
  4. 进程2执行结束,释放20M堆内存,如上图,中间20M内存区域变虚线
  5. 进程3申请分配了20M堆内存,如上图,在中间20M堆内存右边又分配了20M

通过brk分配内存的过程,我们发现,这些分配的堆内存释放后并不会立刻归还系统。所以,内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。

mmap

当申请内存大小大于MMAP_THRESHOLD这个内核参数配置的大小**(默认128K)**时,Linux使用mmap分配内存,上图展示了mmap分配内存的过程,从上到下,假设内存总大小为50 + 20 + 20 = 90M:

  1. 进程1申请分配了50M文件映射段的内存
  2. 进程1执行结束,释放50M文件映射段的内存,如上图,50M内存区域变虚线
  3. 进程2申请分配了20M文件映射段的内存,如上图,在原来50M内存区域内又分配了20M
  4. 进程2执行结束,释放20M文件映射段的内存,如上图,最左边20M内存区域变虚线
  5. 进程3申请分配了40M堆内存,如上图,在原来50M内存区域内又分配了40M内存,剩下10M仍是释放状态,为虚线

通过mmap分配内存的过程,我们发现mmap方式释放内存后会将内存及时归还给系统,避免 OOM。但是频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。欧!!**这里冒出一个新名词,缺页异常?**别着急,我会在后面讲解。

通过malloc调用系统函数申请内存分配的过程,我们发现调用brk函数分配的内存在释放时不会归还给Linux系统,所以,导致了内存碎片,而过多的内存碎片会造成内存利用率下降。所以,Linux引入了一个内存分配器,用来管理和维护这些内存碎片,将碎片内存连接起来,提升内存的利用率。

内存分配器

内存分配器处在用户进程和内核态的内存之间,其采用内存池来管理和维护内存空间,它响应用户的分配请求,向Linux内核申请内存,然后将其返回给用户程序。

目前主流的内存分配器主要有三种:ptmalloc、tcmalloc和jemalloc。

关于内存分配器以及管理内存的策略和算法,将来在《Linux内核深度解读》新专题中,我会详细讲解。

缺页异常

在《mmap》这部分中,我提到了缺页异常,那么,什么是缺页异常呢?

在讲解缺页异常之前,我们先看一下《内存分配》这部分中的第一张图底部的虚拟内存管理器,通过讲解它的工作原理,我们慢慢理解什么是缺页异常。

其实,虚拟内存管理器里面包含了许多组件,通过这些组件,虚拟内存管理器管理和维护进程用户态申请的内存和物理内存的关系。ps:用户态申请的内存,我们一般叫它虚拟内存。

整个虚拟内存管理器包含的组件有:

MMU:全称内存管理单元,它的作用是接收一个虚拟内存地址,将其转换为一个物理内存地址,然后,输出这个物理地址

Page Table:页表,Linux内核通过页表来维护虚拟内存地址和物理内存地址的映射关系,表中的每一条映射关系又叫做Page Table Entry,即页表项,缩写PTE,页表项地址缩写PTEA

讲完这些名词,我们再来看一下这张图:


上图为一个虚拟内存管理器的工作原理,其中,VA全称虚拟地址,即虚拟内存的地址:

  1. 处理器生成一个虚拟地址,并把它传送给 MMU
  2. MMU 根据虚拟地址生成 VPN(虚拟页号,因为CPU与内存交互以页为单位),然后请求内存,获取 PTE 的数据。
  3. 内存向 MMU 返回 PTE 的数据
  4. 由于判断出 PTE 的有效位是 0,即内存中没有虚拟页号对应的物理页,所以 CPU 将触发一次异常中断,将控制权转移给内核中的缺页异常处理程序。
  5. 缺页异常处理程序确定出物理内存中的牺牲页,如果这个页面被修改过了(D 标志位为 1),那么将牺牲页换出到磁盘。
  6. 缺页处理程序从磁盘中调入新的页面到内存中,并且更新 PTE
  7. 缺页处理程序将控制权返回给原来的进程,再次执行导致缺页的指令。再次执行后,就会产生页命中时的情况了。

所以,mmap过程中讲到的缺页异常就是上面过程中所提到的缺页异常。

最后,通过上面Linux内存分配的各部分细节的讲解,我们再来回顾一下《内存分配》部分开头那张图:


server层

Server层内存分配的过程,见上图:

  1. MySQL调用系统函数malloc去申请内存
  2. malloc调用内存分配器向Linux内核申请内存,减少brk分配内存后产生的碎片
  3. 内存分配器调用系统函数brk或mmap向Linux内核申请内存 (1) 当申请内存大小小于MMAP_THRESHOLD这个内核参数配置的大小**(默认128K)**时,使用brk分配内存 (2) 当申请内存大小大于MMAP_THRESHOLD这个内核参数配置的大小**(默认128K)**时,使用mmap分配内存 当出现缺页时,Linux内核使用虚拟内存管理器的几个组件处理缺页异常

InnoDB层

如上图,InnoDB层采用Free、LRU和Flush List三个链表来管理InnoDB引擎相关的内存,也就是管理InnoDB Buffer Pool。

见上图,其中,MySQL在给InnoDB Buffer Pool申请内存时,直接调用系统函数mmap来完成内存的申请,这是由于InnoDB Buffer Pool缓存中的数据包含索引树、Change Buffer等等,这些都是大结构的数据,所以,MySQL不希望这些数据长时间占用内存,导致潜在的系统内存溢出的风险。

小结

通过MySQL底层内存分配和释放的详细分析,我们知道了MySQL在不同的内存结构中,使用了完全不同的内存分配和释放策略:

  1. Server层,即Thread Memory和Sharing:使用malloc申请并分配内存
  2. InnoDB层:即InnoDB Buffer Pool:使用mmap申请并分配内存,并使用Free、LRU和Flush List三个链表来维护内存

回到标题的问题:单表数据规模达到多大时进行分表最佳?

我们表数据(包含索引和记录)属于相对静态的数据,不随连接线程的释放而发生变化,结合MySQL的内存结构及分配和释放的过程,我们发现跟这些数据直接相关的内存区域就是InnoDB Buffer Pool,所以,我们只要看这个pool大小,来决定单表数据规模达到多大进行分表。即如果单表数据规模大小超过

InnoDB Buffer Pool的大小,就需要进行分表了。

InnoDB Buffer Pool的大小可以通过innodb_buffer_pool_size参数得到。


作者:谦虚的小叮当
链接:
https://juejin.cn/post/6957258784385269768

来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...