Cómo optimizar SELECT muy lento con JOINES IZQUIERDOS sobre tablas grandes


14

Estuve buscando en Google, autodidacta y buscando solución durante horas, pero sin suerte. Encontré algunas preguntas similares aquí, pero no este caso.

Mis mesas:

  • personas (~ 10 millones de filas)
  • atributos (ubicación, edad, ...)
  • enlaces (M: M) entre personas y atributos (~ 40 millones de filas)

Descarga completa ~ 280 MB

Situación: trato de seleccionar todos los identificadores de persona ( person_id) de algunas ubicaciones ( location.attribute_value BETWEEN 3000 AND 7000), de algún género ( gender.attribute_value = 1), nacido en algunos años ( bornyear.attribute_value BETWEEN 1980 AND 2000) y con el color de algunos ojos ( eyecolor.attribute_value IN (2,3)).

Esta es mi consulta bruja tomó 3 ~ 4 min. y me gustaría optimizar:

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;

Resultado:

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

Explique extendido:

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

Perfilado:

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

Tablas de estructuras:

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 consulta se realizó en el servidor virtual DigitalOcean con SSD y 1 GB de RAM.

Supongo que puede haber problemas con el diseño de la base de datos. ¿Tiene alguna sugerencia para diseñar mejor esta situación, por favor? ¿O solo para ajustar la selección anterior?


44
Ese es el precio que paga por el diseño EAV. Es posible que desee probar un índice compuesto enattribute (person_id, attribute_type_id, attribute_value)
mustaccio

1
Intentaría agregar estos índices: (attribute_type_id, attribute_value, person_id)y (attribute_type_id, person_id, attribute_value)
ypercubeᵀᴹ

55
Y use InnoDB, deseche MyISAM. Esto es 2015, MyiSAM está muerto hace mucho tiempo.
ypercubeᵀᴹ

2
Lo primero: deshacerse de la combinación IZQUIERDA, no tiene ningún efecto ya que utiliza todas las tablas en su condición WHERE, convirtiendo efectivamente todas las combinaciones en combinaciones INNER (el optimizador debería ser capaz de comprenderlo y optimizarlo, pero mejor no hacerlo más difícil) ) Segunda cosa: deshabilite el caché de consultas a menos que tenga una buena razón para usarlo (= lo probó y midió que le ayuda)
jkavalik

2
OT: ¿no es extraño que uses LIMIT sin nuestro ORDER BY? ¿Esto devolverá algunas 100000 filas al azar?
ibre5041

Respuestas:


7

Elija algunos atributos para incluir person. Indícelos en algunas combinaciones: use índices compuestos, no índices de una sola columna.

Esa es esencialmente la única salida de EAV-sucks-at-performance, que es donde estás.

Aquí hay más discusión: http://mysql.rjweb.org/doc.php/eav, incluida una sugerencia de usar JSON en lugar de la tabla de valores clave.


3

Agregue indeces a attributepara:

  • (person_id, attribute_type_id, attribute_value) y
  • (attribute_type_id, attribute_value, person_id)

Explicación

Con su diseño actual EXPLAINespera que su consulta examine las 1,265,229 * 4 * 4 * 4 = 80,974,656filas attribute. Se puede reducir este número por la adición de un índice compuesto en attributepara (person_id, attribute_type_id). Al usar este índice, su consulta solo examinará 1 en lugar de 4 filas para cada uno de location, eyecolory gender.

Se podría extender ese índice para incluir attribute_type_valueasí: (person_id, attribute_type_id, attribute_value). Esto convertiría este índice en un índice de cobertura para esta consulta, que también debería mejorar el rendimiento.

Además, agregar un índice en (attribute_type_id, attribute_value, person_id)(de nuevo un índice de cobertura al incluir person_id) debería mejorar el rendimiento con solo usar un índice en el attribute_valueque tendrían que examinarse más filas. En este caso, se cerrará el primer paso en su explicación: seleccionar un rango de bornyear.

El uso de esas dos indeces redujo el tiempo de ejecución de su consulta en mi sistema de ~ 2.0 sa ~ 0.2 s con la salida de explicación como esta:

+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
| 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
Gracias por una amplia respuesta y explicación. Hice todo lo que mencionaste, pero la consulta todavía toma ~ 2 min. Por favor, ¿qué tipo de tabla (innodb, myisam) está utilizando y qué consulta exacta realizó?
Martin

1
Además de agregar las indeces, utilicé exactamente los mismos datos y definiciones que usted, así que usé MyISAM. Cambié la primera línea de su consulta a SELECT person.person_idporque de lo contrario no se ejecutaría, obviamente. ¿Lo hiciste ANALYZE TABLE attributedespués de agregar las indeces? Es posible que también desee agregar su nueva EXPLAINsalida (después de agregar indeces) a su pregunta.
wolfgangwalther

3

Supongo que puede haber problemas con el diseño de la base de datos.

Está utilizando un diseño denominado Entidad-Atributo-Valor, que a menudo funciona mal, bien, por diseño.

¿Tiene alguna sugerencia para diseñar mejor esta situación, por favor?

La forma relacional clásica de diseñar esto sería crear una tabla separada para cada atributo. En general, puede hacer que estas mesas separadas: location, gender, bornyear, eyecolor.

Lo siguiente depende de si ciertos atributos siempre se definen para una persona, o no. Y, si una persona puede tener solo un valor de un atributo. Por ejemplo, generalmente la persona tiene un solo género. En su diseño actual, nada le impide agregar tres filas para la misma persona con diferentes valores de género en ellas. También puede establecer un valor de género no en 1 o 2, sino en un número que no tiene sentido, como 987 y no hay restricciones en la base de datos que lo impidan. Pero, esta es otra cuestión separada de mantener la integridad de los datos con el diseño EAV.

Si siempre conoce el género de la persona, entonces tiene poco sentido colocarlo en una tabla separada y es mucho mejor tener una columna no nula GenderIDen la persontabla, que sería una clave foránea para la tabla de búsqueda con la lista de todos los géneros posibles y sus nombres. Si conoce el género de la persona la mayor parte del tiempo, pero no siempre, puede anular esta columna y configurarla NULLcuando la información no esté disponible. Si la mayoría de las veces se desconoce el género de la persona, puede ser mejor tener una tabla separada genderque se vincule a person1: 1 y que tenga filas solo para aquellas personas que tienen un género conocido.

Consideraciones similares se aplican a eyecolory bornyear: es poco probable que la persona tenga dos valores para un eyecoloro bornyear.

Si es posible que una persona tenga varios valores para un atributo, entonces definitivamente lo pondría en una tabla separada. Por ejemplo, no es raro que una persona tenga varias direcciones (casa, trabajo, postal, vacaciones, etc.), por lo que las enumeraría todas en una tabla location. Tablas persony locationestarían vinculadas 1: M.


¿O solo para ajustar la selección anterior?

Si usa el diseño EAV, entonces al menos haría lo siguiente.

  • Las columnas del conjunto attribute_type_id, attribute_value, person_ida NOT NULL.
  • Configure la clave externa que se vincula attribute.person_idcon person.person_id.
  • Crea un índice en tres columnas (attribute_type_id, attribute_value, person_id). El orden de las columnas es importante aquí.
  • Hasta donde yo sé, MyISAM no respeta las claves externas, así que no lo use, use InnoDB en su lugar.

Escribiría la consulta así. Use en INNERlugar de LEFTuniones y escriba explícitamente subconsultas para cada atributo para darle al optimizador todas las posibilidades de usar el índice.

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;

Además, puede valer la pena dividir la attributetabla attribute_type_id.


Precaución de rendimiento: JOIN ( SELECT ... )no se optimiza bien. JOINingdirectamente a la mesa funciona mejor (pero sigue siendo problemático).
Rick James

2

Espero haber encontrado una solución suficiente. Está inspirado en este artículo .

Respuesta corta:

  1. He creado 1 tabla con todos los atributos. Una columna para un atributo. Más columna de clave principal.
  2. Los valores de los atributos se almacenan en celdas de texto (para búsqueda de texto completo) en formato similar a CSV.
  3. Creado índices de texto completo. Antes de eso, es importante configurar ft_min_word_len=1(para MyISAM) en la [mysqld]sección y innodb_ft_min_token_size=1(para InnoDb) en el my.cnfarchivo, reinicie el servicio mysql.
  4. Ejemplo de búsqueda: SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000donde 123, 456a 789son ID en las que las personas deberían haberse asociado attribute_1. Esta consulta tomó menos de 1 seg.

Respuesta detallada:

Paso 1. Crear una tabla con índices de texto completo. InnoDb admite índices de texto completo de MySQL 5.7, por lo que si usa 5.5 o 5.6, debe usar MyISAM. A veces es incluso más rápido para la búsqueda FT que 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

Paso 2. Insertar datos de la tabla EAV (entidad-atributo-valor). Por ejemplo, en cuestión, se puede hacer con 1 SQL simple:

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

El resultado debería ser algo como esto:

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)

Paso 3. Seleccione de la tabla con una consulta como esta:

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 consulta selecciona todas las filas:

  • haciendo coincidir al menos uno de estos ID en attr_1:3000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
  • Y al mismo tiempo juego 1en attr_2(esta columna representa el género por lo que si esta solución se han personalizado, debe ser smallint(1)con índice simple, etc ...)
  • Y al mismo tiempo coincide con al menos uno de 1980, 1981, 1982, 1983 or 1984enattr_3
  • Y al mismo tiempo coincidente 2o 3enattr_4

Conclusión:

Sé que esta solución no es perfecta e ideal para muchas situaciones, pero puede usarse como una buena alternativa para el diseño de tablas EAV.

Espero que ayude a alguien.


1
Me parece muy poco probable que este diseño funcione mejor que su diseño original con índices compuestos. ¿Qué pruebas hiciste para compararlos?
ypercubeᵀᴹ

0

Intente usar sugerencias de índice de consulta que parezcan apropiadas

Indicaciones del índice Mysql


1
Las sugerencias pueden ayudar a una versión de la consulta, pero luego perjudican a otra. Tenga en cuenta que el Optimizer eligió bornyear como la mejor primera tabla, probablemente porque si filtra las filas más indeseables.
Rick James
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.