Бавни заявки
Накратко: Заявката е бавна въпреки индекса, когато колоната е във функция или в LIKE, когато взима всички колони, когато OR сравнява и колона без индекс или когато брои всичко, а пита само „има ли“.
Проблем
Индексът го има, а е бавно
Отчетът за годината брои посещенията през 2026 г. Индекс по seen_at има. Пусни заявката и виж времето:
И плана:
SCAN visits USING COVERING INDEX. Базата чете индекса, защото е по-малък от таблицата, но го чете целия: всичките 300 000 записа. Търсене няма.
Идеята
Колоната трябва да е сама
Индексът е подреден по seen_at. Не е подреден по substr(seen_at, 1, 4). За да провери условието, базата трябва да сметне функцията за всеки запис, а за това трябва да мине през всички.
Същото условие може да се каже с граници, при които колоната е сама от едната страна:
Резултатът е същият, а планът е SEARCH. Сравни времената на двете заявки.
Важи и за сметките. customer_id + 0 = 1234 дава същото като customer_id = 1234, но индексът не помага:
Правилото: не пипай колоната в WHERE. Сметката се прехвърля от другата страна (price > 10 / 1.2, а не price * 1.2 > 10), а функцията върху дата става интервал.
Пробвай
Октомври
В полето е заявката от годишния отчет. Месечният отчет иска посещенията през октомври 2026 г. Напиши условието така, че да ползва индекса по seen_at.
Решение
SELECT COUNT(*) FROM visits
WHERE seen_at >= '2026-10-01' AND seen_at < '2026-11-01';WHERE seen_at >= '2026-10-01' AND seen_at < '2026-11-01'. 9 988 посещения. Проверката гледа и плана: със substr(seen_at, 1, 7) = '2026-10' или LIKE '2026-10%' резултатът е верен, но планът е SCAN и отговорът не минава. С границите планът е SEARCH … (seen_at>? AND seen_at<?). Горната граница е „преди 1 ноември“, а не „до 31 октомври“: така не мислиш колко дни има месецът, а и стойност с час (2026-10-31 18:30) също влиза.
Виж го
LIKE и индексът
Изглежда, че LIKE '2026-10%' трябва да ползва индекса: началото е известно, както в указателя. Пусни плана:
В SQLite е SCAN. Причината: LIKE в SQLite не прави разлика между малки и главни букви, а индексът е подреден с разлика. SQLite ползва индекс за LIKE 'начало%' само при определени условия, например ако колоната или индексът са COLLATE NOCASE. За датите сигурният начин е интервалът от предишната карта.
Шаблон, който започва с %, не може да ползва обикновен индекс в никоя база. Началото не е известно, затова няма откъде да започне търсенето:
По-точно, за любопитните
Изключение от правилото за функциите е индексът по израз: CREATE INDEX idx_visits_year ON visits(substr(seen_at, 1, 4)) прави WHERE substr(seen_at, 1, 4) = '2026' отново SEARCH. Индексът помага само на заявки със същия израз.
В SQLite GLOB '2026-10*' прави разлика между малки и главни букви и ползва индекса (планът е SEARCH). Същото прави и PRAGMA case_sensitive_like = ON, но то променя поведението на всеки LIKE във връзката.
В PostgreSQL обикновеният индекс помага за LIKE 'начало%' само при колация C или ако е направен с text_pattern_ops. За търсене в средата на текста има отделни видове индекси (разширението pg_trgm). Пълнотекстовото търсене е в модула за видовете бази.
Идеята
Само нужните колони
Програмата показва на кои дати са идвали клиентите от 1000 до 1100. Две заявки с едни и същи редове. Сравни времената:
Втората е по-бърза, защото customer_id и seen_at са в индекса (customer_id, seen_at), и тя е покриваща: базата не отваря таблицата. SELECT * иска и product_id, затова за всеки от 6 060 записа базата отива и в таблицата. Пиши в SELECT само колоните, които програмата наистина ползва. Така по-малко данни пътуват и до програмата.
Идеята
OR и IN
OR по една и съща колона е същото като IN. SQLite ги изпълнява еднакво:
Капанът е OR по различни колони. Индекс по product_id няма. Редовете с product_id = 4 може да са навсякъде в таблицата, затова базата чете всичко, и индексът за клиента не помага:
Ако и двете колони имат индекс, SQLite търси във всеки поотделно и събира резултатите (в плана пише MULTI-INDEX OR). Без индекс за едната половина остава SCAN.
Идеята
„Има ли“, а не „колко“
Страницата на продукт 13 показва „Популярен“, ако някой го е разглеждал. Броенето намира всичките 12 000 посещения:
На страницата трябва само „да“ или „не“. EXISTS спира на първия намерен ред:
Индекс по product_id няма, но EXISTS пак е бърз, защото първото посещение на продукта е близо до началото на таблицата. Ако такъв ред няма, EXISTS също ще прочете всичко. Затова и тук индексът помага. Друг начин е SELECT 1 FROM … WHERE … LIMIT 1: празен резултат значи „не“.
Упражнение
Две задачи
1. В полето е старата заявка за 2026 г. Клиент 1234 пита колко пъти е разглеждал продукти през 2025 г. Напиши я така, че планът да е SEARCH и по клиента, и по датата.
Посещенията на клиент 1234 през 2025 г., със SEARCH по двете колони:
Решение
SELECT COUNT(*) FROM visits
WHERE customer_id = 1234
AND seen_at >= '2025-01-01' AND seen_at < '2026-01-01';WHERE customer_id = 1234 AND seen_at >= '2025-01-01' AND seen_at < '2026-01-01'. 33 посещения. Проверката не пуска SCAN. Дали търсенето е и по двете колони, виж с EXPLAIN QUERY PLAN. Двете поправки са колоната без + 0 и интервалът вместо substr. С EXPLAIN QUERY PLAN пред заявката пише SEARCH visits USING COVERING INDEX idx_visits_customer_seen (customer_id=? AND seen_at>? AND seen_at<?).
2. Бутонът „Разгледай пак“ се показва, ако клиент 4321 е разглеждал продукт от 1 октомври 2026 г. нататък. Върни 1 или 0 в колона seen.
Има ли клиент 4321 посещения от 2026-10-01 нататък?
Решение
SELECT EXISTS (
SELECT 1 FROM visits
WHERE customer_id = 4321 AND seen_at >= '2026-10-01'
) AS seen;SELECT EXISTS (SELECT 1 FROM visits WHERE customer_id = 4321 AND seen_at >= '2026-10-01') AS seen; Има: 1. Проверката сравнява само стойността, затова COUNT(*) > 0 ще мине, но ще брои всички посещения, когато трябва само първото.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Подзаявки · Индекси · EXPLAIN: планът на заявката · Индекс по няколко колони
- Задачи по темата: Индекси и скорост