it-swarm.com.de

MySQL zählen Leistung auf sehr großen Tischen

Ich habe eine Tabelle mit mehr als 100 Millionen Zeilen in Innodb.

Ich muss wissen, ob es mehr als 5000 Zeilen gibt, in denen der Fremdschlüssel = 1 ist. Ich benötige nicht die genaue Nummer.

Ich habe einige Tests gemacht:

SELECT COUNT(*) FROM table WHERE fk = 1 => 16 Sekunden
SELECT COUNT(*) FROM table WHERE fk = 1 LIMIT 5000 => 16 Sekunden
SELECT primary FROM table WHERE fk = 1 => 0,6 Sekunden

Ich werde ein größeres Netzwerk und eine längere Behandlungszeit haben, aber es kann eine Überlastung von 15,4 Sekunden sein!

Hast du eine bessere Idee?

Vielen Dank

Bearbeiten: [Relevante Kommentare von OP hinzugefügt]

Ich habe SELECT SQL_NO_CACHE COUNT (fk) FROM table WHERE fk = 1 ausprobiert, aber es hat 25 Sekunden gedauert

Mysql wurde für Innodb mit Mysql Tuner abgestimmt.

CREATE TABLE table ( pk bigint(20) NOT NULL AUTO_INCREMENT,
fk tinyint(3) unsigned DEFAULT '0', 
PRIMARY KEY (pk), KEY idx_fk (fk) USING BTREE ) 
ENGINE=InnoDB AUTO_INCREMENT=100380914 DEFAULT CHARSET=latin1

DB-Zeug:

'have_innodb', 'YES' 'ignore_builtin_innodb', 'OFF' 'innodb_adaptive_hash_index', 'ON'    
'innodb_additional_mem_pool_size', '20971520' 'innodb_autoextend_increment', '8' 
'innodb_autoinc_lock_mode', '1' 'innodb_buffer_pool_size', '25769803776' 
'innodb_checksums', 'ON' 'innodb_commit_concurrency', '0',
'innodb_concurrency_tickets', '500' 'innodb_data_file_path',
'ibdata1:10M:autoextend' 'innodb_data_home_dir', '', 'innodb_doublewrite', 'ON'     
'innodb_fast_shutdown', '1' 'innodb_file_io_threads', '4' 
'innodb_file_per_table', 'OFF', 'innodb_flush_log_at_trx_commit', '1' 
'innodb_flush_method', '' 'innodb_force_recovery', '0' 'innodb_lock_wait_timeout', '50' 
'innodb_locks_unsafe_for_binlog', 'OFF' 'innodb_log_buffer_size', '8388608' 
'innodb_log_file_size', '26214400' 'innodb_log_files_in_group', '2' 
'innodb_log_group_home_dir', './' 'innodb_max_dirty_pages_pct', '90'     
'innodb_max_purge_lag', '0' 'innodb_mirrored_log_groups', '1' 'innodb_open_files', 
'300' 'innodb_rollback_on_timeout', 'OFF' 'innodb_stats_on_metadata', 'ON' 
'innodb_support_xa', 'ON' 'innodb_sync_spin_loops', '20' 'innodb_table_locks', 'ON' 
'innodb_thread_concurrency', '8' 'innodb_thread_sleep_delay', '10000'      
'innodb_use_legacy_cardinality_algorithm', 'ON'

pdate '15: Ich habe die gleiche Methode bisher mit 600 Millionen Zeilen und 640 000 neuen Zeilen pro Tag verwendet. Es funktioniert immer noch gut.

35
hotips

Am schnellsten war es schließlich, die ersten X-Zeilen mit C # abzufragen und die Zeilennummer zu zählen.

Meine Anwendung behandelt die Daten in Chargen. Die Zeitspanne zwischen zwei Chargen hängt von der Anzahl der zu behandelnden Reihen ab

SELECT pk FROM table WHERE fk = 1 LIMIT X

Ich habe das Ergebnis in 0,9 Sekunden erhalten.

Vielen Dank für Ihre Ideen!

1
hotips

Sie scheinen nicht an der tatsächlichen Anzahl interessiert zu sein, probieren Sie es also aus:

SELECT 1 FROM table WHERE fk = 1 LIMIT 5000, 1

Wenn eine Zeile zurückgegeben wird, haben Sie 5000 und mehr Datensätze. Ich gehe davon aus, dass die fk -Spalte indiziert ist.

19
Salman A

Counter-Tabellen oder andere Caching-Mechanismen sind die Lösung:

InnoDB führt keine interne Anzahl von Zeilen in einer Tabelle, da bei gleichzeitigen Transaktionen möglicherweise unterschiedliche Zeilenzahlen gleichzeitig angezeigt werden. Um eine SELECT COUNT (*) FROM t-Anweisung zu verarbeiten, durchsucht InnoDB einen Index der Tabelle, was einige Zeit in Anspruch nimmt, wenn sich der Index nicht vollständig im Pufferpool befindet. Wenn sich Ihre Tabelle nicht oft ändert, ist die Verwendung des MySQL-Abfrage-Cache eine gute Lösung. Um eine schnelle Zählung zu erhalten, müssen Sie eine von Ihnen selbst erstellte Zählertabelle verwenden und Ihre Anwendung entsprechend den Einfügungen und Löschungen aktualisieren lassen. Wenn eine ungefähre Zeilenzahl ausreicht, kann SHOW TABLE STATUS verwendet werden. Siehe Abschnitt 14.3.14.1, „Tipps zur Optimierung der InnoDB-Leistung“ .

19
scriptin

Ich muss noch eine Antwort hinzufügen - Ich habe bis jetzt viele Korrekturen/Ergänzungen an den Kommentaren und Antworten.

Für MyISAM ist SELECT COUNT(*) ohne WHERE absolut sicher - sehr schnell. In allen anderen Situationen (einschließlich der InnoDB in der Frage) muss entweder der BTree der Daten oder der BTree eines Index gezählt werden, um die Antwort zu erhalten. Wir müssen also sehen, wie viel wir durchzählen müssen.

InnoDB speichert Daten- und Indexblöcke zwischen (jeweils 16 KB). Aber wenn die Daten oder der Index der Tabelle BTree größer als innodb_buffer_pool_size Sind, werden Sie garantiert auf die Festplatte stoßen. Das Schlagen des Datenträgers ist fast immer der langsamste Teil von SQL.

Wenn der Abfrage-Cache beteiligt ist, ergeben sich normalerweise Abfragezeiten von etwa 1 Millisekunde. Dies scheint bei keinem der angegebenen Zeitpunkte ein Problem zu sein. Also werde ich nicht weiter darauf eingehen.

Aber ... Das zweimalige Ausführen der gleichen Abfrage in einer Reihe wird häufig Ausstellungsstück:

  • Erster Lauf: 10 Sekunden
  • Zweiter Lauf: 1 Sekunde

Dies ist symptomatisch dafür, dass der erste Lauf die meisten Blöcke von der Festplatte abrufen muss, während der zweite alles in RAM (the buffer_pool) gefunden hat. Ich vermute, dass einige der aufgelisteten Timings falsch sind, weil von nicht realisiert dieses Caching-Problem. (16 Sek. vs 0,6 Sek. kann erklärt werden.)

Ich werde auf "Disk-Hits" oder "Blöcke, die berührt werden mussten" harfen, da die reale Metrik, deren SQL schneller ist.

COUNT(x) prüft x vor dem Zählen auf IS NOT NULL. Dies fügt einen winzigen Verarbeitungsaufwand hinzu, ändert jedoch nicht die Anzahl der Datenträgertreffer.

Die angebotene Tabelle hat eine PK und eine zweite Spalte. Ich frage mich, ob das die echte Tabelle ist? Es macht einen Unterschied -

  • Wenn der Optimierer beschließt, die Daten zu lesen , dh in der Reihenfolge PRIMARY KEY Zu scannen, werden die Daten gelesen. Das ist normalerweise (aber nicht in diesem lahmen Beispiel) viel breiter als BTrees des Sekundärindex.
  • Wenn der Optimierer beschließt, einen sekundären Index zu lesen (aber keine Sortierung durchführen muss), müssen weniger Blöcke berührt werden. Daher schneller.

Kommentare zu den ursprünglichen Abfragen:

SELECT COUNT(*) FROM table WHERE fk = 1 => 16 seconds
    -- INDEX(fk) is optimal, but see below
SELECT COUNT(*) FROM table WHERE fk = 1 LIMIT 5000 => 16 seconds
    -- the LIMIT does nothing, since there is only one row in the result
SELECT primary FROM table WHERE fk = 1 => 0.6 seconds
    -- Again INDEX(fk), but see below

WHERE fk = 1 Bittet um INDEX(fk, ...), vorzugsweise nur um INDEX(fk). Beachten Sie, dass in InnoDB jeder Sekundärindex eine Kopie des Pakets enthält. Das heißt, INDEX(fk) ist effektiv INDEX(fk, primary). Daher kann die dritte Abfrage diese als "Abdeckung" verwenden und muss die Daten nicht berühren.

Wenn die Tabelle wirklich nur aus zwei Spalten besteht, ist wahrscheinlich der Sekundärindex BTree fetter als der Daten-BTree. In realistischen Tabellen ist der Sekundärindex jedoch kleiner. Daher ist ein Index-Scan schneller (weniger Blöcke zum Berühren) als ein Tabellenscan.

Die dritte Abfrage liefert ebenfalls eine große Ergebnismenge. Dies kann dazu führen, dass die Abfrage viel Zeit in Anspruch nimmt - , aber sie wird nicht in die angegebene "Zeit" einbezogen. Es ist Netzwerkzeit, keine Abfragezeit.

innodb_buffer_pool_size = 25,769,803,776 Ich würde vermuten, dass die Tabelle und ihr Sekundärindex (von der FK) jeweils etwa 3-4 GB groß sind. Daher kann es sein, dass jedes Timing zuerst eine Menge Sachen laden muss. Dann würde ein zweiter Lauf vollständig zwischengespeichert. (Natürlich weiß ich nicht, wie viele Zeilen fk=1 Haben; vermutlich weniger als alle Zeilen?)

Aber ... Bei 600 Millionen Zeilen sind die Tabelle und ihr Index jeweils Annäherung an den 25GB Buffer_pool. Es kann also sein, dass der Tag bald kommt, an dem die E/A-Grenze überschritten wird. Dann möchten Sie auf 16 (oder 25) Sekunden zurückkehren. doch du wirst nicht dazu in der Lage sein. Wir können dann über Alternativen zum COUNT sprechen.

SELECT 1 FROM tbl WHERE fk = 1 LIMIT 5000,1 - Lassen Sie uns dies analysieren. Der Index wird durchsucht, aber nach 5000 Zeilen gestoppt. Von allem, was Sie brauchen, ist "mehr als 5K", das ist der beste Weg, um es zu bekommen. Unabhängig von der Gesamtanzahl der Zeilen in der Tabelle ist die Geschwindigkeit konstant hoch (sie berührt nur ein Dutzend Blöcke). (Es unterliegt immer noch den Merkmalen buffer_pool_size und cache des Systems. Aber ein Dutzend Blöcke benötigt viel weniger als eine Sekunde, selbst bei einem kalten Cache.)

MariaDB's LIMIT ROWS_EXAMINED kann einen Blick wert sein. Ohne das könnten Sie tun

SELECT COUNT(*) AS count_if_less_than_5K
    FROM ( SELECT 1 FROM tbl WHERE fk = 1 LIMIT 5000 );

Es kann schneller sein, als die Zeilen an den Client zu liefern. Es muss die Zeilen intern in einer tmp-Tabelle sammeln, aber nur die COUNT liefern.

Eine Randnotiz: 640K Zeilen pro Tag eingefügt - dies nähert sich dem Limit für einzeilige INSERTs in MySQL mit Ihren aktuellen Einstellungen auf einer Festplatte (nicht SDD). Wenn Sie die potenzielle Katastrophe besprechen müssen, öffnen Sie eine andere Frage.

Endeffekt:

  • Vermeiden Sie unbedingt den Abfrage-Cache. (durch Verwendung von SQL_NO_CACHE oder Ausschalten der QC)
  • Führen Sie eine Zeitabfrage zweimal aus. benutze das zweite mal.
  • Verstehen Sie die Struktur und Größe der beteiligten BTrees.
  • Verwenden Sie COUNT(x) nur, wenn Sie die Nullprüfung benötigen.
  • Verwenden Sie nicht die PHP-Schnittstelle mysql_*. Wechseln Sie zu mysqli_* oder PDO.
6
Rick James

Wenn Sie PHP verwenden, können Sie mysql_num_rows über das Ergebnis von SELECT primary FROM table WHERE fk = 1 => 0.6 seconds, Ich denke das wird effizient sein.

Aber hängt davon ab, welche serverseitige Sprache Sie verwenden

1
nischayn22

Wenn Sie nicht an der Anzahl der Zeilen interessiert sind und nur den COUNT-Wert mit einem bestimmten Wert vergleichen möchten, können Sie das folgende Standardskript verwenden:

SELECT 'X'
FROM mytable
WHERE myfield='A'
HAVING COUNT(*) >5

Dies gibt eine einzelne Zeile oder gar keine Zeile zurück, je nachdem, ob die Bedingung erfüllt ist.

Dieses Skript ist ANSI-kompatibel und kann vollständig ausgeführt werden, ohne den vollständigen Wert von COUNT (*) auszuwerten. Wenn MySQL eine Optimierung implementiert, um die Auswertung von Zeilen zu beenden, nachdem eine Bedingung erfüllt ist (ich hoffe wirklich, dass dies der Fall ist), werden Sie eine Leistungsverbesserung erhalten. Leider kann ich dieses Verhalten nicht selbst testen, da mir keine große MySQL-Datenbank zur Verfügung steht. Wenn du diesen Test machst, teile das Ergebnis bitte hier :)

0
Gerardo Lima