Как жить, если у тебя не highload?

Как жить, если у тебя не highload?

Мини-рассказы про реляционные базы данных

Почему я?

Одинаково плохо пишу код на всём

Обо мне

Меня зовут Валерий Горбачев
Мне 41 год
Работаю программистом
Сейчас использую: PHP, JS, MSSQL

В профессии давно

Каково работать в веб-студии

В маленькой веб-студии ты можешь проявить себя в любой роли:

  • Архитектор
  • Бэкенд-разработчик
  • Фронтенд-разработчик
  • DBA
  • DevOps

Все истории происходили в маленькой веб-студии, но иногда происходят и на больших проектах...

Условия, когда БД не будет использовать индекс?

Первое что приходит на ум:
Под критерий отбора попадает большой процент строк таблицы

Интерлюдия

Что можно узнать всего за неделю

Интерлюдия

Нормализация

Третья нормальная форма


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

Определение нормализации с сайта - Википедии

История 1
нормализация
носит

РЕКОМЕНДАТЕЛЬНЫЙ

характер
История 1

Пишем учётную систему для бизнеса

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

Сроки - вчера

История 1

Что спроектировали

Таблицы представлены в урезанном и очень упрощённом виде

История 1

Что спроектировали

Таблицы представлены в урезанном и очень упрощённом виде

История 1

Что хотел заказчик

Если у вас MVP - не стоит тратить кучу времени на выработку супер-нормализованной структуры.

Морали нет. ¯\_(ツ)_/¯
История 1

Хранение заказов

Типовая схема структуры хранения заказов в интернет-магазине. Всё отлично?
 

История 2

Хранение заказов

Типовая схема структуры хранения заказов в интернет-магазине. Всё отлично?
Да, но нет (c) Саша

История 2

Хранение заказов. Денормализация

Итак, что же мы упускаем?

  1. Пользователи интернет-магазинов иногда смотрят свои прошлые заказы
  2. Товары в интернет-магазине могут быть удалены
  3. Производитель оставляет за собой право...
  4. Ну вы же умные, придумайте еще что-нибудь
История 2

Хранение заказов. Денормализация

Денормализация обусловленная доменной логикой.

История 2

Не нормализация, но полезно

Минутка очевидности

Не история

Простые запросы

Не надо усложнять

Задача на собеседовании:

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

Ну и чего думать? Тут же всё просто.

История 3

Не надо усложнять

select * from `banners` b
where `banner_id` not in (select `ub`.`banner_id` from `user_banners` `ub` where `ub`.`user_id` = 1)
order by rand() limit 1
История 3

Решение на поверхности

История 3

Не надо усложнять

select `b`.* from `banners` b
where `b`.`id`>(select `u`.`current_banner_id` from `users` `u` where `u`.`id` = 1)
order by `b`.`id` limit 1

-- Это храним в кэше
select max(`b`.`id`) as maxIndex from `banners` b
История 3

Не используйте * в запросах

Вроде бы всё нормально
select * from `banners` `b`
join `user_banners` `ub` on `ub`.`banner_id` = `b`.`id`
Очевидные советы

Не используйте * в запросах

А если так?
select * from `banners` `b`
join `user_banners` `ub` on `ub`.`banner_id` = `b`.`id`
join `users` `u` on `u`.`id` = `ub`.`user_id`
Очевидные советы

Не используйте * в запросах (иногда можно)

Вот так уже лучше?
select * from `banners` `b`
join `user_banners` `ub` on `ub`.`banner_id` = `b`.`banner_id`
join `users` `u` on `u`.`user_id` = `ub`.`user_id`
Очевидные советы

Не используйте * в запросах (иногда можно)

Стратегия именования полей с добавлением имени таблицы к имени поля - поможет вам
Но я бы порекомендовал указывать необходимые поля, или хотя бы использовать `alias`.*

Очевидные советы

Сортировка и индексы

Мы чинили магазин. Часть первая: Сортировка

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

Из описания задачи

История 4

Мы чинили магазин. Часть первая: Сортировка

SELECT "products"."id","prices"."value" FROM "products"
LEFT JOIN "prices" ON ("products"."id" = "prices"."products_id") AND ("prices"."pricetypes_id"=3)
WHERE ("products"."deleted" IS NULL) AND ("products"."published"=1)
ORDER BY "products"."is_avail" DESC, "prices"."value" DESC
OFFSET 0 LIMIT 20
История 4

Мы чинили магазин. Часть первая: Сортировка

OFFSET 0
LIMIT 20
OFFSET 20
LIMIT 20
История 4

Мы чинили магазин. Часть первая: Сортировка

И снова всё просто - уточняйте сортировку

ORDER BY "products"."is_avail" DESC, "prices"."value" DESC, "products"."id" ASC
После того как запрос выдал таблицу результатов (после обработки списка выборки), её можно отсортировать. Если сортировка не задана, строки возвращаются в неопределённом порядке. Фактический порядок строк в этом случае будет зависеть от плана соединения и сканирования, а также от порядка данных на диске, поэтому полагаться на него нельзя. Определённый порядок выводимых строк гарантируется, только если этап сортировки задан явно.

Цитата из документации, но кто же её читает.

История 4

Индексы никто не любит?

Заметка

Вы используете в PostgreSQL ILIKE?

Мы чинили магазин. Часть вторая: индексы

Очевидные факты

  • Лишние индексы замедляют при частом обновлении
  • Индексы на каждом поле - не заменят составные
  • Индексы по функциям не работают

Очевидные действия: оптимизируем

История 5

Мы чинили магазин. Часть вторая: индексы

Что мы сделали:

История 5

Мы чинили магазин. Часть вторая: индексы

И еще:

История 5

Давайте повторим (только не ругайтесь)

Очевидные советы

Процент использования

Используем Sphinx вместо LIKE

Как сделать релевантный поиск, если клиент хочет чтобы:
История 6

Используем Sphinx вместо LIKE

Решение подсмотренное у колег
$products = $sphinxSearch->search($query);
$ids = ArrayHelper::getColumn($products, 'id');

$productsDetail = Product::find()->where(['in','id',$students_ids])
            ->indexBy('id')->all();

$result = [];
foreach ($products as $product) {
   ...
}

Я подумал, что мы не можем забрасывать проблемы железом.

История 6

Используем Sphinx вместо LIKE

А если по другому? И не перебирать данные в PHP?

История 6

Что удалось нагуглить

Если бы у нас был MySQL

IN () + FIELD( `products`.`id`, 28, 12, 53, 2)
История 6

Решение подсказанное старшими коллегами

PostgreSQL с использованием JSON

SELECT "p".* FROM "products" "p"
JOIN json_to_recordset('[{"ind": 0, "pid": 20296}, {"ind": 1, "pid": 20308}, {"ind": 2, "pid": 20333}]') AS spv(ind int, pid int) ON "spv"."pid" = "p"."id"
ORDER BY "spv"."ind"

И снова - надо было читать документацию

История 6

CTE - обобщённые табличные выражения

Используйте CTE не только для рекурсии, но и к примеру, для построения отчётов. Это удобно.

WITH "sales" (minsum, maxsum, avgsum, uid) AS (
    SELECT MIN("itemsSum"), MAX("itemsSum"), AVG("itemsSum"), "uid"
    FROM "orders"
    GROUP BY "orders"."uid"
)

SELECT "sales".*,"user"."username" FROM "sales"
JOIN "user" ON "user"."id" = "sales"."uid"
Никто не любит читать документацию и сочинять отчёты. Если вы исключение - вам повезло.
НЕ Очевидные советы

Учётная система для бизнеса. Подзапросы

Исходные данные: Давайте к списку задач, выведем сколько у нас не прочитанных комментариев

SELECT `task`.*, (SELECT COUNT(*) FROM `comment` `cm`
   LEFT JOIN `lastvisit` `lv` ON `cm`.`tid` = `lv`.`docid`
   WHERE `cm`.`tid` = `docs`.`id` AND `cm`.`date` > `lv`.`dt` AND `lv`.`uid` = :uid) AS notreadCommentCount ...

А давайте еще добавим текст, автора и дату последнего комментария.

И нам на помощь приходят CTE, OUTER APPLY, LATERAL JOIN
А нет, их же нет в MySQL 5

История 7

Учётная система для бизнеса. Подзапросы

Как мы решили задачу в 2014 году - понятно, просто добавили идентификатор последнего комментария в таблицу задач и сделали LEFT JOIN

Но как можно решить задачу в 2020? (Или просто используя другой SQL диалект)

История 7

SQL Server

SELECT [task].*, cm.*
FROM [task]
OUTER APPLY (
   SELECT TOP 1 cm.uid, cm.text, cm.created
   FROM [comment] cm
   WHERE cm.tid = task.tid
   ORDER BY cm.created DESC
) cm
История 7

PostgreSQL

SELECT "task".*, "cm".*
FROM "task"
LEFT JOIN LATERAL (
   SELECT "cm"."uid", "cm"."text", "cm"."date"
   FROM "comment" "cm"
   WHERE "cm"."tid" = "task"."tid"
   ORDER BY "cm"."created" DESC LIMIT 1
) "cm" ON "cm"."tid" = "task"."tid"
История 7

Что я понял, пока готовил этот доклад:



Спасибо вам, за то что выслушали.

Истории закончились?

Где и что почитать

Ссылка на слайды

Телеграмм для связи: @darkdef_pr