Di solito si consiglia di utilizzare DISTINCT
invece di GROUP BY
, poiché è ciò che desideri effettivamente e lascia che l'ottimizzatore scelga il piano di esecuzione "migliore". Tuttavia, nessun ottimizzatore è perfetto. Usando DISTINCT
l'ottimizzatore può avere più opzioni per un piano di esecuzione. Ma ciò significa anche che ha più opzioni per scegliere un piano sbagliato .
Scrivi che il DISTINCT
la query è "lenta", ma non dici alcun numero. Nel mio test (con 10 volte più righe su MariaDB 10.0.19 e 10.3.13 ) il DISTINCT
la query è come (solo) il 25% più lenta (562 ms/453 ms). Il EXPLAIN
il risultato non è di alcun aiuto. È persino "mentire". Con LIMIT 100, 30
dovrebbe leggere almeno 130 righe (questo è ciò che il mio EXPLAIN
in realtà mostra GROUP BY
), ma ti mostra 65.
Non riesco a spiegare la differenza del 25% nel tempo di esecuzione, ma sembra che il motore stia comunque eseguendo una scansione completa di tabella/indice e ordina il risultato prima di poter saltare 100 e selezionare 30 righe.
Il miglior piano sarebbe probabilmente:
- Leggi le righe da
idx_reg_date
indice (tabellaA
) uno per uno in ordine decrescente - Guarda se c'è una corrispondenza nel
idx_order_id
indice (tabellaB
) - Salta 100 righe corrispondenti
- Invia 30 righe corrispondenti
- Esci
Se ci sono circa il 10% delle righe in A
che non hanno corrispondenza in B
, questo piano leggerebbe qualcosa come 143 righe da A
.
Il meglio che potrei fare per forzare in qualche modo questo piano è:
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
Questa query restituisce lo stesso risultato in 156 ms (3 volte più veloce di GROUP BY
). Ma è ancora troppo lento. E probabilmente sta ancora leggendo tutte le righe nella tabella A
.
Possiamo provare che può esistere un piano migliore con un "piccolo" trucco di subquery:
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
Questa query viene eseguita in "nessun tempo" (~ 0 ms) e restituisce lo stesso risultato sui miei dati di test. E sebbene non sia affidabile al 100%, mostra che l'ottimizzatore non sta facendo un buon lavoro.
Allora quali sono le mie conclusioni:
- L'ottimizzatore non sempre fa il lavoro migliore e talvolta ha bisogno di aiuto
- Anche quando conosciamo "il miglior piano", non sempre possiamo farlo rispettare
DISTINCT
non è sempre più veloce diGROUP BY
- Quando nessun indice può essere utilizzato per tutte le clausole, le cose stanno diventando piuttosto complicate
Schema di test e dati fittizi:
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;
Query:
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