特定のアドレスに接続されているイーサリアムトランザクションを格納するデータベースを構築しています。データベーステーブルeth_addresses_tx
が300万行を超えると、処理速度が大幅に低下し、CPUが100%に達し始めます。ストレージは問題なく、100万行は約300MBです。サーバーにより多くのお金をかける前に、スキーマとクエリを改善する方法があるかどうかを確認したいと思います。プロジェクトの開始行数は1,500〜2,000万行で、近い将来さらに多くの行が含まれる可能性があります。 200〜500の一意のアドレスがあり、これらのアドレスには少量(1,000)から最大(300万)までの行が含まれる場合があります。いくつかアドバイスがあります。ありがとうございました。
pdate:トランザクションテーブルeth_address_tx
のdatetimeTx
にさらに1つのインデックスが追加されました。これにより、この投稿の最後の2つ以外のほとんどすべてのクエリのパフォーマンスが修正されました。
テーブルの目的:
eth_addresses
-追跡されている住所に関する住所とメタデータを保存します。
eth_addresses_stats
-特定の期間のトランザクション数など、トランザクションテーブル(eth_addresses_tx)からクエリされた統計を保存します。プログラミングスクリプトはcronジョブでクエリされます
eth_addresses_tx
-TXはトランザクションの略ですクエリとスキーマ。このテーブルは、特定のアドレスにアタッチされたブロックチェーントランザクションからのデータを格納します。 value
列には、そのトランザクションのETH金額が含まれています。 from_txには1つのインデックスがあります。
MYSQLスキーマ
CREATE DATABASE addresses CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE addresses;
CREATE TABLE `eth_addresses` (
`id` INT(11) NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(32) NOT NULL ,
`website` VARCHAR(255) NOT NULL ,
`address` VARCHAR(1024) NOT NULL COMMENT 'ETH addresses' ,
PRIMARY KEY (`id`), UNIQUE `unique` (`website`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `eth_addresses_stats` (
`id` int(11) NOT NULL,
`balance` decimal(16,6) NULL DEFAULT NULL,
`tx_1h` int(11) NULL DEFAULT NULL COMMENT 'transactions',
`tx_24h` int(11) NULL DEFAULT NULL COMMENT 'transactions',
`tx_7d` int(11) NULL DEFAULT NULL COMMENT 'transactions',
`tx_all` int(11) NULL DEFAULT NULL COMMENT 'transactions',
`volume_1h` decimal(14,6) NULL DEFAULT NULL,
`volume_24h` decimal(14,6) NULL DEFAULT NULL,
`volume_7d` decimal(14,6) NULL DEFAULT NULL,
`volume_all` decimal(14,6) NULL DEFAULT NULL,
`users_1h` int(11) NULL DEFAULT NULL,
`users_24h` int(11) NULL DEFAULT NULL,
`users_7d` int(11) NULL DEFAULT NULL,
`users_all` int(11) NULL DEFAULT NULL,
`last_scraped` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT FOREIGN KEY (id) REFERENCES eth_addresses (id),
UNIQUE (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
そして
CREATE TABLE `eth_addresses_tx` (
`hash` varchar(66) NOT NULL,
`address_id` INT(11) NOT NULL,
`blockNumber` int(11) NOT NULL,
`datetimeTx` datetime NOT NULL,
`from_tx` varchar(42) NOT NULL,
`to_tx` varchar(42) NOT NULL,
`value` decimal(14,6) NOT NULL,
CONSTRAINT FOREIGN KEY (address_id) REFERENCES eth_addresses (id),
PRIMARY KEY (`hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Transactions';
ALTER TABLE `eth_addresses_tx` ADD INDEX `from_tx` (`from_tx`);
遅いクエリ
以下のクエリは定期的に実行され、結果はキャッシュテーブルに保存されます。
これらのクエリは、eth_addresses_tx
に送信されるトランザクションテーブルeth_addresses_stats
から合計統計情報を収集しています。それらを1つのクエリに組み合わせる方法がわからなかったので、4つのクエリを作成しました。
[〜#〜] update [〜#〜]以下の4つのクエリは、datetimeTxのインデックスを使用して非常に高速です。 100万行以上のアドレスに0.14秒かかる
SELECT IFNULL(SUM(value), 0) AS volume_1h,
count(*) AS tx_1h,
COUNT(DISTINCT from_tx) AS users_1h
FROM `eth_addresses_tx`
WHERE datetimeTx >= DATE_SUB(NOW(),INTERVAL 1 HOUR)
AND address_id = $address_id;
-- This query is slow on popular addresses with 1M+ rows
-- 1 row in set (32.30 sec) without explain
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+------------------------+
| 1 | SIMPLE | eth_addresses_tx | NULL | ref | address_id | address_id | 4 | const | 1440328 | 33.33 | Using where |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+------------------------+
SELECT IFNULL(SUM(value), 0) AS volume_24h,
count(*) AS tx_24h, COUNT(DISTINCT from_tx) AS users_24h
FROM `eth_addresses_tx`
WHERE datetimeTx >= DATE_SUB(NOW(),INTERVAL 24 HOUR)
AND address_id = $address_id;
SELECT IFNULL(SUM(value), 0) AS volume_7d,
count(*) AS tx_7d, COUNT(DISTINCT from_tx) AS users_7d
FROM `eth_addresses_tx`
WHERE datetimeTx >= DATE_SUB(NOW(),INTERVAL 7 DAY)
AND address_id = $address_id;
SELECT IFNULL(SUM(value), 0) AS volume_all,
count(*) AS tx_all,
COUNT(DISTINCT from_tx) AS users_all
FROM `eth_addresses_tx`
WHERE address_id = $address_id;
これらのクエリは、いくつかの異なるwhere句を使用してeth_addresses_tx
から最近のトランザクションを取得します。日付順の並べ替えは非常に遅く、クエリの並べ替えは非常に高速です。
[〜#〜] update [〜#〜]以下の3つのクエリは、datetimeTxのインデックスを使用して非常に高速になりました。セット内の10行(0.00秒)
SELECT blockNumber, datetimeTx, address_id, from_tx, to_tx,
value
FROM eth_addresses_tx
WHERE from_tx = $from_tx
ORDER BY datetimeTx DESC
LIMIT $limit;
SELECT blockNumber, datetimeTx, address_id, from_tx, to_tx,
value
FROM eth_addresses_tx
WHERE address_id = $address_id
ORDER BY datetimeTx DESC
LIMIT $limit;
SELECT blockNumber, datetimeTx, address_id, from_tx, to_tx,
value
FROM eth_addresses_tx
ORDER BY datetimeTx DESC
LIMIT $limit;
そして
-- This query without explain = 10 rows in set (1.20 sec)
-- This one is faster then the ones below
EXPLAIN SELECT blockNumber, datetimeTx, address_id, from_tx, to_tx,
value
FROM eth_addresses_tx
WHERE from_tx = $from_tx
ORDER BY datetimeTx DESC
LIMIT 10;
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+-------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+-------+----------+---------------------------------------+
| 1 | SIMPLE | eth_addresses_tx | NULL | ref | from_tx | from_tx | 128 | const | 51680 | 100.00 | Using index condition; Using filesort |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+-------+----------+---------------------------------------+
-- This query without explain = 10 rows in set (31.51 sec)
-- Address ids with a small amount of transactions query fast however address with large amounts of rows query very slow
EXPLAIN SELECT blockNumber, datetimeTx, address_id, from_tx, to_tx,
value
FROM eth_addresses_tx
WHERE address_id = $address_id
ORDER BY datetimeTx DESC
LIMIT 10;
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+---------------------------------------+
| 1 | SIMPLE | eth_addresses_tx | NULL | ref | address_id | address_id | 4 | const | 1440242 | 100.00 | Using index condition; Using filesort |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+---------------------------------------+
-- This query without explain = 10 rows in set (9.93 sec)
EXPLAIN SELECT blockNumber, datetimeTx, address_id, from_tx, to_tx, value FROM eth_addresses_tx ORDER BY datetimeTx DESC LIMIT 10;
+----+-------------+-------------+------------+------+---------------+------+---------+------+---------+----------+---------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+---------+----------+---------------------+
| 1 | SIMPLE | eth_addresses_tx | NULL | ALL | NULL | NULL | NULL | NULL | 2880430 | 100.00 | Using filesort |
+----+-------------+-------------+------------+------+---------------+------+---------+------+---------+----------+---------------------+
これらのクエリは、トランザクションデータをグラフにプロットして、ボリュームを追跡します。 [〜#〜] update [〜#〜]以下の3つのクエリは、datetimeTxのインデックスを使用して非常に高速になりました。セット内の73行(0.02秒)
SELECT value FROM eth_addresses_tx WHERE address_id = $address_id AND value > 0 AND datetimeTx >= DATE_SUB(NOW(),INTERVAL 1 HOUR);
-- This query is slow on popular addresses with 1M+ rows
-- 88 rows in set (31.81 sec) without explain
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+-----------------------+
| 1 | SIMPLE | eth_addresses_tx | NULL | ref | address_id | address_id| 4 | const | 1440342 | 11.11 | Using where |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+---------+----------+-----------------------+
SELECT value FROM eth_addresses_tx WHERE address_id = $address_id AND value > 0 AND datetimeTx >= DATE_SUB(NOW(),INTERVAL 24 HOUR);
SELECT value FROM eth_addresses_tx WHERE address_id = $address_id AND value > 0 AND datetimeTx >= DATE_SUB(NOW(),INTERVAL 7 DAY);
これらのクエリは、誰がそれらのアドレスと最も相互作用しているのかを把握するためのものです
SELECT d.name, count(d.id) AS total
FROM `eth_addresses_tx` as tx
LEFT JOIN eth_addresses AS d ON d.id = tx.address_id
WHERE tx.from_tx = :address group by d.name
-- This query is not bad, pretty fast on smaller amounts
-- 1 row in set (1.06 sec) without explain
+----+-------------+-------+------------+------+---------------+---------+---------+-------+-------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+-------+----------+----------------------------------------------------+
| 1 | SIMPLE | tx | NULL | ref | from_tx | from_tx | 128 | const | 51680 | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | d | NULL | ALL | PRIMARY | NULL | NULL | NULL | 5 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+-------+----------+----------------------------------------------------+
-----------------------------------------------------
SELECT from_tx, count(*) AS total_tx, SUM(value) as total_volume, count(DISTINCT address_id) as total_interacted
FROM `eth_addresses_tx`
GROUP BY from_tx
ORDER BY total_tx DESC, from_tx DESC
LIMIT 100;
-- This query is very bad
-- 100 rows in set (2 min 26.45 sec) without explain
-- sorting with any of the 3 items selected is slow
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+---------+----------+--------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+---------+----------+--------------------------------------+
| 1 | SIMPLE | eth_addresses_tx | NULL | index | from_tx | from_tx | 128 | NULL | 2880587 | 100.00 | Using temporary; Using filesort |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+---------+----------+--------------------------------------+
テーブルが大きい場合、ハッシュインデックスは不適切です
hash
が何らかの「ハッシュ」の形式であると想定すると、_eth_addresses_tx
_は非常に非効率な_PRIMARY KEY
_を持ちます。テーブル(またはインデックス)が、RAM andにキャッシュできるサイズよりも大きくなると、キーが「ランダム」(ハッシュと同様)になると、ブロックが原因で速度が低下します。 RAMにキャッシュされる可能性はますます低くなります。
eth_addresses
_-3M行は3バイトの_MEDIUMINT UNSIGNED
_で処理できます。address VARCHAR(42)
-それが本当に_0x
_と40桁の16進数である場合は、LEFT
とUNHEX
を使用してBINARY(20)
(20バイト)にパックします。DECIMALs
neeをそのように大きくします。 _(14,6)
_には7バイトかかります。 FLOAT
は4をとります(ただし有効桁数は少ない)。 「ボリューム」とはどのような価値ですか?キュービックハロン?株式は売買されましたか?三部作の本?液量オンス?正確な値が必要な場合は、DECIMAL
が必要です。しかし、小数点以下6桁で十分ですか? (GweiやWeiは対象外です。)FLOAT
の7桁の有効数字mayは、合計に十分ですか?より良いインデックス
_WHERE datetimeTx >= DATE_SUB(NOW(),INTERVAL 1 HOUR)
AND address_id = $address_id;
_
この順序でINDEX(address_id, datetimeTx)
が必要です。
_WHERE from_tx = $from_tx
ORDER BY datetimeTx DESC
LIMIT $limit;
_
この順序でINDEX(from_tx, datetimeTx)
が非常に必要です。 _(address_id, datetimeTx)
_の同上。これにより 'filesort'が削除されますが、さらに重要なのは、数千ではなく10行のみを調べる必要があることです(EXPLAINs
を参照)。
これはトリッキーになります:
_WHERE address_id = $address_id
AND value > 0
AND datetimeTx >= DATE_SUB(NOW(), ...)
_
1つのインデックスで2つの「範囲」を効果的に使用することはできません。オプティマイザができる最善のことは、上記のインデックスとINDEX(address_id, value)
の間で選択することです。
_LEFT JOIN
_を使用したクエリは、WHERE
と_GROUP BY
_が複数のテーブルを参照するため、改善が困難です。
_ORDER BY total_tx DESC, from_tx DESC LIMIT 100
_は集計であるため、_total_tx
_を改善できません。 (ただし、以下の「要約表」を参照してください。)
インデックス作成の詳細: http://mysql.rjweb.org/doc.php/index_cookbook_mysql
eth_addresses_tx
これが「大きな」テーブルであると想定すると、その_PRIMARY KEY
_を真剣に検討する必要があります。 hash
が必要であると仮定すると、以下が最善の方法です。
_PRIMARY KEY(address_id, datetimeTx, -- to make queries efficient
hash) -- to assure uniqueness
UNIQUE(hash) -- constraint for `hash`
_
テーブルはPKで「クラスター化」されています。クエリは、このPKで「連続した」行をフェッチします。これは、PK(hash)
によって引き起こされるランダムな性質よりもはるかに優れています。
概要表
しかし、本当の勝者はおそらく要約表を持つことでしょう。時間ごとに新しい行が追加されます。毎日、毎週などのレポートは、このテーブルから計算できます。
要約テーブル: http://mysql.rjweb.org/doc.php/summarytables
チューニング(可能性が低い)
_GLOBAL STATUS and VARIABLES
_を提供していただければ、確認します。しかし、「パフォーマンスの問題から脱却することはできません」。特に、追加するインデックスおよび/または作成するサマリーテーブルがある場合。