我正在尝试优化将数据插入 MySQL 的代码的一部分。我应该链接 INSERT 以制作一个巨大的多行 INSERT 还是多个单独的 INSERT 更快?
13 回答
https://dev.mysql.com/doc/refman/8.0/en/insert-optimization.html
插入一行所需的时间由以下因素决定,其中数字表示近似比例:
- 连接:(3)
- 向服务器发送查询:(2)
- 解析查询:(2)
- 插入行:(1×行大小)
- 插入索引:(1 × 索引数)
- 结束: (1)
由此可见,发送一个大语句将为您节省每个插入语句 7 的开销,在进一步阅读文本时还说:
如果您同时从同一个客户端插入多行,请使用带有多个 VALUES 列表的 INSERT 语句一次插入多行。这比使用单独的单行 INSERT 语句要快得多(在某些情况下快很多倍)。
我知道我在被问到差不多两年半后才回答这个问题,但我只是想提供一些我现在正在从事的项目的硬数据,这表明每次插入确实做多个 VALUE 块是很多的比顺序的单个 VALUE 块 INSERT 语句快。
我在 C# 中为此基准编写的代码使用 ODBC 从 MSSQL 数据源(约 19,000 行,所有行在任何写入开始之前读取)和 MySql .NET 连接器 (Mysql.Data.*) 的东西读入内存通过准备好的语句将内存中的数据插入到 MySQL 服务器上的表中。它的编写方式允许我动态调整每个准备好的 INSERT 的 VALUE 块的数量(即,一次插入 n 行,我可以在运行前调整 n 的值。)我还运行了测试每个 n 多次。
执行单个 VALUE 块(例如,一次 1 行)需要 5.7 - 5.9 秒来运行。其他值如下:
一次 2 行:3.5 - 3.5 秒
一次 5 行:2.2 - 2.2 秒
一次 10 行:1.7 - 1.7 秒
一次 50 行:1.17 - 1.18 秒
一次 100 行:1.1 - 1.4 秒
一次 500 行:1.1 - 1.2 秒
一次 1000 行:1.17 - 1.17 秒
所以是的,即使只是将 2 或 3 个写入捆绑在一起,也可以显着提高速度(运行时间减少 n 倍),直到达到 n = 5 和 n = 10 之间的某个位置,此时改进会显着下降,在 n = 10 到 n = 50 范围内的某个地方,改进变得可以忽略不计。
希望能帮助人们决定 (a) 是否使用 multiprepare 的想法,以及 (b) 每个语句要创建多少个 VALUE 块(假设您要处理的数据可能足够大以将查询推到超过最大查询大小对于 MySQL,我相信很多地方默认为 16MB,可能更大或更小,具体取决于服务器上设置的 max_allowed_packet 的值。)
一个主要因素是您是否使用事务引擎以及是否启用了自动提交。
默认情况下,自动提交是打开的,您可能希望将其保持打开;因此,您所做的每个插入都会执行自己的事务。这意味着如果您每行执行一次插入,您将为每一行提交一个事务。
假设一个线程,这意味着服务器需要为每一行同步一些数据到磁盘。它需要等待数据到达永久存储位置(希望是 RAID 控制器中的电池支持 ram)。这本质上是相当缓慢的,并且可能会成为这些情况下的限制因素。
我当然假设您使用的是事务引擎(通常是 innodb)并且您没有调整设置以降低耐用性。
我还假设您使用单个线程来执行这些插入。使用多线程会使事情变得有点混乱,因为某些版本的 MySQL 在 innodb 中有工作组提交 - 这意味着执行自己提交的多个线程可以共享对事务日志的一次写入,这很好,因为这意味着与持久存储的同步更少.
另一方面,结果是,您真的想使用多行插入。
有一个限制,它会适得其反,但在大多数情况下,它至少是 10,000 行。因此,如果您将它们批处理到 1,000 行,您可能是安全的。
如果你使用的是 MyISAM,还有很多其他的东西,但我不会让你厌烦这些。和平。
一次通过电线发送尽可能多的插入。实际的插入速度应该是相同的,但您会看到网络开销的减少带来的性能提升。
通常,对数据库的调用次数越少越好(意味着更快、更高效),因此请尝试以最小化数据库访问的方式编写插入代码。请记住,除非您使用连接池,否则每个数据库访问都必须创建一个连接,执行 sql,然后断开连接。相当的开销!
这是我做的一个小 PHP 工作台的结果:
我正在尝试使用 PHP 8.0、MySQL 8.1 (mysqli) 以 3 种不同的方式插入 3000 条记录
多个插入查询,具有多个事务:
$start = microtime(true);
for($i = 0; $i < 3000; $i++)
{
mysqli_query($res, "insert into app__debuglog VALUE (null,now(), 'msg : $i','callstack','user','debug_speed','vars')");
}
$end = microtime(true);
echo "Took " . ($end - $start) . " s\n";
做了 5 次,平均:11.132s (+/- 0.6s)
多个插入查询,单个事务:
$start = microtime(true);
mysqli_begin_transaction($res, MYSQLI_TRANS_START_READ_WRITE);
for($i = 0; $i < 3000; $i++)
{
mysqli_query($res, "insert into app__debuglog VALUE (null,now(), 'msg : $i','callstack','user','debug_speed','vars')");
}
mysqli_commit($res);
$end = microtime(true);
echo "Took " . ($end - $start) . " ms\n";
5 次测试的结果:0.48 秒(+/- 0.04 秒)
单个聚合插入查询
$start = microtime(true);
$values = "";
for($i = 0; $i < 3000; $i++)
{
$values .= "(null,now(), 'msg : $i','callstack','user','debug_speed','vars')";
if($i !== 2999)
$values .= ",";
}
mysqli_query($res, "insert into app__debuglog VALUES $values");
$end = microtime(true);
echo "Took " . ($end - $start) . " ms\n";
5 次测试的结果:0.085s (+/- 0.05s)
因此,对于 3000 行插入,看起来像:
- 在单个写入事务中使用多个查询比为每个插入使用多个事务进行多个查询快约 22 倍。
- 使用单个聚合插入语句仍然比使用单个写入事务的多个查询快约 6 倍
你可能想要 :
- 检查自动提交是否关闭
- 打开连接
- 在单个事务中发送多批插入(大约 4000-10000 行的大小?你看)
- 关闭连接
根据您的服务器的扩展程度(使用PostgreSQl
,Oracle
和绝对可以MSSQL
),使用多线程和多连接执行上述操作。
通常,由于连接开销,多次插入会更慢。一次执行多个插入将降低每次插入的开销成本。
根据您使用的语言,您可以在进入数据库之前用您的编程/脚本语言创建一个批处理并将每个插入添加到批处理中。然后,您将能够使用一个连接操作执行大批量。这是Java中的示例。
MYSQL 5.5 一个 sql 插入语句花费了 ~300 到 ~450 毫秒。而以下统计信息用于内联多个插入语句。
(25492 row(s) affected)
Execution Time : 00:00:03:343
Transfer Time : 00:00:00:000
Total Time : 00:00:03:343
我会说内联是要走的路:)
我只是做了一个小基准测试,似乎对于很多线路来说它并没有更快。这是我插入 280 000 行的结果:
- 10 000 : 164.96 秒
- 5 000 : 37 秒
- 1000 : 12.56 秒
- 600 : 12.59 秒
- 500 : 13.81 秒
- 250 : 17.96 秒
- 400 : 14.75 秒
- 100 : 27 秒
看来 1000 x 1000 是最好的选择。
在插入方面,Mysql 和 MariaDB 的优化是多么糟糕,这太荒谬了。我测试了 mysql 5.7 和 mariadb 10.3,它们没有真正的区别。
我已经在具有 NVME 磁盘、70,000 IOPS、1.1 GB/秒 seq 吞吐量的服务器上对此进行了测试,这可能是全双工(读取和写入)。
该服务器也是高性能服务器。
给它 20 GB 的内存。
数据库完全为空。
在进行多行插入时,我收到的速度是每秒 5000 次插入(尝试使用 1MB 到 10MB 的数据块)
现在的线索:
如果我添加另一个线程并插入到相同的表中,我突然有 2x5000 /sec。再多一个线程,我总共有 15000 个/秒
考虑一下:当执行 ONE 线程插入时,这意味着您可以按顺序写入磁盘(索引除外)。使用线程时,您实际上会降低可能的性能,因为它现在需要执行更多的随机访问。但现实检查表明 mysql 的优化非常糟糕,线程帮助很大。
这种服务器的实际性能可能是每秒数百万,CPU 空闲,磁盘空闲。
原因很明显,mariadb 和 mysql 一样有内部延迟。
我会添加信息,即一次太多行取决于它们的内容可能导致得到一个大于 'max_allowed_packet' 的数据包。
也许考虑使用PHP 的 array_chunk 之类的函数为您的大数据集进行多次插入。
多个插入速度更快,但它有 thredshould。另一个问题是禁用约束检查临时使插入更快。不管你的桌子有没有。例如测试禁用外键并享受速度:
SET FOREIGN_KEY_CHECKS=0;
当然,您应该在插入后重新打开它:
SET FOREIGN_KEY_CHECKS=1;
这是插入大量数据的常用方法。数据完整性可能会破坏,因此您应该在禁用外键检查之前注意这一点。