J'étais sur Google, autodidacte et à la recherche d'une solution pendant des heures mais sans chance. J'ai trouvé quelques questions similaires ici, mais pas ce cas.
Mes tables:
Situation: J'essaie de sélectionner tous les identifiants des personnes (person_id
) À partir de certains emplacements (location.attribute_value BETWEEN 3000 AND 7000
), Étant un sexe (gender.attribute_value = 1
), Né quelques années (bornyear.attribute_value BETWEEN 1980 AND 2000
) Et ayant la couleur de certains yeux (eyecolor.attribute_value IN (2,3)
).
C'est ma requête qui a pris 3 ~ 4 min. et j'aimerais optimiser:
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;
Résultat:
+-----------+
| person_id |
+-----------+
| 233 |
| 605 |
| ... |
| 8702599 |
| 8703617 |
+-----------+
100000 rows in set (3 min 42.77 sec)
Expliquez étendu:
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| 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)
Profilage:
+------------------------------+-----------+
| 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)
Structures des tables:
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 requête avait été effectuée sur le serveur virtuel DigitalOcean avec SSD et 1 Go de RAM.
Je suppose qu'il peut y avoir un problème avec la conception de la base de données. Avez-vous des suggestions pour mieux concevoir cette situation, s'il vous plaît? Ou tout simplement pour ajuster la sélection ci-dessus?
J'espère avoir trouvé une solution suffisante. Il est inspiré par cet article .
ft_min_Word_len=1
(Pour MyISAM) dans la section [mysqld]
Et innodb_ft_min_token_size=1
(Pour InnoDb) dans le fichier my.cnf
, Redémarrez le service mysql.SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000
où 123
, 456
A 789
Sont des identifiants que les personnes devraient avoir associées dans attribute_1
. Cette requête a pris moins de 1 seconde.Étape 1. Création d'une table avec des index fulltext. InnoDb prend en charge les index de texte intégral de MySQL 5.7, donc si vous utilisez 5.5 ou 5.6, vous devez utiliser MyISAM. C'est parfois encore plus rapide pour la recherche FT qu'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
Étape 2. Insérez les données de la table EAV (entity-attribute-value). Par exemple indiqué en question cela peut se faire avec 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
Le résultat devrait être quelque chose comme ceci:
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)
Étape 3. Sélectionnez dans le tableau avec une requête comme celle-ci:
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 requête sélectionne toutes les lignes:
attr_1
: 3000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
1
Dans attr_2
(Cette colonne représente gender donc si cette solution a été personnalisée, elle devrait être smallint(1)
avec index simple, etc ...)1980, 1981, 1982, 1983 or 1984
Dans attr_3
2
Ou 3
Dans attr_4
Conclusion:
Je sais que cette solution n'est pas parfaite et idéale pour de nombreuses situations, mais peut être utilisée comme une bonne alternative pour la conception de tables EAV.
J'espère que ça va aider quelqu'un.
Choisissez un quelques attributs à inclure dans person
. Indexez-les en quelques combinaisons - utilisez des index composites, pas des index à colonne unique.
C'est essentiellement le seul moyen de sortir de l'EAV-sucks-at-performance, qui est là où vous êtes.
Voici plus de discussion: http://mysql.rjweb.org/doc.php/eav y compris une suggestion d'utiliser JSON au lieu de la table de valeurs-clés.
Je suppose qu'il peut y avoir un problème avec la conception de la base de données.
Vous utilisez une conception dite Entité-Attribut-Valeur, qui fonctionne souvent mal, bien, par conception.
Avez-vous des suggestions pour mieux concevoir cette situation, s'il vous plaît?
La manière relationnelle classique de concevoir cela serait de créer une table distincte pour chaque attribut. En général, vous pouvez avoir ces tables distinctes: location
, gender
, bornyear
, eyecolor
.
Les éléments suivants varient selon que certains attributs sont toujours définis pour une personne ou non. Et, si une personne ne peut avoir qu'une seule valeur d'un attribut. Par exemple, la personne n'a généralement qu'un seul sexe. Dans votre conception actuelle, rien ne vous empêche d'ajouter trois lignes pour la même personne avec des valeurs de sexe différentes. Vous pouvez également définir une valeur de genre non pas à 1 ou 2, mais à un nombre qui n'a pas de sens, comme 987 et aucune contrainte dans la base de données ne l'empêcherait. Mais, c'est une autre question distincte de maintenir l'intégrité des données avec la conception EAV.
Si vous connaissez toujours le sexe de la personne, cela n'a aucun sens de le placer dans un tableau séparé et il est préférable d'avoir une colonne non nulle GenderID
dans le tableau person
, ce qui être une clé étrangère de la table de recherche avec la liste de tous les genres possibles et leurs noms. Si vous connaissez le sexe de la personne la plupart du temps, mais pas toujours, vous pouvez rendre cette colonne annulable et la définir sur NULL
lorsque les informations ne sont pas disponibles. Si la plupart du temps, le sexe de la personne n'est pas connu, il peut être préférable d'avoir une table distincte gender
qui relie à person
1: 1 et ne comporte des lignes que pour les personnes qui ont une le genre.
Des considérations similaires s'appliquent à eyecolor
et bornyear
- il est peu probable que la personne ait deux valeurs pour un eyecolor
ou bornyear
.
S'il est possible pour une personne d'avoir plusieurs valeurs pour un attribut, alors vous le mettriez certainement dans une table séparée. Par exemple, il n'est pas rare qu'une personne ait plusieurs adresses (domicile, travail, courrier, vacances, etc.), vous devez donc toutes les répertorier dans un tableau location
. Les tables person
et location
seraient liées 1: M.
Ou tout simplement pour ajuster la sélection ci-dessus?
Si vous utilisez la conception EAV, je ferais au moins ce qui suit.
attribute_type_id
, attribute_value
, person_id
à NOT NULL
.attribute.person_id
avec person.person_id
.(attribute_type_id, attribute_value, person_id)
. L'ordre des colonnes est important ici.J'écrirais la requête comme ceci. Utilisez INNER
au lieu de LEFT
jointures et écrivez explicitement une sous-requête pour chaque attribut pour donner à l'optimiseur toutes les chances d'utiliser l'index.
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;
En outre, cela peut valoir la peine partitionnement la table attribute
par attribute_type_id
.
Ajoutez des indéces à attribute
pour:
(person_id, attribute_type_id, attribute_value)
et(attribute_type_id, attribute_value, person_id)
Explication
Avec votre conception actuelle, EXPLAIN
s'attend à ce que votre requête examine 1,265,229 * 4 * 4 * 4 = 80,974,656
lignes dans attribute
. Vous pouvez réduire ce nombre en ajoutant un index composite sur attribute
pour (person_id, attribute_type_id)
. En utilisant cet index, votre requête examinera seulement 1 au lieu de 4 lignes pour chacun des location
, eyecolor
et gender
.
Vous pouvez étendre cet index pour inclure attribute_type_value
ainsi que: (person_id, attribute_type_id, attribute_value)
. Cela transformerait cet index en index de couverture pour cette requête, ce qui devrait également améliorer les performances.
Ajout d'un index sur (attribute_type_id, attribute_value, person_id)
(encore un indice de couverture en incluant person_id
) devrait améliorer les performances par rapport à la simple utilisation d'un index sur attribute_value
où d'autres lignes devraient être examinées. Dans ce cas, cela accélérera la première étape de votre explication: la sélection d'une plage parmi bornyear
.
L'utilisation de ces deux indéces a réduit le temps d'exécution de votre requête sur mon système de ~ 2,0 s à ~ 0,2 s avec la sortie d'explication ressemblant à ceci:
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
| 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 |
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
Essayez d'utiliser des indices d'index de requête qui semblent appropriés