MySQL richiede FORCE INDEX su un tavolo enorme e semplici SELECT


8

Abbiamo un'applicazione che memorizza articoli da diverse fonti in una tabella MySQL e consente agli utenti di recuperare quegli articoli ordinati per data. Gli articoli vengono sempre filtrati per origine, quindi per i SELECT client abbiamo sempre

WHERE source_id IN (...,...) ORDER BY date DESC/ASC

Stiamo usando IN, perché gli utenti hanno molti abbonamenti (alcuni ne hanno migliaia).

Ecco lo schema della tabella degli articoli:

CREATE TABLE `articles` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `source_id` INTEGER(11) UNSIGNED NOT NULL,
  `date` DOUBLE(16,6) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `source_id_date` (`source_id`, `date`),
  KEY `date` (`date`)
)ENGINE=InnoDB
AUTO_INCREMENT=1
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';

Abbiamo bisogno dell'indice (data), perché a volte eseguiamo operazioni in background su questa tabella senza filtrare per origine. Gli utenti tuttavia non possono farlo.

La tabella ha circa 1 miliardo di record (sì, stiamo prendendo in considerazione lo sharding per il futuro ...). Una query tipica è simile alla seguente:

SELECT a.id, a.date, s.name
FROM articles a FORCE INDEX (source_id_date)
     JOIN sources s ON s.id = a.source_id
WHERE a.source_id IN (1,2,3,...)
ORDER BY a.date DESC
LIMIT 10

Perché FORCE INDEX? Perché si è scoperto che MySQL a volte sceglie di utilizzare l'indice (data) per tali query (forse a causa della sua lunghezza inferiore?) E questo si traduce in scansioni di milioni di record. Se rimuoviamo l'INDICE FORZA in produzione, i core della CPU del nostro server di database vengono massimizzati in pochi secondi (si tratta di applicazioni OLTP e query come quelle sopra vengono eseguite a velocità di circa 2000 al secondo).

Il problema con questo approccio è che alcune query (sospettiamo che siano in qualche modo correlate al numero di source_ids nella clausola IN) funzionano davvero più velocemente con l'indice della data. Quando eseguiamo EXPLAIN su quelli vediamo che l'indice source_id_date analizza decine di milioni di record, mentre l'indice della data analizza solo alcune migliaia. Di solito è il contrario, ma non possiamo trovare una relazione solida.

Idealmente, volevamo scoprire perché MySQL Optimizer sceglie l'indice errato e rimuove l'istruzione FORCE INDEX, ma un modo per prevedere quando forzare l'indice delle date funzionerà anche per noi.

Alcuni chiarimenti:

La query SELECT sopra è molto semplificata ai fini di questa domanda. Ha diversi JOIN su tabelle con circa 100 milioni di righe ciascuna, uniti al PK (articles_user_flags.id = article.id), che aggrava il problema quando ci sono milioni di righe da ordinare. Inoltre alcune query hanno ulteriori dove, ad esempio:

SELECT a.id, a.date, s.name
FROM articles a FORCE INDEX (source_id_date)
     JOIN sources s ON s.id = a.source_id
     LEFT JOIN articles_user_flags auf ON auf.article_id=a.id AND auf.user_id=1
WHERE a.source_id IN (1,2,3,...)
AND auf.starred=1
ORDER BY a.date DESC
LIMIT 10

Questa query elenca solo articoli speciali per l'utente specifico (1).

Il server esegue MySQL versione 5.5.32 (Percona) con XtraDB. L'hardware è 2xE5-2620, 128 GB di RAM, 4HDDx1TB RAID10 con controller a batteria. I SELECT problematici sono completamente associati alla CPU.

my.cnf è il seguente (rimosse alcune direttive non correlate come id server, porta, ecc ...):

transaction-isolation           = READ-COMMITTED
binlog_cache_size               = 256K
max_connections                 = 2500
max_user_connections            = 2000
back_log                        = 2048
thread_concurrency              = 12
max_allowed_packet              = 32M
sort_buffer_size                = 256K
read_buffer_size                = 128K
read_rnd_buffer_size            = 256K
join_buffer_size                = 8M
myisam_sort_buffer_size         = 8M
query_cache_limit               = 1M
query_cache_size                = 0
query_cache_type                = 0
key_buffer                      = 10M
table_cache                     = 10000
thread_stack                    = 256K
thread_cache_size               = 100
tmp_table_size                  = 256M
max_heap_table_size             = 4G
query_cache_min_res_unit        = 1K
slow-query-log                  = 1
slow-query-log-file             = /mysql_database/log/mysql-slow.log
long_query_time                 = 1
general_log                     = 0
general_log_file                = /mysql_database/log/mysql-general.log
log_error                       = /mysql_database/log/mysql.log
character-set-server            = utf8

innodb_flush_method             = O_DIRECT
innodb_flush_log_at_trx_commit  = 2
innodb_buffer_pool_size         = 105G
innodb_buffer_pool_instances    = 32
innodb_log_file_size            = 1G
innodb_log_buffer_size          = 16M
innodb_thread_concurrency       = 25
innodb_file_per_table           = 1

#percona specific
innodb_buffer_pool_restore_at_startup           = 60

Come richiesto, ecco alcuni SPIEGAZIONE delle domande problematiche:

mysql> EXPLAIN SELECT a.id,a.date AS date_double
    -> FROM articles a
    -> FORCE INDEX (source_id_date)
    -> JOIN sources s ON s.id = a.source_id WHERE
    -> a.source_id IN (...) --Around 1000 IDs
    -> ORDER BY a.date LIMIT 20;
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
| id | select_type | table | type   | possible_keys   | key            | key_len | ref                       | rows     | Extra                                    |
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
|  1 | SIMPLE      | a     | range  | source_id_date  | source_id_date | 4       | NULL                      | 13744277 | Using where; Using index; Using filesort |
|  1 | SIMPLE      | s     | eq_ref | PRIMARY         | PRIMARY        | 4       | articles_db.a.source_id   |        1 | Using where; Using index                 |
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
2 rows in set (0.01 sec)

L'attuale SELECT richiede circa un minuto ed è completamente associato alla CPU. Quando cambio l'indice in (data) che in questo caso l'ottimizzatore MySQL sceglie anche automaticamente:

mysql> EXPLAIN SELECT a.id,a.date AS date_double
    -> FROM articles a
    -> FORCE INDEX (date)
    -> JOIN sources s ON s.id = a.source_id WHERE
    -> a.source_id IN (...) --Around 1000 IDs
    -> ORDER BY a.date LIMIT 20;

+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref                       | rows | Extra                    |
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
|  1 | SIMPLE      | a     | index  | NULL          | date    | 8       | NULL                      |   20 | Using where              |
|  1 | SIMPLE      | s     | eq_ref | PRIMARY       | PRIMARY | 4       | articles_db.a.source_id   |    1 | Using where; Using index |
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+

2 rows in set (0.01 sec)

E il SELEZIONA richiede solo 10ms.

Ma EXPLAINs può essere molto rotto qui! Ad esempio, se SPIEGO una query con un solo source_id nella clausola IN e un indice forzato il (data), mi dice che scansionerà solo 20 righe, ma ciò non è possibile, poiché la tabella ha oltre 1 miliardo di righe e solo alcune corrisponde a questo source_id.


"Quando corriamo analizziamo quelli ..." Vuoi dire EXPLAIN? ANALYZEè qualcosa di diverso, ed è probabilmente qualcosa da considerare se non lo hai fatto, come una possibile spiegazione è che le statistiche dell'indice distorte stanno distraendo l'ottimizzatore dalla scelta saggia. Non penso che ci sia bisogno di my.cnf nella domanda, e quello spazio potrebbe essere meglio usato per pubblicare alcuni EXPLAINoutput delle variazioni nel comportamento che vedi ... dopo aver investigato ANALYZE [LOCAL] TABLE...
Michael - sqlbot

Sì, questo è stato un refuso, grazie per la correzione. L'ho risolto. Ovviamente abbiamo fatto ANALISI, ma questo non ha aiutato affatto. Proverò a catturare alcuni EXPLAIN più tardi.
Giacca il

Ed dateè un DOUBLE...?
ypercubeᵀᴹ

Sì, perché qui abbiamo bisogno della precisione dei microsecondi. La tariffa di inserimento in questa tabella è di circa 400.000 voci all'ora e abbiamo bisogno che le date siano il più uniche possibile.
Giacca,

@Jacket Puoi pubblicare un EXPLAIN fuori dalla query offensiva? penso che perché è legato alla CPU il tuo server sta eseguendo il quicksorting ("usando filesort per spiegare)" il tuo set di risultati ..
Raymond Nijland

Risposte:


4

È possibile verificare il valore per il parametro innodb_stats_sample_pages . Controlla il numero di immersioni dell'indice eseguite da MySQL su una tabella durante l'aggiornamento delle statistiche dell'indice, che a loro volta vengono utilizzate per calcolare il costo di un piano di join candidato. Il valore predefinito era 8 per la versione che stavamo usando. Lo abbiamo cambiato in 128 e abbiamo osservato meno piani di join inattesi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.