Come ottimizzare SELEZIONA molto lentamente con JOIN SINISTRA su grandi tavoli


14

Stavo cercando su Google, autodidatta e cercavo una soluzione per ore ma senza fortuna. Ho trovato alcune domande simili qui, ma non in questo caso.

I miei tavoli:

  • persone (~ 10 M file)
  • attributi (posizione, età, ...)
  • collegamenti (M: M) tra persone e attributi (~ 40 M righe)

Dump completo ~ 280 MB

Situazione: provo a selezionare tutti gli ID persona ( person_id) da alcune posizioni ( location.attribute_value BETWEEN 3000 AND 7000), essendo un po 'di genere ( gender.attribute_value = 1), nato in alcuni anni ( bornyear.attribute_value BETWEEN 1980 AND 2000) e con il colore degli occhi ( eyecolor.attribute_value IN (2,3)).

Questa è la mia domanda che la strega ha impiegato 3 ~ 4 minuti. e vorrei ottimizzare:

SELECT person_id
FROM person
    LEFT JOIN attribute location ON location.attribute_type_id = 1 AND location.person_id = person.person_id
    LEFT JOIN attribute gender ON gender.attribute_type_id = 2 AND gender.person_id = person.person_id
    LEFT JOIN attribute bornyear ON bornyear.attribute_type_id = 3 AND bornyear.person_id = person.person_id
    LEFT JOIN attribute eyecolor ON eyecolor.attribute_type_id = 4 AND eyecolor.person_id = person.person_id
WHERE 1
    AND location.attribute_value BETWEEN 3000 AND 7000
    AND gender.attribute_value = 1
    AND bornyear.attribute_value BETWEEN 1980 AND 2000
    AND eyecolor.attribute_value IN (2,3)
LIMIT 100000;

Risultato:

+-----------+
| person_id |
+-----------+
|       233 |
|       605 |
|       ... |
|   8702599 |
|   8703617 |
+-----------+
100000 rows in set (3 min 42.77 sec)

Spiegare esteso:

+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| id | select_type | table    | type   | possible_keys                               | key             | key_len | ref                      | rows    | filtered | Extra                    |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
|  1 | SIMPLE      | bornyear | range  | attribute_type_id,attribute_value,person_id | attribute_value | 5       | NULL                     | 1265229 |   100.00 | Using where              |
|  1 | SIMPLE      | location | ref    | attribute_type_id,attribute_value,person_id | person_id       | 5       | test1.bornyear.person_id |       4 |   100.00 | Using where              |
|  1 | SIMPLE      | eyecolor | ref    | attribute_type_id,attribute_value,person_id | person_id       | 5       | test1.bornyear.person_id |       4 |   100.00 | Using where              |
|  1 | SIMPLE      | gender   | ref    | attribute_type_id,attribute_value,person_id | person_id       | 5       | test1.eyecolor.person_id |       4 |   100.00 | Using where              |
|  1 | SIMPLE      | person   | eq_ref | PRIMARY                                     | PRIMARY         | 4       | test1.location.person_id |       1 |   100.00 | Using where; Using index |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
5 rows in set, 1 warning (0.02 sec)

profiling:

+------------------------------+-----------+
| Status                       | Duration  |
+------------------------------+-----------+
| Sending data                 |  3.069452 |
| Waiting for query cache lock |  0.000017 |
| Sending data                 |  2.968915 |
| Waiting for query cache lock |  0.000019 |
| Sending data                 |  3.042468 |
| Waiting for query cache lock |  0.000043 |
| Sending data                 |  3.264984 |
| Waiting for query cache lock |  0.000017 |
| Sending data                 |  2.823919 |
| Waiting for query cache lock |  0.000038 |
| Sending data                 |  2.863903 |
| Waiting for query cache lock |  0.000014 |
| Sending data                 |  2.971079 |
| Waiting for query cache lock |  0.000020 |
| Sending data                 |  3.053197 |
| Waiting for query cache lock |  0.000087 |
| Sending data                 |  3.099053 |
| Waiting for query cache lock |  0.000035 |
| Sending data                 |  3.064186 |
| Waiting for query cache lock |  0.000017 |
| Sending data                 |  2.939404 |
| Waiting for query cache lock |  0.000018 |
| Sending data                 |  3.440288 |
| Waiting for query cache lock |  0.000086 |
| Sending data                 |  3.115798 |
| Waiting for query cache lock |  0.000068 |
| Sending data                 |  3.075427 |
| Waiting for query cache lock |  0.000072 |
| Sending data                 |  3.658319 |
| Waiting for query cache lock |  0.000061 |
| Sending data                 |  3.335427 |
| Waiting for query cache lock |  0.000049 |
| Sending data                 |  3.319430 |
| Waiting for query cache lock |  0.000061 |
| Sending data                 |  3.496563 |
| Waiting for query cache lock |  0.000029 |
| Sending data                 |  3.017041 |
| Waiting for query cache lock |  0.000032 |
| Sending data                 |  3.132841 |
| Waiting for query cache lock |  0.000050 |
| Sending data                 |  2.901310 |
| Waiting for query cache lock |  0.000016 |
| Sending data                 |  3.107269 |
| Waiting for query cache lock |  0.000062 |
| Sending data                 |  2.937373 |
| Waiting for query cache lock |  0.000016 |
| Sending data                 |  3.097082 |
| Waiting for query cache lock |  0.000261 |
| Sending data                 |  3.026108 |
| Waiting for query cache lock |  0.000026 |
| Sending data                 |  3.089760 |
| Waiting for query cache lock |  0.000041 |
| Sending data                 |  3.012763 |
| Waiting for query cache lock |  0.000021 |
| Sending data                 |  3.069694 |
| Waiting for query cache lock |  0.000046 |
| Sending data                 |  3.591908 |
| Waiting for query cache lock |  0.000060 |
| Sending data                 |  3.526693 |
| Waiting for query cache lock |  0.000076 |
| Sending data                 |  3.772659 |
| Waiting for query cache lock |  0.000069 |
| Sending data                 |  3.346089 |
| Waiting for query cache lock |  0.000245 |
| Sending data                 |  3.300460 |
| Waiting for query cache lock |  0.000019 |
| Sending data                 |  3.135361 |
| Waiting for query cache lock |  0.000021 |
| Sending data                 |  2.909447 |
| Waiting for query cache lock |  0.000039 |
| Sending data                 |  3.337561 |
| Waiting for query cache lock |  0.000140 |
| Sending data                 |  3.138180 |
| Waiting for query cache lock |  0.000090 |
| Sending data                 |  3.060687 |
| Waiting for query cache lock |  0.000085 |
| Sending data                 |  2.938677 |
| Waiting for query cache lock |  0.000041 |
| Sending data                 |  2.977974 |
| Waiting for query cache lock |  0.000872 |
| Sending data                 |  2.918640 |
| Waiting for query cache lock |  0.000036 |
| Sending data                 |  2.975842 |
| Waiting for query cache lock |  0.000051 |
| Sending data                 |  2.918988 |
| Waiting for query cache lock |  0.000021 |
| Sending data                 |  2.943810 |
| Waiting for query cache lock |  0.000061 |
| Sending data                 |  3.330211 |
| Waiting for query cache lock |  0.000025 |
| Sending data                 |  3.411236 |
| Waiting for query cache lock |  0.000023 |
| Sending data                 | 23.339035 |
| end                          |  0.000807 |
| query end                    |  0.000023 |
| closing tables               |  0.000325 |
| freeing items                |  0.001217 |
| logging slow query           |  0.000007 |
| logging slow query           |  0.000011 |
| cleaning up                  |  0.000104 |
+------------------------------+-----------+
100 rows in set (0.00 sec)

Tabelle strutture:

CREATE TABLE `attribute` (
  `attribute_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `attribute_type_id` int(11) unsigned DEFAULT NULL,
  `attribute_value` int(6) DEFAULT NULL,
  `person_id` int(11) unsigned DEFAULT NULL,
  PRIMARY KEY (`attribute_id`),
  KEY `attribute_type_id` (`attribute_type_id`),
  KEY `attribute_value` (`attribute_value`),
  KEY `person_id` (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=40000001 DEFAULT CHARSET=utf8;

CREATE TABLE `person` (
  `person_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `person_name` text CHARACTER SET latin1,
  PRIMARY KEY (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=20000001 DEFAULT CHARSET=utf8;

La query era stata eseguita sul server virtuale DigitalOcean con SSD e 1 GB di RAM.

Presumo che potrebbe esserci un problema con la progettazione del database. Hai qualche suggerimento per progettare meglio questa situazione, per favore? O semplicemente per regolare la selezione sopra?


4
Questo è il prezzo da pagare per il design EAV. Puoi provare un indice composito suattribute (person_id, attribute_type_id, attribute_value)
mustaccio,

1
Vorrei provare ad aggiungere questi indici: (attribute_type_id, attribute_value, person_id)e (attribute_type_id, person_id, attribute_value)
ypercubeᵀᴹ

5
E usa InnoDB, butta via MyISAM. Questo è il 2015, MyiSAM è morto da tempo.
ypercubeᵀᴹ

2
Prima cosa: sbarazzarsi del join LEFT, non ha alcun effetto poiché si utilizzano tutti i tavoli nella propria condizione WHERE, trasformando efficacemente tutti i join in join INNER (l'ottimizzatore dovrebbe essere in grado di capirlo e ottimizzarlo, ma meglio non renderlo più difficile ). Seconda cosa: disabilitare la cache delle query a meno che non si abbia un valido motivo per usarla (= l'hai testata e hai misurato che ti aiuta)
jkavalik

2
OT: non è strano che usi LIMIT con ORDER BY? Questo restituirà alcune casuali 100000 righe?
ibre5041,

Risposte:


7

Scegli alcuni attributi da includere person. Indicizzali in alcune combinazioni: usa indici compositi, non indici a colonna singola.

Questa è essenzialmente l'unica via d'uscita da EAV-sucks-at-performance, che è dove sei.

Ecco altre discussioni: http://mysql.rjweb.org/doc.php/eav tra cui un suggerimento sull'uso di JSON invece della tabella dei valori-chiave.


3

Aggiungi indeces a attributeper:

  • (person_id, attribute_type_id, attribute_value) e
  • (attribute_type_id, attribute_value, person_id)

Spiegazione

Con il tuo progetto attuale, la EXPLAINtua query prevede di esaminare le 1,265,229 * 4 * 4 * 4 = 80,974,656righe attribute. È possibile ridurre questo numero aggiungendo un indice composito su attributeper (person_id, attribute_type_id). Usando questo indice la tua query esaminerà solo 1 invece di 4 righe per ciascuna di location, eyecolore gender.

Si potrebbe estendere tale indice per includere attribute_type_valuepure: (person_id, attribute_type_id, attribute_value). Ciò trasformerebbe questo indice in un indice di copertura per questa query, che dovrebbe migliorare anche le prestazioni.

Inoltre l'aggiunta di un indice su (attribute_type_id, attribute_value, person_id)(sempre un indice di copertura includendo person_id) dovrebbe migliorare le prestazioni rispetto al solo utilizzo di un indice su attribute_valuecui dovrebbero essere esaminate più righe. In questo caso, verrà fissato il primo passo nel tuo spiegare: selezionando un intervallo da bornyear.

L'uso di questi due indici ha abbassato il tempo di esecuzione della tua query sul mio sistema da ~ 2.0 sa ~ 0.2 s con l'output di spiegazione simile al seguente:

+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
| id | select_type | table    | type   | possible_keys                       | key               | key_len | ref                            |    rows | filtered | Extra                    |
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
|  1 | SIMPLE      | bornyear | range  | person_type_value,type_value_person | type_value_person |       9 |                                | 1861881 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | location | ref    | person_type_value,type_value_person | person_type_value |       8 | bornyear.person_id,const       |       1 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | eyecolor | ref    | person_type_value,type_value_person | person_type_value |       8 | bornyear.person_id,const       |       1 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | gender   | ref    | person_type_value,type_value_person | person_type_value |      13 | bornyear.person_id,const,const |       1 |   100.00 | Using index              |
|  1 | SIMPLE      | person   | eq_ref | PRIMARY                             | PRIMARY           |       4 | bornyear.person_id             |       1 |   100.00 | Using index              |
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+

1
Grazie per un'ampia risposta e spiegazione. Ho fatto tutto ciò di cui hai parlato, ma la query richiede ancora ~ 2 minuti. Per favore, quale tipo di tabella (innodb, myisam) stai usando e quale query esatta è stata eseguita?
Martin,

1
Oltre ad aggiungere gli indici ho usato gli stessi dati e le stesse definizioni che hai fatto, quindi ho usato MyISAM. Ho cambiato la prima riga della tua query SELECT person.person_idperché altrimenti non funzionerebbe, ovviamente. Hai fatto ANALYZE TABLE attributedopo aver aggiunto gli indeces? Potresti voler aggiungere il tuo nuovo EXPLAINoutput (dopo aver aggiunto indeces) anche alla tua domanda.
Wolfgangwalther,

3

Presumo che potrebbe esserci un problema con la progettazione del database.

Stai usando un cosiddetto design Entity-Attribute-Value, che spesso si comporta male, bene, dal design.

Hai qualche suggerimento per progettare meglio questa situazione, per favore?

Il modo relazionale classico per progettare questo sarebbe la creazione di una tabella separata per ciascun attributo. In generale, è possibile avere questi tavoli separati: location, gender, bornyear, eyecolor.

Quanto segue dipende dal fatto che determinati attributi siano sempre definiti o meno per una persona. E se una persona può avere un solo valore di un attributo. Ad esempio, di solito la persona ha un solo genere. Nel tuo progetto attuale nulla ti impedisce di aggiungere tre righe per la stessa persona con valori diversi per genere in esse. Puoi anche impostare un valore di genere non su 1 o 2, ma su un numero che non ha senso, come 987 e non esiste alcun vincolo nel database che lo impedisca. Ma questo è un altro problema separato relativo al mantenimento dell'integrità dei dati con la progettazione EAV.

Se conosci sempre il genere della persona, ha poco senso metterlo in una tabella separata ed è molto meglio avere una colonna non nulla GenderIDnella persontabella, che sarebbe una chiave esterna alla tabella di ricerca con l'elenco di tutti i sessi possibili e i loro nomi. Se conosci il genere della persona il più delle volte, ma non sempre, puoi rendere nulla questa colonna e impostarla su NULLquando le informazioni non sono disponibili. Se la maggior parte delle volte il sesso della persona non è noto, allora potrebbe essere meglio avere una tabella separata genderche si collega a person1: 1 e ha righe solo per quelle persone che hanno un genere noto.

Considerazioni simili si applicano a eyecolore bornyear- è improbabile che la persona abbia due valori per un eyecoloro bornyear.

Se è possibile per una persona avere diversi valori per un attributo, lo inseriresti sicuramente in una tabella separata. Ad esempio, non è raro che una persona abbia diversi indirizzi (casa, lavoro, posta, festività, ecc.), Quindi li elenchi tutti in una tabella location. Tabelle persone locationsarebbero collegate 1: M.


O semplicemente per regolare la selezione sopra?

Se si utilizza il design EAV, almeno farei quanto segue.

  • Set colonne attribute_type_id, attribute_value, person_ida NOT NULL.
  • Imposta una chiave esterna che si collega attribute.person_ida person.person_id.
  • Crea un indice su tre colonne (attribute_type_id, attribute_value, person_id). L'ordine delle colonne è importante qui.
  • Per quanto ne so, MyISAM non onora le chiavi esterne, quindi non usarlo, usa invece InnoDB.

Scriverei la query in questo modo. Utilizzare INNERinvece di LEFTjoin e scrivere esplicitamente una sottoquery per ciascun attributo per offrire all'ottimizzatore tutte le possibilità di utilizzare l'indice.

SELECT person.person_id
FROM
    person
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 1
            AND location.attribute_value BETWEEN 3000 AND 7000
    ) AS location ON location.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 2
            AND location.attribute_value = 1
    ) AS gender ON gender.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 3
            AND location.attribute_value BETWEEN 1980 AND 2000
    ) AS bornyear ON bornyear.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 4
            AND location.attribute_value IN (2, 3)
    ) AS eyecolor ON eyecolor.person_id = person.person_id
LIMIT 100000;

Inoltre, potrebbe valere la pena di partizionare la attributetabella in base a attribute_type_id.


Attenzione alle prestazioni: JOIN ( SELECT ... )non ottimizza bene. JOINingdirettamente alla tabella funziona meglio (ma è ancora problematico).
Rick James,

2

Spero di aver trovato una soluzione sufficiente. È ispirato a questo articolo .

Risposta breve:

  1. Ho creato 1 tabella con tutti gli attributi. Una colonna per un attributo. Più colonna chiave primaria.
  2. I valori degli attributi sono memorizzati in celle di testo (per la ricerca full-text) in formato CSV.
  3. Creazione di indici full-text. Prima di ciò è importante impostare ft_min_word_len=1(per MyISAM) nella [mysqld]sezione e innodb_ft_min_token_size=1(per InnoDb) nel my.cnffile, riavviare il servizio mysql.
  4. Esempio di ricerca: SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000dove 123, 456a 789sono ID a cui le persone dovrebbero essere associate attribute_1. Questa query ha richiesto meno di 1 secondo.

Risposta dettagliata:

Passaggio 1. Creazione di una tabella con indici full-text. InnoDb supporta gli indici full-text di MySQL 5.7, quindi se usi 5.5 o 5.6, dovresti usare MyISAM. A volte è persino più veloce per la ricerca FT di InnoDb.

CREATE TABLE `person_attribute_ft` (
  `person_id` int(11) NOT NULL,
  `attr_1` text,
  `attr_2` text,
  `attr_3` text,
  `attr_4` text,
  PRIMARY KEY (`person_id`),
  FULLTEXT KEY `attr_1` (`attr_1`),
  FULLTEXT KEY `attr_2` (`attr_2`),
  FULLTEXT KEY `attr_3` (`attr_3`),
  FULLTEXT KEY `attr_4` (`attr_4`),
  FULLTEXT KEY `attr_12` (`attr_1`,`attr_2`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

Passaggio 2. Inserire i dati dalla tabella EAV (entità-attributo-valore). Ad esempio indicato in questione, può essere fatto con 1 semplice SQL:

INSERT IGNORE INTO `person_attribute_ft`
SELECT
    p.person_id,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 1 AND a.person_id = p.person_id LIMIT 10) attr_1,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 2 AND a.person_id = p.person_id LIMIT 10) attr_2,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 3 AND a.person_id = p.person_id LIMIT 10) attr_3,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 4 AND a.person_id = p.person_id LIMIT 10) attr_4
FROM person p

Il risultato dovrebbe essere qualcosa del genere:

mysql> select * from person_attribute_ft limit 10;
+-----------+--------+--------+--------+--------+
| person_id | attr_1 | attr_2 | attr_3 | attr_4 |
+-----------+--------+--------+--------+--------+
|         1 | 541    | 2      | 1927   | 3      |
|         2 | 2862   | 2      | 1939   | 4      |
|         3 | 6573   | 2      | 1904   | 2      |
|         4 | 2432   | 1      | 2005   | 2      |
|         5 | 2208   | 1      | 1995   | 4      |
|         6 | 8388   | 2      | 1973   | 1      |
|         7 | 107    | 2      | 1909   | 4      |
|         8 | 5161   | 1      | 2005   | 1      |
|         9 | 8022   | 2      | 1953   | 4      |
|        10 | 4801   | 2      | 1900   | 3      |
+-----------+--------+--------+--------+--------+
10 rows in set (0.00 sec)

Passaggio 3. Selezionare dalla tabella con query come questa:

mysql> SELECT SQL_NO_CACHE *
    -> FROM `person_attribute_ft`
    -> WHERE 1 AND MATCH(attr_1) AGAINST ("3000 3001 3002 3003 3004 3005 3006 3007" IN BOOLEAN MODE)
    -> AND MATCH(attr_2) AGAINST ("1" IN BOOLEAN MODE)
    -> AND MATCH(attr_3) AGAINST ("1980 1981 1982 1983 1984" IN BOOLEAN MODE)
    -> AND MATCH(attr_4) AGAINST ("2,3" IN BOOLEAN MODE)
    -> LIMIT 10000;
+-----------+--------+--------+--------+--------+
| person_id | attr_1 | attr_2 | attr_3 | attr_4 |
+-----------+--------+--------+--------+--------+
|     12131 | 3002   | 1      | 1982   | 2      |
|     51315 | 3007   | 1      | 1984   | 2      |
|    147283 | 3001   | 1      | 1984   | 2      |
|    350086 | 3005   | 1      | 1982   | 3      |
|    423907 | 3004   | 1      | 1982   | 3      |
... many rows ...
|   9423907 | 3004   | 1      | 1982   | 3      |
|   9461892 | 3007   | 1      | 1982   | 2      |
|   9516361 | 3006   | 1      | 1980   | 2      |
|   9813933 | 3005   | 1      | 1982   | 2      |
|   9986892 | 3003   | 1      | 1981   | 2      |
+-----------+--------+--------+--------+--------+
90 rows in set (0.17 sec)

La query seleziona tutte le righe:

  • abbinando almeno uno di questi ID in attr_1:3000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
  • E allo stesso tempo la corrispondenza 1in attr_2(questa colonna rappresenta il genere, quindi se questa soluzione fosse personalizzata, dovrebbe essere smallint(1)con un indice semplice, ecc ...)
  • E allo stesso tempo abbinare almeno uno di 1980, 1981, 1982, 1983 or 1984inattr_3
  • E allo stesso tempo corrispondenti 2o 3inattr_4

Conclusione:

So che questa soluzione non è perfetta e ideale per molte situazioni, ma può essere utilizzata come buona alternativa per la progettazione di tavoli EAV.

Spero che possa aiutare qualcuno.


1
Trovo molto improbabile che questo design funzioni meglio del tuo design originale con indici compositi. Quali test hai fatto per confrontarli?
ypercubeᵀᴹ

0

Prova a utilizzare i suggerimenti sull'indice di query che sembrano appropriati

Suggerimenti sull'indice Mysql


1
I suggerimenti possono aiutare una versione della query, ma poi danneggiarne un'altra. Si noti che l'ottimizzatore è stato scelto come migliore prima tabella, probabilmente perché filtrato per le righe più indesiderate.
Rick James,
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.