Wie kann ich die Geschwindigkeit einer Abfrage für eine Zeilentabelle mit mehr als 20 Millionen verbessern?


7

Ich habe eine Abfrage, die zum Abrufen von Internet-Verkehrsstatistiken bestimmter IP-Adressen verwendet wird.

Es gibt separate IP-Adressfelder für hostsund IP- Blocks, die aufgerufen werden assignments. Die Daten werden in 5-Minuten-Intervallen gespeichert.

Die Abfrageergebnisse werden in der Zeitspalte gruppiert, und die Gesamtzahl der SUMs in und aus diesen 5-Minuten-Intervallen wird zum Zeichnen eines Diagramms verwendet.

Die Tabelle wird aufgerufen trafficund enthält (Ende des Monats) rund 21 Millionen Datensätze.

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;

explainFür die obige Abfrage wird Folgendes ausgegeben:

+----+-------------+---------+-------+---------------+--------------+---------+------+--------+----------------------------------------------+
| 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      |         |
+---------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Diese Abfrage dauert zwischen 30 Sekunden und 30 Minuten. Ich hoffe, ich kann die Dinge mit besseren Indizes oder vielleicht mit einer anderen Abfrage verbessern, aber ich kann es nicht herausfinden.

AKTUALISIEREN:

Auf Anraten der hilfreichen Kommentatoren habe ich einen Primärschlüssel erstellt und den Index hinzugefügt traffic_pk (time, type, type_id, id). Leider stellt sich heraus, dass die Kardinalität dieses neuen Index gleich / niedriger als mein ursprünglicher Index (basic_select) ist und MySQL weiterhin meinen ursprünglichen Schlüssel verwendet.

UPDATE 2: Ich habe meinen ursprünglichen Index gelöscht basic_selectund jetzt EXPLAINzeigt der einen höheren rowsWert, aber weniger Schritte in den EXTRAFeldern. Auch die Ausführungszeit der Abfrage ist auf unter eine Minute gesunken! (immer noch etwas zu langsam, aber eine große Verbesserung!).

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

Die Indizes in der Tabelle:

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      |         |
+--------------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Außerdem habe ich die Abfrage vereinfacht, indem ich nicht Folgendes verwendet habe 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;

Alte Ausführung dieser Abfrage:

3980 rows in set (6 min 15.27 sec)

Neue Ausführungszeit:

3980 rows in set (24.80 sec)

EXPLAIN-Ausgabe:

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

Der Zeilenwert ist immer noch ziemlich hoch. Ich denke, ich kann dies verbessern, indem ich die Reihenfolge von typeund type_idim Index ändere, da nur 4 Typen möglich sind und viele weitere type_ids.

Ist das eine richtige Annahme?


Schneiden Sie Spalten in der Tabelle aus, die nicht Ihrer Abfrage entsprechen.
Aaron Kempf

Antworten:


6

1. Tabellenpartitionierung

Aufgrund der Klausel [AND Traffic.time> = 1343772000 AND Traffic.time <1346450399] stelle ich mir vor, dass Sie niemals Daten aus dieser Tabelle löschen oder dass die Tabelle derzeit Daten für mehrere Monate speichert. Die Werte in der Spalte [Zeit] scheinen Unix-Zeitstempel zu sein (1346450399 = Fr, 31. August 2012, 21:59:59 GMT) Partitionieren Sie die Tabelle basierend auf der Zeitspalte. Dies beschleunigt den Datenabruf, da die Datenbank die entsprechende Partition scannt (viel schneller als das Scannen der gesamten Tabelle).

2. Schreiben Sie die Abfrage neu

Aufgrund des "ODER" in Ihrem WHERE-Block verwendet der Optimierer den definierten Index nicht. Versuchen Sie, die Abfrage in zwei Auswahlen aufzuteilen und eine Union zu bilden.

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. Neuer Index basierend auf Datenkardinalität

Aufgrund Ihrer EXPLAIN-Ausgabe wird der verwendete Index nicht verwendet. Möglicherweise, weil der Optimierer entscheidet, dass es einfacher (billiger) ist, einen vollständigen Tabellenscan durchzuführen, als dem Index zu folgen. Außerdem hat in Ihrem aktuellen Index die erste Spalte eine niedrigere Kardinalität als die nächsten beiden. Die erste Spalte in einem Index sollte die Spalte mit der besten (maximalen) Kardinalität sein.

Erstellen Sie einen neuen Index als:

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

@Steven V: Wenn Sie Hilfe bei der Partitionierung + automatischen Generierung von Bereichen benötigen, bin ich Ihnen gerne behilflich.
MTIhai

Vielen Dank an @MTIhai für Ihre informative Antwort! Ich habe viele neue Informationen zu prüfen, beginne jedoch mit der Erstellung des neuen Index, da die Verarbeitung einige Stunden dauern wird. Wenn ich Hilfe brauche, werde ich dir eine Nachricht schicken :)
Steven V

Die 3 Empfehlungen sollten als "Gruppe" verstanden werden. Nur das Erstellen des neuen Index (um der besseren Kardinalität zu entsprechen) garantiert nicht, dass Sie eine sichtbare Optimierung erhalten. Führen Sie die Liste R2 + R3 aus (Neuer Index + Abfrage mit Vereinigung). Nachdem Sie den Index erstellt haben, veröffentlichen Sie bitte den Erklärungsplan für die Abfrage bei der Gewerkschaft (ich habe eine seltsame Neugier :) ...)
MTIhai

Die UNIONAbfrage entspricht nicht dem Original.
Ypercubeᵀᴹ

Verwendung UNION ALLwird das beheben, nehme ich an?
Steven V

4

Ich schlage einen zusammengesetzten Index vor (time, type, type_id, bytes_in, bytes_out).

Wenn die (type_id, time, type)Kombination eindeutig ist (was ist übrigens der Primärschlüssel der Tabelle?), Können Sie einfach den Primärschlüssel definieren (time, type, type_id). Dann wäre der Clustered-Index der Tabelle dieser Primärschlüssel, und Sie würden den obigen zusammengesetzten Index nicht benötigen. Abhängig davon, was Ihre häufigsten Abfragen sind (wenn sie diese haben group by timeund / oder where time >=? and time <?mögen), werden sie die Effizienz verbessern, da sie den Clustered-Index verwenden können.

Sie können die Abfrage auch so umschreiben

  • Verwenden =anstelle von LIKEund
  • Kombination GROUP BYmit ORDER BY(proprietäre MySQL-Syntax, die die Effizienz verbessern kann):

    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 ;

Update + Korrektur

Wenn Sie für eine (InnoDB) -Tabelle keinen PRIMARYund keinen UNIQUEIndex definiert haben , wird eine versteckte 6-Byte-Spalte erstellt und als Clustered-Index der Tabelle verwendet.

Daher ist es möglicherweise besser, eine automatisch inkrementierte 4-Byte-Ganzzahlspalte explizit zu definieren und sie in Kombination mit der timeSpalte (oder allen 3 obigen Spalten) als PRIMARYoder UNIQUESchlüssel zu verwenden. Für keinen anderen Zweck als einen Clustered-Index zu haben, der für Ihre Abfragen nützlich ist:

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) ;

oder (um einen engeren Primärschlüssel zu haben):

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) ;

ein weiterer Vorschlag :

Diese beiden gruppierten Indizes entsprechen in etwa (time, type, type_id, bytes_in, bytes_out)den zu Beginn vorgeschlagenen.

Der einzige andere Index, der eine bessere Leistung erzielen könnte, ist der (type, type_id, time, bytes_in, bytes_out). Es hängt jedoch davon ab, wie möglicherweise type_iddiese Listen aufgeführt sind und auf welchen Prozentsatz der Daten sie sich beziehen.


Vielen Dank für Ihre Antwort! Die Tabelle hat keinen eindeutigen Schlüssel, da wir den Verkehr an mehreren Standorten unseres Netzwerks messen, können pro Zeitintervall mehrere Datensätze mit einer bestimmten Kombination aus Typ und Typ_ID eingefügt werden. Was das LIKE betrifft, stimme ich zu, dass das = bequemer ist, aber da ich keine Platzhalter verwende, gehe ich davon aus, dass MySQL dies optimieren wird.
Steven V

Können Sie SHOW CREATE TABLE traffic ;die Ausgabe bei der Frage ausführen und hinzufügen? (oder haben Sie das schon getan?)
ypercubeᵀᴹ

In der Tat ist das Show Create das, was Sie unter "the table Create:" sehen.
Steven V

Ist es richtig, was ich bisher gesehen habe, dass InnoDb keine kombinierten Primärschlüssel mit Autoincrement-Spalten akzeptiert? Wenn ja, denke ich, dass Ihr vorgeschlagener Index nur mit MyISAM funktioniert?
Steven V

Nein, es sollte auch mit InnoDB gut funktionieren.
Ypercubeᵀᴹ
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.