MySQL медленный запрос с SELECT / ORDER BY в одной таблице с WHERE в другой, LIMIT результатов - PullRequest
2 голосов
/ 28 мая 2020

Я пытаюсь запросить верхние N строк из пары таблиц. Предложение WHERE относится к списку столбцов в одной таблице, тогда как предложение ORDER BY относится к столбцам в другой. Похоже, что MySQL выбирает таблицу, используемую в моем предложении WHERE, для своего первого прохода фильтрации (которая мало фильтрует), тогда как ORDER BY влияет на строки, возвращаемые после того, как я применяю LIMIT. Если я заставляю MySQL использовать индекс покрытия для ORDER BY, запрос немедленно возвращается с желаемыми строками. К сожалению, я не могу передавать подсказки индекса в MySQL через JPA, и переписывание всего с помощью собственных запросов потребует значительного объема работы. Вот наглядный пример:

CREATE TABLE person (
    id INTEGER PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(255),
    last_name VARCHAR(255)
) engine=InnoDB;

CREATE TABLE membership (
    id INTEGER PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL
) engine=InnoDB;

CREATE TABLE employee (
    id INTEGER PRIMARY KEY AUTO_INCREMENT,
    membership_id INTEGER NOT NULL,
    type VARCHAR(15),
    enabled BIT NOT NULL,
    person_id INTEGER NOT NULL REFERENCES person ( id ),
    CONSTRAINT fk_employee_membership_id FOREIGN KEY ( membership_id ) REFERENCES membership ( id ),
    CONSTRAINT fk_employee_person_id FOREIGN KEY ( person_id ) REFERENCES person ( id )
) engine=InnoDB;

CREATE UNIQUE INDEX uk_employee_person_id ON employee ( person_id );

CREATE INDEX idx_person_first_name_last_name ON person ( first_name, last_name );

Я написал сценарий для вывода группы операторов INSERT для заполнения таблиц 200000 строками:

#!/bin/bash
#
echo "INSERT INTO membership ( id, name ) VALUES ( 1, 'Default Membership' );"
for seq in {1..200000}; do
    echo "INSERT INTO person ( id, first_name, last_name ) VALUES ( $seq, 'firstName$seq', 'lastName$seq' );"
    echo "INSERT INTO employee ( id, membership_id, type, enabled, person_id ) VALUES ( $seq, 1, 'INDIVIDUAL', 1, $seq );"
done

Моя первая попытка:

SELECT e.*
FROM person p INNER JOIN employee e ON p.id = e.person_id
WHERE e.membership_id = 1 AND type = 'INDIVIDUAL' AND enabled = 1
ORDER BY p.first_name ASC, p.last_name ASC, p.id ASC
LIMIT 100;
-- 100 rows in set (1.43 sec)

и EXPLAIN:

+----+-------------+-------+------------+--------+-------------------------------------------------+---------------------------+---------+--------------------+-------+----------+----------------------------------------------+
| id | select_type | table | partitions | type   | possible_keys                                   | key                       | key_len | ref                | rows  | filtered | Extra                                        |
+----+-------------+-------+------------+--------+-------------------------------------------------+---------------------------+---------+--------------------+-------+----------+----------------------------------------------+
|  1 | SIMPLE      | e     | NULL       | ref    | uk_employee_person_id,fk_employee_membership_id | fk_employee_membership_id | 4       | const              | 99814 |     5.00 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | p     | NULL       | eq_ref | PRIMARY                                         | PRIMARY                   | 4       | qsuite.e.person_id |     1 |   100.00 | NULL                                         |
+----+-------------+-------+------------+--------+-------------------------------------------------+---------------------------+---------+--------------------+-------+----------+----------------------------------------------+

Теперь я заставляю MySQL использовать индекс (first_name, last_name) для человека:

SELECT e.*
FROM person p USE INDEX ( idx_person_first_name_last_name )
    INNER JOIN employee e ON p.id = e.person_id
WHERE e.membership_id = 1 AND type = 'INDIVIDUAL' AND enabled = 1
ORDER BY p.first_name ASC, p.last_name ASC, p.id ASC
LIMIT 100;
-- 100 rows in set (0.00 sec)

Он возвращается мгновенно . И объяснение:

+----+-------------+-------+------------+--------+-------------------------------------------------+---------------------------------+---------+-------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys                                   | key                             | key_len | ref         | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+-------------------------------------------------+---------------------------------+---------+-------------+------+----------+-------------+
|  1 | SIMPLE      | p     | NULL       | index  | NULL                                            | idx_person_first_name_last_name | 2046    | NULL        |  100 |   100.00 | Using index |
|  1 | SIMPLE      | e     | NULL       | eq_ref | uk_employee_person_id,fk_employee_membership_id | uk_employee_person_id           | 4       | qsuite.p.id |    1 |     5.00 | Using where |
+----+-------------+-------+------------+--------+-------------------------------------------------+---------------------------------+---------+-------------+------+----------+-------------+

Обратите внимание, что предложение WHERE в этом примере фактически не фильтрует какие-либо строки. Это в значительной степени репрезентативно для имеющихся у меня данных и большинства запросов к этой таблице. Есть ли способ уговорить MySQL использовать этот индекс или какой-то не очень деструктивный способ реструктуризации для повышения производительности?

Спасибо.

Изменить: я отказался от оригинального покрытия index и добавил по одному в каждую из таблиц:

CREATE INDEX idx_person_id_first_name_last_name ON person ( id, first_name, last_name );
CREATE INDEX idx_employee_etc ON employee ( membership_id, type, enabled, person_id );

Кажется, это немного ускоряет, но MySQL по-прежнему настаивает на том, чтобы сначала пройти через таблицу сотрудников:

+----+-------------+-------+------------+--------+--------------------------------------------+------------------+---------+--------------------+-------+----------+----------------------------------------------+
| id | select_type | table | partitions | type   | possible_keys                              | key              | key_len | ref                | rows  | filtered | Extra                                        |
+----+-------------+-------+------------+--------+--------------------------------------------+------------------+---------+--------------------+-------+----------+----------------------------------------------+
|  1 | SIMPLE      | e     | NULL       | ref    | uk_employee_person_id,idx_employee_etc     | idx_employee_etc | 68      | const,const,const  | 97311 |   100.00 | Using index; Using temporary; Using filesort |
|  1 | SIMPLE      | p     | NULL       | eq_ref | PRIMARY,idx_person_id_first_name_last_name | PRIMARY          | 4       | qsuite.e.person_id |     1 |   100.00 | NULL                                         |
+----+-------------+-------+------------+--------+--------------------------------------------+------------------+---------+--------------------+-------+----------+----------------------------------------------+

Ответы [ 2 ]

0 голосов
/ 02 июня 2020

Резервное хранение имени и фамилии в таблице employee - это вариант, но с недостатками. Вам придется управлять избыточностью. Чтобы гарантировать согласованность, вы можете сделать эти столбцы частью внешнего ключа. ON UPDATE CASCADE потребует от вас работы. Но вам все равно придется переписать операторы INSERT или использовать триггеры. Если first_name и last_name являются частью таблицы employee, вы сможете создать оптимальный индекс для своего запроса. Таблица будет выглядеть следующим образом:

CREATE TABLE employee (
    id INTEGER PRIMARY KEY AUTO_INCREMENT,
    membership_id INTEGER NOT NULL,
    type VARCHAR(15),
    enabled BIT NOT NULL,
    person_id INTEGER NOT NULL REFERENCES person ( id ),
    CONSTRAINT fk_employee_membership_id FOREIGN KEY ( membership_id ) REFERENCES membership ( id ),
    CONSTRAINT fk_employee_person FOREIGN KEY ( person_id, first_name, last_name ) 
                                  REFERENCES person ( id, first_name, last_name ),
    INDEX (membership_id, type, enabled, first_name, last_name, person_id)
) engine=InnoDB;

Запрос изменится на:

SELECT e.*
FROM employee e
WHERE e.membership_id = 1 AND e.type = 'INDIVIDUAL' AND e.enabled = 1
ORDER BY e.first_name ASC, e.last_name ASC, e.person_id ASC
LIMIT 100;

Однако - я бы по возможности избегал таких изменений. Могут быть и другие способы использования индекса для ORDER BY. Сначала я бы попытался переместить условия WHERE в коррелированный подзапрос EXISTS:

SELECT e.*
FROM person p INNER JOIN employee e ON p.id = e.person_id
WHERE EXISTS (
  SELECT *
  FROM employee e1
  WHERE e1.person_id = p.id
    AND e1.membership_id = 1
    AND e1.type = 'INDIVIDUAL'
    AND e1.enabled = 1
)
ORDER BY p.first_name ASC, p.last_name ASC, p.id ASC
LIMIT 100;

Теперь, чтобы оценить подзапрос, движку требуется p.id, поэтому он должен начать чтение данных из person таблица (которую вы увидите в плане выполнения). И я думаю, он будет достаточно умным, чтобы прочитать это по индексу. Обратите внимание, что в InnoDB первичный ключ всегда является частью любого вторичного ключа. Таким образом, индекс idx_person_first_name_last_name на самом деле находится на (first_name, last_name, id).

0 голосов
/ 28 мая 2020

Я бы сделал ваш второй индекс в таблице людей на (id, first_name, last_name) и избавился бы от второго индекса, если только вы действительно не будете запрашивать имя человека в качестве основного.

Для таблица сотрудников, имейте индекс на (membership_id, type, enabled, person_id)

Наличие правильного индекса в таблице сотрудников поможет вернуть все соответствующие записи. Наличие имени человека и информации об идентификаторе в индексе не позволяет механизму перейти к страницам необработанных данных для извлечения столбцов для окончательного упорядочивания / ограничения

SELECT 
        e.*
    FROM 
        employee e 
            INNER JOIN person p 
                ON e.person_id = p.id
    WHERE 
            e.membership_id = 1 
        AND e.type = 'INDIVIDUAL' 
        AND e.enabled = 1
    ORDER BY 
        p.first_name ASC, 
        p.last_name ASC, 
        p.id ASC
    LIMIT 
        100;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...