Я гуглил, занимался самообразованием и искал решение в течение нескольких часов, но безуспешно. Я нашел несколько подобных вопросов здесь, но не этот случай.
Мои таблицы:
- человек (~ 10 млн рядов)
- атрибуты (местоположение, возраст, ...)
- связи (M: M) между людьми и атрибутами (~ 40M строк)
Ситуация:
Я пытаюсь выбрать все идентификаторы лиц ( person_id
) из некоторых мест ( location.attribute_value BETWEEN 3000 AND 7000
), пола ( gender.attribute_value = 1
), рожденного в несколько лет ( bornyear.attribute_value BETWEEN 1980 AND 2000
) и имеющего цвет глаз ( eyecolor.attribute_value IN (2,3)
).
Это мой запрос на ведьм 3 ~ 4 мин. и я хотел бы оптимизировать:
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;
Результат:
+-----------+
| person_id |
+-----------+
| 233 |
| 605 |
| ... |
| 8702599 |
| 8703617 |
+-----------+
100000 rows in set (3 min 42.77 sec)
Объясните расширенный:
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| 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)
Профилирование:
+------------------------------+-----------+
| 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)
Структуры таблиц:
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;
Запрос был выполнен на виртуальном сервере DigitalOcean с SSD и 1 ГБ оперативной памяти.
Я предполагаю, что могут быть проблемы с дизайном базы данных. Есть ли у вас какие-либо предложения по улучшению этой ситуации, пожалуйста? Или просто отрегулировать выбор выше?
attribute (person_id, attribute_type_id, attribute_value)
(attribute_type_id, attribute_value, person_id)
и(attribute_type_id, person_id, attribute_value)
Ответы:
Выберите несколько атрибутов для включения
person
. Индексируйте их в нескольких комбинациях - используйте составные индексы, а не одноколонные индексы.По сути, это единственный выход из EAV-sucks-at-performance, где вы находитесь.
Вот еще обсуждение: http://mysql.rjweb.org/doc.php/eav, включая предложение использовать JSON вместо таблицы ключ-значение.
источник
Добавить значения в
attribute
для:(person_id, attribute_type_id, attribute_value)
и(attribute_type_id, attribute_value, person_id)
объяснение
С вашим текущим дизайном
EXPLAIN
ожидает, что ваш запрос будет проверять1,265,229 * 4 * 4 * 4 = 80,974,656
строки вattribute
. Вы можете уменьшить это число, добавив составной индекс наattribute
для(person_id, attribute_type_id)
. Используя этот индекс, ваш запрос будет проверять только 1, а не 4 строки для каждого изlocation
,eyecolor
иgender
.Вы можете расширить этот индекс , чтобы включить ,
attribute_type_value
а также:(person_id, attribute_type_id, attribute_value)
. Это превратит этот индекс в покрывающий индекс для этого запроса, что также должно повысить производительность.Кроме того, добавление индекса
(attribute_type_id, attribute_value, person_id)
(снова включающего индекс покрытияperson_id
) должно повысить производительность по сравнению с использованием только индекса, вattribute_value
котором нужно исследовать больше строк. В этом случае это будет первый шаг в вашем объяснении: выбор диапазона изbornyear
.Использование этих двух индексов уменьшило время выполнения вашего запроса в моей системе с ~ 2,0 с до ~ 0,2 с, а вывод объяснения выглядел следующим образом:
источник
SELECT person.person_id
потому что иначе он не запустился бы, очевидно. Вы сделалиANALYZE TABLE attribute
после добавления Indedes? Возможно, вы захотите добавить свой новыйEXPLAIN
вывод (после добавления индексов) к вашему вопросу.Вы используете так называемый дизайн Entity-Attribute-Value, который часто работает плохо, ну, по дизайну.
Классическим реляционным способом разработки этого было бы создание отдельной таблицы для каждого атрибута. В общем, вы можете иметь эти отдельные таблицы:
location
,gender
,bornyear
,eyecolor
.Следующее зависит от того, всегда ли определенные атрибуты определены для человека или нет. И может ли человек иметь только одно значение атрибута. Например, обычно человек имеет только один пол. В вашем текущем дизайне ничто не мешает вам добавить три строки для одного и того же человека с разными значениями пола в них. Вы также можете установить значение пола не 1 или 2, а какое-то число, которое не имеет смысла, например 987, и в базе данных нет ограничений, которые могли бы его предотвратить. Но это еще одна отдельная проблема поддержания целостности данных с помощью дизайна EAV.
Если вы всегда знаете пол человека, то не имеет смысла помещать его в отдельную таблицу, и лучше иметь
GenderID
вperson
таблице ненулевой столбец , который будет внешним ключом для таблицы поиска со списком все возможные полы и их имена. Если вы знаете пол человека большую часть времени, но не всегда, вы можете сделать этот столбец обнуляемым и установить его,NULL
когда информация недоступна. Если в большинстве случаев пол человека неизвестен, то может быть лучше иметь отдельную таблицу,gender
которая ссылается наperson
1: 1 и содержит строки только для людей с известным полом.Аналогичные соображения применимы к
eyecolor
иbornyear
- у человека вряд ли будет два значения дляeyecolor
илиbornyear
.Если человек может иметь несколько значений для атрибута, то вы определенно поместите его в отдельную таблицу. Например, человек нередко имеет несколько адресов (домашний, рабочий, почтовый, праздничный и т. Д.), Поэтому вы должны перечислить их все в таблице
location
. Таблицыperson
иlocation
будут связаны 1: М.Если использовать дизайн EAV, то я бы по крайней мере сделал следующее.
attribute_type_id
,attribute_value
,person_id
кNOT NULL
.attribute.person_id
сperson.person_id
.(attribute_type_id, attribute_value, person_id)
. Порядок столбцов здесь важен.Я бы написал запрос так. Используйте
INNER
вместоLEFT
объединений и явно напишите подзапрос для каждого атрибута, чтобы дать оптимизатору все шансы использовать индекс.Кроме того , он может быть стоит секционирования в
attribute
таблицеattribute_type_id
.источник
JOIN ( SELECT ... )
не оптимизировать хорошо.JOINing
напрямую к столу работает лучше (но все же проблематично).Я надеюсь, что нашел достаточное решение. Это вдохновлено этой статьей .
Короткий ответ:
ft_min_word_len=1
(для MyISAM) в[mysqld]
разделе иinnodb_ft_min_token_size=1
(для InnoDb) вmy.cnf
файле, перезапустить службу MySQL.SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000
где123
,456
a789
- идентификаторы, с которыми должны были связаться людиattribute_1
. Этот запрос занял менее 1 сек.Подробный ответ:
Шаг 1. Создание таблицы с полнотекстовыми индексами. InnoDb поддерживает полнотекстовые индексы из MySQL 5.7, поэтому, если вы используете 5.5 или 5.6, вы должны использовать MyISAM. Иногда это даже быстрее для поиска FT, чем InnoDb.
Шаг 2. Вставьте данные из таблицы EAV (entity-attribute-value). Например, указанный в вопросе это может быть сделано с 1 простым SQL:
Результат должен быть примерно таким:
Шаг 3. Выберите из таблицы запрос:
Запрос выбирает все строки:
attr_1
:3000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
1
вattr_2
(этот столбец представляет пол, поэтому, если это решение было настроено, оно должно бытьsmallint(1)
с простым индексом и т. Д ...)1980, 1981, 1982, 1983 or 1984
вattr_3
2
или3
вattr_4
Вывод:
Я знаю, что это решение не идеально и идеально подходит для многих ситуаций, но может быть использовано в качестве хорошей альтернативы для дизайна таблицы EAV.
Надеюсь, это кому-нибудь поможет.
источник
Попробуйте использовать подсказки индекса запроса, которые выглядят уместно
Mysql Index Hints
источник