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

attribute (person_id, attribute_type_id, attribute_value)(attribute_type_id, attribute_value, person_id)y(attribute_type_id, person_id, attribute_value)Respuestas:
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.
fuente
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 las1,265,229 * 4 * 4 * 4 = 80,974,656filasattribute. Se puede reducir este número por la adición de un índice compuesto enattributepara(person_id, attribute_type_id). Al usar este índice, su consulta solo examinará 1 en lugar de 4 filas para cada uno delocation,eyecolorygender.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 incluirperson_id) debería mejorar el rendimiento con solo usar un índice en elattribute_valueque tendrían que examinarse más filas. En este caso, se cerrará el primer paso en su explicación: seleccionar un rango debornyear.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:
fuente
SELECT person.person_idporque de lo contrario no se ejecutaría, obviamente. ¿Lo hicisteANALYZE TABLE attributedespués de agregar las indeces? Es posible que también desee agregar su nuevaEXPLAINsalida (después de agregar indeces) a su pregunta.Está utilizando un diseño denominado Entidad-Atributo-Valor, que a menudo funciona mal, bien, por diseño.
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 lapersontabla, 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 configurarlaNULLcuando 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 separadagenderque se vincule aperson1: 1 y que tenga filas solo para aquellas personas que tienen un género conocido.Consideraciones similares se aplican a
eyecolorybornyear: es poco probable que la persona tenga dos valores para uneyecolorobornyear.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. Tablaspersonylocationestarían vinculadas 1: M.Si usa el diseño EAV, entonces al menos haría lo siguiente.
attribute_type_id,attribute_value,person_idaNOT NULL.attribute.person_idconperson.person_id.(attribute_type_id, attribute_value, person_id). El orden de las columnas es importante aquí.Escribiría la consulta así. Use en
INNERlugar deLEFTuniones y escriba explícitamente subconsultas para cada atributo para darle al optimizador todas las posibilidades de usar el índice.Además, puede valer la pena dividir la
attributetablaattribute_type_id.fuente
JOIN ( SELECT ... )no se optimiza bien.JOINingdirectamente a la mesa funciona mejor (pero sigue siendo problemático).Espero haber encontrado una solución suficiente. Está inspirado en este artículo .
Respuesta corta:
ft_min_word_len=1(para MyISAM) en la[mysqld]sección yinnodb_ft_min_token_size=1(para InnoDb) en elmy.cnfarchivo, reinicie el servicio mysql.SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000donde123,456a789son ID en las que las personas deberían haberse asociadoattribute_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.
Paso 2. Insertar datos de la tabla EAV (entidad-atributo-valor). Por ejemplo, en cuestión, se puede hacer con 1 SQL simple:
El resultado debería ser algo como esto:
Paso 3. Seleccione de la tabla con una consulta como esta:
La consulta selecciona todas las filas:
attr_1:3000, 3001, 3002, 3003, 3004, 3005, 3006 or 30071enattr_2(esta columna representa el género por lo que si esta solución se han personalizado, debe sersmallint(1)con índice simple, etc ...)1980, 1981, 1982, 1983 or 1984enattr_32o3enattr_4Conclusió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.
fuente
Intente usar sugerencias de índice de consulta que parezcan apropiadas
Indicaciones del índice Mysql
fuente