Отличный против группы By
У меня есть две таблицы вроде этого.
Таблица "порядок" имеет 21886 строк.
CREATE TABLE `order` (
`id` bigint(20) unsigned NOT NULL,
`reg_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_reg_date` (`reg_date`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
CREATE TABLE `order_detail_products` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) unsigned NOT NULL,
`order_detail_id` int(11) NOT NULL,
`prod_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_order_detail_id` (`order_detail_id`,`prod_id`),
KEY `idx_order_id` (`order_id`,`order_detail_id`,`prod_id`)
) ENGINE=InnoDB AUTO_INCREMENT=572375 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Мой вопрос здесь.
MariaDB [test]> explain
-> SELECT DISTINCT A.id
-> FROM order A
-> JOIN order_detail_products B ON A.id = B.order_id
-> ORDER BY A.reg_date DESC LIMIT 100, 30;
+------+-------------+-------+-------+---------------+--------------+---------+-------------------+-------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------+-------+---------------+--------------+---------+-------------------+-------+----------------------------------------------+
| 1 | SIMPLE | A | index | PRIMARY | idx_reg_date | 8 | NULL | 22151 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | B | ref | idx_order_id | idx_order_id | 8 | bom_20140804.A.id | 2 | Using index; Distinct |
+------+-------------+-------+-------+---------------+--------------+---------+-------------------+-------+----------------------------------------------+
2 rows in set (0.00 sec)
MariaDB [test]> explain
-> SELECT A.id
-> FROM order A
-> JOIN order_detail_products B ON A.id = B.order_id
-> GROUP BY A.id
-> ORDER BY A.reg_date DESC LIMIT 100, 30;
+------+-------------+-------+-------+---------------+--------------+---------+-------------------+------+------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------+-------+---------------+--------------+---------+-------------------+------+------------------------------+
| 1 | SIMPLE | A | index | PRIMARY | idx_reg_date | 8 | NULL | 65 | Using index; Using temporary |
| 1 | SIMPLE | B | ref | idx_order_id | idx_order_id | 8 | bom_20140804.A.id | 2 | Using index |
+------+-------------+-------+-------+---------------+--------------+---------+-------------------+------+------------------------------+
Перечисленное выше, два запроса возвращают один и тот же результат, но разные слишком медленные (объясните слишком много строк).
Какая разница?
Ответы
Ответ 1
Я считаю, что ваш выбор отличается медленным, потому что вы разбили индекс, сопоставляя его с другой таблицей. В большинстве случаев выбор отдельных будет быстрее. Но в этом случае, поскольку вы сопоставляетесь с параметрами другой таблицы, индекс разбивается и работает намного медленнее.
Ответ 2
Обычно рекомендуется использовать DISTINCT
вместо GROUP BY
, поскольку это именно то, что вам действительно нужно, и позволить оптимизатору выбрать "лучший" план выполнения. Однако оптимизатор не идеален. Используя DISTINCT
оптимизатор может иметь больше опций для плана выполнения. Но это также означает, что у него больше возможностей выбрать плохой план.
Вы пишете, что запрос DISTINCT
является "медленным", но вы не говорите никаких чисел. В моем тесте (с 10-кратным числом строк в MariaDB 10.0.19 и 10.3.13) запрос DISTINCT
примерно на 25% медленнее (562 мс /453 мс). Результат EXPLAIN
ничем не помогает. Это даже "врет". С LIMIT 100, 30
ему нужно прочитать не менее 130 строк (это то, что на самом деле показывает мой EXPLAIN
для GROUP BY
), но показывает 65.
Я не могу объяснить разницу во времени выполнения на 25%, но кажется, что движок в любом случае выполняет полное сканирование таблицы/индекса и сортирует результат, прежде чем пропустить 100 и выбрать 30 строк.
Лучший план, вероятно, будет:
- Чтение строк из индекса
idx_reg_date
(таблица A
) по idx_reg_date
в порядке убывания - Посмотрите, есть ли совпадение в индексе
idx_order_id
(таблица B
) - Пропустить 100 подходящих строк
- Отправить 30 подходящих строк
- Выход
Если в A
примерно 10% строк не имеют совпадений в B
, этот план будет читать примерно 143 строки из A
Лучшее, что я мог сделать, чтобы как-то заставить этот план:
SELECT A.id
FROM 'order' A
WHERE EXISTS (SELECT * FROM order_detail_products B WHERE A.id = B.order_id)
ORDER BY A.reg_date DESC
LIMIT 30
OFFSET 100
Этот запрос возвращает тот же результат за 156 мс (в 3 раза быстрее, чем GROUP BY
). Но это все еще слишком медленно. И это, вероятно, все еще читая все строки в таблице A
Мы можем доказать, что лучший план может существовать с помощью "небольшого" трюка с подзапросом:
SELECT A.id
FROM (
SELECT id, reg_date
FROM 'order'
ORDER BY reg_date DESC
LIMIT 1000
) A
WHERE EXISTS (SELECT * FROM order_detail_products B WHERE A.id = B.order_id)
ORDER BY A.reg_date DESC
LIMIT 30
OFFSET 100
Этот запрос выполняется в "без времени" (~ 0 мс) и возвращает тот же результат на моих тестовых данных. И хотя это не на 100% надежно, это показывает, что оптимизатор не делает хорошую работу.
Итак, каковы мои выводы:
- Оптимизатор не всегда делает лучшую работу и иногда нуждается в помощи
- Даже когда мы знаем "лучший план", мы не всегда можем его применять
-
DISTINCT
не всегда быстрее чем GROUP BY
- Когда ни один индекс не может быть использован для всех предложений - все становится довольно сложно
Тестовая схема и фиктивные данные:
drop table if exists 'order';
CREATE TABLE 'order' (
'id' bigint(20) unsigned NOT NULL AUTO_INCREMENT,
'reg_date' timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ('id'),
KEY 'idx_reg_date' ('reg_date')
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
insert into 'order'(reg_date)
select from_unixtime(floor(rand(1) * 1000000000)) as reg_date
from information_schema.COLUMNS a
, information_schema.COLUMNS b
limit 218860;
drop table if exists 'order_detail_products';
CREATE TABLE 'order_detail_products' (
'id' int(10) unsigned NOT NULL AUTO_INCREMENT,
'order_id' bigint(20) unsigned NOT NULL,
'order_detail_id' int(11) NOT NULL,
'prod_id' int(11) NOT NULL,
PRIMARY KEY ('id'),
KEY 'idx_order_detail_id' ('order_detail_id','prod_id'),
KEY 'idx_order_id' ('order_id','order_detail_id','prod_id')
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
insert into order_detail_products(id, order_id, order_detail_id, prod_id)
select null as id
, floor(rand(2)*218860)+1 as order_id
, 0 as order_detail_id
, 0 as prod_id
from information_schema.COLUMNS a
, information_schema.COLUMNS b
limit 437320;
Запросы:
SELECT DISTINCT A.id
FROM 'order' A
JOIN order_detail_products B ON A.id = B.order_id
ORDER BY A.reg_date DESC
LIMIT 30 OFFSET 100;
-- 562 ms
SELECT A.id
FROM 'order' A
JOIN order_detail_products B ON A.id = B.order_id
GROUP BY A.id
ORDER BY A.reg_date DESC
LIMIT 30 OFFSET 100;
-- 453 ms
SELECT A.id
FROM 'order' A
WHERE EXISTS (SELECT * FROM order_detail_products B WHERE A.id = B.order_id)
ORDER BY A.reg_date DESC
LIMIT 30 OFFSET 100;
-- 156 ms
SELECT A.id
FROM (
SELECT id, reg_date
FROM 'order'
ORDER BY reg_date DESC
LIMIT 1000
) A
WHERE EXISTS (SELECT * FROM order_detail_products B WHERE A.id = B.order_id)
ORDER BY A.reg_date DESC
LIMIT 30 OFFSET 100;
-- ~ 0 ms