2013-01-17 14 views
9

Ho una query per un sistema di messaggistica di contatti che sta rallentando esponenzialmente il numero maggiore di join che faccio.La query SQL diventa esponenzialmente più lenta

La struttura della tabella è fondamentalmente una tabella contatti e una tabella campi contatti.

La query unisce la tabella dei campi di contatto molte volte e per ogni join effettuato, richiede il doppio del tempo.

Questa è la query.

SELECT SQL_CALC_FOUND_ROWS 
    `contact_data`.`id`, 
    `contact_data`.`name`, 
    `fields0`.`value` AS `fields0`, 
    `fields1`.`value` AS `fields1`, 
    `fields2`.`value` AS `fields2`, 
    ...etc... 
    CONTACT_DATA_TAGS(
     GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`), 
     GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`), 
     GROUP_CONCAT(DISTINCT `contact_data_read`.`user`) 
    ) AS `tags`, 
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`, 
    `contact_data`.`updated`, 
    `contact_data`.`created` 
FROM 
    `contact_data` 
LEFT JOIN contact_data_tags ON contact_data.`id` = contact_data_tags.`data` 
LEFT JOIN contact_data_assignment ON contact_data.`id` = contact_data_assignment.`data` 
LEFT JOIN contact_data_read ON contact_data.`id` = contact_data_read.`data` 
LEFT JOIN contact_data_fields AS fields0 ON contact_data.`id` = fields0.`contact_data_id` AND fields0.`key` = :field1 
LEFT JOIN contact_data_fields AS fields1 ON contact_data.`id` = fields1.`contact_data_id` AND fields1.`key` = :field2 
LEFT JOIN contact_data_fields AS fields2 ON contact_data.`id` = fields2.`contact_data_id` AND fields2.`key` = :field3 
...etc... 
GROUP BY contact_data.`id` 
ORDER BY `id` DESC 

Questa è la struttura della tabella:

CREATE TABLE IF NOT EXISTS `contact_data` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
    `name` varchar(200) NOT NULL, 
    `format` varchar(50) NOT NULL, 
    `fields` longtext NOT NULL, 
    `url` varchar(2000) NOT NULL, 
    `referer` varchar(2000) DEFAULT NULL, 
    `ip` varchar(40) NOT NULL, 
    `agent` varchar(1000) DEFAULT NULL, 
    `created` datetime NOT NULL, 
    `updated` datetime NOT NULL, 
    `updater` int(10) unsigned DEFAULT NULL, 
    PRIMARY KEY (`id`), 
    KEY `name` (`name`), 
    KEY `url` (`url`(333)), 
    KEY `ip` (`ip`), 
    KEY `created` (`created`), 
    KEY `updated` (`updated`), 
    KEY `updater` (`updater`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

CREATE TABLE IF NOT EXISTS `contact_data_assignment` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
    `user` int(10) unsigned NOT NULL, 
    `data` int(10) unsigned NOT NULL, 
    `created` datetime NOT NULL, 
    `updater` int(10) unsigned DEFAULT NULL, 
    PRIMARY KEY (`id`), 
    UNIQUE KEY `unique_assignment` (`user`,`data`), 
    KEY `user` (`user`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

CREATE TABLE IF NOT EXISTS `contact_data_fields` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
    `contact_data_id` int(10) unsigned NOT NULL, 
    `key` varchar(200) NOT NULL, 
    `value` text NOT NULL, 
    `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 
    PRIMARY KEY (`id`), 
    KEY `contact_data_id` (`contact_data_id`), 
    KEY `key` (`key`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

CREATE TABLE IF NOT EXISTS `contact_data_read` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
    `user` int(10) unsigned NOT NULL, 
    `data` int(10) unsigned NOT NULL, 
    `type` enum('admin','email') NOT NULL, 
    `created` datetime NOT NULL, 
    PRIMARY KEY (`id`), 
    KEY `user` (`user`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

CREATE TABLE IF NOT EXISTS `contact_data_tags` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
    `name` varchar(200) NOT NULL, 
    `data` int(10) unsigned NOT NULL, 
    `created` datetime NOT NULL, 
    `updater` int(10) unsigned DEFAULT NULL, 
    PRIMARY KEY (`id`), 
    UNIQUE KEY `unique_tag` (`name`,`data`), 
    KEY `name` (`name`), 
    KEY `data` (`data`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

DELIMITER $$ 
CREATE FUNCTION `contact_data_tags`(`tags` TEXT, `assigned` BOOL, `read` BOOL) RETURNS text CHARSET latin1 
BEGIN 
    RETURN CONCAT(
     ',', 
     IFNULL(`tags`, ''), 
     ',', 
     IF(`tags` IS NULL OR FIND_IN_SET('Closed', `tags`) = 0, 'Open', ''), 
     ',', 
     IF(`assigned` IS NULL, 'Unassigned', ''), 
     ',', 
     IF(`read` IS NULL, 'New', ''), 
     ',' 
    ); 
END$$ 

DELIMITER ; 

Qualcuno sa il motivo per cui funziona così lento? Cosa posso fare per renderlo più veloce? Devo modificare la query (preferirei non regolare la struttura)? C'è qualche opzione di configurazione che posso impostare per velocizzarlo?

È strano anche che sembra funzionare più velocemente sul mio computer di sviluppo Windows, rispetto al mio server di produzione di Debain (quasi istantaneo, rispetto ai 30+ secondi).

Ma la macchina Windows è molto meno potente del server Debain (8 core Xeon, 32 GB RAM).

Esecuzione di MySQL 5.1.49 su Debian (che non posso aggiornare) e 5.5.28 su Windows.

Quindi leggere che EAV non funziona bene in RDBMS (o almeno nel mio caso), è l'opzione di configurazione che potrei aumentare per farlo correre più velocemente (cioè posso solo lanciare più RAM su di esso)?

+4

Ah, la gioia che è l'Entità-Attributo-Valore-Modello. Non funziona troppo bene nei database relazionali. http://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model A RDBMS piace conoscere i suoi campi in anticipo. Hai bisogno di esporre tutti questi configurabili contact_data_fields al database (per le query)? In caso contrario, è possibile memorizzare un JSON in un CLOB. O utilizzare un database di documenti NoSQL. O se non sono configurabili, basta usare le colonne "regolari". – Thilo

+0

@Thilo sì ho bisogno di esporli per le domande. Mi sono trasferito da JSON, per questo motivo. Correzione – Petah

+1

: EAV funziona piuttosto bene in alcuni database, con la chiave giusta e/o la struttura della tabella di clustering. – wildplasser

risposta

5

Un modo per accelerare la query potrebbe essere quella di creare un collegamento a contact_data_fieldsuna sola volta (su contact_data.id = contact_data_fields.contact_data_id) e modificare i campi colonne di essere max espressioni - in questo modo:

SELECT SQL_CALC_FOUND_ROWS 
    `contact_data`.`id`, 
    `contact_data`.`name`, 
    MAX(CASE WHEN fields.`key` = :field1 THEN fields.`value` END) AS `fields0`, 
    MAX(CASE WHEN fields.`key` = :field2 THEN fields.`value` END) AS `fields1`, 
    MAX(CASE WHEN fields.`key` = :field3 THEN fields.`value` END) AS `fields2`, 
    ...etc... 
    CONTACT_DATA_TAGS(
     GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`), 
     GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`), 
     GROUP_CONCAT(DISTINCT `contact_data_read`.`user`) 
    ) AS `tags`, 
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`, 
    `contact_data`.`updated`, 
    `contact_data`.`created` 
FROM 
    `contact_data` 
LEFT JOIN contact_data_tags ON contact_data.`id` = contact_data_tags.`data` 
LEFT JOIN contact_data_assignment ON contact_data.`id` = contact_data_assignment.`data` 
LEFT JOIN contact_data_read ON contact_data.`id` = contact_data_read.`data` 
LEFT JOIN contact_data_fields AS fields 
     ON contact_data.`id` = fields.`contact_data_id` 
...etc... 
GROUP BY contact_data.`id` 
ORDER BY `id` DESC 
+0

+1, ho pensato di suggerirlo come bonus aggiuntivo, nel caso in cui il suo tavolo 'contact_data_fields' venga scaricato completamente, ma dubito it =) – newtover

+0

Non l'ho ancora provato, ma lo farò presto e ti faccio sapere. – Petah

+0

Questo ha funzionato a meraviglia, grazie. Aggiungerò un'altra taglia per premiarti. – Petah

3

Sfortunatamente, vi sono molte inefficienze nella query. Non credo che si riuscirà a risolvere il problema appena messa a punto alcuni parametri e l'aggiunta di più RAM:

  • Per cominciare, non sappiamo le dimensioni delle tabelle, e perché si avrebbe bisogno di scaricare l'intero tabella contact_data. Non ci sono condizioni e limiti aggiuntivi (che di solito contano).
  • Non sappiamo anche se possono esserci più record con lo stesso (contact_data_id, chiave) per un dato contact_data.id. Penso che possano essere {0, 1} record, e questo può essere reso più esplicito se si ha l'indice univoco corrispondente (che alla fine è richiesto come indice per l'efficienza della query)
  • SQL_CALC_FOUND_ROWS è un ulteriore killer (nel caso abbiate intenzione di usare LIMIT), dato che rende MySQL in grado di calcolare e analizzare l'intero risultato per contare le righe (conterei solo le righe con una query separata che recupera id spogli e ne memorizza i risultati. cache potrebbe essere sufficiente se le tabelle non sono cambiate molto frequentemente)

non appena si aggiunge un indice su (contact_data_id, key), vorrei isolare il raggruppamento e l'ordinamento in un subquery e poi LEFT JOIN sul contact_data_fields (senza alcun ordinamento). La query corrente esegue lo stesso confronto LEFT JOIN per ogni riga nel prodotto di contact_data, contact_data_tags, contact_data_assignment, contact_data_read prima di essere raggruppati (senza menzionare che il server memorizza quell'intero risultato intermedio prima che sia archiviato e che i dati duplicati vengano eliminati) .

+0

La dimensione dei dati è di circa 20.000 righe in 'contact_data' e 200.000 in' contact_data_fields'. Gli altri join non sono significativi e potrebbero essere rimossi se necessario. Non ci sono extra dove dichiarazioni, e a volte non ci sarà limite (esportazione in xls). Sì, ci possono essere più chiavi uguali per un 'contact_data_id'. 'SQL_CALC_FOUND_ROWS' è richiesto in quanto ho bisogno di statistiche per" mostrare 100 su 10.000 filtrati da 100.000 righe " – Petah

1

Sulla base di Mark interrogazione Bannisters, possibilmente usare qualcosa di simile per restituire i dettagli campo/valore come un elenco delimitato: -

SELECT SQL_CALC_FOUND_ROWS 
    `contact_data`.`id`, 
    `contact_data`.`name`, 
    GROUP_CONCAT(CONCAT_WS(',', contact_data_fields.`key`, contact_data_fields.`value`)), 
    CONTACT_DATA_TAGS(
     GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`), 
     GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`), 
     GROUP_CONCAT(DISTINCT `contact_data_read`.`user`) 
    ) AS `tags`, 
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`, 
    `contact_data`.`updated`, 
    `contact_data`.`created` 
FROM 
    `contact_data` 
LEFT JOIN contact_data_tags ON contact_data.`id` = contact_data_tags.`data` 
LEFT JOIN contact_data_assignment ON contact_data.`id` = contact_data_assignment.`data` 
LEFT JOIN contact_data_read ON contact_data.`id` = contact_data_read.`data` 
LEFT JOIN contact_data_fields ON contact_data.`id` = contact_data_fields.`contact_data_id` 
WHERE contact_data_fields.`key` IN (:field1, :field2, :field3, etc) 
GROUP BY contact_data.`id` 
ORDER BY `id` DESC 

a seconda del insensibile er di righe corrispondenti nelle tabelle contact_data_tags, contact_data_assignment e contact_data_read (e quindi possibilmente il numero di righe intermedie per ogni contact_data.id) quindi è may essere più veloce per ottenere la chiave di contatto/dettagli valore da una sottoselezione.

SELECT SQL_CALC_FOUND_ROWS 
    `contact_data`.`id`, 
    `contact_data`.`name`, 
    Sub1.ContactKeyValue, 
    CONTACT_DATA_TAGS(
     GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`), 
     GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`), 
     GROUP_CONCAT(DISTINCT `contact_data_read`.`user`) 
    ) AS `tags`, 
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`, 
    `contact_data`.`updated`, 
    `contact_data`.`created` 
FROM 
    `contact_data` 
LEFT JOIN contact_data_tags ON contact_data.id = contact_data_tags.`data` 
LEFT JOIN contact_data_assignment ON contact_data.id = contact_data_assignment.`data` 
LEFT JOIN contact_data_read ON contact_data.id = contact_data_read.`data` 
LEFT JOIN (SELECT contact_data_id, GROUP_CONCAT(CONCAT_WS(',', contact_data_fields.`key`, contact_data_fields.`value`)) AS ContactKeyValue FROM contact_data_fields 
WHERE fields.`key` IN (:field1, :field2, :field3, etc) GROUP BY contact_data_id) Sub1 ON contact_data.id = Sub1.contact_data_id 
GROUP BY contact_data.id 
ORDER BY `id` DESC 
+0

Il problema con valori concatenati è che non può essere ricercato e ordinato. O mi sta sfuggendo qualcosa? – Petah

+0

Non ideale per eseguirne la ricerca e possibilmente abbastanza lento da utilizzare qualsiasi risparmio di prestazioni, ma dipende da come si desidera eseguirne la ricerca. Nel codice che legge questo SQL probabilmente sarebbe abbastanza facile, ma non posso commentare con le informazioni pubblicate qui. All'interno di SQL è più difficile anche se, se necessario, è possibile utilizzare find nel set. Puoi anche fare a meno dei valori concatenati e avere una riga per ogni campo1, campo2, ecc. E occupartene successivamente in codice. – Kickstart

2

io aggiungo a tutti tesi interestings commenti mie esperienze con le query Entity-Attribute-Value-Girl e MySQL.

Innanzitutto non dimenticare di avere un limite basso in MySQL sul numero di join 61 joins. All'inizio sembra un grande numero. Ma con questo modello potrebbe facilmente bloccare le tue query con un bel SQLSTATE[HY000]: General error: 1116.

Ho sperimentato anche questi rallentamenti esponenziali. Quando raggiungiamo per la prima volta più di 20 secondi per le query con 50 join su 50.000 righe di tabelle, abbiamo rilevato che 14.5s su questi 15s sono stati persi in Query Optimizer - sembra stia cercando di indovinare il miglior ordine di join per questi 50 join - . Quindi, semplicemente aggiungendo le parole chiave STRAIGHT_JOIN subito dopo la parola chiave SELECT, siamo tornati al normale orario. Ovviamente facendo ciò significa che è necessario ottenere uno schema di indicizzazione e che è necessario scrivere le query con un ordine di join intelligente (le tabelle con i migliori indici e la riduzione della popolazione migliore dovrebbero apparire prima).

SELECT STRAIGHT_JOIN (...) 

Si noti che questa parola chiave può essere utilizzata anche nella sintassi JOIN.

STRAIGHT_JOIN forza l'ottimizzatore a unirsi alle tabelle nell'ordine in cui sono elencate nella clausola FROM. È possibile utilizzare questo per accelerare una query se l'ottimizzatore unisce le tabelle in ordine non ottimale.

vorrei aggiungere "o se ci vuole il 95% di te tempo di risposta di indovinare questo ordine" :-)

Controllare anche this page per altre impostazioni di query optimizer direttamente sulla query.

Quindi ci sono differenze tra 5.1 e 5.5 ... beh ci sono così tante differenze tra queste versioni, è come lavorare con due server di database diversi. Dovresti davvero considerare l'utilizzo di 5.5 in produzione, per i miglioramenti di velocità (controlla anche Percona) ma anche per le improvvisazioni di transazioni e serrature, e se hai bisogno di un solo motivo è che avrai degli errori di produzione che non hai in dev .

Le domande che contengono molti join saranno per definizione per il server. Per il controllo del comportamento del server è necessario disporre della regolazione fine nel file my.cnf.Ad esempio, provare ad evitare la creazione di tabelle temporanee (controllare l'output di spiegazione sulla query). Una query 2s può diventare una query 120s solo perché si raggiunge un limite e si passa a file temporanei per gestire i 20 o 30 join e ordinamenti e raggruppare per. Mettere i dati su disco è davvero molto lento rispetto al lavoro in memoria. Questo è particolarmente controllato da due impostazioni: tesi

tmp_table_size = 1024M 
max_heap_table_size = 1024M 

diciamo qui che "tenere a memoria di lavoro per la richiesta, se ci vuole meno di 1GB di RAM". Ovviamente se lo fai evita di avere 500 script paralleli che gestiscono queste richieste - se ne hai bisogno regolarmente per molte richieste parallele, considera di evitare questo schema di dati.

Questo porta anche a un punto importante. Stai raggiungendo la frontiera di una complessità richiesta. Il server SQL è in genere più veloce dell'applicazione per aggregare i dati in un risultato. Ma quando la dimensione dei dati è grande e si aggiungono molti indici nella query (almeno uno per join) e si ordinano, si raggruppano e anche si aggregano i risultati con group_contact ... MySQL utilizzerà sicuramente file temporanei e sarà lento. Utilizzando diverse query brevi (una query principale senza gruppo di e quindi 10 o 200 query per ottenere il contenuto che avresti per i campi group_contact, ad esempio) potresti essere più veloce evitando l'utilizzo di file temporanei.

+0

Grazie per le informazioni. Per quanto riguarda l'aggiornamento della produzione, ho notato che la mia installazione 5.5 è molto più rigida di 5.1 e quindi rompe parte del mio codice. – Petah

+0

^Petah. Inoltre, mi dimentico di aggiungere un punto, controllare che vengano utilizzati solo numeri per gli indici, che gli indici di colonne di testo siano davvero pessimi e utilizzino lo spazio. – regilero