web-dev-qa-db-ja.com

2つのmysqlテーブル間の更新結合クエリを高速化する方法は?

さて、話は短く、私は強力な専用サーバーを持っています。

Intel  I7-6700K -
64GB DDR4 2400 MHz
1x480GB   SSD

mysqlサーバーをnginx、phpとともに実行する

innodb-ft-min-token-size = 1
innodb-ft-enable-stopword = 0

innodb_buffer_pool_size = 40G
max_connections = 2000

[deploy@ns540545 ~]$ free -h
              total        used        free      shared  buff/cache   available
Mem:            62G         45G         11G        107M        6.4G         16G
Swap:          2.0G        1.4G        640M

それは高価だったので、コスト削減のために別の専用サーバーを手に入れましたそれほど強力ではない専用サーバー

Intel  i3-2130  
8GB DDR3 1333 MHz   
2TB 

mysqlサーバーをnginx、phpとともに実行する

innodb-ft-min-token-size = 1
innodb-ft-enable-stopword = 0

innodb_buffer_pool_size = 4G
max_connections = 2000

[root@privateserver deploy]# free -h
              total        used        free      shared  buff/cache   available
Mem:           7.7G        7.5G         73M         24M        150M         79M
Swap:           39G        7.8G         32G

データベースを強力なサーバーからそれほど強力ではないサーバーに移動しました。

単純なクエリを実行すると、パフォーマンスがわずかに低下したように感じるかもしれませんが、強力なサーバーで2分かかっていたこの1つのクエリは、かかります。 26.6525時間、それほど強力ではないサーバーを使用しています

UPDATE content a JOIN peers_data b ON a.hash = b.hash SET a.seeders = b.seeders, a.leechers = b.leechers, a.is_updated = b.is_updated

両方の専用サーバーでまったく同じテーブルに関する詳細情報

CREATE TABLE `peers_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `hash` char(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `seeders` int(11) NOT NULL DEFAULT '0',
  `leechers` int(11) NOT NULL DEFAULT '0',
  `is_updated` int(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`hash`),
  UNIQUE KEY `id` (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `content` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content_id` int(11) unsigned NOT NULL DEFAULT '0',
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `tags` varchar(1000) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `category` smallint(3) unsigned NOT NULL DEFAULT '0',
  `category_name` varchar(50) CHARACTER SET ascii COLLATE ascii_bin DEFAULT '',
  `sub_category` smallint(3) unsigned NOT NULL DEFAULT '0',
  `sub_category_name` varchar(50) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
  `size` bigint(20) unsigned NOT NULL DEFAULT '0',
  `seeders` int(11) unsigned NOT NULL DEFAULT '0',
  `leechers` int(11) unsigned NOT NULL DEFAULT '0',
  `upload_date` datetime DEFAULT NULL,
  `uploader` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0',
  `uploader_level` varchar(10) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
  `comments_count` int(11) unsigned NOT NULL DEFAULT '0',
  `is_updated` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`content_id`),
  UNIQUE KEY `unique` (`id`) USING BTREE,
  KEY `hash` (`hash`),
  KEY `uploader` (`uploader`),
  KEY `sub_category` (`sub_category`),
  KEY `category` (`category`),
  KEY `title_index` (`title`),
  KEY `category_sub_category` (`category`,`sub_category`),
  KEY `seeders` (`seeders`),
  KEY `uploader_sub_category` (`uploader`,`sub_category`),
  KEY `upload_date` (`upload_date`),
  KEY `uploader_upload_date` (`uploader`,`upload_date`),
  KEY `leechers` (`leechers`),
  KEY `size` (`size`),
  KEY `uploader_seeders` (`uploader`,`seeders`),
  KEY `uploader_size` (`uploader`,`size`),
  FULLTEXT KEY `title` (`title`),
  FULLTEXT KEY `tags` (`tags`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;


mysql> explain UPDATE content a JOIN peers_data b ON a.hash = b.hash SET a.seeders = b.seeders, a.leechers = b.leechers, a.is_updated = b.is_updated ;
+----+-------------+-------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | UPDATE      | a     | NULL       | ALL    | NULL          | NULL    | NULL    | NULL | 4236260 |   100.00 | NULL        |
|  1 | SIMPLE      | b     | NULL       | eq_ref | PRIMARY       | PRIMARY | 160     | func |       1 |   100.00 | Using where |
+----+-------------+-------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
2 rows in set (0.00 sec)

peers_dataのレコード6,367,417
内容の記録4,236,268

上記の更新結合クエリを高速化するにはどうすればよいですか?私はそれほど強力でないサーバーで約1時間と予想していましたが、26時間以上は多すぎます。

私は何を間違っていますか?またはここで欠落していますか?

RAM 32GB +スワップスペースを設定することでそれほど強力でないサーバー上で補正しようとしました。innodbバッファプール4 GBは多すぎますか?


これまでに試しました。

  1. 両方の文字セットタイプを同じに修正
  2. innodbバッファープールサイズをデフォルトの128Mに設定

そして、タスクは19.93時間で完了しました。


RolandoMySQLDBAの提案に従って、

SUGGESTION#2を試みています。これらのインデックスは、サイトが正しく機能するために必要です。

私は設定しましたinnodb_buffer_pool_size = 5G

そして試して

SET GLOBAL innodb_change_buffer_max_size = 50; UPDATE content a JOIN peers_data b ON a.hash = b.hash SET a.seeders = b.seeders、a.leechers = b.leechers、a.is_updated = b.is_updated; SET GLOBAL innodb_change_buffer_max_size = 25;

25時間以上経過していて、クエリはまだ実行中です。クエリが完了すると更新されます。

もう1つは、データはそれほど重要ではなく、すぐに利用できるデータであり、毎日バックアップを取っているため、後で提案3も試してみます。どちらか速い方を使用します。


アップデート2:

提案2の完了には38.36667時間かかりました。

現在提案を試みています3。

2
AMB

あなたの質問

クエリには時間がかかるはずです。なぜ ???クエリを見てください:

UPDATE content a JOIN peers_data b ON a.hash = b.hash
SET a.seeders = b.seeders, a.leechers = b.leechers, a.is_updated = b.is_updated;

contentテーブルのクエリによって列が更新され始めることに注意してください。

  • seeders
  • leechers
  • is_updated

これらの列のどれにインデックスが付けられますか?

  • seeders
  • leechers

これらの列を含むcontentテーブルにはどのインデックスがありますか?

KEY `seeders` (`seeders`), <<<<-------------------------------- THIS ONE !!!
KEY `uploader_sub_category` (`uploader`,`sub_category`),
KEY `upload_date` (`upload_date`),
KEY `uploader_upload_date` (`uploader`,`upload_date`),
KEY `leechers` (`leechers`), <<<<------------------------------ THIS ONE !!!
KEY `size` (`size`),
KEY `uploader_seeders` (`uploader`,`seeders`), <<<<------------ THIS ONE !!!

フードの下で何が起こっているのですか?

seedersleechesの値を変更している場合、これらの3つのインデックスは、リーフノードが再シャッフルされています。

値の大部分が変更されていない場合でも、行がロックされ、データのコピーがUNDOログに蓄積されます(MVCCのため)。この結果は追加のディスクI/Oです(ibdata1は増加しているはずです)

INNODB

InnoDBバッファープールは "Perfect Storm" を通過します。なぜ ???

InnoDBアーキテクチャに注意してください(Percona CTO Vadim Tkachenkoからの画像)

InnoDB Plumbing

Insert Bufferに注意してください。 これは、MySQLドキュメントがそれについて言っていることです

変更バッファーは、影響を受けるページがバッファープールにない場合に、セカンダリインデックスページへの変更をキャッシュする特別なデータ構造です。バッファーに入れられた変更は、INSERT、UPDATE、またはDELETE操作(DML)の結果として生じる可能性があり、後で他の読み取り操作によってページがバッファープールに読み込まれるときにマージされます。

クラスター化インデックスとは異なり、セカンダリインデックスは通常一意ではなく、セカンダリインデックスへの挿入は比較的ランダムな順序で行われます。同様に、削除と更新は、インデックスツリーに隣接して配置されていないセカンダリインデックスページに影響を与える可能性があります。キャッシュされた変更を後でマージすると、影響を受けるページが他の操作によってバッファープールに読み込まれるときに、ディスクからセカンダリインデックスページを読み込むために必要となる実質的なランダムアクセスI/Oが回避されます。

定期的に、システムがほとんどアイドル状態のとき、または遅いシャットダウン中に実行される削除操作は、更新されたインデックスページをディスクに書き込みます。パージ操作では、一連のインデックス値のディスクブロックを、各値がすぐにディスクに書き込まれた場合よりも効率的に書き込むことができます。

更新するセカンダリインデックスが多数あり、影響を受ける行が多い場合、変更バッファーのマージには数時間かかることがあります。この間、ディスクI/Oが増加するため、ディスクにバインドされたクエリの速度が大幅に低下する可能性があります。トランザクションがコミットされた後も、変更バッファーのマージが引き続き発生する場合があります。実際、サーバーのシャットダウンと再起動後も、変更バッファーのマージが引き続き発生する可能性があります(詳細については、セクション14.21.2「InnoDBリカバリの強制」を参照してください)。

メモリ内で、変更バッファーはInnoDBバッファープールの一部を占有します。ディスクでは、変更バッファはシステムテーブルスペースの一部であるため、データベースの再起動後もインデックスの変更はバッファされたままになります。

ここが "Perfect Storm" の出番です。デフォルトでは、InnoDBストレージエンジンは変更バッファリング用に最大25%のバッファプールを予約します。 3つのセカンダリインデックスに対するすべての変更は、InnoDBバッファプールの挿入バッファに蓄積する必要があります。実際のインデックスページがバッファープールに到達すると、マージプロセスがディスクにプッシュされ(ibdata1内のバッファーの挿入に注意)、より多くのディスクI/Oが生成されます。

提案

提案#1

それらのインデックスを削除します。なぜ ???これにより、一括更新中にこれらのインデックスを管理するために必要な変更バッファリングが不要になります。

次のクエリを実行します。

SELECT
    COUNT(1) rowcount,
    COUNT(DISTINCT seeders) seeders_count,
    COUNT(DISTINCT leechers) leechers_count
FROM content;

seeders_countrowcountの5%未満の場合、seeders_countのカーディナリティはseedersの使用を無効にする可能性があります。 Smaeはleechersを使用しています。

そのインデックスuploader_seedersについて、次のクエリを実行します。

SELECT COUNT(1) uploader_seeders_count FROM
(SELECT DISTINCT uploader,seeders FROM content) A;

uploader_seeders_countrowcountの5%未満の場合(前のクエリから)、uploader_seedersインデックスを削除します。

これらのインデックスを削除するには、次のコマンドを実行します。

ALTER TABLE content DROP INDEX uploader_seeders,DROP INDEX seeders,DROP INDEX leechers;

提案#2

バッファサイズを最大値に変更 を変更し、クエリを実行します。

SET GLOBAL innodb_change_buffer_max_size = 50;
UPDATE content a JOIN peers_data b ON a.hash = b.hash
SET a.seeders = b.seeders, a.leechers = b.leechers, a.is_updated = b.is_updated;
SET GLOBAL innodb_change_buffer_max_size = 25;

また、バッファープールサイズ( innodb_buffer_pool_size )を20Gに設定します。 my.cnfでその値を変更します。

MySQL 5.7を使用している場合は、単に実行します

mysql> SET GLOBAL innodb_buffer_pool_size = 1024 * 1024 * 1024 * 20;

MySQL 5.6以前を使用している場合は、mysqldを再起動する必要があります。

提案#3(危険)

インデックスを削除したくない場合は、一括更新中に変更バッファリングを無効にして、後で有効にすることができます。

以下を実行します。

SET GLOBAL innodb_change_buffering = 'none';
UPDATE content a JOIN peers_data b ON a.hash = b.hash
SET a.seeders = b.seeders, a.leechers = b.leechers, a.is_updated = b.is_updated;
FLUSH TABLES;
SET GLOBAL innodb_change_buffering = 'all';

これは、クラッシュや再起動の際に回復するためのバッファリングがないために、インデックスへの変更を高速化するため、危険です。

提案#4(危険)

別の無頓着なアプローチは、 ダブルライトバッファを無効にする です。再起動が必要なので、次のようにします。

  • service mysql restart --skip-innodb_doublewrite
  • あなたのミサUPDATE
  • service mysql restart

これはプロダクションには推奨されません。開発とステージングのみしてください!!!

3
RolandoMySQLDBA
_hash char(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''
_

ハッシュは16進数ですよね? utf8mb4にしないでください。アスキーを使用します。

BINARY(20)を使用して、ハッシュをUNHEX()にパックします。現在はわずか20バイトです。

あらゆる種類のハッシュは非常に「ランダム」です。おそらく何が起こっているのか-ハッシュはキャッシュできないため、多くの、おそらくほとんどの行でディスクにアクセスしています。これは、単なるCPUではなくディスクI/Oを意味します。

より高速なマシンでも、テーブルが大きくなりすぎると(buffer_pool_sizeに対して)、最終的には大幅にスローダウンします。

ハッシュ、UUIDなどにはこの欠点があります。それらを避けるようにしてください。

seedersが単なるフラグの場合、TINYINT(4バイト)の代わりにINT(1バイト)を使用します。

なぜあなたはこれらの両方を持っているのですか?

_  PRIMARY KEY (`hash`),
  UNIQUE KEY `id` (`id`)
_

他のことを行って、テーブルのディスクフットプリントを縮小します。小さい->キャッシュ可能-> I/Oが少ない->高速。

1
Rick James

is_updated列の定義が異なります。それらは同一である必要があります。

作成テーブルに日付を追加して再投稿し、あなたがどこにいるかを確認できるようにします。

A)SHOW INDEX FROMシーダーの完全な結果を投稿してください。 B)リーチャからインデックスを表示;機会があれば。

0
Wilson Hauck

上記の更新は数分かかるので、

  • その時に更新された行数は?
  • テーブルのレコード数が増えましたか?
  • キャッシュは完全にいっぱいでしたか?

また、そのクエリは非常に積極的であるように思われるので、すべての更新を含むファイルを作成し、後でpercona pt-fifo-splitを使用してファイルを次のように抽出することをお勧めします。

mysql -e "select concat( 'UPDATE content a SET a.seeders ='、b.seeders、 '、a.leechers ='、b.leechers、 '、a.is_updated ='、b.is_updated、 'where a。 hash = '、b.hash)from peers_data b "> /tmp/updating_seeders.sql

(構文を再確認してください)。

このプロセスには2分はかかりません。高負荷/ラグを削減するために、500回の更新ごとに1秒のスリープを追加する可能性が高く、数時間と、上記のファイルを作成する時間を加えます。

(4236268(合計レコード)/ 500(1秒あたりのレコード))/ 60(分)/ 60(時間)

0
andres

サーバー1

              total        used        free      shared  buff/cache   available
Mem:            62G         45G         11G        107M        6.4G         16G
Swap:          2.0G        1.4G <--    640M

サーバー2

              total        used        free      shared  buff/cache   available
Mem:           7.7G        7.5G         73M         24M        150M         79M
Swap:           39G        7.8G <--     32G

私は32 GB +スワップスペースを設定することで、それほど強力でないサーバーでRAMを補償しようとしました

スワップは、RAMの不足を補うものであってはなりません。はい、これは十分なRAMが利用できない場合に使用されますが、プログラムのクラッシュを防ぐためにのみ使用されます(クラウドサーバーに関して)。OSがサーバー2で積極的にスワップしていることがわかります。

開発マシンでは、スワップで補正しても問題ありません。ただし、専用サーバーの場合、1つのアプリケーションによってトリガーされたとしても、スワップによりOSのすべてのプログラムの速度が低下します(Webアプリケーションの速度が低下します)。ここでの主なボトルネックはIO(積極的なスワップ+ mysql IO)であると想定できます。swappinessを設定してサーバーRAMを16GBに増やしてみてください1に。


これまでに試しました。

  1. 両方の文字セットタイプを同じに修正

  2. innodbバッファープールサイズをデフォルトの128Mに設定

バッファープールを4Gから128Mに減らした後でも、システムには4G(7.8G-3.8G)RAM swap for no-swapのメモリーが必要です。アプリケーションサーバーはスワップしないでください。プログラムのクラッシュを防ぐため以外は、2TBディスクがSSDかHDDかを指定します。

0
The Coder