如何尽可能快地更新MySQL单个表中的1000万行以上?


32

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

我有一张大表,其中包含大约一千万条记录。

我每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了7分钟5.21秒,而在InnoDB中,花了7分钟。然后我做了:

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)

更新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)

评论中问题的澄清:

  • 该文件将更新表中约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

这是更新查询,它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

更新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

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


您有复合材料 INDEX(field2, field3, field4)(任何顺序)吗?请给我们看看SHOW CREATE TABLE
里克·詹姆斯

1
12和8索引是您问题的重要组成部分。MyISAM是另一个重要部分。InnoDB或TokuDB在使用多个索引时表现更好。
里克·詹姆斯

你有两个不同的地方 UPDATEs。请确切告诉我们从csv数据更新表的简单语句什么样的。 然后,我们可能可以帮助您设计出满足您要求的技术。
里克·詹姆斯

@RickJames只有一个update,请检查更新的问题。,谢谢
AMB

Answers:


17

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

LOAD DATA INFILE语句以很高的速度将行从文本文件读入表中。

我在Internet上找到的示例Load Data示例。我在盒子上测试了这个例子,并正常工作

示例表

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

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

从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');

结果

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 |
+----+---------+---------+---------+------------+

IGNORE只是简单地忽略第一行即列标题。

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

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

这应该足以让您入门。


我可以使用加载数据在临时表中加载数据,然后使用其他查询在主表中更新数据。,谢谢
AMB 2015年

14

根据提到的所有内容,看起来瓶颈是联接本身。

方面#1:连接缓冲区大小

很可能您的join_buffer_size太低了。

根据有关MySQL 如何使用联接缓冲区高速缓存的MySQL文档

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

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

您有1000万行乘以每个键4个字节。大约是4000万。

尝试在会话中将其增加到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;

如果这可以解决问题,请继续将其添加到 my.cnf

[mysqld]
join_buffer_size = 42M

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

mysql> SET GLOBAL join_buffer_size = 1024 * 1024 * 42;

方面2:加入操作

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

根据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';

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

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

[mysqld]
innodb_write_io_threads = 16

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

试试看 !!!


真好!+1为可调连接缓冲区提示。如果您要加入,请加入内存。好提示!
彼得·迪克森·摩西

3
  1. CREATE TABLE 与CSV匹配
  2. LOAD DATA 到那张桌子
  3. UPDATE real_table JOIN csv_table ON ... SET ..., ..., ...;
  4. DROP TABLE csv_table;

第3步将比逐行快很多,但是它仍将锁定表中的所有行一段很短的时间。如果此锁定时间比整个过程花费的时间更重要,则...

如果没有其他东西在写表,那么...

  1. CREATE TABLE与CSV匹配;没有索引除外所需要的JOINUPDATE。如果唯一,请使其PRIMARY KEY
  2. LOAD DATA 到那张桌子
  3. 复制real_tablenew_tableCREATE ... SELECT
  4. UPDATE new_table JOIN csv_table ON ... SET ..., ..., ...;
  5. RENAME TABLE real_table TO old, new_table TO real_table;
  6. DROP TABLE csv_table, old;

第3步比更新快,尤其是如果遗漏了不必要的索引时。
步骤5是“即时”的。


以秒为例,在步骤3之后,我们正在执行步骤4,然后将新数据插入到real_table中,因此我们将在new_table中丢失该数据?解决方法是什么?谢谢
AMB 2015年

看什么pt-online-schema-digest; 它通过来处理此类问题TRIGGER
里克·詹姆斯

你可能不会需要从表中的任何索引LOAD DATA。添加不必要的索引的成本很高(及时)。
里克·詹姆斯

根据最新信息,我倾向于只将CSV文件加载到MyISAM表中AUTO_INCREMENT,然后根据PK一次将1K行分块。但是我需要先查看所有要求表架构,然后再尝试阐明细节。
里克·詹姆斯

我已将hash设置为PRIMARY index,但是使用顺序查询将50k分块会花费更多时间。如果我创建自动增量会更好吗?并将其设置为PRIMARY index
AMB 2015年

3

您已经说过:

  • 更新影响表的6-25%
  • 您想尽快执行此操作(<1小时)
  • 没有锁
  • 不必单笔交易
  • 但是(在评论里克·詹姆斯的回答中),您对比赛条件表示担忧

这些陈述中有许多是矛盾的。例如,没有锁定表的大型更新。或通过一项巨额交易避免竞争条件。

另外,由于您的表的索引很高,因此插入和更新都可能很慢。


避免比赛条件

如果能够在表中添加更新的时间戳,则可以解决竞争条件,同时还避免在单个事务中记录50万次更新。

这使您可以自由执行逐行更新(如您当前所做的那样),但是可以使用自动提交或更合理的事务批处理。

通过检查尚未进行的后续更新(UPDATE ... WHERE pk = [pk] AND updated < [batchfile date])来避免竞争条件(逐行更新时)

而且,重要的是,这使您可以运行并行更新。


尽可能快地运行-并行化

有了这个时间戳,现在检查:

  1. 将批处理文件拆分为一些合理大小的块(例如,每个文件50,000行)
  2. 并行地,让脚本读取每个文件并输出带有50,000条UPDATE语句的文件。
  3. 并行执行一次(2),然后mysql运行每个sql文件。

(例如,在bashsplitxargs -P各种方法来轻松地运行并行命令多方面的。并行度取决于有多少线程你愿意全身心地投入到更新


请记住,“行由行”很可能是10倍慢于在至少100批次做事
里克·詹姆斯

为了确保这种情况,您必须对其进行基准测试。更新表的6%到25%(更新的列涉及8个索引),我可能会感到索引维护成为瓶颈。
彼得·迪克森·摩西

我的意思是,在某些情况下,删除索引,批量更新并在之后重新创建它们可能会更快...但是OP不想停机。
彼得·迪克森-摩西

1

大型更新受I / O约束。我会建议:

  1. 创建一个不同的表,该表将存储您的3个经常更新的字段。我们将其中一个表称为资产静态表,保存静态数据,将另一个表动态资产存储上载器,下载器和经过验证的表。
  2. 如果可以,请将MEMORY引擎用于assets_dynamic表。(每次更新后备份到磁盘)。
  3. 按照您的更新4 更新轻巧灵活的asset_dynamic(即LOAD INFILE ... INTO temp;在a.id = b.id SET上更新asset_dynamic a JOIN temp b [必须更新的内容]。这应该少于a分钟(在我们的系统上,assets_dynamic为95M,更新影响约600万行,仅需40多)。
  4. 当您运行Sphinx的索引器时,请联接资产_静态资产_动态(假设您要使用这些字段之一作为属性)。

0

为了UPDATE快速运行,您需要

INDEX(uploaders, downloaders, verified)

它可以在任何一张桌子上。这三个字段可以按任何顺序排列。

这将有助于UPDATE能够快速匹配两个表之间的行。

并且使数据类型的两个表(无论是在相同INT SIGNED或两者INT UNSIGNED)。


这实际上减慢了更新速度。
AMB

嗯...请提供EXPLAIN UPDATE ...;
瑞克·詹姆斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.