Як оптимізувати дуже повільно SELECT за допомогою лівих приєднань за великими таблицями


15

Я гуляв, самоосвічувався і шукав рішення годинами, але не пощастило. Тут я знайшов кілька подібних питань, але не цей випадок.

Мої таблиці:

  • осіб (~ 10М рядків)
  • атрибути (місцеположення, вік, ...)
  • посилання (M: M) між особами та атрибутами (~ 40М рядків)

Повний відвал ~ 280 Мб

Ситуація: я намагаюся вибрати всі ідентифікатори ( 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 Гб оперативної пам’яті.

Я припускаю, що можуть виникнути проблеми з дизайном бази даних. Чи є якісь пропозиції, щоб краще розробити цю ситуацію? Або просто відрегулювати вибране вище?


4
Це ціна, яку ви платите за дизайн EAV. Ви можете спробувати складений покажчикattribute (person_id, attribute_type_id, attribute_value)
mustaccio

1
Я б спробував додати ці індекси: (attribute_type_id, attribute_value, person_id)і (attribute_type_id, person_id, attribute_value)
ypercubeᵀᴹ

5
І використовуйте InnoDB, викиньте MyISAM. Це 2015 рік, MyiSAM давно помер.
ypercubeᵀᴹ

2
Перше - позбутися від лівого приєднання, це не має ефекту, оскільки ви використовуєте всі таблиці в своєму стані WHERE, ефективно перетворюючи всі з'єднання на INNER приєднується (оптимізатор повинен вміти розуміти та оптимізувати це, але краще не робити це складніше ). Друга річ - вимкнути кеш запитів, якщо у вас немає вагомих причин використовувати його (= ви перевірили його і виміряли, що він вам допомагає)
jkavalik

2
ОТ: Чи не дивно, що ви використовуєте ОБМЕЖЕННЯ за допомогою ЗАМОВЛЕННЯ? Це поверне кілька випадкових 100000 рядків?
ibre5041

Відповіді:


8

Виберіть кілька атрибутів, які потрібно включити до person. Індексуйте їх у кількох комбінаціях - використовуйте складені індекси, а не одноколонні індекси.

Це, по суті, єдиний вихід з EAV-смокче-на-працездатності, саме там ви знаходитесь.

Ось додаткова дискусія: http://mysql.rjweb.org/doc.php/eav, включаючи пропозицію використовувати JSON замість таблиці ключ-значення.


3

Додати доданки 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 с.

+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
| 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
Дякуємо за широку відповідь та пояснення. Я зробив все, що ви згадали, але запит все ще займає ~ 2 хв. Будь ласка, який тип таблиці (innodb, myisam) ви використовуєте та який саме запит виконували?
Мартін

1
Крім додавання індексів, я використовував ті самі дані та визначення, які ви робили, тому я використовував MyISAM. Я змінив перший рядок вашого запиту, SELECT person.person_idоскільки, очевидно, він не запустився. Ви робили ANALYZE TABLE attributeпісля додавання індексу? Можливо, ви хочете додати новий запит EXPLAIN(після додавання індексів) і до свого питання.
wolfgangwalther

3

Я припускаю, що можуть виникнути проблеми з дизайном бази даних.

Ви користуєтесь так званим дизайном Entity-Attribute-Value, який часто працює погано і добре.

Чи є якісь пропозиції, щоб краще розробити цю ситуацію?

Класичним реляційним способом спроектувати це було б створення окремої таблиці для кожного атрибута. Загалом, ви можете мати ці окремі таблиці: location, gender, bornyear, eyecolor.

Далі залежить від того, чи завжди визначені певні атрибути для людини, чи ні. І, чи може людина мати лише одне значення атрибута. Наприклад, зазвичай людина має лише одну стать. У вашому поточному дизайні ніщо не заважає вам додавати три ряди для однієї і тієї ж людини з різними значеннями для статі в них. Ви також можете встановити значення статі не на 1 або 2, а на деяке число, яке не має сенсу, як-от 987, і в базі даних немає обмежень, які б це заважали. Але це ще одне окреме питання збереження цілісності даних із дизайном EAV.

Якщо ви завжди знаєте стать цієї людини, то це НЕ має ніякого сенсу , щоб помістити його в окрему таблицю , і це краще , щоб мати стовпець непорожній GenderIDв personтаблиці, яка була б зовнішнім ключем до таблиці пошуку зі списком всі можливі статі та їх імена. Якщо ви знаєте стать людини більшу частину часу, але не завжди, ви можете зробити цей стовпчик нульовим і встановити його, NULLколи інформація недоступна. Якщо більшу частину часу стать людини невідома, то, можливо, краще мати окрему таблицю, genderяка посилається на person1: 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). Тут важливий порядок стовпців.
  • Наскільки я знаю, MyISAM не шанує іноземні ключі, тому не використовуйте їх, використовуйте замість InnoDB.

Я б написав запит так. Використовуйте INNERзамість LEFTприєднання та явно пишіть підзапит для кожного атрибуту, щоб дати оптимізатору всі шанси використовувати індекс.

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;

Крім того , він може бути варто секціонування в attributeтаблиці attribute_type_id.


Обережність виконання: JOIN ( SELECT ... )не оптимізується добре. JOINingбезпосередньо до таблиці працює краще (але все ще проблематично).
Рік Джеймс

3

Я сподіваюся, що знайшов достатнє рішення. Натхненна цією статтею .

Коротка відповідь:

  1. Я створив 1 таблицю з усіма атрибутами. Один стовпець для одного атрибута. Плюс стовпчик первинного ключа.
  2. Значення атрибутів зберігаються у текстових комірках (для повнотекстового пошуку) у форматі CSV.
  3. Створено повнотекстові покажчики. Перед цим важливо встановити ft_min_word_len=1(для MyISAM) у [mysqld]розділі та innodb_ft_min_token_size=1(для InnoDb) у my.cnfфайлі, перезапустити службу mysql.
  4. Приклад пошуку: SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000де 123, 456а - 789це посвідчення особи, до яких повинні були бути пов’язані особи attribute_1. Цей запит зайняв менше 1 сек.

Детальна відповідь:

Крок 1. Створення таблиці з повнотекстовими індексами. InnoDb підтримує повнотекстові індекси з MySQL 5.7, тому якщо ви використовуєте 5.5 або 5.6, ви повинні використовувати MyISAM. Іноді це навіть швидше пошук FT, ніж 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

Крок 2. Вставте дані з таблиці EAV (сутність-атрибут-значення). Наприклад, зазначене в питанні, це можна зробити за допомогою 1 простого SQL:

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

Результат повинен бути приблизно таким:

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)

Крок 3. Виберіть із таблиці такий запит:

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)

Запит вибирає всі рядки:

  • відповідність хоча б одному з цих ідентифікаторів у 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.

Я сподіваюся, що це комусь допоможе.


1
Я вважаю дуже малоймовірним, що ця конструкція буде краще, ніж ваш оригінальний дизайн із складеними покажчиками. Які тести ви зробили, щоб порівняти їх?
ypercubeᵀᴹ

0

Спробуйте скористатись підказками індексу запитів, які виглядають доречно

Підказки Mysql


1
Підказки можуть допомогти одній версії запиту, але потім зашкодять іншій. Зауважте, що оптимізатор вибрав борний рік як найкращу першу таблицю, ймовірно, тому, що якщо відфільтрувати найбільш небажані рядки.
Рік Джеймс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.