Esiste un modo per ottimizzare l'ordinamento per colonne di tabelle unite?


10

Questa è la mia domanda lenta:

SELECT `products_counts`.`cid`
FROM
  `products_counts` `products_counts`

  LEFT OUTER JOIN `products` `products` ON (
  `products_counts`.`product_id` = `products`.`id`
  )
  LEFT OUTER JOIN `trademarks` `trademark` ON (
  `products`.`trademark_id` = `trademark`.`id`
  )
  LEFT OUTER JOIN `suppliers` `supplier` ON (
  `products_counts`.`supplier_id` = `supplier`.`id`
  )
WHERE
  `products_counts`.product_id IN
  (159, 572, 1075, 1102, 1145, 1162, 1660, 2355, 2356, 2357, 3236, 6471, 6472, 6473, 8779, 9043, 9095, 9336, 9337, 9338, 9445, 10198, 10966, 10967, 10974, 11124, 11168, 16387, 16689, 16827, 17689, 17920, 17938, 17946, 17957, 21341, 21352, 21420, 21421, 21429, 21544, 27944, 27988, 30194, 30196, 30230, 30278, 30699, 31306, 31340, 32625, 34021, 34047, 38043, 43743, 48639, 48720, 52453, 55667, 56847, 57478, 58034, 61477, 62301, 65983, 66013, 66181, 66197, 66204, 66407, 66844, 66879, 67308, 68637, 73944, 74037, 74060, 77502, 90963, 101630, 101900, 101977, 101985, 101987, 105906, 108112, 123839, 126316, 135156, 135184, 138903, 142755, 143046, 143193, 143247, 144054, 150164, 150406, 154001, 154546, 157998, 159896, 161695, 163367, 170173, 172257, 172732, 173581, 174001, 175126, 181900, 182168, 182342, 182858, 182976, 183706, 183902, 183936, 184939, 185744, 287831, 362832, 363923, 7083107, 7173092, 7342593, 7342594, 7342595, 7728766)
ORDER BY
  products_counts.inflow ASC,
  supplier.delivery_period ASC,
  trademark.sort DESC,
  trademark.name ASC
LIMIT
  0, 3;

Il mio tempo medio di query è di 4,5 secondi sul mio set di dati e questo è inaccettabile.

Soluzioni che vedo:

Aggiungi tutte le colonne dalla clausola order alla products_countstabella. Ma ho ~ 10 tipi di ordine nell'applicazione, quindi dovrei creare molte colonne e indici. Inoltre products_countsho aggiornamenti / inserti / eliminazioni molto intensi, quindi devo eseguire immediatamente l'aggiornamento di tutte le colonne relative all'ordine (usando i trigger?).

C'è un'altra soluzione?

Spiegare:

+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
| id | select_type | table           | type   | possible_keys                               | key                    | key_len | ref                              | rows | Extra                                        |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | products_counts | range  | product_id_supplier_id,product_id,pid_count | product_id_supplier_id | 4       | NULL                             |  227 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | products        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.product_id  |    1 |                                              |
|  1 | SIMPLE      | trademark       | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products.trademark_id       |    1 |                                              |
|  1 | SIMPLE      | supplier        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.supplier_id |    1 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+

Tabelle struttura:

CREATE TABLE `products_counts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` int(11) unsigned NOT NULL,
  `supplier_id` int(11) unsigned NOT NULL,
  `count` int(11) unsigned NOT NULL,
  `cid` varchar(64) NOT NULL,
  `inflow` varchar(10) NOT NULL,
  `for_delete` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid` (`cid`),
  UNIQUE KEY `product_id_supplier_id` (`product_id`,`supplier_id`),
  KEY `product_id` (`product_id`),
  KEY `count` (`count`),
  KEY `pid_count` (`product_id`,`count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `category_id` int(11) unsigned NOT NULL,
  `trademark_id` int(11) unsigned NOT NULL,
  `photo` varchar(255) NOT NULL,
  `sort` int(11) unsigned NOT NULL,
  `otech` tinyint(1) unsigned NOT NULL,
  `not_liquid` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `applicable` varchar(255) NOT NULL,
  `code_main` varchar(64) NOT NULL,
  `code_searchable` varchar(128) NOT NULL,
  `total` int(11) unsigned NOT NULL,
  `slider` int(11) unsigned NOT NULL,
  `slider_title` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`),
  KEY `category_id` (`category_id`),
  KEY `trademark_id` (`trademark_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `trademarks` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `country_id` int(11) NOT NULL,
  `sort` int(11) unsigned NOT NULL DEFAULT '0',
  `sort_list` int(10) unsigned NOT NULL DEFAULT '0',
  `is_featured` tinyint(1) unsigned NOT NULL,
  `is_direct` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `suppliers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `code` varchar(64) NOT NULL,
  `name` varchar(255) NOT NULL,
  `delivery_period` tinyint(1) unsigned NOT NULL,
  `is_default` tinyint(1) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Informazioni sul server MySQL:

mysqld  Ver 5.5.45-1+deb.sury.org~trusty+1 for debian-linux-gnu on i686 ((Ubuntu))

3
Potete fornire un violino SQL con indici, schema di tabella e dati di test? Inoltre qual è il tuo tempo target? Stai cercando di completarlo in 3 secondi, 1 secondo, 50 millisecondi? Quanti record hai nelle varie tabelle 1k, 100k, 100M?
Erik,

Se quei campi per i quali stai ordinando non sono indicizzati e il set di dati è molto grande, potresti forse vedere un problema sort_buffer_size? Puoi provare a modificare il valore sulla sessione ed eseguire la query per vedere se migliora.
Brian Efting,

Hai provato ad aggiungere un indice su (inflow, product_id)?
ypercubeᵀᴹ

Assicurati di avere un discreto innodb_buffer_pool_size. In genere circa il 70% della RAM disponibile è buono.
Rick James,

Risposte:


6

Revisionare le definizioni delle tabelle mostra che ci sono indici corrispondenti tra le tabelle interessate. Ciò dovrebbe far sì che i join avvengano il più rapidamente possibile entro i limiti della MySQL'slogica dei join.

Tuttavia, l' ordinamento da più tabelle è più complesso.

Nel 2007 Sergey Petrunia ha descritto i 3 MySQLalgoritmi di ordinamento in ordine di velocità per MySQL: http://s.petrunia.net/blog/?m=201407

  1. Utilizzare il metodo di accesso basato sull'indice che produce l'output ordinato
  2. Utilizzare filesort()sulla prima tabella non costante
  3. Mettere risultato unirsi in una tabella temporanea e usare filesort()su di esso

Dalle definizioni della tabella e dai join mostrati sopra, puoi vedere che non otterrai mai l'ordinamento più veloce . Ciò significa che sarai dipendente filesort()dai criteri di ordinamento che stai utilizzando.

Tuttavia, se si progetta e si utilizza una vista materializzata , sarà possibile utilizzare l' algoritmo di ordinamento più veloce.

Per vedere i dettagli definiti per MySQL 5.5i metodi di ordinamento, consultare: http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html

Per MySQL 5.5(in questo esempio) aumentare la ORDER BYvelocità se non è possibile MySQLutilizzare gli indici anziché una fase di ordinamento aggiuntiva, provare le seguenti strategie:

• Aumentare il sort_buffer_sizevalore della variabile.

• Aumentare il read_rnd_buffer_sizevalore della variabile.

• Utilizzare meno RAM per riga dichiarando le colonne delle dimensioni necessarie per la memorizzazione dei valori effettivi. [Ad esempio, ridurre un varchar (256) in varchar (ActualLongestString)]

• Modificare la tmpdirvariabile di sistema in modo che punti a un file system dedicato con grandi quantità di spazio libero. (Altri dettagli sono disponibili nel link sopra.)

Ci sono più dettagli forniti nella MySQL 5.7documentazione per aumentare la ORDERvelocità, alcuni dei quali possono essere comportamenti leggermente aggiornati :

http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

Viste materializzate : un approccio diverso all'ordinamento delle tabelle unite

Hai accennato a Materialized Views con la tua domanda che si riferisce all'uso dei trigger. MySQL non ha funzionalità integrate per creare una vista materializzata ma hai gli strumenti necessari. Utilizzando i trigger per distribuire il carico è possibile mantenere la vista materializzata fino al momento.

La vista materializzata è in realtà una tabella che viene popolata attraverso il codice procedurale per creare o ricostruire la vista materializzata e gestita da trigger per mantenere aggiornati i dati.

Poiché stai creando una tabella che avrà un indice , la Visualizzazione materializzata quando richiesto può utilizzare il metodo di ordinamento più veloce : Usa il metodo di accesso basato sull'indice che produce l'output ordinato

Poiché MySQL 5.5utilizza i trigger per mantenere una vista materializzata , per creare la vista materializzata iniziale è inoltre necessario un processo, uno script o una procedura memorizzata .

Ma questo è ovviamente un processo troppo pesante per essere eseguito dopo ogni aggiornamento delle tabelle di base in cui gestisci i dati. È qui che entrano in gioco i trigger per mantenere aggiornati i dati quando vengono apportate modifiche. In questo modo ciascuno insert, updatee deletepropagherà le proprie modifiche, utilizzando i trigger, alla vista materializzata .

L'organizzazione FROMDUAL su http://www.fromdual.com/ ha un codice di esempio per mantenere una vista materializzata . Quindi, piuttosto che scrivere i miei campioni, ti indicherò i loro campioni:

http://www.fromdual.com/mysql-materialized-views

Esempio 1: creazione di una vista materializzata

DROP TABLE sales_mv;
CREATE TABLE sales_mv (
    product_name VARCHAR(128)  NOT NULL
  , price_sum    DECIMAL(10,2) NOT NULL
  , amount_sum   INT           NOT NULL
  , price_avg    FLOAT         NOT NULL
  , amount_avg   FLOAT         NOT NULL
  , sales_cnt    INT           NOT NULL
  , UNIQUE INDEX product (product_name)
);

INSERT INTO sales_mv
SELECT product_name
    , SUM(product_price), SUM(product_amount)
    , AVG(product_price), AVG(product_amount)
    , COUNT(*)
  FROM sales
GROUP BY product_name;

Questo ti dà la vista materializzata al momento dell'aggiornamento. Tuttavia, poiché si dispone di un database in rapido movimento, si desidera anche mantenere questa vista il più aggiornata possibile.

Pertanto, le tabelle dei dati di base interessate devono disporre di trigger per propagare le modifiche da una tabella di base alla tabella Vista materializzata . Ad esempio:

Esempio 2: inserimento di nuovi dati in una vista materializzata

DELIMITER $$

CREATE TRIGGER sales_ins
AFTER INSERT ON sales
FOR EACH ROW
BEGIN

  SET @old_price_sum = 0;
  SET @old_amount_sum = 0;
  SET @old_price_avg = 0;
  SET @old_amount_avg = 0;
  SET @old_sales_cnt = 0;

  SELECT IFNULL(price_sum, 0), IFNULL(amount_sum, 0), IFNULL(price_avg, 0)
       , IFNULL(amount_avg, 0), IFNULL(sales_cnt, 0)
    FROM sales_mv
   WHERE product_name = NEW.product_name
    INTO @old_price_sum, @old_amount_sum, @old_price_avg
       , @old_amount_avg, @old_sales_cnt
  ;

  SET @new_price_sum = @old_price_sum + NEW.product_price;
  SET @new_amount_sum = @old_amount_sum + NEW.product_amount;
  SET @new_sales_cnt = @old_sales_cnt + 1;
  SET @new_price_avg = @new_price_sum / @new_sales_cnt;
  SET @new_amount_avg = @new_amount_sum / @new_sales_cnt;

  REPLACE INTO sales_mv
  VALUES(NEW.product_name, @new_price_sum, @new_amount_sum, @new_price_avg
       , @new_amount_avg, @new_sales_cnt)
  ;

END;
$$
DELIMITER ;

Naturalmente, avrai anche bisogno di trigger per mantenere l' eliminazione dei dati da una vista materializzata e aggiornare i dati in una vista materializzata . I campioni sono disponibili anche per questi trigger.

ULTIMO: In che modo questo rende più veloce l'ordinamento delle tabelle unite?

La vista materializzata viene costantemente creata man mano che vengono apportati gli aggiornamenti. Pertanto è possibile definire l' indice (o gli indici ) che si desidera utilizzare per ordinare i dati nella vista o nella tabella materializzata .

Se l'overhead del mantenimento dei dati non è troppo pesante, allora stai spendendo alcune risorse (CPU / IO / ecc.) Per ogni modifica dei dati rilevanti per mantenere la vista materializzata e quindi i dati dell'indice sono aggiornati e prontamente disponibili. Pertanto, la selezione sarà più veloce, dal momento che:

  1. Ho già speso CPU incrementale e IO per preparare i dati per il tuo SELECT.
  2. L'indice nella vista materializzata può utilizzare il metodo di ordinamento più veloce disponibile per MySQL, ovvero utilizzare il metodo di accesso basato sull'indice che produce l'output ordinato .

A seconda delle circostanze e di come ti senti riguardo al processo generale, potresti voler ricostruire le Viste materializzate ogni notte durante un periodo lento.

Nota: nelle Microsoft SQL Server viste materializzate si fa riferimento alle viste indicizzate e vengono aggiornate automaticamente in base ai metadati della vista indicizzata .


6

Non c'è molto da fare qui, ma suppongo che il problema principale sia che stai creando una tabella temporanea abbastanza grande e ordina i file su disco ogni volta. Il motivo è:

  1. Stai utilizzando UTF8
  2. Per l'ordinamento stai utilizzando alcuni campi varchar (255) di grandi dimensioni

Ciò significa che la tabella temporanea e il file di ordinamento potrebbero essere abbastanza grandi, come quando si crea la tabella temporanea i campi vengono creati alla lunghezza MAX e quando si ordinano i record sono tutti alla lunghezza MAX (e UTF8 è di 3 byte per carattere). Anche questi probabilmente precludono l'uso di una tabella temporanea in memoria. Per ulteriori informazioni, vedere i dettagli delle tabelle temporanee interne .

Anche qui il LIMIT non è utile, poiché dobbiamo materializzare e ordinare l'intero set di risultati prima di sapere quali sono le prime 3 righe.

Hai provato a spostare il tuo tmpdir in un filesystem tmpfs ? Se / tmp non sta già utilizzando tmpfs (MySQL utilizza tmpdir=/tmpper impostazione predefinita su * nix), è possibile utilizzare direttamente / dev / shm. Nel tuo file my.cnf:

[mysqld]
...
tmpdir=/dev/shm  

Quindi dovrai riavviare mysqld.

Ciò potrebbe fare una differenza enorme . Se è probabile che si verifichi una pressione della memoria sul sistema, probabilmente si desidera limitare le dimensioni (in genere linux distros cap tmpfs al 50% della RAM totale per impostazione predefinita) per evitare di scambiare i segmenti di memoria sul disco, o anche peggio una situazione OOM . Puoi farlo modificando la riga in /etc/fstab:

tmpfs                   /dev/shm                tmpfs   rw,size=2G,noexec,nodev,noatime,nodiratime        0 0

Puoi ridimensionarlo anche "online". Per esempio:

mount -o remount,size=2G,noexec,nodev,noatime,nodiratime /dev/shm

Potresti anche eseguire l'aggiornamento a MySQL 5.6, che ha subquery e tabelle derivate, e giocare un po 'di più con la query. Non credo che vedremo grandi vittorie su quella strada, però, da quello che vedo.

In bocca al lupo!


Grazie per la tua risposta. Spostare tmpdir in tmpfs ha dato un buon guadagno in termini di prestazioni.
Stanislav Gamayunov,
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.