Я пытаюсь запросить верхние 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 |
+----+-------------+-------+------------+--------+--------------------------------------------+------------------+---------+--------------------+-------+----------+----------------------------------------------+