EXPLAIN: планът на заявката
Накратко: EXPLAIN показва плана на заявката, тоест как базата ще я изпълни: дали ще прочете цялата таблица, или ще търси по индекс (в SQLite EXPLAIN QUERY PLAN пише SCAN или SEARCH).
Проблем
„Справката е бавна“
Колега казва, че справката за посещенията на един клиент е бавна. Може да гадаеш: дали таблицата е голяма, дали условието е сложно, дали липсва индекс. Може и да питаш базата как точно смята да я изпълни.
Всяка база първо прави план на заявката (query plan): от коя таблица да започне, дали да чете всичко или да търси в индекс, дали да сортира. Едва после го изпълнява. Планът може да се види.
Идеята
EXPLAIN QUERY PLAN
В SQLite пишеш EXPLAIN QUERY PLAN пред заявката. Тя не се изпълнява. Вместо резултат получаваш плана:
Важната колона е detail. id и parent подреждат стъпките в дърво, когато са повече, а notused не се ползва. Тук може да не им обръщаш внимание. SCAN visits значи: прочети цялата таблица ред по ред.
В това поле индексът по customer_id вече е направен:
SEARCH значи търсене по индекс. В скобите е какво търси: customer_id=?, тоест една стойност на customer_id. SCAN или SEARCH е първото, което гледаш в плана.
Пробвай
„Моите поръчки“
Страницата „Моите поръчки“ търси поръчките на клиент по customer_id. Виж плана в полето, после направи индекс idx_orders_customer, така че планът на същата заявка да стане SEARCH.
Решение
CREATE INDEX idx_orders_customer ON orders(customer_id);CREATE INDEX idx_orders_customer ON orders(customer_id); Проверката пуска плана на SELECT * FROM orders WHERE customer_id = 5 след твоя отговор и го сравнява с плана при решението, затова името на индекса трябва да е същото. Забележи, че customer_id е външен ключ, а индекс по него няма. SQLite и PostgreSQL не правят такъв индекс сами. MySQL (InnoDB) го прави.
Идеята
Покриващ индекс
Сравни плана на SELECT * по-горе с плана на COUNT(*) със същото условие:
Тук пише USING COVERING INDEX. Индексът пази стойността на customer_id и номера на реда. За COUNT(*) това стига: базата брои записите в индекса и не отваря таблицата. Това се казва покриващ индекс (covering index).
За SELECT * трябват и product_id, и seen_at. Те ги няма в индекса, затова за всеки намерен запис базата отива и в таблицата. Номерът на реда (id) е във всеки запис на индекса, затова SELECT id със същото условие е покриваща заявка:
Виж го
Скритото сортиране
Страницата показва посещенията на клиента от най-новото. Пусни плана:
Вторият ред е USE TEMP B-TREE FOR ORDER BY. Индексът е помогнал да се намерят редовете, но те не са подредени по seen_at. Затова базата ги сортира сама, във временна структура. За 60 реда това е нищо. За 60 000 вече се усеща. В следващия урок ще направим индекс, който дава редовете вече подредени.
Търсенето по първичен ключ има свой запис в плана:
Идеята
Как да разбереш защо е бавна
Пусни плана на бавната заявка и потърси:
- SCAN на голяма таблица, а условието избира малко редове. Липсва индекс или условието не може да го ползва (за това е последният урок от модула).
- USE TEMP B-TREE. Базата сортира или групира сама. Индекс в правилния ред може да го спести.
- SEARCH, но пак бавно. Условието избира много редове, или индексът не покрива заявката и за всеки ред се чете таблицата.
Планът казва какво ще стане. Колко време отнема, виждаш, когато пуснеш самата заявка.
По-точно, за любопитните
В SQLite EXPLAIN без QUERY PLAN показва програмата, в която SQLite превежда заявката (команди за неговата виртуална машина). Тя е за хората, които пишат самия SQLite.
В PostgreSQL EXPLAIN показва плана с оценки: колко реда очаква всяка стъпка и колко ще „струва“. EXPLAIN ANALYZE изпълнява заявката наистина и добавя истинското време и истинския брой редове на всяка стъпка. Когато оценката и истината се разминават много, оттам обикновено идва бавният план. Там пълното четене се казва Seq Scan, търсенето по индекс Index Scan, а покриващият индекс Index Only Scan. Внимавай: EXPLAIN ANALYZE на UPDATE или DELETE наистина променя данните, затова го пускай в транзакция и после ROLLBACK.
MySQL има EXPLAIN, а от версия 8.0.18 и EXPLAIN ANALYZE.
Упражнение
Две задачи
1. Отчетът за октомври брои посещенията от 1 октомври 2026 г. нататък. Направи индекс idx_visits_seen, така че планът да е SEARCH.
Индекс, с който планът на отчета за октомври става SEARCH:
Решение
CREATE INDEX idx_visits_seen ON visits(seen_at);CREATE INDEX idx_visits_seen ON visits(seen_at); Планът става SEARCH visits USING COVERING INDEX idx_visits_seen (seen_at>?). Индексът е подреден, затова за >= базата намира първата дата и чете нататък до края. И е покриващ, защото COUNT(*) не иска нищо от таблицата.
2. Клиент 4321 иска данните му да бъдат изтрити. Програмата първо трябва само номерата на посещенията му. Индексът по customer_id вече е направен. Напиши заявката така, че базата да не чете таблицата.
Номерата (id) на посещенията на клиент 4321, само от индекса:
Решение
SELECT id FROM visits WHERE customer_id = 4321;SELECT id FROM visits WHERE customer_id = 4321; 60 посещения. Провери с EXPLAIN QUERY PLAN пред заявката: пише USING COVERING INDEX. Със SELECT * резултатът щеше да е друг, а и базата щеше да отваря всеки от 60-те реда.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: ORDER BY, LIMIT и DISTINCT · Агрегатни функции: COUNT, SUM, AVG · Първичен и външен ключ · Индекси
- Продължава в: Индекс по няколко колони · Бавни заявки · OLTP и OLAP · Търсене по смисъл
- Задачи по темата: Индекси и скорост