2011-10-20 9 views
5

Mi sembra una domanda SQL per principianti, ma qui ci sono. Questo è quello che sto cercando di fare:Ottimizzazione della query MySQL su tabelle JOINed con GROUP BY e ORDER BY senza utilizzare query nidificate

  • unire tre tabelle insieme, prodotti, tag e una tabella di collegamento.
  • aggregare i tag in un singolo campo delimitato da virgole (da qui il group_concat e GROUP BY)
  • limite i risultati (30)
  • hanno i risultati in ordine di data 'creato'
  • evita di utilizzare sottointerrogazioni ove possibile, in quanto sono particolarmente sgradevole di codice utilizzando un framework Active Record

ho descritto le tabelle coinvolte in fondo a questo post, ma qui è la query che sto eseguendo

SELECT p.*, GROUP_CONCAT(pt.name) 
    FROM products p 
LEFT JOIN product_tags_for_products pt4p ON (pt4p.product_id = p.id) 
LEFT JOIN product_tags pt ON (pt.id = pt4p.product_tag_id) 
GROUP BY p.id 
ORDER BY p.created 
    LIMIT 30; 

Ci sono circa 280.000 prodotti, 130 etichette, 524.000 record di collegamento e ANALYZE ha creato le tabelle. Il problema è che ci vogliono oltre 80 anni per funzionare (su hardware decente), cosa che mi sembra sbagliata.

Ecco il spiegare i risultati:

id select_type table type possible_keys     key        key_len ref     rows Extra 
1 SIMPLE   p  index NULL        created       4   NULL     30 "Using temporary" 
1 SIMPLE   pt4p  ref  idx_product_tags_for_products idx_product_tags_for_products 3   s.id     1  "Using index" 
1 SIMPLE   pt  eq_ref PRIMARY       PRIMARY       4   pt4p.product_tag_id 1  

Penso che sta facendo le cose in ordine errato, vale a dire l'ordinazione i risultati dopo il join, utilizzando una grande tabella temporanea, e quindi limitante. Il piano di query nella mia testa sarebbe andato qualcosa come questo:

  • ORDINE tabella prodotti utilizzando il 'creato' chiave
  • Passo attraverso ogni fila, SINISTRA unirsi contro gli altri tavoli fino al limite del 30 è stato raggiunto.

Sembra semplice, ma non sembra funzionare così - mi manca qualcosa?


CREATE TABLE `products` (
    `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, 
    `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL, 
    `rating` float NOT NULL, 
    `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', 
    `last_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 
    `active` tinyint(1) NOT NULL, 
    PRIMARY KEY (`id`), 
    KEY `created` (`created`), 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 

CREATE TABLE `product_tags_for_products` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT, 
    `product_id` mediumint(8) unsigned NOT NULL, 
    `product_tag_id` int(10) unsigned NOT NULL, 
    PRIMARY KEY (`id`), 
    UNIQUE KEY `idx_product_tags_for_products` (`product_id`,`product_tag_id`), 
    KEY `product_tag_id` (`product_tag_id`), 
    CONSTRAINT `product_tags_for_products_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`), 
    CONSTRAINT `product_tags_for_products_ibfk_2` FOREIGN KEY (`product_tag_id`) REFERENCES `product_tags` (`id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 


CREATE TABLE `product_tags` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
    `name` varchar(100) COLLATE utf8_unicode_ci NOT NULL, 
    PRIMARY KEY (`id`), 
    UNIQUE KEY `name` (`name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 

Aggiornato con le informazioni di profiling su richiesta di Salman A:

Status, 
    Duration,CPU_user,CPU_system,Context_voluntary,Context_involuntary,Block_ops_in,Block_ops_out,Messages_sent,Messages_received,Page_faults_major,Page_faults_minor,Swaps,Source_function,Source_file,Source_line 
starting,    
    0.000124,0.000106,0.000015,0,0,0,0,0,0,0,0,0,NULL,NULL,NULL 
"Opening tables",  
    0.000022,0.000020,0.000003,0,0,0,0,0,0,0,0,0,"unknown function",sql_base.cc,4519 
"System lock", 
    0.000007,0.000004,0.000002,0,0,0,0,0,0,0,0,0,"unknown function",lock.cc,258 
"Table lock", 
    0.000011,0.000009,0.000002,0,0,0,0,0,0,0,0,0,"unknown function",lock.cc,269 
init,   
    0.000055,0.000054,0.000001,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,2524 
optimizing,  
    0.000008,0.000006,0.000002,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,833 
statistics,  
    0.000116,0.000051,0.000066,0,0,0,0,0,0,0,1,0,"unknown function",sql_select.cc,1024 
preparing,  
    0.000027,0.000023,0.000003,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,1046 
"Creating tmp table", 
    0.000054,0.000053,0.000002,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,1546 
"Sorting for group", 
    0.000018,0.000015,0.000003,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,1596 
executing,  
    0.000004,0.000002,0.000001,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,1780 
"Copying to tmp table", 
    0.061716,0.049455,0.013560,0,18,0,0,0,0,0,3680,0,"unknown function",sql_select.cc,1927 
"converting HEAP to MyISAM", 
    0.046731,0.006371,0.017543,3,5,0,3,0,0,0,32,0,"unknown function",sql_select.cc,10980 
"Copying to tmp table on disk", 
10.700166,3.038211,1.191086,538,1230,1,31,0,0,0,65,0,"unknown function",sql_select.cc,11045 
"Sorting result", 
    0.777887,0.155327,0.618896,2,137,0,1,0,0,0,634,0,"unknown function",sql_select.cc,2201 
"Sending data", 
    0.000336,0.000159,0.000178,0,0,0,0,0,0,0,1,0,"unknown function",sql_select.cc,2334 
end, 
    0.000005,0.000003,0.000002,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,2570 
"removing tmp table", 
    0.106382,0.000058,0.080105,4,9,0,11,0,0,0,0,0,"unknown function",sql_select.cc,10912 
end, 
    0.000015,0.000007,0.000007,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,10937 
"query end", 
    0.000004,0.000002,0.000001,0,0,0,0,0,0,0,0,0,"unknown function",sql_parse.cc,5083 
"freeing items", 
    0.000012,0.000012,0.000001,0,0,0,0,0,0,0,0,0,"unknown function",sql_parse.cc,6107 
"removing tmp table", 
    0.000010,0.000009,0.000001,0,0,0,0,0,0,0,0,0,"unknown function",sql_select.cc,10912 
"freeing items", 
    0.000084,0.000022,0.000057,0,1,0,0,1,0,0,0,0,"unknown function",sql_select.cc,10937 
"logging slow query", 
    0.000004,0.000001,0.000001,0,0,0,0,0,0,0,0,0,"unknown function",sql_parse.cc,1723 
"logging slow query", 
    0.000049,0.000031,0.000018,0,0,0,0,0,0,0,0,0,"unknown function",sql_parse.cc,1733 
"cleaning up", 
    0.000007,0.000005,0.000002,0,0,0,0,0,0,0,0,0,"unknown function",sql_parse.cc,1691 

Le tabelle sono:

Prodotti = 84.1MiB (ci sono campi aggiuntivi nella tabella prodotti che ho omesso per chiarezza) Tags = 32KiB Tabella di collegamento = 46.6MiB

+0

Hai controllato le impostazioni di MySQL (e soprattutto InnoDB)? –

+0

Puoi pubblicare (i) l'output di 'SET PROFILING = 1;/* la query precedente con la parola chiave SQL_NO_CACHE * /; SHOW PROFILE ALL; '(ii) la dimensione delle tre tabelle in termini di KB. –

+0

Post aggiornato con informazioni di profilazione. Misteriosamente la query è scesa a 10 secondi anziché a 80, ma è ancora lenta. – Ben

risposta

3

vorrei provare a limitare il numero di prodotti a 30 prima e poi unirsi con solo 30 prodotti:

SELECT p.*, GROUP_CONCAT(pt.name) as tags 
    FROM (SELECT p30.* FROM products p30 ORDER BY p30.created LIMIT 30) p 
LEFT JOIN product_tags_for_products pt4p ON (pt4p.product_id = p.id) 
LEFT JOIN product_tags pt ON (pt.id = pt4p.product_tag_id) 
GROUP BY p.id 
ORDER BY p.created 

So che hai detto non subqueries ma non ha spiegato perché, e Non vedo nessun altro modo per risolvere il tuo problema.

Si noti che è possibile eliminare la subselect mettendo che in una vista:

CREATE VIEW v_last30products AS 
    SELECT p30.* FROM products p30 ORDER BY p30.created LIMIT 30; 

Quindi la query è semplificata per:

SELECT p.*, GROUP_CONCAT(pt.name) as tags 
    FROM v_last30products p 
LEFT JOIN product_tags_for_products pt4p ON (pt4p.product_id = p.id) 
LEFT JOIN product_tags pt ON (pt.id = pt4p.product_tag_id) 
GROUP BY p.id 
ORDER BY p.created 

Altro problema, il vostro n-to-n tavolo product_tags_for_products

Non ha senso, lo ristrutturerei in questo modo:

Questo dovrebbe rendere più veloce la query:
- accorciamento della chiave utilizzata (In InnoDB il PK è sempre incluso nelle chiavi secondarie);
- Consente di utilizzare il PK, che dovrebbe essere più veloce rispetto all'utilizzo di una chiave secondaria;

Più velocità emette
Se si sostituisce la select * con solo i campi necessari select p.title, p.rating, ... FROM che sarà anche accelerare le cose un po '.

+0

Sarebbe una sottoquery. Significa anche che se voglio filtrare per tag in futuro, aggiungendo una clausola WHERE o cambiando a UN INIZIO INNER, allora è molto probabile che otterrò meno di 30 risultati. – Ben

+0

"EXPLAIN" pubblicato da OP suggerisce che MySQL limita la tabella dei prodotti a 30 righe all'inizio. –

+0

@SalmanA In effetti, che mi confonde, perché dovrebbe richiedere così tanto tempo? – Ben

0

Ah - Vedo che nessuno dei tasti su cui si inserisce GROUP BY è BTREE, per impostazione predefinita le chiavi PRIMARY sono hash. Aiuta a raggruppare quando c'è un indice di ordinamento ... altrimenti deve scansionare ...

Ciò che intendo è, penso che sarebbe di grande aiuto se si aggiungesse un indice basato su BTREE per p.id e p .creato. In tal caso, penso che il motore eviterà di dover analizzare/ordinare tutte quelle chiavi per eseguire il gruppo e ordinare per.

+0

Secondo le definizioni della tabella pubblicate dall'OP, tutte le loro tabelle utilizzano InnoDB, che [supporta solo indici B-tree] (http://dev.mysql.com/doc/refman/5.6/en/create-index. html). (InnoDB ha anche [indici hash adattativi] (http://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_adaptive_hash_index), ma questi vengono generati automaticamente in base agli indici B-tree esistenti.) –

0

Per quanto riguarda il filtraggio sui tag (che lei ha citato nei commenti su Johan's answer), se l'evidente

SELECT p.*, GROUP_CONCAT(pt.name) AS tags 
FROM products p 
    JOIN product_tags_for_products pt4p2 ON (pt4p2.product_id = p.id) 
    JOIN product_tags pt2 ON (pt2.id = pt4p2.product_tag_id) 
    LEFT JOIN product_tags_for_products pt4p ON (pt4p.product_id = p.id) 
    LEFT JOIN product_tags pt ON (pt.id = pt4p.product_tag_id) 
WHERE pt2.name IN ('some', 'tags', 'here') 
GROUP BY p.id 
ORDER BY p.created LIMIT 30 

non viene eseguito abbastanza veloce, si può sempre provare questo:

CREATE TEMPORARY TABLE products30 
    SELECT p.* 
    FROM products p 
    JOIN product_tags_for_products pt4p ON (pt4p.product_id = p.id) 
    JOIN product_tags pt ON (pt.id = pt4p.product_tag_id) 
    WHERE pt.name IN ('some', 'tags', 'here') 
    GROUP BY p.id 
    ORDER BY p.created LIMIT 30 

SELECT p.*, GROUP_CONCAT(pt.name) AS tags 
FROM products30 p 
    LEFT JOIN product_tags_for_products pt4p ON (pt4p.product_id = p.id) 
    LEFT JOIN product_tags pt ON (pt.id = pt4p.product_tag_id) 
GROUP BY p.id 
ORDER BY p.created 

(Ho usato un tavolo temporaneo perché hai detto "no subquery", non so se sono più facili da usare in un framework Active Record, ma almeno è un altro modo per farlo)


Ps. Un'idea davvero offensiva sul tuo problema originale: farebbe qualche differenza se hai modificato la clausola GROUP BY p.id in GROUP BY p.created, p.id? Probabilmente no, ma almeno proverei.