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
attribute
para:(person_id, attribute_type_id, attribute_value)
y(attribute_type_id, attribute_value, person_id)
Explicación
Con su diseño actual
EXPLAIN
espera que su consulta examine las1,265,229 * 4 * 4 * 4 = 80,974,656
filasattribute
. Se puede reducir este número por la adición de un índice compuesto enattribute
para(person_id, attribute_type_id)
. Al usar este índice, su consulta solo examinará 1 en lugar de 4 filas para cada uno delocation
,eyecolor
ygender
.Se podría extender ese índice para incluir
attribute_type_value
así:(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_value
que 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_id
porque de lo contrario no se ejecutaría, obviamente. ¿Lo hicisteANALYZE TABLE attribute
después de agregar las indeces? Es posible que también desee agregar su nuevaEXPLAIN
salida (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
GenderID
en laperson
tabla, 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 configurarlaNULL
cuando 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 separadagender
que se vincule aperson
1: 1 y que tenga filas solo para aquellas personas que tienen un género conocido.Consideraciones similares se aplican a
eyecolor
ybornyear
: es poco probable que la persona tenga dos valores para uneyecolor
obornyear
.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
. Tablasperson
ylocation
estarían vinculadas 1: M.Si usa el diseño EAV, entonces al menos haría lo siguiente.
attribute_type_id
,attribute_value
,person_id
aNOT NULL
.attribute.person_id
conperson.person_id
.(attribute_type_id, attribute_value, person_id)
. El orden de las columnas es importante aquí.Escribiría la consulta así. Use en
INNER
lugar deLEFT
uniones 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
attribute
tablaattribute_type_id
.fuente
JOIN ( SELECT ... )
no se optimiza bien.JOINing
directamente 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.cnf
archivo, reinicie el servicio mysql.SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000
donde123
,456
a789
son 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 3007
1
enattr_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 1984
enattr_3
2
o3
enattr_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.
fuente
Intente usar sugerencias de índice de consulta que parezcan apropiadas
Indicaciones del índice Mysql
fuente