#mysql #sql #mariadb #union
Вопрос:
Я запрашиваю тележки для покупок в системе магазинов, например:
DROP TABLE IF EXISTS c;
CREATE TABLE c (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user` int(10) unsigned DEFAULT NULL,
`email` VARCHAR(255) NOT NULL DEFAULT '',
`number` VARCHAR(20) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `user`(`user`),
KEY `email`(`email`),
UNIQUE KEY `number`(`number`)
) ENGINE=InnoDB;
INSERT INTO c SET user=1, email="test1@example.com", number="00001";
INSERT INTO c SET user=2, email="test2@example.com", number="00002";
INSERT INTO c SET user=3, email="test3@example.com", number="00003";
INSERT INTO c SET user=4, email="test1@example.com", number="00004";
INSERT INTO c SET user=1, email="test1@example.com", number="00005";
Мне нужно запросить записи c с помощью столбца, который показывает количество тележек, в которых один и тот же пользователь ИЛИ один и тот же адрес электронной почты. Так что я делаю:
SELECT c.number,
(SELECT COUNT(DISTINCT (id)) FROM c AS c2
WHERE c2.email = c.email OR c2.user = c.user
) AS ordercount
FROM c;
-------- ------------
| number | ordercount |
-------- ------------
| 00001 | 3 |
| 00002 | 1 |
| 00003 | 1 |
| 00004 | 3 |
| 00005 | 3 |
-------- ------------
Это работает, но проблема в том, что операционная система работает очень медленно, потому что MySQL/MariaDB не использует никакого ключа в подзапросе:
EXPLAIN SELECT c.number,
(SELECT COUNT(DISTINCT (id)) FROM c AS c2
WHERE c2.email = c.email OR c2.user = c.user
) AS ordercount
FROM c;
---- -------------------- ------- ------------ ------ --------------------------- -- ---- --------- ------ ------ ---------- -------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- -------------------- ------- ------------ ------ --------------------------- ------ --------- ------ ------ ---------- -------------
| 1 | PRIMARY | c | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 100.00 | NULL |
| 2 | DEPENDENT SUBQUERY | c2 | NULL | ALL | PRIMARY,number,user,email | NULL | NULL | NULL | 5 | 36.00 | Using where |
---- -------------------- ------- ------------ ------ --------------------------- ------ --------- ------ ------ ---------- -------------
Даже принудительное использование индекса не приводит к тому, что БД использует его:
EXPLAIN SELECT c.number,
(SELECT COUNT(DISTINCT (id)) FROM c AS c2 FORCE INDEX(email, user)
WHERE c2.email = c.email OR c2.user = c.user
) AS ordercount
FROM c;
---- -------------------- ------- ------------ ------ --------------------------- -- ---- --------- ------ ------ ---------- -------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- -------------------- ------- ------------ ------ --------------------------- ------ --------- ------ ------ ---------- -------------
| 1 | PRIMARY | c | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 100.00 | NULL |
| 2 | DEPENDENT SUBQUERY | c2 | NULL | ALL | PRIMARY,number,user,email | NULL | NULL | NULL | 5 | 36.00 | Using where |
---- -------------------- ------- ------------ ------ --------------------------- ------ --------- ------ ------ ---------- -------------
Использование столбца «электронная почта» или столбца «пользователь» работает нормально, используется ключ:
EXPLAIN SELECT c.number,
(SELECT COUNT(DISTINCT (id)) FROM c AS c2 WHERE c2.email = c.email) AS ordercount
FROM c;
---- -------------------- ------- ------------ ------ --------------------------- ------- --------- -------------- ------ ---------- -------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- -------------------- ------- ------------ ------ --------------------------- ------- --------- -------------- ------ ---------- -------------
| 1 | PRIMARY | c | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 100.00 | NULL |
| 2 | DEPENDENT SUBQUERY | c2 | NULL | ref | PRIMARY,number,user,email | email | 767 | test.c.email | 3 | 100.00 | Using index |
---- -------------------- ------- ------------ ------ --------------------------- ------- --------- -------------- ------ ---------- -------------
Проблема в том, что запрос выполняется для большой таблицы с примерно 500 000 записями, в результате чего запрос занимает около 30 секунд только для запроса подмножества из 50 записей. Выполнение запроса только с совпадением для «электронной почты» или только с совпадением для «пользователя» занимает всего около 1 секунды для 50 записей.
Поэтому мне нужно оптимизировать запрос. Я попытался превратить ОПЕРАЦИОННУЮ в ПРОФСОЮЗ:
SELECT c.number,
(SELECT COUNT(DISTINCT (id)) FROM
((SELECT u1.id FROM c AS u1 WHERE
u1.email = c.email
)
UNION DISTINCT
(SELECT u2.id FROM c AS u2 WHERE
u2.user = c.user
)) AS u2
) AS ordercount
FROM c;
но я получаю ошибку:
ОШИБКА 1054 (42S22): Неизвестный столбец «c.электронная почта» в предложении «где»
Есть идеи, как сделать этот запрос с использованием индексов быстрее?
Комментарии:
1. Я думаю, что более новые версии MySQL включают оператор «Индекс ИЛИ». Это MariaDB или MySQL? И какая версия?. Кстати, хороший вопрос.
2. Спасибо за ваш ответ. Я попробовал это на MySQL 5.7.24 и MariaDB 10.5.6 с тем же результатом. К сожалению, MySQL 8.x недоступен на сервере.
3. (Вы должны добавить другую корзину-с тем же пользователем, но с другим адресом электронной почты.)
Ответ №1:
Это альтернативный подход, использующий два left join
s:
select c.*,
count(distinct coalesce(ce.id, cu.id))
from c left join
c ce
on c.email = ce.email left join
c cu
on c.user = cu.user and not cu.email <=> ce.email
group by c.id;
Это может использовать отдельные индексы на c(user)
и c(email)
.
По сути, это объединяет два отдельных измерения, а затем объединяет их для count(distinct)
. Есть несколько худших сценариев, в которых может быть много совпадений по обоим измерениям. Однако во многих случаях это может оказаться весьма эффективным, поскольку оно может использовать индексы, а не сканировать всю таблицу для каждой строки.
Комментарии:
1. Да, я думаю, что это должно хорошо работать, если мощность низкая. Я уже наполовину думал об этом, а потом отвлекся.
Ответ №2:
(Я предполагаю, что «с» означает «тележка».)
(начинаем все сначала)
Так number
как есть UNIQUE
, то это вполне может быть так PRIMARY KEY
. Также избавьтесь от id
.
CREATE FUNCTION Ct(_user INT, _email VARCHAR(255))
RETURNS VARCHAR(20)
RETURN
SELECT COUNT(DISTINCT number)
FROM
( SELECT number
FROM c
WHERE user = _user
) UNION ALL
( SELECT number
FROM c
WHERE email = _email
);
Тогда сделай
SELECT number, Ct(user, email)
FROM c;
Обратите внимание, что я избегал двойного РАЗЛИЧЕНИЯ. И, поскольку PK неявно является частью каждого вторичного индекса, внутренние выборки имеют индексы «покрытия».
Комментарии:
1. Спасибо за ваш ответ! Не могли бы вы немного объяснить это, пожалуйста? К сожалению, я получаю «пустой набор» при запуске вашего ВЫБОРА в моих тестовых данных. Я добавил результат, ожидаемый для тестовых данных, это набор результатов с 2 столбцами «число» и «количество заказов».
2. @Werner — Вам нужен список c.номеров, у которых есть электронные письма или пользователи dup? Или просто один — единственный счет?
3. @Вернер — я начал все сначала.