web-dev-qa-db-ja.com

MySQL単一テーブルで1000万行以上を可能な限り高速に更新する方法は?

ほとんどのテーブルでMySQL 5.6とInnoDBストレージエンジンを使用します。 InnoDBバッファープールのサイズは15 GBで、Innodb DB +インデックスは約10 GBです。サーバーには32GB RAMがあり、Cent OS 7 x64を実行しています。

約1000万件以上のレコードを含む大きなテーブルが1つあります。

24時間ごとにリモートサーバーから更新されたダンプファイルを取得します。ファイルはcsv形式です。私はそのフォーマットを制御できません。ファイルは約750 MBです。行ごとにMyISAMテーブルにデータを挿入しようとしましたが、35分かかりました。

ファイルから10〜12の値を1行あたり3つだけ取得し、データベースで更新する必要があります。

このようなことを達成するための最良の方法は何ですか?

これを毎日行う必要があります。

現在のフローは次のとおりです:

  1. mysqli_begin_transaction
  2. ダンプファイルを1行ずつ読み取る
  3. 各レコードを1行ずつ更新します。
  4. mysqli_commit

上記の操作には約-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)

アップデート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つのインデックスには更新フィールドが含まれています。
  • 1つのトランザクションで更新を行う必要はありません。時間はかかりますが、24時間以内です。後でこのテーブルに依存するスフィンクスインデックスを更新する必要があるため、テーブル全体をロックせずに1時間で完了できるようにしています。データベースが他のタスクで使用可能である限り、ステップの所要時間が長くてもかまいません。
  • 前処理ステップでcsv形式を変更できます。重要なのは、迅速な更新とロックなしです。
  • 表2はMyISAMです。これは、load data 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_csv_dump_tempのデータを使用してcontentテーブルを更新する更新クエリです

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を改善しようとしています、ベンチマークが行われると更新されます。

35
AMB

私の経験に基づいて、私は 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

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の後、インポートする列を指定します(列2をスキップ)。これは、質問の基準の1つに一致します。

次に、Oracleからの別の例を示します。 LOAD DATA INFILEの例

これはあなたが始めるのに十分なはずです。

17
Craig Efrein

言及したすべてのことを考慮すると、ボトルネックが結合自体であるように見えます。

側面#1:バッファサイズの結合

おそらく、あなたの join_buffer_size はおそらく低すぎます。

MySQLが結合バッファキャッシュを使用する方法 のMySQLドキュメントによると==

行全体ではなく、使用した列のみを結合バッファーに格納します。

この場合は、結合バッファのキーをRAMに残してください。

1,000万行×各キーの4バイトがあります。それは約4,000万です。

セッションでそれを4200万(4千万より少し大きい)に増やす

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のドキュメントによると Block Nested-Loop and Batched Key Access Joins

BKAが使用される場合、join_buffer_sizeの値は、ストレージエンジンへの各リクエスト内のキーのバッチの大きさを定義します。バッファーが大きいほど、結合操作の右側のテーブルへの順次アクセスが増えるため、パフォーマンスが大幅に向上します。

BKAを使用するには、optimizer_switchシステム変数のbatched_key_accessフラグをオンに設定する必要があります。 BKAはMRRを使用するため、mrrフラグもオンにする必要があります。現在、MRRのコスト見積もりはあまりにも悲観的です。したがって、BKAを使用するには、mrr_cost_basedをオフにする必要もあります。

この同じページはこれを行うことをお勧めします:

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を再起動する必要があります

試してみる !!!

16
RolandoMySQLDBA

あなたは言った:

  • 更新はテーブルの6-25%に影響します
  • これをできるだけ速くしたい(<1時間)
  • ロックなし
  • 単一のトランザクションである必要はありません
  • まだ(リックジェームズの回答についてのコメントで)、あなたは競争状態についての懸念を表明します

これらのステートメントの多くは、矛盾する可能性があります。たとえば、テーブルをロックしない大規模な更新。または、1つの巨大なトランザクションを使用せずに競合状態を回避します。

また、テーブルのインデックスは頻繁に作成されるため、挿入と更新の両方が遅くなる可能性があります。


競合状態の回避

updatedタイムスタンプをテーブルに追加できる場合は、競合状態を解決しながら、50万の更新を単一トランザクション。

これにより、(現在のように)行ごとの更新を実行できますが、自動コミットまたはより適切なトランザクションバッチを使用できます。

後の更新がまだ行われていないことを確認することにより(行ごとに更新しながら)競合状態を回避します(UPDATE ... WHERE pk = [pk] AND updated < [batchfile date]

そして重要なことに、これによりparallel更新を実行できます。


できるだけ早く実行する—並列化

このタイムスタンプチェックを実行すると、次のようになります。

  1. バッチファイルを適切なサイズのチャンクに分割します(たとえば、ファイルあたり50,000行)
  2. 並行して、スクリプトで各ファイルを読み取り、50,000のUPDATEステートメントを含むファイルを出力します。
  3. 並行して、(2)が終了したら、mysqlに各SQLファイルを実行させます。

(たとえば、bashでは、splitxargs -Pを見て、コマンドを並列に多くの方法で簡単に実行する方法を確認してください。並列処理の程度は、専用のスレッドをどれだけ更新

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_tableからnew_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
Rick James

大規模な更新はI/Oバウンドです。私はお勧めします:

  1. 頻繁に更新される3つのフィールドを格納する個別のテーブルを作成します。 1つのテーブルを呼び出してみましょうassets_static静的データを保持する場所、そしてもう1つのassets_dynamicはアップローダー、ダウンローダーを格納し、検証済みです。
  2. 可能であれば、assets_dynamicテーブルにMEMORYエンジンを使用します。 (更新ごとにディスクにバックアップします)。
  3. 軽量で機敏な更新assets_dynamic更新4に従って(つまり、LOAD INFILE ... INTO temp; UPDATE assets_dynamic a JOIN temp b on a.id = b.id SET [何を更新する必要があるか]。これには1分もかかりません(このシステムでは、assets_dynamicは95M行あり、更新は40秒強の〜6M行に影響します)。
  4. Sphinxのインデクサーを実行すると、JOIN assets_staticおよびassets_dynamicになります(これらのフィールドの1つを属性として使用する場合)。
1
user3127882

UPDATEを高速に実行するには、

INDEX(uploaders, downloaders, verified)

どちらのテーブルにも配置できます。 3つのフィールドは任意の順序にすることができます。

これにより、UPDATEが2つのテーブル間の行をすばやく照合できるようになります。

And 2つのテーブルでデータ型を同じにします(両方のINT SIGNED または両方 INT UNSIGNED)。

0
Rick James