代码改变世界

为什么大表会导致MySQL变慢

2023-01-28 11:52  abce  阅读(593)  评论(0编辑  收藏  举报

虽然技术已经发展成熟,但仍有一些人认为MySQL仅适用于小型项目,或者它不能很好地处理大型表。

一些初创公司在早期就采用了MySQL,如Facebook、Uber、Pinterest等,这些公司现在都是成功的大公司,证明MySQL可以在大型数据库和大量使用的网站上运行。

随着磁盘速度越来越快,CPU和内存资源越来越便宜,我们可以很容易地说MySQL可以以良好的性能处理tb级的数据。

在这,我们将回顾在MySQL中更有效地管理大型数据集需要考虑的关键主题。

1.主键

这是在MySQL中创建新表时要考虑的最重要的事情之一,我们应该总是显式的创建一个主键(PK)。InnoDB将按主键顺序对数据进行排序,这将用于引用磁盘上的实际数据页。如果我们没有指定主键,MySQL将检查其他唯一索引作为PK的候选索引,如果没有,它将创建一个内部聚集索引作为主键,这不是最优的。

当没有应用程序逻辑或可选择的候选主键时,可以使用auto_increment列作为主键。

注意:在MySQL 8.0.30中,引入了不可见主键来在没有明确定义PK时添加一个不可见主键。

此外,请记住,主键将被添加到每个辅助索引的末尾,因此尽量避免选择字符串作为主键,因为这会使辅助索引更大,并且性能不会达到最佳。

2.冗余索引

众所周知,在大多数情况下,通过获取索引访问行比通过表扫描更有效。但是,在某些情况下,为了提供不同的查询模式,在多个索引上定义了相同的列,有时为同一列创建的某些索引是冗余的,这导致在插入或删除数据时(当索引更新时)增加了更多的开销,并增加了用于存储表索引的磁盘空间。

可以使用工具pt-duplicate-key checker来检测重复的索引。

示例:

> show create table emp\G
*************************** 1. row ***************************
       Table: emp
Create Table: CREATE TABLE `emp` (
  `emp_no` int NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` enum('M','F') NOT NULL,
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`),
  KEY `idx_last_name` (`last_name`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

现在,我们需要通过last_name和hire_date字段进行过滤,可以创建以下索引:

ALTER TABLE emp ADD INDEX idx_last_name_hire_date (last_name,hire_date);

表定义就变成:

> show create table emp\G
*************************** 1. row ***************************
       Table: emp
Create Table: CREATE TABLE `emp` (
  `emp_no` int NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` enum('M','F') NOT NULL,
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`),
  KEY `idx_last_name` (`last_name`),
  KEY `idx_last_name_hire_date` (`last_name`,`hire_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

现在,索引idx_last_name和idx_last_name_hire_date具有相同的前缀(last_name)。

新的索引idx_last_name_hire_date可用于仅通过last_name过滤查询,或者通过last_name和hire_date过滤查询,保留last_name索引变的冗余。

可以使用pt-duplicate-key-checker来证实这一点:

$ pt-duplicate-key-checker -d emp
# ########################################################################
# emp.emp                                                     
# ########################################################################
​
# idx_last_name is a left-prefix of idx_last_name_hire_date
# Key definitions:
#   KEY `idx_last_name` (`last_name`),
#   KEY `idx_last_name_hire_date` (`last_name`,`hire_date`)
# Column types:
#   `last_name` varchar(16) not null
#   `hire_date` date not null
# To remove this duplicate index, execute:
ALTER TABLE `emp`.`emp` DROP INDEX `idx_last_name`;
​
​
# ########################################################################
# Summary of indexes                                                      
# ########################################################################
​
​
# Size Duplicate Indexes   350357634
# Total Duplicate Indexes  1
# Total Indexes            17

3.数据类型

发现数据类型不正确的数据库并不罕见。在很多情况下,可以存储在smallint字段中用int,或者固定大小的char字段存储在可变大小的varchar字段中。对于小表来说,这可能不是一个大问题,但对于有数百万条记录的表来说,过度配置数据类型只会使表的大小和性能更大,而不是最优的。

确保在规划表的未来增长时正确地设计了数据类型。

例子:创建四个简单的表来存储字符串,但使用不同的数据类型

> CREATE TABLE tb1 (id int auto_increment primary key, test_text char(200)); 
Query OK, 0 rows affected (0.11 sec)
​
> CREATE TABLE tb2 (id int auto_increment primary key, test_text varchar(200)); 
Query OK, 0 rows affected (0.05 sec)
​
> CREATE TABLE tb3 (id int auto_increment primary key, test_text tinytext); 
Query OK, 0 rows affected (0.13 sec)
​
> CREATE TABLE tb4 (id int auto_increment primary key, test_text text); 
Query OK, 0 rows affected (0.11 sec)

分别插入2000行记录:

$ for i in {1..2000}; do for tb in {1..4}; do mysql test -e "INSERT INTO tb$tb (test_text) VALUES ('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse euismod, nulla sit amet rhoncus venenatis, massa dolor lobortis nisi, in.');"; done; done

每个表都是2000条记录:

$ mysql test -e "select count(*) from tb1; select count(*) from tb2; select count(*) from tb3; select count(*) from tb4;"
+----------+
| count(*) |
+----------+
|     2000 |
+----------+
+----------+
| count(*) |
+----------+
|     2000 |
+----------+
+----------+
| count(*) |
+----------+
|     2000 |
+----------+
+----------+
| count(*) |
+----------+
|     2000 |
+----------+

来看看每个表的大小:

$ sudo ls -lh /var/lib/mysql/test/|grep tb
-rw-r-----. 1 mysql mysql 592K Dec 30 02:48 tb1.ibd
-rw-r-----. 1 mysql mysql 464K Dec 30 02:48 tb2.ibd
-rw-r-----. 1 mysql mysql 464K Dec 30 02:48 tb3.ibd
-rw-r-----. 1 mysql mysql 464K Dec 30 02:48 tb4.ibd

我们可以看到tb1比其他表更大,因为它将文本存储在一个固定大小的char(200)字段中,该字段将存储定义的200个字符,而不关心实际插入的字符串长度,而varchar、tinytext和text字段是可变大小的字段,只存储字符串的实际长度(在示例中,我们插入了143个字符)。

4.压缩

压缩是通过改变编码来重新构造数据的过程,以便将其存储在更少的字节中。目前有许多数据压缩工具和算法。

MySQL支持使用Zlib库和LZ77压缩算法对InnoDB表进行本地压缩。它节省磁盘空间和内存中的数据,但以压缩和解压缩数据的CPU占用为代价。如果CPU使用不是你的瓶颈,可以利用压缩,因为它可以提高性能,这意味着需要从磁盘读取和写入内存的数据更少,索引也会被压缩。它可以帮助我们节省存储成本和备份时间。

压缩比取决于多个因素,但与任何其他压缩方法一样,它在文本上比在二进制文件上更有效,因此带有文本字段的表将具有更好的压缩比。

创建一个新表emp_compressed:

mysql> CREATE TABLE emp_compressed LIKE emp;
Query OK, 0 rows affected (0.12 sec)
​
mysql> ALTER TABLE emp_compressed ROW_FORMAT=COMPRESSED;
Query OK, 0 rows affected (0.14 sec)
Records: 0  Duplicates: 0  Warnings: 0
​
mysql> INSERT INTO emp_compressed SELECT * FROM emp;

大小比较:

​$ sudo ls -lh /var/lib/mysql/emp/|grep emp
-rw-r-----. 1 mysql mysql 704M Dec 30 02:28 emp.ibd
-rw-r-----. 1 mysql mysql 392M Dec 30 17:19 emp_compressed.ibd

这个例子中,压缩率是45%。

5.归档或清除旧的或不使用的数据

有些公司为了遵守法规或满足业务需求,不得不将数据保留多年。然而,在许多情况下,数据被存储只需要很短的时间;例如,为什么要将应用程序会话信息保存多年?

虽然MySQL可以处理大型数据集,但总是建议在数据库中只保留使用的数据,因为这将使数据访问更有效,也将有助于节省存储和备份成本。

6.分区

分区是一种允许根据分区键将一个大表划分为更小的子表的特性。表分区最常见的用例是按日期划分数据。

例如:如果拥有多年的数据,并且你的查询模式是按年过滤的,那么按年划分表是有益的。在这种情况下,只读取一个较小的分区比读取一个包含多年信息的大表更有效。

在基于查询模式进行分区之前分析分区键是非常重要的,因为如果查询并不总是使用分区键作为过滤条件,那么它们将需要扫描一个或多个分区来获得所需的数据,这将导致巨大的性能损失。

这是一个很酷的特性,但如上所述,它并不适合所有工作负载,需要仔细规划,因为选择一个糟糕的分区键可能会导致巨大的性能损失。

7.分片

分片是水平分割数据的概念,即将数据分布到多个服务器(分片),这意味着给定表的数据的不同部分可能存储在许多不同的服务器上。这有助于将大数据集分割成存储在多个服务器上的小数据集。

数据以类似于分区的方式进行分割,使用一个分片键(sharding key),这是数据如何在分片之间进行分割和分布的模式。这需要在应用程序层处理,并且有一个协调器来读取查询并将查询分发到存储数据的特定分片。

此外,根据对表的查询模式仔细选择适当的分片键是很重要的,以便通过只路由到一个分片来解决大多数查询,因为必须从许多分片中查找信息,然后对其进行过滤、处理和聚合是一项昂贵的操作。

综上所述,并不是所有的应用程序或工作负载都适合分片,并且需要对应用程序进行适当的处理,这可能会增加环境的复杂性。

MongoDB本身支持分片,但是MySQL不支持,但是MySQL世界中有一些实现分片的努力:

·MySQL集群

MySQL NDB集群是Oracle为MySQL开发的内存数据库集群解决方案。它支持对应用程序透明的本机分片。它可以付费订阅。

·ProxySQL

它是一个功能丰富的开源MySQL代理解决方案,允许对最常见的MySQL架构(PXC/Galera, Replication, Group Replication等)进行查询路由。

它允许通过配置一组后端服务器(分片)和一组查询规则进行分片,从而将应用程序查询路由到指定的分片。

注意,它需要在应用程序上进行一些处理,因为它不支持合并和从多个分片检索数据。

你可以在Marco的博客中找到更多信息:MySQL Sharding with ProxySQL

·Vitess

它是由PlanetScale创建的开源数据库集群解决方案,与MySQL引擎兼容。

MyRocks

MyRocks是一个由Facebook开发的开源存储引擎。它是为优化大数据集的数据存储和访问而开发的。MyRocks是在Percona的MySQL中发布的。

8.查询优化

通常会发现应用程序在开始时性能非常好,但随着数据的增长,性能开始下降。最常见的原因是编写得不好的查询或糟糕的模式设计,然而,随着数据的增长,所有这些问题都会暴露出来。可以使用slow_query_log和pt-query-digest来查找有问题的查询。

9.管理

在大型表上执行管理任务可能会很痛苦,特别是模式更改和备份。

对于模式更改,Percona有一个工具pt-online-schema-change,它可以帮助我们在对数据库影响最小的情况下执行模式更改。它通过应用所需的模式更改创建一个新表,并将现有数据批量从原始表复制到新表。使用触发器将正在进行的更改从原始表复制到新表。

通过这种方式,在一个大型表中,pt-OSC可以在后台运行,而不需要对一个alter进行大量的阻塞操作,从而将性能影响最小化。

对于大型数据集的备份,Percona XtraBackup可以帮助减少备份和恢复的时间,它是一种热物理备份解决方案,复制表的数据文件,同时将数据库正在进行的更改保存为重做日志。它支持本地压缩和加密。

结论

关于MySQL不能处理大型数据集的旧说法只是一个谬论。随着硬件越来越强大和便宜,以及技术的发展,现在在MySQL中管理大型表比以往任何时候都更容易。