Отличный против группы 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