2013-03-26 16 views
14

Hai bisogno di aiuto per ottimizzare una query mysql. Prendiamo per esempio una tabella semplice.Ottimizzazione query di ricerca MySQL

CREATE TABLE `Modules` (
`ID` int(11) NOT NULL AUTO_INCREMENT, 
`moduleName` varchar(100) NOT NULL, 
`menuName` varchar(255) NOT NULL, 
PRIMARY KEY (`ID`), 
KEY `moduleName` (`moduleName`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 

Lets riempirlo con alcuni dati:

INSERT INTO `Modules` (`moduleName` ,`menuName`) 
VALUES 
    ('abc1', 'name1'), 
    ('abc', 'name2'), 
    ('ddf', 'name3'), 
    ('ccc', 'name4'), 
    ('fer', 'name5'); 

E qualche stringa campione. Lascia che sia abc_def;

Tradizionalmente stiamo cercando di trovare tutte le righe contenenti la stringa di ricerca.

Al contrario, il mio compito è trovare tutte le righe che contengono moduleName nella stringa di input. Per ora ho seguente query per ottenere il risultato desiderato:

SELECT `moduleName` ,`menuName` 
FROM `Modules` 
WHERE 'abc_def' LIKE(CONCAT(`moduleName`,'%')) 

Ciò restituirà

moduleName | menuName 
--------------------------- 
abc   | name2 

Il problema è, che questa query non utilizza indice.

C'è un modo per forzare l'utilizzo di uno?

+0

È possibile creare indice di copertura e si può vedere la tabella utilizza l'indice automaticamente . –

risposta

11

Sembra che tu abbia frainteso cosa è indice e come può aiutare a velocizzare un query.

Diamo un'occhiata a quale è il tuo indice moduleName. È fondamentalmente un elenco ordinato di mapping da moduleName a ID. E cosa stai selezionando?

SELECT moduleName, menuName 
FROM Modules 
WHERE 'abc_def' LIKE CONCAT(moduleName,'%'); 

Cioè vuoi un po 'due campi per ogni riga che ha qualche relazione con un valore in qualche modo mappato di campo moduleName. Come mai l'indice può aiutarti? Non esiste una corrispondenza esatta e non c'è modo di trarre vantaggio dal fatto che abbiamo i nomi dei moduli ordinati.

Quello che vi serve per trarre vantaggio dall'indice, è quello di avere un assegno di corrispondenza esatta nella condizione:

SELECT moduleName, menuName 
FROM Modules 
WHERE moduleName = LEFT('abc_def', LENGTH(moduleName)); 

Ora noi abbiamo una corrispondenza esatta, ma dal momento che la parte destra della condizione dipende anche su moduleName, questa condizione verrà controllata per ogni riga. Dal momento che nel suo caso, MySQL non è in grado di prevedere il numero di righe corrispondenti, ma può prevedere che avrà bisogno di accedere al disco randon per recuperare menuName per ogni riga corrispondente, MySQL non utilizzerà l'indice.

in modo da avere sostanzialmente due approcci:

  1. se si sa che la condizione restringe il numero di righe corrispondenti in modo significativo, allora si può solo imporre l'indice
  2. un'altra opzione è quella di estendere l'indice a un coprendo l'indice composito (moduleName, menuName), quindi tutti i risultati per la query verranno recuperati direttamente dall'indice (cioè dalla memoria).

L'approccio n. 2 (vedere SQLfiddle) consente di ottenere un indice con una query semplice e offrire prestazioni molto migliori su un tavolo più grande. Sui tavolini, I (i.e., lserni - vedere il commento) non credo valga la pena.

+0

Non avevo capito che avevi già indicato la mia stessa soluzione, ma l'ho fatta prima e meglio. +1, ma aggiungerò un collegamento al mio SQLfiddle alla tua risposta, quindi ritratterò il mio. – LSerni

+0

... piuttosto ho modificato la mia risposta per affrontare un caso d'uso possibile, più complesso, in cui potrebbe essere consigliabile evitare grandi indici di copertura. – LSerni

+0

@Iserni, grazie – newtover

4

Prova ad aggiungere un index hint alla tua domanda.

SELECT `moduleName` ,`menuName` 
FROM `Modules` USE INDEX (col1_index,col2_index) 
WHERE 'abc_def' LIKE(CONCAT(`moduleName`,'%')) 
+0

: 'possible_keys: moduleName' ' chiave: NULL' 'Using where' –

+0

Hai sostituito' col1_index, col2_index' con il nome dell'indice? –

+0

sicuramente! =) SELECT 'moduleName',' menuName' DALL'INDICE '' Moduli' USE ('moduleName') WHERE 'abc_def' LIKE (CONCAT (' moduleName', '%')) –

3

DECLARE @SEARCHING_TEXT AS VARCHAR (500)

SET @SEARCHING_TEXT = 'ab'

SELEZIONA 'moduleName', 'nomemenu' FROM [MODULI] WHERE FREETEXT (MODULENAME, @SEARCHING_TEXT);

7

Si sta effettivamente facendo una regex sul campo, quindi nessuna chiave funzionerà. Tuttavia, nel tuo esempio, si potrebbe fare un po 'più efficiente in quanto ogni moduleName che corrisponde deve essere inferiore o uguale a 'abc_def', in modo da poter aggiungere:

and moduleName <= 'abc_def' 

L'unica altra alternativa che posso pensare is:

where modleName in ('a','ab','abc','abc_','abc_d','abc_de','abc_def') 

Non carino.

+0

'id | select_type | tabella \t | tipo | possibili_keys | chiave | key_len | ref | righe | Extra' '1 | SEMPLICE | Moduli | TUTTI | moduleName | NULL | NULL | NULL | 7 | Usando dove' –

+0

Gli indici funzionano bene con LIKE 'blabla%'. – Vatev

+0

Non in questo caso. Non sta facendo campo come 'blabla%', sta facendo 'stringa' come il campo% – SteveP

3

io non sono sicuro che questa è davvero una bella domanda, ma fa uso dell'indice:

SELECT `moduleName` ,`menuName` 
FROM `Modules` WHERE LEFT('abc_def', 7) = `moduleName` 
UNION ALL 
SELECT `moduleName` ,`menuName` 
FROM `Modules` WHERE LEFT('abc_def', 6) = `moduleName` 
UNION ALL 
SELECT `moduleName` ,`menuName` 
FROM `Modules` WHERE LEFT('abc_def', 5) = `moduleName` 
UNION ALL 
SELECT `moduleName` ,`menuName` 
FROM `Modules` WHERE LEFT('abc_def', 4) = `moduleName` 
UNION ALL 
SELECT `moduleName` ,`menuName` 
FROM `Modules` WHERE LEFT('abc_def', 3) = `moduleName` 
UNION ALL 
SELECT `moduleName` ,`menuName` 
FROM `Modules` WHERE LEFT('abc_def', 2) = `moduleName` 
UNION ALL 
SELECT `moduleName` ,`menuName` 
FROM `Modules` WHERE LEFT('abc_def', 1) = `moduleName` 

soluzione generale

e questa è una soluzione generale, usando una query dinamica:

SET @search='abc_def'; 

SELECT 
    CONCAT(
    'SELECT `moduleName` ,`menuName` FROM `Modules` WHERE ', 
    GROUP_CONCAT(
     CONCAT(
     'moduleName=\'', 
     LEFT(@search, ln), 
     '\'') SEPARATOR ' OR ') 
    ) 
FROM 
    (SELECT DISTINCT LENGTH(moduleName) ln 
    FROM Modules 
    WHERE LENGTH(moduleName)<=LENGTH(@search)) s 
INTO @sql; 

Questo creerà una stringa con una query SQL che ha una condizione WHERE moduleName='abc' OR moduleName='abc_' OR ... e dovrebbe essere abl e per creare rapidamente la stringa a causa dell'indice (se non lo è, può essere migliorato molto usando una tabella indicizzata temporanea con numeri da 1 alla lunghezza massima consentita della stringa, esempio in fiddle dato).Poi si può solo eseguire la query:

PREPARE stmt FROM @sql; 
EXECUTE stmt; 

Vedere violino here.

3

mia risposta può più complessa

alter table Modules add column name_index int 
alter table Modules add index name_integer_index(name_index); 

quando si inserisce a tavola moduli che caculate un valore int di moduleName, qualcosa come select ascii('a')

quando eseguire la query, è sufficiente eseguire

SELECT `moduleName`, `menuName` 
FROM `Modules` 
WHERE name_index > 
    (select ascii('a')) and name_index < (select ascii('abc_def')) 

utilizzerà name_integr_index

+0

Da [documentazione MySQL] (http://dev.mysql.com/doc/refman/5.0/en/string-functions.html#function_ascii): "ASCII() - Restituisce il valore numerico del carattere più a sinistra". Così ascii ('a') e ascii ('abc_def') restituiscono lo stesso valore. –

3

Simile al suggerimento di fthiella, ma più flessibile (come si può far fronte con la stringa più facilmente): -

SELECT DISTINCT `moduleName` ,`menuName` 
FROM `Modules` 
CROSS JOIN (SELECT a.i + b.i * 10 + c.i * 100 + 1 AS anInt FROM integers a, integers b, integers c) Sub1 
WHERE LEFT('abc_def', Sub1.anInt) = `moduleName` 

Questa (come digitato) affronta con stringa di lunghezza massima di 1000 caratteri, ma è più lento di soluzione fthiellas. Può essere facilmente ridotto per stringhe lunghe fino a 100 caratteri, a quel punto sembra leggermente più veloce della soluzione fthiellas.

Mettere un assegno per la lunghezza in esso non accelerarlo un po ': -

SELECT SQL_NO_CACHE DISTINCT `moduleName` ,`menuName` 
FROM `Modules` 
INNER JOIN (SELECT a.i + b.i * 10 + c.i * 100 + 1 AS anInt FROM integers a, integers b, integers c) Sub1 
ON Sub1.anInt <= LENGTH('abc_def') AND Sub1.anInt <= LENGTH(`moduleName`) 
WHERE LEFT('abc_def', Sub1.anInt) = `moduleName` 

o con un leggero modifica per portare le possibili sottostringhe ritorno dalla subselect: -

SELECT SQL_NO_CACHE DISTINCT `moduleName` ,`menuName` 
FROM `Modules` 
CROSS JOIN (SELECT DISTINCT LEFT('abc_def', a.i + b.i * 10 + c.i * 100 + 1) AS aStart FROM integers a, integers b, integers c WHERE(a.i + b.i * 10 + c.i * 100 + 1) <= LENGTH('abc_def')) Sub1 
WHERE aStart = `moduleName` 

Nota che queste soluzioni dipendono da una tabella di numeri interi con una singola colonna e righe con i valori da 0 a 9.

3

come query non utilizzano indici ... ma in alternativa è possibile definire un indice di testo completo per la ricerca di stringhe come questa. ma il motore di innodb non lo supporta, solo myisam lo supporta.

3

Aggiungi chiave indice per moduleName controllo http://dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html B-tree indice Caratteristiche per maggiori informazioni

Non certo perché si utilizza LIKE, la sua sempre meglio evitarlo. Il mio suggerimento sarebbe quello di ottenere tutte le righe salvarlo in JSON e quindi eseguire la ricerca AJAX su di esso.

+0

Non posso garantirvi con JSON. Quando ho avuto una situazione simile con più di 30.000 righe dalla query, l'uso di JSON mi ha dato un risultato migliore rispetto ad altri esperimenti che ho fatto. –

4

Dal momento che, il motore dtabase è "InnoDB" Tutti i dati degli utenti per impostazione predefinita in InnoDB viene memorizzato in pagine che compongono un indice B-tree

B-tree are good for following lookups: 
● Exact full value (= xxx) 
● Range of values (BETWEEN xx AND yy) 
● Column prefix (LIKE 'xx%') 
● Leftmost prefix 

Così, per la tua ricerca, piuttosto che utilizzare indice o qualcosa da ottimizzare, , possiamo pensare a velocizzando la query.

È possibile velocizzare la query creando indice di copertura.

Un indice di copertura si riferisce al caso in cui all fields selected in a query are covered by an index, in tal caso InnoDB (non MyISAM) will never read the data in the table, but only use the data in the index, significantly speeding up the select. Si noti che in InnoDB la chiave primaria è inclusa in tutti gli indici secondari, quindi in un certo senso tutti gli indici secondari sono indici composti. Questo significa che se si esegue la seguente query su InnoDB:

SELECT `moduleName` ,`menuName` 
FROM `Modules1` 
WHERE 'abc_def' LIKE(CONCAT(`moduleName`,'%')) 

MySQL will always use a covering index and will not access the actual table 

To believe, go to **Explain** 

What does Explain statement mean? 

table: Indica quale tabella l'uscita è interessato.

type: Mostra il tipo di join utilizzato. Dal migliore al peggiore i tipi sono: il sistema, const, eq_ref, ref, gamma, indice, tutto

possible_keys: Indica che gli indici MySQL può scegliere per trovare le righe in questa tabella

key: Indica la chiave (indice) che MySQL ha effettivamente deciso di utilizzare. Se MySQL decide di utilizzare uno degli indici possible_keys per cercare le righe, tale indice viene elencato come valore chiave.

key_len: È la lunghezza della chiave utilizzata. Più corto è, meglio è.

ref: quale colonna (o costante) viene utilizzato

rows: Il numero di righe MySQL ritiene esso deve verificare per eseguire la query.

extra Extra info: i cattivi da vedere qui sono "utilizzando temporaneo" e "utilizzando FileSort"

ho avuto 1.990 righe.

miei esperimenti:

mi sento di raccomandare la soluzione di Isern per cui la clausola

case 1) no indexes 
explain select `moduleName` ,`menuName` FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName)); 
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra  | 
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 
| 1 | SIMPLE  | Modules | ALL | NULL   | NULL | NULL | NULL | 2156 | Using where | 
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 
1 row in set (0.00 sec) 

modi di creare che coprono indici

case 2) ALTER TABLE `test`.`Modules1` ADD index `mod_name` (`moduleName`) 

explain select `moduleName` ,`menuName` FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName)); 
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra  | 
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 
| 1 | SIMPLE  | Modules | ALL | NULL   | NULL | NULL | NULL | 2156 | Using where | 
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 

Qui, mostra l'indice utilizzato. Vedere le colonne: chiave, Extra

case 3) ALTER TABLE `test`.`Modules1` DROP INDEX `mod_name` , 
ADD INDEX `mod_name` ( `moduleName` , `menuName`) 

    explain select `moduleName` ,`menuName` FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName)); 
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+ 
| id | select_type | table | type | possible_keys | key  | key_len | ref | rows | Extra     | 
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+ 
| 1 | SIMPLE  | Modules | index | NULL   | mod_name | 1069 | NULL | 2066 | Using where; Using index | 
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+ 
1 row in set (0.00 sec) 


case 4) ALTER TABLE `test`.`Modules1` DROP INDEX `mod_name` , 
ADD INDEX `mod_name` ( `ID` , `moduleName` , `menuName`) 

    explain select `moduleName` ,`menuName` FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName)); 
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+ 
| id | select_type | table | type | possible_keys | key  | key_len | ref | rows | Extra     | 
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+ 
| 1 | SIMPLE  | Modules | index | NULL   | mod_name | 1073 | NULL | 2061 | Using where; Using index | 
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+ 
1 row in set (0.00 sec) 

edit:

use where moduleName regexp "^(a|ab|abc|abc_|abc_d|abc_de|abc_def)$"; 
in place of substring() 
+0

Possiamo provare di più per ottimizzare diminuendo la colonna key_len che aumenterà le prestazioni –

3

(parte precedente della risposta cancellato - vedi la risposta di newtover che è lo stesso, ma meglio, per questo).

di newtover approccio # 2 (vedere SQLfiddle) ti porterà un indice colpito con una semplice query, e dovrebbe offrire prestazioni migliori su tavoli più lunghi:

SELECT `moduleName`, `menuName` 
FROM `Modules` 
WHERE moduleName = LEFT('abc_def', LENGTH(moduleName)); 

Se avete bisogno di dati da un sacco di colonne (anziché solo menuName), vale a dire se Modules è più grande e più lungo, è meglio servire spostando moduleName in una tabella di ricerca contenente solo ID, moduleName e la sua lunghezza (per salvare una chiamata di funzione).

Lo spazio effettivo aggiuntivo necessario è piccolo, e se moduleName ha una cardinalità bassa, vale a dire, si hanno poche moduleName s ripetuti lungo un sacco di menuName s, si potrebbe effettivamente finire notevole risparmio di spazio.

Il nuovo schema sarà:

moduleName_id integer, keys to Lookup.id 
...all the fields in Modules except moduleName... 


Lookup table 
    id   primary key 
    moduleName varchar 
    moduleLength integer 

e la query:

SELECT `Lookup`.`moduleName`,`menuName` 
FROM `Modules` INNER JOIN `Lookup` 
    ON (`Modules`.`moduleName_id` = Lookup.id) 
WHERE `Lookup`.`moduleName` = LEFT('abc_def', 
     `Lookup`.`moduleLength`); 

Questo SQLfiddle parte dallo schema e lo modifica per realizzare quanto sopra. I miglioramenti dello spazio di archiviazione e della velocità dipendono fortemente dai dati inseriti nelle tabelle.Mi sono messo intenzionalmente nelle migliori condizioni (molti campi brevi nei moduli, una media di cento menuName s per ogni moduleName) ed è stato in grado di risparmiare circa il 30% dello spazio di archiviazione; le prestazioni di ricerca erano solo di circa 3 volte più veloci e probabilmente distorte dal caching degli I/O, quindi a meno che qualcuno non esegua test più approfonditi, lo lascerei a "spazio apprezzabile e risparmi di tempo sono possibili".

D'altra parte, su tabelle piccole e semplici e un numero uguale di menu e moduli (ad esempio 1: 1), ci sarà una leggera penalità di memorizzazione per nessun guadagno di velocità apprezzabile. In quella situazione tuttavia spazi e tempi coinvolti saranno molto piccoli, quindi forse la forma più "normalizzata" sopra potrebbe ancora essere la strada da percorrere, nonostante la complessità aggiunta.

0

Siamo in grado di ottenere con un funciton stesso instead di 2 funzioni SUBSTRING ('abc_def', 1, Lunghezza (moduleName))

where locate(moduleName, 'abc_def'); 
Problemi correlati