$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогSQL-диагностика на практике: как понять, почему запрос медленный, по статистике, плану и индексовому покрытию

SQL-диагностика на практике: как понять, почему запрос медленный, по статистике, плану и индексовому покрытию

$ sudo teach IT
·14 августа 2026 г.·11 мин·29
SQL-диагностика на практике: как понять, почему запрос медленный, по статистике, плану и индексовому покрытию

Покажем пошаговый подход: от EXPLAIN/ANALYZE до проверки селективности и покрытия индексами. На реальных типовых ошибках разберём, что менять в запросе и схеме, чтобы ускоряться стабильно.

Содержание
1) Начинаем с правильных артефактов: что и почему измеряем1.1. Сначала фиксируем контекст1.2. Делаем замер не только по времени2) EXPLAIN/ANALYZE: как понять, что оптимизатор делает на самом деле2.1. План без исполнения: что смотреть в EXPLAIN2.2. План с исполнением: ключ к реальной проблеме2.3. Сигналы «план неверен»3) Разбор по слоям: запрос → селективность → индексы → покрытие3.1. Слой A: селективность фильтров3.2. Слой B: выражения и типы (почему индекс «не видят»)3.3. Слой C: join-условия и порядок4) Индексы: не просто «есть/нет», а «поддерживают ли нужный доступ»4.1. Проверяем, какой тип индекса действительно подходит4.2. Покрытие индексом (index coverage): ускорение без доступа к таблице5) Практический сценарий: методика «от плана к исправлениям»5.1. Шаг 1: воспроизвести EXPLAIN ANALYZE и понять главную цену5.2. Шаг 2: проверить, почему индекс не выбран5.3. Шаг 3: предложить индексы под запрос, а не «в общем»5.4. Шаг 4: оценить покрытие6) Статистика: когда всё вроде правильно, но план всё равно плохой6.1. Что значит «неверная статистика» в терминах оптимизатора6.2. Когда обновлять статистику и как понимать эффект6.3. Подводные камни статистики7) Типовые ошибки, которые ломают производительность (и что менять)7.1. Ошибка: условие в WHERE построено так, что ломает sargability7.2. Ошибка: join по типам с приведением7.3. Ошибка: порядок колонок в составном индексе не соответствует условиям7.4. Ошибка: сортировка перед LIMIT7.5. Ошибка: «погоня за одним индексом» вместо понимания плана8) Как оценить индексовое покрытие и понять, стоит ли делать INCLUDE8.1. Что именно считать «покрытием»8.2. Стоимость индексов: не делайте «сверхширокие» без измерения9) Репродуцируемый чеклист: что делать при следующем медленном запросе10) Как углубиться: когда нужна системная практикаВывод

Медленный запрос — почти всегда симптом. Причина может лежать в статистике оптимизатора, в том, как сформулирован SQL, в выборе индексов, в кардинальности (сколько реально строк проходит на каждом шаге) или в «скрытых» факторах вроде типов данных и коллаторации. В этой статье разберём практический и воспроизводимый подход к диагностике: как пройти путь от EXPLAIN/ANALYZE до проверки селективности и индексового покрытия, а затем понять, что именно менять — в запросе или в схеме — чтобы ускоряться стабильно.

Материал ориентирован на типичную линейку реляционных СУБД (PostgreSQL / MySQL / аналоги). Термины и команды могут отличаться деталями, но логика диагностики — одна.


1) Начинаем с правильных артефактов: что и почему измеряем

1.1. Сначала фиксируем контекст

Прежде чем смотреть планы, уточните:

  • Какая СУБД и версия (оптимизатор, форматы EXPLAIN, поддержка покрывающих индексов).
  • Тип запроса: SELECT, UPDATE, DELETE, INSERT ... SELECT, агрегаты, оконные функции.
  • Размер данных: порядок величин таблиц и распределение по фильтрам.
  • Частота и окно времени: «медленно иногда» может означать промахи по кэшу, рост статистики, блокировки или план, который устарел.

1.2. Делаем замер не только по времени

Обычно полезны два вида метрик:

  • Время выполнения (wall time).
  • Сколько строк реально обработал план (по rows, actual rows и т. п.).

На практике самая частая ошибка — диагностировать по времени, но игнорировать фактическую кардинальность. Оптимизатор может выбрать «не ту» стратегию не потому, что индекс плохой, а потому что оценки кардинальности у него неверные.


2) EXPLAIN/ANALYZE: как понять, что оптимизатор делает на самом деле

2.1. План без исполнения: что смотреть в EXPLAIN

EXPLAIN (без ANALYZE) покажет предполагаемые оценки:

  • Типы шагов: Index Scan, Seq Scan, Nested Loop, Hash Join, Sort, Aggregate.
  • Порядок соединений (join order).
  • Оценки строк: rows (например, ожидаемые 1 000 при реальных 10 млн).
  • Используемые индексы и условия Filter / Index Cond.

Важное правило: план — это гипотеза оптимизатора. Он может ошибаться, особенно при устаревшей статистике, неравномерном распределении или сложных выражениях.

2.2. План с исполнением: ключ к реальной проблеме

EXPLAIN ANALYZE (или аналог) покажет:

  • actual rows — фактическое число строк.
  • loops — сколько раз выполнялся шаг.
  • время по узлам плана (зависит от СУБД).

И вот тут часто выясняется, что узкое место — не тот шаг, который «кажется логическим». Пример сценария:

  • Вы думаете, что тормозит сортировка.
  • Но EXPLAIN ANALYZE показывает, что сортировка делается уже по 200 строкам, а проблема — в Seq Scan на 200 млн строк из-за неиспользования индекса.

2.3. Сигналы «план неверен»

На практике красные флаги:

  • expected rows сильно отличается от actual rows (на порядки).
  • В плане есть Seq Scan по большим таблицам, хотя очевидные фильтры существуют.
  • Join выполняется Nested Loop, но ожидаемо должны быть более «масштабные» стратегии (или наоборот).
  • Много Sort / Hash по промежуточным результатам из-за неправильных индексов или формулировки запроса.

3) Разбор по слоям: запрос → селективность → индексы → покрытие

Дальше удобно мыслить слоями, как инженер, который не «угадывает», а проверяет гипотезы.

3.1. Слой A: селективность фильтров

Селективность — доля строк, которую «оставляют» условия WHERE. Оптимизатор выбирает план, ориентируясь на оценки селективности.

Типичная проблема

Фильтр вида:

code
WHERE status = 'ACTIVE'

может быть либо суперселективным (1%), либо почти неразборчивым (90%) — зависит от данных.

Если статистика устарела, оптимизатор может думать, что status='ACTIVE' редкое значение, и выбирать индексный план, который превращается в чтение миллионов строк.

Как проверить

  • Посмотрите оценки в EXPLAIN.
  • Посмотрите реальные пропуски — либо через отдельные COUNT(*) с теми же условиями (временно), либо через анализ распределения (в зависимости от СУБД).

Пример «приближённой диагностики»:

code
SELECT
  status,
  COUNT(*) AS cnt
FROM orders
GROUP BY status
ORDER BY cnt DESC;

3.2. Слой B: выражения и типы (почему индекс «не видят»)

Очень частая причина медленности: индекс есть, но не применяется из‑за преобразований выражений.

Например, если столбец created_at типа timestamp, а условие написано так, что приводит к вычислению функции от колонки:

code
WHERE DATE(created_at) = '2026-08-01'

Оптимизатор часто не сможет использовать обычный btree по created_at, потому что индекс по created_at не соответствует выражению DATE(created_at).

Вместо этого лучше писать:

code
WHERE created_at >= TIMESTAMP '2026-08-01'
  AND created_at <  TIMESTAMP '2026-08-02'

То же относится к:

  • приведениям типов (неявные CAST);
  • преобразованиям строк (например, LOWER(email) = ...);
  • применениям функций к полям в join/where.

3.3. Слой C: join-условия и порядок

Join может внезапно стать «дорогим», если:

  • соединение выполняется по неиндексированному ключу;
  • условие join содержит вычисления;
  • выбран не тот ключ для соединения;
  • кардинальность оценивается неправильно.

Схема мышления:

  1. Для каждого join посмотрите, какой стороне доступен индекс.
  2. В плане посмотрите, есть ли Index Cond по ключу join.
  3. Убедитесь, что соединение не «взорвало» кардинальность из-за неправильной логики (например, join на неуникальные поля без дополнительных условий).

4) Индексы: не просто «есть/нет», а «поддерживают ли нужный доступ»

4.1. Проверяем, какой тип индекса действительно подходит

Для btree полезны:

  • равенства (=, IN);
  • диапазоны (>=, <, BETWEEN);
  • сортировка и группировка по левому префиксу составного индекса.

Для полнотекстового поиска нужны специализированные индексы (GIN/GiST, FULLTEXT и т. п.).

Для LIKE с префиксом LIKE 'abc%' обычно подходит btree (в зависимости от колляции/настроек). Но LIKE '%abc%' обычно требует другие подходы.

4.2. Покрытие индексом (index coverage): ускорение без доступа к таблице

Покрытие — это когда СУБД может получить нужные колонки только из индекса, без обращения к heap/кластеру таблицы.

Результат: меньше I/O и ускорение, особенно на больших таблицах.

Простой пример:

  • есть индекс (user_id, created_at)
  • запрос делает выборку только user_id и created_at (и фильтрует по user_id)
  • оптимизатор может сделать Index Only Scan (в PostgreSQL) или аналогичный сценарий.

В терминах диагностики:

  • в плане ищите узлы типа Index Only Scan (PostgreSQL) или признаки «покрытия».
  • смотрите, есть ли Heap Fetches (для PostgreSQL), которые означают обращения к таблице даже при наличии покрытия.

5) Практический сценарий: методика «от плана к исправлениям»

Ниже — шаблонный процесс, который удобно применять к вашему запросу.

5.1. Шаг 1: воспроизвести EXPLAIN ANALYZE и понять главную цену

Допустим, запрос:

code
SELECT
  o.id,
  o.user_id,
  o.total_amount
FROM orders o
WHERE o.status = 'ACTIVE'
  AND o.created_at >= '2026-08-01'
ORDER BY o.created_at DESC
LIMIT 50;

План без ANALYZE может быть оптимистичным. Нас интересуют узлы:

  • где самый большой вклад во время;
  • сколько фактически строк отфильтровано.

Если видим Seq Scan по orders, либо индекс не используется для status/created_at, то идём дальше.

5.2. Шаг 2: проверить, почему индекс не выбран

Типовые причины:

  1. Фильтр не соответствует индексу (функция от колонки, неправильные типы).
  2. Низкая селективность (оптимизатор считает, что индекс не выгоден).
  3. Устаревшая статистика.
  4. Слишком много сортировки/скачков из-за порядка ORDER BY.

В нашем запросе важны два аспекта:

  • фильтрация по status и диапазону по created_at;
  • сортировка по created_at DESC + LIMIT.

Если подходящий составной индекс существует не в том порядке, вы можете получить дорогую сортировку.

5.3. Шаг 3: предложить индексы под запрос, а не «в общем»

Для btree классическая стратегия — составной индекс, который поддерживает и фильтр, и порядок сортировки.

Если часто делаете status = ... и ORDER BY created_at DESC LIMIT ..., то кандидат:

  • (status, created_at DESC).

Например:

code
CREATE INDEX idx_orders_status_created_at
ON orders (status, created_at DESC);

Что это даст:

  • поиск по status;
  • затем уже отсортированный доступ по created_at без отдельного Sort.

5.4. Шаг 4: оценить покрытие

Если запрос ещё выбирает id и total_amount, а таблица не кластеризована под индекс, может остаться обращение к строкам таблицы.

В PostgreSQL вы увидите Heap Fetches. Если их много — покрытие неполное.

Варианты:

  • добавить остальные колонки в индекс (создать «covering index» ценой увеличения размера индекса);
  • или переписать запрос так, чтобы получать минимум колонок до следующей операции;
  • иногда выгодно хранить производные/ключевые колонки в отдельной таблице, чтобы уменьшить ширину.

Пример «приближения к покрытию» (идея, а не универсальный рецепт):

code
CREATE INDEX idx_orders_cover
ON orders (status, created_at DESC)
INCLUDE (id, total_amount);

Если СУБД не поддерживает INCLUDE, придётся либо:

  • добавить колонки в ключ индекса (но это может ухудшить использование из-за изменения структуры),
  • либо оставить как есть и принять heap fetch как неизбежность.

6) Статистика: когда всё вроде правильно, но план всё равно плохой

6.1. Что значит «неверная статистика» в терминах оптимизатора

Оптимизатор оценивает:

  • число строк после фильтров (WHERE);
  • число строк после join;
  • распределение значений (histogram, MCV — most common values);
  • селективность диапазонов.

Если данные сильно меняются (например, статусы часто переключаются), но статистика не обновляется, оценки начинают «врать».

6.2. Когда обновлять статистику и как понимать эффект

В PostgreSQL есть ANALYZE, в MySQL — ANALYZE TABLE, в других — аналогичные команды.

Практический подход:

  1. Выполнить EXPLAIN ANALYZE до обновления статистики.
  2. Обновить статистику для таблиц, участвующих в запросе.
  3. Повторить EXPLAIN ANALYZE и сравнить:
    • изменились ли оценки rows;
    • поменялся ли тип скана/джойна;
    • сократилось ли число обработанных строк.

Типичный «симптом» устаревшей статистики:

  • план выглядит логично, но фактические строки отличаются на порядки;
  • оптимизатор выбирает индексный план, который на практике делает почти то же самое, что Seq Scan.

6.3. Подводные камни статистики

  • Композиции условий: отдельные столбцы статистики могут быть корректными, но их совместное распределение — нет. Например, status='ACTIVE' встречается часто, но сочетание status='ACTIVE' AND region='EU' — редкое. Если оптимизатор не умеет достаточно точно оценивать корреляции, план может быть неверным.
  • Сложные выражения: если фильтр вычисляется функциями, статистика может не применяться напрямую.
  • Низкая частота некоторых значений: редкие значения трудно оценивать точно.

Иногда решение не в «обнови статистику», а в переписывании условия так, чтобы оно выглядело «индексируемо».


7) Типовые ошибки, которые ломают производительность (и что менять)

7.1. Ошибка: условие в WHERE построено так, что ломает sargability

Классический пример:

code
WHERE DATE(created_at) = '2026-08-01'

Исправление:

code
WHERE created_at >= TIMESTAMP '2026-08-01'
  AND created_at <  TIMESTAMP '2026-08-02'

7.2. Ошибка: join по типам с приведением

Если один ключ user_id — INT, а другой — VARCHAR, то сравнение может вызвать преобразования. В плане вы можете увидеть, что индекс не применяется.

Исправление:

  • привести типы на уровне схемы;
  • или привести в запросе так, чтобы преобразование делалось на «константу», а не на колонку.

Правило: если приходится CAST(column AS ...), это часто красный флаг.

7.3. Ошибка: порядок колонок в составном индексе не соответствует условиям

Составной btree использует левый префикс. Индекс (created_at, status) не поможет так же хорошо, как (status, created_at), если фильтр по status равенство, а потом диапазон по created_at.

Исправление:

  • пересобрать индекс под наиболее частый паттерн фильтрации и сортировки;
  • подтвердить EXPLAIN ANALYZE.

7.4. Ошибка: сортировка перед LIMIT

Если запрос требует ORDER BY и LIMIT, но индекс не поддерживает нужный порядок, СУБД может отсортировать огромный набор.

Исправление:

  • обеспечить индекс, который покрывает и фильтры, и порядок ORDER BY;
  • минимизировать ширину выбираемых колонок до момента, где это критично.

7.5. Ошибка: «погоня за одним индексом» вместо понимания плана

Иногда создают индекс, но всё равно не ускоряется: причина в другой части плана — например, в дорогом join или в aggregate.

Метод:

  • по EXPLAIN ANALYZE определить самый дорогой узел;
  • индексировать и переписывать именно его входные данные.

8) Как оценить индексовое покрытие и понять, стоит ли делать INCLUDE

8.1. Что именно считать «покрытием»

Покрытие — это не только «есть нужный индекс». Это:

  • нужные колонки для SELECT;
  • плюс условия фильтра и сортировка/группировка (в идеале);
  • плюс минимизация «обращений к таблице».

Проверка (на примере PostgreSQL):

  • узел Index Only Scan означает потенциальное покрытие;
  • Heap Fetches показывает, что покрытие неполное (например, из-за видимости/хранения MVCC).

8.2. Стоимость индексов: не делайте «сверхширокие» без измерения

Индексы ускоряют чтение, но:

  • увеличивают время вставок/обновлений;
  • увеличивают размер базы;
  • могут ухудшить работу кэшей.

Подход:

  • создайте один разумный индекс под критичный запрос;
  • проверьте EXPLAIN ANALYZE;
  • если проблема в heap fetch — добавляйте покрытие точечно.

9) Репродуцируемый чеклист: что делать при следующем медленном запросе

  1. Снимаем EXPLAIN ANALYZE и находим узел с максимальным вкладом во время.
  2. Сравниваем expected vs actual rows на ключевых узлах. Если разница огромная — думайте о статистике или непредвиденной селективности.
  3. Проверяем, применяется ли индекс:
    • есть ли Index Cond по ключевым условиям;
    • не ломает ли sargability функции/CAST;
    • не конфликтует ли тип данных.
  4. Проверяем порядок индекса и ORDER BY/GROUP BY:
    • поддерживает ли индекс нужный порядок?
    • не превращается ли ORDER BY в дорогую Sort?
  5. Если LIMIT, убеждаемся, что СУБД не сортирует миллионы строк.
  6. Оцениваем индексовое покрытие:
    • есть ли Index Only Scan;
    • есть ли дорогие heap fetch.
  7. Обновляем статистику и смотрим, меняется ли план/оценки.
  8. Только после этого фиксируем «узкое место»:
    • переписываем запрос (sargability, join condition);
    • или меняем схему (составные индексы, INCLUDE/cоверы, возможно — перепланировка хранения).

10) Как углубиться: когда нужна системная практика

Этот подход хорошо работает как «операционная инструкция», но практика диагностирования быстро приводит к более глубоким темам: как оптимизатор оценивает кардинальность, как читаются планы в конкретной СУБД, как корректно моделировать индексы под реальные паттерны запросов и как избежать ложных оптимизаций, которые ломают другие сценарии. Если хотите углубиться именно в систематизацию навыков (а не только в разовые советы), полезно посмотреть структурированный материал вроде этого курса — в нем обычно разбирают практику анализа планов, индексов и причин «почему так выбрал оптимизатор».


Вывод

SQL-диагностика — это не гадание по «похоже, тут индекс нужен». Это инженерная процедура: фиксируем факты (EXPLAIN/ANALYZE), ищем расхождения между оценками и реальностью, проверяем селективность, убеждаемся, что условия индексируемы (sargability), затем проверяем индексовое покрытие и соответствие индекса WHERE/ORDER BY/LIMIT. Большинство ускорений получается не от «добавить любой индекс», а от точечного изменения того, что мешает оптимизатору (устаревшая статистика, неверные выражения, несоответствие порядка составного индекса) и того, что заставляет СУБД делать лишнюю работу (сортировки, heap fetch, дорогие join-стратегии).

Если выстроить эту диагностику как чеклист и регулярно сравнивать план до/после, скорость вы начнете повышать предсказуемо — без магии и без риска случайно ухудшить другие запросы.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Ownership и Borrowing в Rust: почему компилятор умнее вас (и это хорошо)
ownership

Ownership и Borrowing в Rust: почему компилятор умнее вас (и это хорошо)

Пошагово разбираем систему владения Rust, объясняем, почему она исключает целые классы ошибок памяти и как научиться думать в её терминах с первых дней.

16 июля 2026 г.
590
GIL в CPython: почему многопоточность не всегда ускоряет ваш Python-код
cpython

GIL в CPython: почему многопоточность не всегда ускоряет ваш Python-код

Разбираем, как устроен Global Interpreter Lock, почему потоки в Python не работают параллельно на CPU-задачах и когда стоит переключиться на multiprocessing или asyncio.

16 июля 2026 г.
550
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1171
Что такое переменная простыми словами: примеры из жизни и первый код
такое

Что такое переменная простыми словами: примеры из жизни и первый код

Разберём, что такое переменная без терминов: как “хранить” значение в памяти и как читать/менять его в программе. Дальше — мини-примеры на вводе/выводе и задания для новичка, чтобы закрепить понимание прямо в коде.

25 сентября 2026 г.
60
FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь
fastapi

FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь

Поймём различия между синхронной обработкой, BackgroundTasks и внешними очередями. Разберём idempotency, ретраи и мониторинг фоновых процессов.

22 июля 2026 г.
810
Какой первый проект выбрать новичку, чтобы не бросить обучение
первый

Какой первый проект выбрать новичку, чтобы не бросить обучение

Подберём 5–7 идей под уровень “с нуля”, объясним, что делать по шагам и как довести проект до результата без перегруза. В конце — как оформить мини-портфолио и что показать, даже если проект маленький.

23 сентября 2026 г.
260

Комментарии

Пока нет комментариев

Содержание

1) Начинаем с правильных артефактов: что и почему измеряем1.1. Сначала фиксируем контекст1.2. Делаем замер не только по времени2) EXPLAIN/ANALYZE: как понять, что оптимизатор делает на самом деле2.1. План без исполнения: что смотреть в EXPLAIN2.2. План с исполнением: ключ к реальной проблеме2.3. Сигналы «план неверен»3) Разбор по слоям: запрос → селективность → индексы → покрытие3.1. Слой A: селективность фильтров3.2. Слой B: выражения и типы (почему индекс «не видят»)3.3. Слой C: join-условия и порядок4) Индексы: не просто «есть/нет», а «поддерживают ли нужный доступ»4.1. Проверяем, какой тип индекса действительно подходит4.2. Покрытие индексом (index coverage): ускорение без доступа к таблице5) Практический сценарий: методика «от плана к исправлениям»5.1. Шаг 1: воспроизвести EXPLAIN ANALYZE и понять главную цену5.2. Шаг 2: проверить, почему индекс не выбран5.3. Шаг 3: предложить индексы под запрос, а не «в общем»5.4. Шаг 4: оценить покрытие6) Статистика: когда всё вроде правильно, но план всё равно плохой6.1. Что значит «неверная статистика» в терминах оптимизатора6.2. Когда обновлять статистику и как понимать эффект6.3. Подводные камни статистики7) Типовые ошибки, которые ломают производительность (и что менять)7.1. Ошибка: условие в WHERE построено так, что ломает sargability7.2. Ошибка: join по типам с приведением7.3. Ошибка: порядок колонок в составном индексе не соответствует условиям7.4. Ошибка: сортировка перед LIMIT7.5. Ошибка: «погоня за одним индексом» вместо понимания плана8) Как оценить индексовое покрытие и понять, стоит ли делать INCLUDE8.1. Что именно считать «покрытием»8.2. Стоимость индексов: не делайте «сверхширокие» без измерения9) Репродуцируемый чеклист: что делать при следующем медленном запросе10) Как углубиться: когда нужна системная практикаВывод