¿Hay alguna manera de optimizar la ordenación por columnas de tablas unidas?

10

Esta es mi consulta 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;

El tiempo promedio de consulta es de 4.5s en mi conjunto de datos y esto es inaceptable.

Soluciones que veo:

Agregue todas las columnas de la cláusula order a la products_countstabla. Pero tengo ~ 10 tipos de orden en la aplicación, por lo que debería crear muchas columnas e índices. Además, products_countstengo actualizaciones / inserciones / eliminaciones muy intensas, por lo que necesito realizar una actualización inmediata de todas las columnas relacionadas con el orden (¿usando disparadores?).

¿Hay otra solución?

Explique:

+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
| 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 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+

Estructura de tablas:

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;

Información del servidor MySQL:

mysqld  Ver 5.5.45-1+deb.sury.org~trusty+1 for debian-linux-gnu on i686 ((Ubuntu))
Stanislav Gamayunov
fuente
3
¿Puede proporcionar un violín SQL con índices, esquema de tabla y datos de prueba? Además, ¿cuál es su tiempo objetivo? ¿Estás buscando que se complete en 3 segundos, 1 segundo, 50 milisegundos? ¿Cuántos registros tiene en las diferentes tablas 1k, 100k, 100M?
Erik
Si los campos por los que está ordenando no están indexados y el conjunto de datos es realmente grande, ¿podría estar estudiando un problema sort_buffer_size? Puede intentar modificar su valor en su sesión y ejecutar la consulta para ver si mejora.
Brian Efting
¿Has intentado agregar un índice (inflow, product_id)?
ypercubeᵀᴹ
Asegúrate de tener un decente innodb_buffer_pool_size. Por lo general, alrededor del 70% de la RAM disponible es buena.
Rick James

Respuestas:

6

La revisión de las definiciones de la tabla muestra que tiene índices coincidentes entre las tablas involucradas. Esto debería hacer que las uniones sucedan lo más rápido posible dentro de los límites de la MySQL'slógica de unión.

Sin embargo, ordenar de varias tablas es más complejo.

En 2007, Sergey Petrunia describió los 3 MySQLalgoritmos de clasificación por orden de velocidad MySQLen: http://s.petrunia.net/blog/?m=201407

  1. Utilice el método de acceso basado en índices que produce resultados ordenados
  2. Usar filesort()en la primera tabla no constante
  3. Ponga unirse resultado en una tabla temporal y utilizar filesort()en ella

De las definiciones de tabla y combinaciones que se muestran arriba, puede ver que nunca obtendrá la clasificación más rápida . Eso significa que dependerá filesort()de los criterios de clasificación que esté utilizando.

Sin embargo, si diseña y usa una Vista Materializada , podrá usar el algoritmo de ordenación más rápido.

Para ver los detalles definidos para MySQL 5.5los métodos de clasificación, consulte: http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html

Para MySQL 5.5(en este ejemplo) aumentar la ORDER BYvelocidad si no puede MySQLusar índices en lugar de una fase de clasificación adicional, pruebe las siguientes estrategias:

• Aumentar el sort_buffer_sizevalor de la variable.

• Aumentar el read_rnd_buffer_sizevalor de la variable.

• Use menos RAM por fila declarando columnas tan grandes como sea necesario para que se almacenen los valores reales. [Por ejemplo, reducir un varchar (256) a varchar (ActualLongestString)]

• Cambie la tmpdirvariable del sistema para que apunte a un sistema de archivos dedicado con grandes cantidades de espacio libre. (Otros detalles se ofrecen en el enlace de arriba).

Se proporcionan más detalles en la MySQL 5.7documentación para aumentar la ORDERvelocidad, algunos de los cuales pueden ser comportamientos ligeramente actualizados :

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

Vistas materializadas : un enfoque diferente para clasificar tablas unidas

Aludió a Vistas materializadas con su pregunta que se refiere al uso de disparadores. MySQL no tiene una funcionalidad integrada para crear una vista materializada, pero sí tiene las herramientas necesarias. Al utilizar disparadores para distribuir la carga, puede mantener la Vista materializada hasta el momento.

La Vista materializada es en realidad una tabla que se rellena a través de un código de procedimiento para construir o reconstruir la Vista materializada y que se mantiene mediante disparadores para mantener los datos actualizados.

Como está creando una tabla que tendrá un índice , la Vista materializada cuando se le consulte puede usar el método de clasificación más rápido : use un método de acceso basado en índice que produzca resultados ordenados

Dado que MySQL 5.5utiliza disparadores para mantener una Vista materializada , también necesitará un proceso, secuencia de comandos o procedimiento almacenado para construir la Vista materializada inicial .

Pero ese es obviamente un proceso demasiado pesado para ejecutarse después de cada actualización de las tablas base donde administra los datos. Ahí es donde los disparadores entran en juego para mantener los datos actualizados a medida que se realizan los cambios. De esta manera insert, cada , updatey deletepropagará sus cambios, usando sus disparadores, a la Vista Materializada .

La organización FROMDUAL en http://www.fromdual.com/ tiene un código de muestra para mantener una Vista materializada . Entonces, en lugar de escribir mis propias muestras, te señalaré sus muestras:

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

Ejemplo 1: crear una vista materializada

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;

Esto le brinda la Vista materializada en el momento de la actualización. Sin embargo, dado que tiene una base de datos que se mueve rápidamente, también desea mantener esta vista lo más actualizada posible.

Por lo tanto, las tablas de datos base afectadas deben tener activadores para propagar los cambios de una tabla base a la tabla Vista materializada . Como un ejemplo:

Ejemplo 2: Insertar datos nuevos en una vista materializada

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 ;

Por supuesto, también necesitará disparadores para mantener Eliminar datos de una vista materializada y actualizar datos en una vista materializada . Las muestras también están disponibles para estos desencadenantes.

AL FIN: ¿Cómo hace que la clasificación de tablas unidas sea más rápida?

La Vista materializada se construye constantemente a medida que se le realizan actualizaciones. Por lo tanto, puede definir el índice (o índices ) que desea usar para ordenar los datos en la vista o tabla materializada .

Si la sobrecarga de mantener los datos no es demasiado pesada, entonces está gastando algunos recursos (CPU / IO / etc.) para cada cambio de datos relevante para mantener la Vista materializada y, por lo tanto, los datos del índice están actualizados y fácilmente disponibles. Por lo tanto, la selección será más rápida, ya que usted:

  1. Ya pasé CPU e IO incrementales para preparar los datos para su SELECT.
  2. El índice en la Vista Materializada puede usar el método de clasificación más rápido disponible para MySQL, a saber, Usar el método de acceso basado en índice que produce una salida ordenada .

Dependiendo de sus circunstancias y de cómo se sienta sobre el proceso general, es posible que desee reconstruir las Vistas materializadas todas las noches durante un período lento.

Nota: En las Microsoft SQL Server vistas materializadas se hace referencia a las vistas indexadas y se actualizan automáticamente en función de los metadatos de la vista indexada .

RLF
fuente
6

No hay mucho que hacer aquí, pero supongo que el problema principal es que está creando una tabla temporal bastante grande y ordena el archivo en el disco cada vez. La razón es:

  1. Estás usando UTF8
  2. Estás utilizando algunos campos grandes de varchar (255) para ordenar

Esto significa que su tabla temporal y el archivo de clasificación podrían ser bastante grandes, ya que al crear la tabla temporal los campos se crean en la longitud MÁXIMA, y al ordenar los registros están todos en la longitud MÁXIMA (y UTF8 es de 3 bytes por carácter). También es probable que esto impida el uso de una tabla temporal en memoria. Para obtener más información, consulte los detalles de las tablas temporales internas .

El LÍMITE tampoco nos sirve de nada aquí, ya que necesitamos materializar y ordenar todo el conjunto de resultados antes de saber cuáles son las primeras 3 filas.

¿Has intentado mover tu tmpdir a un sistema de archivos tmpfs ? Si / tmp aún no está usando tmpfs (MySQL usa tmpdir=/tmpde forma predeterminada en * nix), entonces podría usar / dev / shm directamente. En su archivo my.cnf:

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

Entonces necesitarías reiniciar mysqld.

Eso podría hacer una gran diferencia. Si es probable que se vea sometido a una presión de memoria en el sistema, es probable que desee limitar el tamaño (por lo general, las distribuciones de Linux cap tmpfs al 50% de la RAM total de forma predeterminada) para evitar intercambiar segmentos de memoria en el disco, o incluso peor una situación OOM . Puede hacerlo editando la línea en /etc/fstab:

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

También puede cambiar su tamaño "en línea". Por ejemplo:

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

También puede actualizar a MySQL 5.6, que tiene subconsultas de rendimiento y tablas derivadas, y jugar un poco más con la consulta. Sin embargo, no creo que veamos grandes victorias en esa ruta, por lo que veo.

¡Buena suerte!

Matt Lord
fuente
Gracias por tu respuesta. Mover tmpdir a tmpfs dio una buena ganancia de rendimiento.
Stanislav Gamayunov