Перейти к содержимому
Войти Регистрация

Топ 6 ошибок в SQL-запросах

Топ 6 ошибок в SQL-запросах

Запрос, который отлично работает на сотне тестовых строк, на боевых данных умеет положить весь сервис. Шесть ошибок, которые встречаются чаще всего, — и объяснение, почему каждая из них больно бьёт именно в проде.

Почему это не видно на этапе разработки

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

Вторая причина — прослойка. Многие пишут не SQL, а вызовы библиотеки, которая генерирует SQL за них. Запрос выглядит как обращение к обычному объекту, и понять по коду, что за этой строкой скрывается двести обращений к базе, невозможно.

Главный инструмент. План выполнения. Почти в любой базе есть команда, которая показывает, как именно СУБД собирается выполнять запрос: пойдёт ли она по индексу или переберёт всю таблицу, в каком порядке соединит таблицы, сколько строк ожидает получить. Читать план — навык, который окупается быстрее всех остальных в этой теме.

Примеры ниже нейтральны к конкретной базе: детали синтаксиса и поведение в мелочах отличаются, но все шесть ошибок одинаково живут и в PostgreSQL, и в MySQL, и в остальных реляционных базах.

На двадцати тестовых строках быстро работает даже самый плохой запрос.
На двадцати тестовых строках быстро работает даже самый плохой запрос. Фото: Schill · BY.

Топ 6 ошибок

Звёздочка в рабочем коде

удобно писать, дорого выполнять

SELECT * FROM таблица — первое, чему учат, и первое, от чего стоит отвыкнуть в коде приложения. Запрос тянет все колонки, включая те, которые вам не нужны: длинные текстовые поля, картинки в двоичном виде, служебные метки. По сети едет в разы больше данных, чем требуется, и всё это ещё нужно разобрать на стороне приложения.

Что ломается неочевидно. Звёздочка мешает покрывающему индексу. Если бы вы попросили две колонки, которые уже лежат в индексе, база отдала бы ответ прямо из индекса. Со звёздочкой ей придётся за каждой найденной строкой лезть в саму таблицу — и выигрыш от индекса испаряется.

И хрупкость. Кто-то добавил колонку — и запрос молча начал возвращать другой набор полей. Если код рассчитывает на порядок колонок или складывает результат в структуру с фиксированным набором полей, поломка приедет неожиданно и не туда, где меняли схему. Отдельно больно, когда звёздочка спрятана внутри представления или подзапроса: там она тянет за собой лишние колонки через все уровни.

Где допустимо. Разовые запросы руками в консоли, когда вы просто смотрите на данные. В коде — перечисляйте колонки явно.

  • Исправляется механически, без изменения логики
  • Сразу уменьшает объём передаваемых данных
  • Явный список колонок документирует, что нужно коду
  • Списки колонок приходится поддерживать при изменении схемы
  • В генераторах запросов звёздочка часто стоит по умолчанию

частая ошибка · оценка 9,0

Запрос внутри цикла

самая дорогая ошибка, и в коде её почти не видно

Классика: получили список из ста записей одним запросом, а дальше в цикле для каждой запрашиваем связанные данные — автора, категорию, количество комментариев. Вместо одного обращения к базе получается сто один. Отсюда и название — проблема N плюс один.

Почему это дороже, чем кажется. Дело не в самих запросах — каждый из них выполняется быстро. Дело в накладных расходах: на каждый запрос уходит сетевой круг до базы, разбор запроса, построение плана. Сто маленьких запросов легко оказываются медленнее одного большого в десятки раз, а при удалённой базе — в сотни.

Как лечится. Три способа. Соединить таблицы в одном запросе. Собрать все идентификаторы и запросить связанные записи одним обращением с условием на список значений. Или, если вы пользуетесь библиотекой отображения объектов, включить для связи «жадную» загрузку — почти в каждой такой библиотеке это одна строка.

Как заметить. Выводите в лог количество запросов к базе на каждый запрос к приложению. Если при открытии одной страницы счётчик показывает трёхзначное число, вы нашли ровно это. У большинства фреймворков есть панель отладки, которая показывает список запросов и повторы в нём.

  • После исправления ускорение видно невооружённым глазом
  • Обнаруживается счётчиком запросов за пять минут
  • Средства борьбы есть в любой библиотеке
  • Жадная загрузка «на всякий случай» создаёт обратную проблему — тянет лишнее
  • Соединение вместо цикла усложняет запрос и разбор результата

частая ошибка · оценка 9,5

Нет индекса под условие — или он есть, но не работает

полный перебор таблицы там, где хватило бы одного обращения по ключу

Без индекса база честно читает всю таблицу и проверяет каждую строку. На десяти тысячах строк это незаметно, на десяти миллионах — секунды на каждый запрос. Индекс нужен под колонки, по которым вы фильтруете, соединяете таблицы и сортируете.

Порядок колонок в составном индексе важен. Индекс по паре колонок работает для условия по первой колонке и для условия по обеим сразу, но не для условия только по второй. Это самая частая причина ситуации «индекс же есть, а он им не пользуется».

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

Поиск подстроки. Условие с шаблоном, начинающимся с любого символа, индексом не ускоряется в принципе. Для поиска по тексту нужен полнотекстовый индекс или отдельный поисковый движок, а не хитрости с обычным.

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

  • Один правильный индекс превращает секунды в миллисекунды
  • Добавляется без изменения кода приложения
  • План запроса сразу показывает, помог он или нет
  • Каждый индекс — плата при записи и место на диске
  • Построение индекса на большой таблице может заблокировать её
  • Работает не под каждое условие, и это надо знать заранее

частая ошибка · оценка 9,4

Неявные преобразования типов

тихо превращает индекс в бесполезный

Колонка числовая, а в условие приезжает строка — или наоборот. База не ругается: она приводит одно к другому и выполняет запрос. Результат обычно верный, но чтобы сравнить, ей приходится преобразовать значение в каждой строке, и индекс по этой колонке перестаёт применяться.

Где это берётся. Чаще всего — из кода: идентификатор пришёл из адреса страницы строкой, так и уехал в запрос. Или в схеме числовой по смыслу столбец описан как текстовый, потому что «так было проще при импорте».

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

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

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

частая ошибка · оценка 8,8

Логика с пустыми значениями

NULL — не значение, а отметка о его отсутствии

Отсюда всё странное поведение. Сравнение с пустым значением никогда не даёт истину: условие «колонка равна NULL» не найдёт ничего, даже если пустых строк полтаблицы. Нужны отдельные проверки — IS NULL и IS NOT NULL.

Классическая ловушка. Условие «значение не входит в список», где в списке или в подзапросе встретился NULL, возвращает пустой результат целиком. Не часть строк, а вообще ничего — и это выглядит как поломка приложения, а не как логика базы. Безопаснее строить такие проверки через отсутствие связанной записи, а не через отрицание списка.

Агрегаты. Подсчёт по конкретной колонке пропускает пустые значения, подсчёт строк — нет, и два числа в отчёте расходятся. Среднее считается только по заполненным строкам, поэтому «среднее по всем» и «среднее по тем, у кого есть значение» — разные числа, и надо понимать, какое из них вам нужно.

Соединение таблиц. Внешнее соединение с условием на правую таблицу, вынесенным в общий блок условий, тихо превращается в обычное: строки без пары отсеиваются. Такие условия должны стоять в самом соединении, а не в блоке фильтрации.

Что помогает. Запрещать пустые значения в схеме везде, где по смыслу их быть не должно, и подставлять значение по умолчанию функцией вида COALESCE там, где пустота допустима.

  • Правила простые, выучиваются за вечер
  • Запрет пустых значений в схеме убирает проблему в корне
  • Тесты на граничные случаи ловят такое надёжно
  • Ошибка даёт не сбой, а неверные данные — заметить трудно
  • Изменить схему на живой таблице не всегда просто

частая ошибка · оценка 9,2

Забытое ограничение количества строк

запрос, который работал год, однажды кладёт сервис

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

Про постраничный вывод. Смещение на больших страницах работает медленно: чтобы отдать строки с миллионного места, база вынуждена пройти весь миллион до них. Для длинных списков надёжнее листать по ключу — запрашивать записи после последнего показанного значения.

Сортировка обязательна. Без явного порядка база не обязана возвращать строки одинаково от запроса к запросу. При постраничном выводе это даёт дубли на одних страницах и пропуски на других, и такую жалобу пользователей потом очень долго воспроизводят.

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

  • Ограничение добавляется в одну строку
  • Листание по ключу работает одинаково быстро на любой странице
  • Проверка выборкой перед изменением стоит десять секунд
  • Листание по ключу неудобно, если нужны произвольные номера страниц
  • Ограничение без сортировки создаёт иллюзию порядка

критичная ошибка · оценка 9,3

Инфографика: частота ошибки, её цена и время на исправление
Дешевле всего чинятся звёздочка и забытое ограничение строк — с них и стоит начать.

Сравнение по главному

ОшибкаКак частоЦенаИсправить
Звёздочка в выборкеочень частосредняяминуты
Запрос в циклечастовысокаячасы
Нет индексачастовысокаяминуты
Преобразование типовиногдасредняячасы
Логика с NULLчастовысокаячасы
Нет ограничения строкиногдакритичнаяминуты
Диаграмма важности шести ошибок в SQL-запросах
Оценка — насколько дорого ошибка обходится и насколько часто встречается на практике.

Как находить это у себя

Читайте план запроса. Полный перебор большой таблицы, неожиданная сортировка на диске, оценка в миллионы строк там, где вы ждали десяток, — всё это видно сразу. Достаточно уметь опознавать три-четыре типовые картины, глубокая теория не нужна.

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

Считайте запросы на страницу. Один счётчик в логе отладки закрывает всю тему запросов в цикле раз и навсегда.

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

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

Частые вопросы

Нужно ли знать SQL, если есть библиотека отображения объектов?

Да, и именно из-за таких ошибок. Библиотека прекрасно пишет простые запросы, но не понимает ваших намерений: она не знает, что вы сейчас в цикле и что связанные данные лучше подтянуть заранее. Уметь посмотреть сгенерированный SQL и его план — обязательный навык, иначе вы отлаживаете чёрный ящик.

Как понять, что нужен индекс?

По журналу медленных запросов и плану: если база перебирает всю таблицу, а вам нужны единицы строк — индекс нужен. Ориентир для схемы: индексы обычно оправданы под внешние ключи, под колонки, по которым вы регулярно фильтруете, и под сортировку в длинных списках. Всё остальное — по результатам замеров, а не по ощущениям.

Сколько индексов на таблицу — нормально?

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

Что делать, если запрос не ускоряется?

Сначала проверьте, нужен ли он в таком виде: часто отчёт можно посчитать заранее и хранить готовый результат. Дальше по нарастающей — кеширование ответа, предпосчитанные таблицы, разделение большой таблицы на части. И только в конце — переезд на другое хранилище: это самый дорогой шаг, и до него доходят реже, чем кажется.

Итог

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

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

0
Оценили 0 читателей

Комментарии

0
Г
Без регистрации можно оставить один комментарий к публикации. Войдите, чтобы участвовать в обсуждении дальше и получать ответы. Ссылки в комментариях скрываются.
Пока нет комментариев

Будьте первым, кто ответит автору.