如何尽可能快地更新 MySQL 单表中 1000 万+ 行?

47 mysql myisam performance update mysql-5.6

大多数表使用 MySQL 5.6 和 InnoDB 存储引擎。InnoDB 缓冲池大小为 15 GB,Innodb DB + 索引大约为 10 GB。服务器有 32GB RAM 并运行 Cent OS 7 x64。

我有一张包含大约 1000 万条记录的大表。

我每 24 小时从远程服务器获取更新的转储文件。该文件为 csv 格式。我无法控制那种格式。该文件约为 750 MB。我尝试将数据逐行插入 MyISAM 表,花了 35 分钟。

我只需要从文件中的每行 10-12 中取出 3 个值并在数据库中更新它。

实现这样的目标的最佳方法是什么?

我需要每天都这样做。

目前 Flow 是这样的:

  1. mysqli_begin_transaction
  2. 逐行读取转储文件
  3. 逐行更新每条记录。
  4. mysqli_commit

以上操作大约需要30-40 分钟才能完成,在执行此操作时,还有其他更新正在进行中,这给了我

超过锁等待超时;尝试重新启动事务

更新 1

使用LOAD DATA LOCAL INFILE. 在 MyISAM 中,38.93 sec在 InnoDB 中需要 7 分 5.21 秒。然后我做了:

UPDATE table1 t1, table2 t2
SET 
t1.field1 = t2.field1,
t1.field2 = t2.field2,
t1.field3 = t2.field3
WHERE t1.field10 = t2.field10

Query OK, 434914 rows affected (22 hours 14 min 47.55 sec)
Run Code Online (Sandbox Code Playgroud)

更新 2

与连接查询相同的更新

UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4

(14 hours 56 min 46.85 sec)
Run Code Online (Sandbox Code Playgroud)

评论中问题的澄清:

  • 表中大约 6% 的行将被文件更新,但有时可能高达 25%。
  • 正在更新的字段上有索引。表上有 12 个索引,其中 8 个索引包含更新字段。
  • 没有必要在一次事务中进行更新。这可能需要时间,但不会超过 24 小时。我希望在 1 小时内完成它而不锁定整个表,因为稍后我必须更新依赖于该表的 sphinx 索引。只要数据库可用于其他任务,这些步骤是否需要更长的持续时间并不重要。
  • 我可以在预处理步骤中修改 csv 格式。唯一重要的是快速更新且无需锁定。
  • 表 2 是 MyISAM。它是使用加载数据 infile 从 csv 文件新创建的表。MYI 文件大小为 452 MB。表 2 在 field1 列上建立索引。
  • MyISAM 表的 MYD 为 663MB。

更新 3:

这里有关于这两个表的更多细节。

CREATE TABLE `content` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `og_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `keywords` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `files_count` smallint(5) unsigned NOT NULL DEFAULT '0',
  `more_files` smallint(5) unsigned NOT NULL DEFAULT '0',
  `files` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '0',
  `category` smallint(3) unsigned NOT NULL DEFAULT '600',
  `size` bigint(19) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) NOT NULL DEFAULT '0',
  `completed` int(11) NOT NULL DEFAULT '0',
  `uploaders` int(11) NOT NULL DEFAULT '0',
  `creation_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `upload_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `last_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `vote_up` int(11) unsigned NOT NULL DEFAULT '0',
  `vote_down` int(11) unsigned NOT NULL DEFAULT '0',
  `comments_count` int(11) NOT NULL DEFAULT '0',
  `imdb` int(8) unsigned NOT NULL DEFAULT '0',
  `video_sample` tinyint(1) NOT NULL DEFAULT '0',
  `video_quality` tinyint(2) NOT NULL DEFAULT '0',
  `audio_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `subtitle_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `uploader` int(11) unsigned NOT NULL DEFAULT '0',
  `anonymous` tinyint(1) NOT NULL DEFAULT '0',
  `enabled` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `tfile_size` int(11) unsigned NOT NULL DEFAULT '0',
  `scrape_source` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `record_num` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`record_num`),
  UNIQUE KEY `hash` (`hash`),
  KEY `uploaders` (`uploaders`),
  KEY `tfile_size` (`tfile_size`),
  KEY `enabled_category_upload_date_verified_` (`enabled`,`category`,`upload_date`,`verified`),
  KEY `enabled_upload_date_verified_` (`enabled`,`upload_date`,`verified`),
  KEY `enabled_category_verified_` (`enabled`,`category`,`verified`),
  KEY `enabled_verified_` (`enabled`,`verified`),
  KEY `enabled_uploader_` (`enabled`,`uploader`),
  KEY `anonymous_uploader_` (`anonymous`,`uploader`),
  KEY `enabled_uploaders_upload_date_` (`enabled`,`uploaders`,`upload_date`),
  KEY `enabled_verified_category` (`enabled`,`verified`,`category`),
  KEY `verified_enabled_category` (`verified`,`enabled`,`category`)
) ENGINE=InnoDB AUTO_INCREMENT=7551163 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=FIXED


CREATE TABLE `content_csv_dump_temp` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `category_id` int(11) unsigned NOT NULL DEFAULT '0',
  `uploaders` int(11) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) unsigned NOT NULL DEFAULT '0',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Run Code Online (Sandbox Code Playgroud)

这是更新查询,它content使用来自的数据更新表content_csv_dump_temp

UPDATE content a JOIN content_csv_dump_temp b 
ON a.hash = b.hash 
SET 
a.uploaders = b.uploaders,
a.downloaders = b.downloaders,
a.verified = b.verified
Run Code Online (Sandbox Code Playgroud)

更新 4:

以上所有测试都是在测试机上完成的,但现在我在生产机上做了同样的测试,查询速度非常快。

mysql> UPDATE content_test a JOIN content_csv_dump_temp b
    -> ON a.hash = b.hash
    -> SET
    -> a.uploaders = b.uploaders,
    -> a.downloaders = b.downloaders,
    -> a.verified = b.verified;
Query OK, 2673528 rows affected (7 min 50.42 sec)
Rows matched: 7044818  Changed: 2673528  Warnings: 0
Run Code Online (Sandbox Code Playgroud)

我为我的错误道歉。最好使用 join 而不是每个记录更新。现在我正在尝试使用 rick_james 建议的索引来改进 mpre,一旦完成基准测试就会更新。

Cra*_*ein 16

根据我的经验,我会使用LOAD DATA INFILE来导入您的 CSV 文件。

LOAD DATA INFILE 语句以非常高的速度将文本文件中的行读取到表中。

我在互联网负载数据示例上找到的示例。我在我的盒子上测试了这个例子并且工作正常

示例表

CREATE TABLE example (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Column2` varchar(14) NOT NULL,
  `Column3` varchar(14) NOT NULL,
  `Column4` varchar(14) NOT NULL,
  `Column5` DATE NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB
Run Code Online (Sandbox Code Playgroud)

示例 CSV 文件

# more /tmp/example.csv
Column1,Column2,Column3,Column4,Column5
1,A,Foo,sdsdsd,4/13/2013
2,B,Bar,sdsa,4/12/2013
3,C,Foo,wewqe,3/12/2013
4,D,Bar,asdsad,2/1/2013
5,E,FOObar,wewqe,5/1/2013
Run Code Online (Sandbox Code Playgroud)

从 MySQL 控制台运行的导入语句

LOAD DATA LOCAL INFILE '/tmp/example.csv'
    -> INTO TABLE example
    -> FIELDS TERMINATED BY ','
    -> LINES TERMINATED BY '\n'
    -> IGNORE 1 LINES
    -> (id, Column3,Column4, @Column5)
    -> set
    -> Column5 = str_to_date(@Column5, '%m/%d/%Y');
Run Code Online (Sandbox Code Playgroud)

结果

MySQL [testcsv]> select * from example;
+----+---------+---------+---------+------------+
| Id | Column2 | Column3 | Column4 | Column5    |
+----+---------+---------+---------+------------+
|  1 |         | Column2 | Column3 | 0000-00-00 |
|  2 |         | B       | Bar     | 0000-00-00 |
|  3 |         | C       | Foo     | 0000-00-00 |
|  4 |         | D       | Bar     | 0000-00-00 |
|  5 |         | E       | FOObar  | 0000-00-00 |
+----+---------+---------+---------+------------+
Run Code Online (Sandbox Code Playgroud)

IGNORE 只是简单地忽略作为列标题的第一行。

在 IGNORE 之后,我们指定要导入的列(跳过 column2),它与您的问题中的条件之一匹配。

这是直接来自 Oracle 的另一个示例: LOAD DATA INFILE 示例

这应该足以让您入门。


Rol*_*DBA 16

鉴于所有提到的事情,看起来瓶颈是连接本身。

方面 #1:加入缓冲区大小

很有可能,您的join_buffer_size可能太低了。

根据关于 MySQL如何使用连接缓冲区缓存的 MySQL 文档

我们只将使用过的列存储在连接缓冲区中,而不是整行。

在这种情况下,让连接缓冲区的键留在 RAM 中。

每个键有 1000 万行乘以 4 个字节。那大约是40M。

尝试在会话中将其提高到 42M(略大于 40M)

SET join_buffer_size = 1024 * 1024 * 42;
UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4;
Run Code Online (Sandbox Code Playgroud)

如果这样做成功,请继续将其添加到 my.cnf

[mysqld]
join_buffer_size = 42M
Run Code Online (Sandbox Code Playgroud)

新连接不需要重新启动 mysqld。赶紧跑

mysql> SET GLOBAL join_buffer_size = 1024 * 1024 * 42;
Run Code Online (Sandbox Code Playgroud)

方面#2:加入操作

您可以通过调整优化器来操纵连接操作的样式

根据关于Block Nested-Loop 和 Batched Key Access Joins 的MySQL 文档

使用 BKA 时,join_buffer_size 的值定义了每次向存储引擎请求的密钥批次的大小。缓冲区越大,对连接操作的右手表的顺序访问就越多,这可以显着提高性能。

要使用 BKA,必须将 optimizer_switch 系统变量的 batched_key_access 标志设置为 on。BKA 使用 MRR,因此也必须打开 mrr 标志。目前,对 MRR 的成本估算过于悲观。因此,还需要关闭 mrr_cost_based 才能使用 BKA。

同一页面建议这样做:

mysql> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
Run Code Online (Sandbox Code Playgroud)

方面 #3:将更新写入磁盘(可选)

大多数人忘记增加innodb_write_io_threads更快地将脏页从缓冲池中写入。

[mysqld]
innodb_write_io_threads = 16
Run Code Online (Sandbox Code Playgroud)

您必须重新启动 MySQL 才能进行此更改

试一试 !!!