web-dev-qa-db-ja.com

2,000万行以上の行テーブルに対するクエリの速度を向上させるにはどうすればよいですか?

特定のIPアドレスのインターネットトラフィック統計を取得するために使用するクエリがあります。

hostsの個別のIPアドレスフィールドと、assignmentsと呼ばれるIPのブロックがあります。データは5分間隔で保存されます。

クエリ結果は時間列でグループ化され、これらの5分間隔の内外の合計SUMがグラフのプロットに使用されます。

テーブルはtrafficと呼ばれ、(月末に)約2100万件のレコードが含まれます。

SHOW CREATE table traffic:
CREATE TABLE `traffic` (
  `type` enum('v4_assignment','v4_Host','v6_subnet','v6_assignment','v6_Host') NOT NULL,
  `type_id` int(11) unsigned NOT NULL,
  `time` int(32) unsigned NOT NULL,
  `bytesin` bigint(20) unsigned NOT NULL default '0',
  `bytesout` bigint(20) unsigned NOT NULL default '0',
  KEY `basic_select` (`type_id`,`time`,`type`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
SELECT traffic.time, SUM(traffic.bytesin), SUM(traffic.bytesout) FROM traffic 
WHERE (
    ( traffic.type = 'v4_assignment' AND type_id IN (231, between 20 to 100 ids,265)) OR 
    ( traffic.type = 'v4_Host' AND type_id IN (131, ... a lot of ids... ,1506))) 
    AND traffic.time >= 1343772000 AND traffic.time < 1346450399 
GROUP BY traffic.time
ORDER BY traffic.time;

以下は、上記のクエリのexplain出力です。

+----+-------------+---------+-------+---------------+--------------+---------+------+--------+----------------------------------------------+
| id | select_type | table   | type  | possible_keys | key          | key_len | ref  | rows   | Extra                                        |
+----+-------------+---------+-------+---------------+--------------+---------+------+--------+----------------------------------------------+
|  1 | SIMPLE      | traffic | range | basic_select  | basic_select | 8       | NULL | 891319 | Using where; Using temporary; Using filesort |
+----+-------------+---------+-------+---------------+--------------+---------+------+--------+----------------------------------------------+

show indexes from traffic;
+---------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table   | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+---------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| traffic |          1 | basic_select |            1 | type_id     | A         |       13835 |     NULL | NULL   |      | BTREE      |         |
| traffic |          1 | basic_select |            2 | time        | A         |    18470357 |     NULL | NULL   |      | BTREE      |         |
| traffic |          1 | basic_select |            3 | type        | A         |    18470357 |     NULL | NULL   |      | BTREE      |         |
+---------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

このクエリの完了には、30秒から30分かかります。より良いインデックスを使用したり、別のクエリを使用したりして、改善できるといいのですが、それを理解することができません。

更新:

役立つコメンターのアドバイスに従って、主キーを作成し、インデックスtraffic_pk (time, type, type_id, id)を追加しました。残念ながら、この新しいインデックスのカーディナリティは私の元のインデックス(basic_select)と同じかそれよりも低く、MySQLはまだ元のキーを使用しています。

PDATE 2:元のインデックスbasic_selectを削除したところ、EXPLAINrowsの値が高くなりましたが、EXTRAフィールドのステップは少なくなりました。また、クエリの実行時間も1分未満になりました。 (まだ少し遅すぎますが、大きな改善です!)。

mysql> SHOW CREATE TABLE traffic_test \G;
*************************** 1. row ***************************
       Table: traffic_test
Create Table: CREATE TABLE `traffic_test` (
  `traffic_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `type` enum('v4_assignment','v4_Host','v6_subnet','v6_assignment','v6_Host') NOT NULL,
  `type_id` int(11) unsigned NOT NULL,
  `time` int(32) unsigned NOT NULL,
  `bytesin` bigint(20) unsigned NOT NULL DEFAULT '0',
  `bytesout` bigint(20) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`time`,`type`,`type_id`,`traffic_id`),
  KEY `traffic_id_IDX` (`traffic_id`)
) ENGINE=InnoDB AUTO_INCREMENT=24545159 DEFAULT CHARSET=latin1

テーブルのインデックス:

mysql> SHOW INDEX FROM traffic;
+--------------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table        | Non_unique | Key_name       | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| traffic_test |          0 | PRIMARY        |            1 | time        | A         |          18 |     NULL | NULL   |      | BTREE      |         |
| traffic_test |          0 | PRIMARY        |            2 | type        | A         |       38412 |     NULL | NULL   |      | BTREE      |         |
| traffic_test |          0 | PRIMARY        |            3 | type_id     | A         |    24545609 |     NULL | NULL   |      | BTREE      |         |
| traffic_test |          0 | PRIMARY        |            4 | traffic_id  | A         |    24545609 |     NULL | NULL   |      | BTREE      |         |
| traffic_test |          1 | traffic_id_IDX |            1 | traffic_id  | A         |    24545609 |     NULL | NULL   |      | BTREE      |         |
+--------------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

また、ORを使用しないことでクエリを簡略化しました。

SELECT SQL_NO_CACHE traffic.time, SUM(traffic.bytesin), SUM(traffic.bytesout) 
FROM    traffic
WHERE traffic.type LIKE 'v4_Host' AND type_id IN (131,1974,1976,1514,1516,2767,2730,2731,2732,2733,2734,2769,2994,2709,1,4613,4614,4615,4616,326,1520,2652,1518,1521,1522,1523,1524,1525,2203,1515,1513,1467,1508,1973,1510,1975,1511,1475,1476,1468,1469,1470,1471,1472,1473,1500,1507,1478,1480,1481,1482,1483,1484,1485,1479,1486,1487,1488,1489,1490,1491,1495,1499,1494,2269,1474,1519,2204,2976,1922,1493,1492,1497,1496,1498,1501,1502,1503,1526,1509,1506) 
AND traffic.time >= 1342181721 
AND traffic.time < 1343391321 
GROUP BY traffic.time ASC;

このクエリの古い実行:

3980 rows in set (6 min 15.27 sec)

新しい実行時間:

3980 rows in set (24.80 sec)

EXPLAIN出力:

+----+-------------+---------+-------+---------------+---------+---------+------+----------+-------------+
| id | select_type | table   | type  | possible_keys | key     | key_len | ref  | rows     | Extra       |
+----+-------------+---------+-------+---------------+---------+---------+------+----------+-------------+
|  1 | SIMPLE      | traffic | range | PRIMARY       | PRIMARY | 4       | NULL | 12272804 | Using where |
+----+-------------+---------+-------+---------------+---------+---------+------+----------+-------------+

行の値はまだかなり高いです。インデックスでtypetype_idの順序を切り替えることで、これを改善できると思います。可能なタイプは4つだけで、type_idの数が多いためです。

これは正しい仮定ですか?

7
Steven V

1.テーブル分割

[AND traffic.time> = 1343772000 AND traffic.time <1346450399]句があるため、このテーブルからデータを削除しないか、またはテーブルが現在複数月のデータを格納していると思います。列[時間]の値はUNIXタイムスタンプのようです(1346450399 =金、2012年8月31日21:59:59 GMT)時間列に基づいてテーブルを分割します。 DBが対応するパーティションをスキャンするため、データの取得が高速化されます(テーブル全体をスキャンするよりもはるかに高速です)。

  • 優れたパーティショニングチュートリアルはここにあります: http://www.arachna.com/roller/spidaman/entry/scaling_Rails_with_mysql_table
  • そのためのタイムスタンプ範囲を計算する必要がありますが、難しいことではありません。
  • 例:(1346450399-1343772000)/ 60/60/24 =〜31日。したがって、9月のデータを保持するパーティションの最大値(31日もある)は、1346450399 +(31 * 24 * 60 * 60)になります。
  • UNIXから日付までの計算機はここにあります: http://www.onlineconversion.com/unix_time.htm

2.クエリを書き直す

WHEREブロックの「OR」のため、オプティマイザは定義されたインデックスを使用しないことを選択します。クエリを2つの選択に分割して、和集合を作成します。

SELECT 
    traffic.time, 
    SUM(traffic.bytesin), 
    SUM(traffic.bytesout) 
FROM 
    traffic 
WHERE traffic.type LIKE 'v4_assignment' 
    AND type_id IN (1,2,3,4)
    AND traffic.time >= 1343772000 AND traffic.time <= 1346450399 
GROUP BY 
    traffic.time
UNION
SELECT 
    traffic.time, 
    SUM(traffic.bytesin), 
    SUM(traffic.bytesout) 
FROM 
    traffic 
WHERE traffic.type LIKE 'v4_Host' 
    AND type_id IN (5,6,7,8)
    AND traffic.time >= 1343772000 AND traffic.time <= 1346450399 
GROUP BY 
    traffic.time
ORDER BY 
    traffic.time

3.データのカーディナリティに基づく新しいインデックス

あなたの説明の出力に基づいて、使用されているインデックスが表示されないようです。おそらく、オプティマイザがフルテーブルスキャンを行ってからインデックスをたどる方が簡単(安価)であると判断したためでしょう。また、現在のインデックスでは、最初の列のカーディナリティが次の2つの列よりも低くなっています。インデックスの最初の列は、カーディナリティが最高(最大)の列にする必要があります。

新しいインデックスを次のように作成します。

MYSQL> CREATE INDEX MTIhai_traffic_idx1 ON traffic(time, type, type_id)
6
MTIhai

(time, type, type_id, bytes_in, bytes_out)の複合インデックスをお勧めします。

(type_id, time, type)の組み合わせが一意である場合(ところで、テーブルの主キーは何ですか?)、主キーを(time, type, type_id)と定義するだけで済みます。次に、テーブルのクラスター化インデックスがこの主キーとなり、上記の複合インデックスは必要ありません。最も一般的なクエリが何であるかに応じて(このようなgroup by timewhere time >=? and time <?がある場合)、クラスター化インデックスを使用できるため、効率が向上します。

このようにクエリを書き換えることもできます

  • LIKEの代わりに=を使用し、
  • GROUP BYORDER BYの組み合わせ(MySQL独自の構文により効率が向上する可能性があります):

    SELECT t.time, SUM(t.bytesin), SUM(t.bytesout) 
    FROM traffic AS t 
    WHERE ( t.type = 'v4_assignment' AND t.type_id IN (231, between 20 to 100 ids,265)
         OR t.type = 'v4_Host' AND t.type_id IN (131, ... a lot of ids... ,1506)
          ) 
        AND t.time >= 1343772000 AND t.time < 1346450399 
    GROUP BY t.time ASC ;
    

更新+修正

(InnoDB)テーブルでPRIMARYおよびUNIQUEインデックスを定義していない場合、非表示の6バイト列が作成され、テーブルのクラスター化インデックスとして使用されます。

したがって、4バイトの自動インクリメント整数列を明示的に定義し、それをtime列(または上記の3つの列すべて)と組み合わせて、PRIMARYまたはUNIQUEキーとして使用することをお勧めします。クエリに役立つクラスター化インデックスを作成する以外の目的はありません。

ALTER TABLE traffic
  ADD COLUMN
    traffic_id INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
  ADD CONSTRAINT traffic_PK 
    PRIMARY KEY (time, type, type_id, traffic_id) 
  ADD INDEX traffic_id_IDX (traffic_id) ;

または(より狭い主キーを持つため):

ALTER TABLE traffic
  ADD COLUMN
    traffic_id INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
  ADD CONSTRAINT traffic_PK 
    PRIMARY KEY (time, traffic_id), 
  ADD INDEX traffic_id_IDX (traffic_id) ;

別の提案

これらのクラスター化されたインデックスはどちらも、最初に提案した(time, type, type_id, bytes_in, bytes_out)とほぼ同じです。

パフォーマンスが向上する可能性がある他の唯一のインデックスは(type, type_id, time, bytes_in, bytes_out)です。ただし、これらのリストにtype_idがどのように含まれるか、およびそれらが参照するデータのパーセンテージによって異なります。

4
ypercubeᵀᴹ