Оптимизация запросов MySQL — ускоряем запросы

  • Одна из распространённых причин медленных запросов — обработка лишних данных

Это может быть запрос лишних строк из большой таблицы, где не все они нужны. Можно использовать LIMIT или ограничивать выборку WHERE

SELECT * FROM huge_table LIMIT 10;
-- или
SELECT * FROM huge_table WHERE id BETWEEN 0 AND 1000;

Это может быть запрос всех столбцов в таблице с большим их количеством или в нескольких таблицах, соединенных JOIN. Лучше, запрашивать только необходимые столбцы

SELECT id, name, email FROM users LIMIT 1000;

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

  • Другая причина медленных запрсов — анализ лишних данных

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

Каким образом и сколько данных анализируется, можно посмотреть используя команду EXPLAIN и строка rows в результатах

  • Эффективным способом ускорения сложного запроса может быть разбиение его на более простые, которые выполняются быстрее первоначального
-- Было
SELECT * FROM tag 
JOIN tag_post ON tag_post.tag_id = tag.id
JOIN post ON tag_post.post_id = post.id
WHERE tag.tag_name = ‘mysql’;

-- Стало
SELECT * FROM tag WHERE tag_name = ‘mysql’;
SELECT * FROM tag_post WHERE tag_id = 1234;
SELECT * FROM post WHERE id IN (123, 456,789);
  • Или разбиение очень длительного запроса на более короткие порции
-- Было
DELETE FROM messages WHER created < DATE_SUB(NOW(), INTERVAL 3 MONTH);

-- Стало
DELETE FROM messages WHER created < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 1000; 
-- несколько раз
  • Оптимизатор MySQL плохо оптимизирует корректированные подзапрос вида
SELECT * FROM films WHERE id IN (
    SELECT film_id FROM film_actors WHERE actor_id = 100
);

В этом случае, лучше разделить и связать их на бекенде

SELECT film_id FROM film_actors WHERE actor_id = 100;
-- И затем
SELECT * FROM films WHERE id IN (1,5,9,13,77);
  • Не стоит злоупотреблять сортировкой ORDER BY, так как, без индекса на сортируемом столбце, она может значительно увеличить время выполнения запроса. Можно использовать LIMIT для ускорения сортировки, если она необходима
  • Запросы с UNION часто идет с ограничением LIMIT, и для ускорения, следует добавить LIMIT в оба под-запроса
-- Было
(SELECT id, name FROM film_actors order by name)
UNION ALL
(SELECT id, name FROM theatre_actors order by name)
LIMIT 20;

-- Стало
(SELECT id, name FROM film_actors order by name LIMIT 20)
UNION ALL
(SELECT id, name FROM theate_actors order by name LIMIT 20)
LIMIT 20;
  • Запросы с LIMIT и OFFSET на больших таблицах плохо работают с большими OFFSET, а это часто встречается в пагинации

Например для:

SELECT * FROM users ORDER BY id LIMIT 100 OFFSET 40000;

Сервер сгенерирует  40100 строк и отбросит первые 40000

Стоит заменить на:

SELECT * FROM users WHERE id > 40000 ORDER BY id LIMIT 100;
  • Ну и на последок, на бекенде часто используется ORM для генерации запросов. И нужно понимать, что для сложных запросов ORM формирует неоптимальный медленный запрос. В таком случае, следует переписать ORM запрос на чистый SQL

Обзор Книги — Не заставляйте меня думать — Стива Круга

Стив Круг — Не заставляйте меня думать

Самое актуальное издание книги — третье. Опубликовано автором в 2013м году. Почти десять лет прошло.

Считается, что книгу нужно прочитать одной из первых специалистам по Пользовательскому опыту на веб сайтах (UX-специалисты). Материал в книге подается просто и понятно. Специалистов по пользовательскому опыту на веб-сайтах пока еще мало и не каждая компания имеет такого. Поэтому стоит ознакомиться с книгой и разработчикам. Тем более что объем книги небольшой.

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

Мысли, которые почерпнул для себя из книги Стива Круга:

  • Со временем эволюции веба, у пользователей уже сложились представления о том, как должен выглядеть сайт. Где находится логотип, где меню, где разделы, где основная информация. Лучше размещать эти разделы так и там где пользователь привык их видеть. Не стоит удивлять пользователя и заставлять его искать привычные вещи.
  • Привычные названия для действий — это хорошо (кнопка «Регистрация»). Не привычные, остроумные, вычурные — плохо (вместо «Войти» — кнопка «Вперед за впечатлениями!»), так как заставляет пользователя задуматься и потерять время.
  • Основная навигация сайта (лого, слоган, меню, футер) и сервисные функции (войти, зарегистрироваться, поиск) должны быть на каждой странице и выглядит одинаково
  • Пользователи проводят на нашем сайте гораздо меньше времени, чем мы думаем. Редко они читают заготовленный текст полностью. Только если это не статья по интересующей его теме или «рецепт ребрышек в духовке». Пользователи хватают информацию кусочками: пару слов из целого предложения; первое предложение из абзаца.
  • Текст необходимо форматировать так, чтобы его было удобно просматривать («просматривать» отличается от «читать»). Иерархия заголовков. Короткие абзацы. Маркирование списки. Выделенные ключевые слова или понятия
  • Эффективная визуальная иерархия. Чем важнее элемент, тем он должен быть заметнее. Визуально связываем логически-связаные элементы. Часть чего-то большего оформляем вложенностью в это «большее»
  • Логические области (лого, меню, контент, баннеры, навигация, футер) должны быть четко разграничены, чтобы облегчить пользователю ориентацию на сайте
  • Кликабельные области (кнопки, ссылки, табы) должны быть выделены, привычным образом. Чтобы не было сомнения, что это кнопка или ссылка
  • Логотип сайта находится слева сверху. Рядом — слоган, которые четко объясняет куда мы попали и чего ожидать от сайта («Лучшие свежие торты Санкт-Петербурга»)
  • Хлебные крошки на сайте — это хороший тон. Если иерархия страниц больше 2х уровней.
  • Обязательно включаем поиск по сайту. Некоторые пользователи сразу же приступают к поиску, а не навигированию через меню
  • Пункты навигационного меню должны быть лаконичными, но понятными. Включать в свои разделы точно идентифицируемые области, а не пересекающиеся. Чтобы у пользователя не возникало вопроса «Бензопила» находится в разделе «Домашнее хозяйство» или «Для дачи»
  • На главной странице сделать акцент на то, от куда начать пользователю. Это или Регистрация или Покупка или Чтение лучших статей.
  • В сражении за место на главной странице между отделами (маркетинг, сео, продакт, заказчик, директор, ux-дизайнер и другие) необходимо, чтобы ответы на 4 основных вопроса легко находились. 
  1. Что это за сайт?
  2. Что я могу здесь делать?
  3. Почему я должен остаться здесь, а не где-либо еще?
  4. Какие материалы здесь есть?
  • Главная страница все еще важна. Даже если на ваш сайт попадают из поисковика и не на главную.
  • Юзабилити-тестирование очень полезная технология. Можно получить много идей для улучшения веб-сайта, пригласив сторонних людей попробовать с ним работать

Оптимизация запросов MySQL — архитектура таблиц

Когда мы говорим про оптимизацию запросов, в первую очередь, стоит оптимизировать сами типы столбцов таблицы. Лучше это делать при создании таблиц или при добавлении новых столбцов в таблицу.

  • Меньшие по размеру типы данных обычно быстрее, так как занимают меньше места на диске, в памяти и кэше процессора
  • Следует упрощать сложные типы данных, если такое возможно. 

Например, IP адреса лучше хранить в целочисленных данных, так как сравнение строковых данных работает намного медленнее. 

Даты и время рекомендуется хранить во встроенных типах данных MySQL. Из DATETIME и TIMESTAMP, последний предпочтительнее, так как требует вдвое меньше места для хранения

  • Целые числа: TINYINT (8 бит), SMALLINT (16 бит), MEDIUMINT (24 бит), INT (32 бит), BIGINT(64 бит) и диапазоны значений от -2^(N — 1) до 2^(N — 1). В зависимости от планируемого диапазона значений — выбираем более простой тип

Часто, отрицательные значений не требуются (например, авто-инкрементирующийся ID) и можно расширить диапазон в два раза благодаря атрибуту UNSIGNED.

  • Вещественные числа занимают значительно больше места.

Расчеты с данными с плавающей точкой высокозатратные и могут приводить к ошибкам на бекенде, когда «1» на самом деле берется как «0.9999987». В том числе поэтому, целесообразнее выбрать Целочисленный тип вместо Вещественного. Если нам известно, что у данных максимум 2 знака после запятой, «109.40» храним и обрабатываем как «10940»

  • Наиболее часто используемый строковый тип — VARCHAR. Используется при хранении данных переменной длины.
  • В отличии от VARCHAR, CHAR имеет фиксированную длину и лучше оптимизирован для сравнения строк с примерно одинаковой длинной (например для хранения MD5 сверток данных пользователя). 

CHAR так же эффективен для хранения часто изменяемых данных, так как не подвержен фрагментации при записи

  • Для хранения больших данных используются: TEXT — символьные данные, BLOB — двоичные данные
  • Если количество вариаций данных ограничено, то можно использовать ENUM

ENUM работает быстро, так как, по сути, хранит целое число. Недостатком ENUM будет то, что при добавление нового значения в набор, нужно будет менять схему таблицы (ALTER TABLE) или писать миграцию на бекенде

  • Лучше избегать значений NULL, если они не нужны. 

Стоит объявлять столбец как NOT NULL. MySQL тяжелее оптимизировать запросы к столбцам, допускающим NULL

Оптимизация запросов MySQL — производительные индексы

Индексы (или ключи) — это специальные структуры данных, которые использует подсистемы хранения для ускорения нахождения строк.

  • Индексы бесполезны, и, даже, вредны, если данных в таблицы мало (тысячи или меньше строк)
  • Если данных много (десятки тысяч строк и больше) и есть запрос WHERE по одному столбцу (или же сортировка ORDER BY по этому столбцу), который выполняется часто, лучше «повесить» на него простой индекс
  • Индекс хорошо работают по полному значению, диапазону значений
  • Составной индекс включает набор индексов (например, по трем столбцам A + B + C)
  • В это случае, индекс хорошо работает по полному набору проиндексированных столбцов («A + B + C»), по префиксам (левой части индекса: «A» или «A + B») и НЕ работает по пост-фиксам (правой части индекса: «C» или «B + C»). В случае необходимости поиска по «B» или «B + C», можно создать отдельный индекс — «B + C». Опять таки, индекс «B + C» будет неэффективен при поиске по C.
  • Альтернативой при существующем индексе «A + B + C» и необходимости поиска по пост-фиксу индекса, может быть включение в поиск столбца A искусственным образом:
SELECT * FROM TABLE WHERE B=2 AND C=3;
-- преобразуем в:
SELECT * FROM TABLE WHERE A="2022-10-14" AND B=2 AND C=3;
  • Так же, необходимо рассмотреть возможность смены порядка следования столбцов в запросе WHERE. Если, имеем индекс «A + B + C», при этом наш запрос выглядит так:
-- было:
SELECT * FROM TABLE WHERE A = 1 AND C = 2 AND B = 3;
-- стало:
SELECT * FROM TABLE WHERE A = 1 AND B = 3 AND C = 2;
  • Нужно понимать, что, если в запросе есть поиск по одному столбцу и сортировка по другому, хорошо подойдет составно индекс по этим столбцам и плохо будет работать отдельный индекс по одному из столбцов
  • В случае индексов на столбец строкового типа, индекс хорошо работает по полному совпадению и при поиске по префиксу (WHERE column LIKE «abc%»). При это у индекса есть ограничения, и по длинной строке поиск будет работать плохо
  • Чем меньше памяти потребляет тип столбца, тем быстрее работает и индекс. Поэтому поиск по индексированному целочисленному столбцу гораздо быстрее, чем по строковому
  • В случае длинных строк, и необходимости поиска по ним, можно ввести индексируемый столбец с хешем по длинной строке.

Хороший пример данных, это столбец с адресом веб-страниц url и поддержка столбца  c хешем этой строки — url_hash. При сохранение данных, записываем в столбец url оригинальный адрес веб-страницы, а в url_hash — хеш строки этого адреса (например кодировкой CRC32 или другой кодировкой в цифровое значение). А при поиске ведем поиск только по индексированному url_hash.

-- долгий запрос:
SELECT * FROM TABLE WHERE url = "https://long-adress-url.com";
-- заменяем на:
SELECT * FROM TABLE WHERE url_hash = CRC32("https://long-adress-url.com");
  • Другой частый случай плохо-индексируемых данных — это IP-адреса. Для них, так же есть специальный функции на бекенде, которые превращают строку IP-адреса в числовой тип для отдельного индексируемого столбца.

Конкретные функции зависят от языка программирования. Например, для PHP подойдет функция ip2long для шифровки и long2ip для расшифровки