web-dev-qa-db-ja.com

データベースは数百万行で遅くなりますが、スキーマとクエリを最適化する方法は?

特定のアドレスに接続されているイーサリアムトランザクションを格納するデータベースを構築しています。データベーステーブルeth_addresses_txが300万行を超えると、処理速度が大幅に低下し、CPUが100%に達し始めます。ストレージは問題なく、100万行は約300MBです。サーバーにより多くのお金をかける前に、スキーマとクエリを改善する方法があるかどうかを確認したいと思います。プロジェクトの開始行数は1,500〜2,000万行で、近い将来さらに多くの行が含まれる可能性があります。 200〜500の一意のアドレスがあり、これらのアドレスには少量(1,000)から最大(300万)までの行が含まれる場合があります。いくつかアドバイスがあります。ありがとうございました。

pdate:トランザクションテーブルeth_address_txdatetimeTxにさらに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 |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+---------+----------+--------------------------------------+
3
cyberkali

テーブルが大きい場合、ハッシュインデックスは不適切です

hashが何らかの「ハッシュ」の形式であると想定すると、_eth_addresses_tx_は非常に非効率な_PRIMARY KEY_を持ちます。テーブル(またはインデックス)が、RAM andにキャッシュできるサイズよりも大きくなると、キーが「ランダム」(ハッシュと同様)になると、ブロックが原因で速度が低下します。 RAMにキャッシュされる可能性はますます低くなります。

  • 非ランダムなインデックスの使用を検討してください
  • データを圧縮する-フィールドに4バイトのINTが必要ですか? _eth_addresses_-3M行は3バイトの_MEDIUMINT UNSIGNED_で処理できます。
  • address VARCHAR(42)-それが本当に_0x_と40桁の16進数である場合は、LEFTUNHEXを使用してBINARY(20)(20バイト)にパックします。
  • From/to_txを正規化できますか?それらもハッシュですか?
  • 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_を提供していただければ、確認します。しかし、「パフォーマンスの問題から脱却することはできません」。特に、追加するインデックスおよび/または作成するサマリーテーブルがある場合。

1
Rick James