Оптимизируйте два запроса заказов

MySQLBeginner
Практиковаться сейчас

Введение

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

Вы сохраните два запроса MySQL для повторного использования и скрипт индексов, примените полезные индексы и обеспечите для обеих первых страниц соблюдение общего ограничения: два вторичных индекса и 64 диагностических обращения за строками на страницу. Исходный синтетический снимок содержит более 96 000 заказов. Это занятие оптимизирует первые страницы; следующее независимое занятие обучает полной пагинации с курсором.

Нужны базовые навыки навигации в терминале, подключения к MySQL и знание SELECT/WHERE. Параметры, полуоткрытые диапазоны времени, сортировка, индексы и наблюдения фактических планов объясняются перед использованием. Отдельный сервер MySQL, исходные данные и Terminal готовы к работе; разработка на Python или создание интерфейса не требуются.

Работайте в /home/labex/project/order-desk. Создайте или отредактируйте queue.sql, history.sql и indexes.sql и управляйте не более чем двумя вторичными индексами. Сохраните исходные заказы, столбцы, первичный ключ, идентификаторы, состояния, точные временные метки приложения, центы, заметки и исполняемые инструменты. Не удаляйте и не переписывайте исходные строки, не меняйте настройки диагностики, не добавляйте подсказки оптимизатору и не используйте внешний кеш. Имена индексов и эквивалентный SQL могут отличаться. Метки и вывод работающих инструментов остаются на английском.

В подготовленных диагностических соединениях отключён index condition pushdown — оптимизация движка, которая может скрывать фильтрацию внутри механизма хранения. Так сравнения числа обращений за строками легче интерпретировать. Это относится только к этим соединениям; упражнение не измеряет физический дисковый ввод-вывод и не обещает кратного ускорения в рабочей системе. Миллисекунды и одна точная строка плана не являются целями приёмки.

Изучите нагрузку и оптимизируйте очередь выдачи

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

Начните во вкладке Terminal с обычной командной оболочки. cd переходит в рабочий каталог; ./ запускает программу в текущем каталоге. db открывает подготовленный клиент MySQL для чтения заказов и управления индексами курса. Here-document отправляет строки между маркером SQL в кавычках и завершающим SQL этому клиенту. Завершающий маркер стоит отдельно. Инструкции SQL заканчиваются точкой с запятой, а это клиентское соединение закрывается по завершении блока. При опечатке в пути или имени команды используйте ls, чтобы изучить настоящие имена, и повторите исправленную команду; сохраните исходные данные нетронутыми.

Изучите готовые заказы магазина 3 за 10 мая. Сравнение >= включает начальную границу; < исключает следующую полночь. ORDER BY placed_at,order_id сортирует по времени, затем по ID, по возрастанию. ID разрешает равенство временных меток. total_cents хранит целое число центов, а placed_at имеет тип DATETIME(6): это временная метка приложения, сохраняющая шесть цифр микросекунд. Эти предоставленные временные метки не требуют преобразования часовых поясов.

cd /home/labex/project/order-desk
./db <<'SQL'
SELECT order_id,shop_id,customer_id,state,placed_at,total_cents
FROM orders
WHERE shop_id=3 AND state='ready'
  AND placed_at>='2025-05-10' AND placed_at<'2025-05-11'
ORDER BY placed_at,order_id;
SQL

Есть десять подходящих заказов: девять разных ID имеют время 09:15, а заказ 900021 приходится на 23:59:59.999999. Заказ 900004 стоит ноль и всё равно готов к выдаче. Большинство заказов с одинаковым временем стоят 1250 центов, но равная цена не является идентификатором. Точная следующая полночь относится к следующему дню. Сохраните эти факты при изменении путей доступа.

Индекс — дополнительная упорядоченная структура, помогающая MySQL находить строки. Он требует места и обслуживания при записи, поэтому цель — не добавление всех возможных индексов. Составной индекс упорядочивает записи по нескольким столбцам. Первичный ключ уже идентифицирует order_id; эта система допускает не более двух дополнительных вторичных индексов на обе нагрузки.

План описывает, как MySQL получает результат. Обычный EXPLAIN содержит оценки. EXPLAIN ANALYZE действительно выполняет SELECT и сообщает наблюдения итераторов, например actual ... rows=... loops=.... FORMAT=TREE показывает шаги выполнения, называемые итераторами, как текст с отступами для родительских и дочерних элементов. LIMIT 7 запрашивает не более семи упорядоченных строк. Этот верхний итератор может вернуть семь строк, пока нижний итератор сканирования таблицы читает гораздо больший набор. Время выполнения меняется и не оценивается. DATE(placed_at) ниже извлекает календарную дату, поэтому именно этот базовый запрос всё ещё выбирает правильный отдельный день.

./db <<'SQL'
EXPLAIN ANALYZE FORMAT=TREE
SELECT order_id,shop_id,customer_id,state,placed_at,total_cents
FROM orders
WHERE shop_id=3 AND state='ready' AND DATE(placed_at)='2025-05-10'
ORDER BY placed_at,order_id
LIMIT 7;
SQL

Читайте нижний итератор доступа, а не только семь строк наверху. Без подходящего индекса он сканирует примерно 96 000 заказов. Диагностическое соединение оставляет index condition pushdown выключенным, чтобы фильтрация была видна; сохраните эту подготовленную настройку сравнения. Одна лишь небольшая возвращённая страница не доказывает небольшого объёма работы базы данных.

Сохраните запрос, который может обрабатывать разные запросы магазина и состояния и произвольные полуоткрытые диапазоны времени. Префикс @ обозначает переменную сеанса MySQL, обрабатываемую самой базой данных. Программа передаёт @group_id, @state, @from_at и @until_at для каждого запроса в его соединении. Переменная, заданная вручную в одном соединении db, не сохраняется в другом. Не помещайте инструкции SET в сохранённый файл запроса.

Оба интерфейса запросов возвращают шесть столбцов в этом порядке: order_id, group_id, actor_id, state, event_at, amount_cents. AS даёт выбранному столбцу имя интерфейса: магазин — группа, покупатель — действующее лицо. Сохраняйте настоящие значения вместо замены их сводками. LIMIT 7 — контракт страницы из семи заказов; его уменьшение скрыло бы заказы, а не оптимизировало страницу. ASC явно задаёт порядок возрастания.

cat > queue.sql записывает следующий полный текст в этот файл, заменяя прежнее содержимое. Маркер here-document в кавычках предотвращает подстановки оболочки. Вставьте весь блок с завершающим маркером на отдельной строке:

cat > queue.sql <<'SQL'
SELECT order_id,shop_id AS group_id,customer_id AS actor_id,
       state,placed_at AS event_at,total_cents AS amount_cents
FROM orders
WHERE shop_id=@group_id AND state=@state
  AND placed_at>=@from_at AND placed_at<@until_at
ORDER BY placed_at ASC,order_id ASC
LIMIT 7;
SQL
./desk page queue

page выполняет ваш сохранённый SQL без изменений и показывает фактические строки. Первая страница содержит ID от 900001 до 900007, включая заказ на ноль центов. Встроенный показатель handler_requests считает запросы на границе сервера и механизма хранения в этом диагностическом соединении; это не число физических операций ввода-вывода. До индексации этот показатель остаётся большим. Показываемый курсор следующего запроса берётся только из фактической последней возвращённой строки; этот запрос первой страницы пока его не использует. Входные значения курсора, показанные как null, означают отсутствие предыдущей строки при запросе первой страницы.

Полезный префикс доступа сначала фиксирует магазин и состояние, затем следует запрошенному диапазону времени и порядку (time,ID). Два столбца равенства могут меняться местами для этих запросов; порядок их записи в WHERE не определяет порядок индекса. Индекс только по времени всё ещё может перебирать посторонние магазины или состояния в других запросах. Сохраните выбранный кандидат в indexes.sql, чтобы его можно было применить и к новой копии.

cat > indexes.sql <<'SQL'
CREATE INDEX queue_time ON orders(shop_id,state,placed_at,order_id);
SQL
./db < indexes.sql

< передаёт файл клиенту в качестве входного SQL. Это создаёт структуру доступа, а не новые заказы. Если позже снова выполнить тот же CREATE INDEX, MySQL сообщит о дублирующемся имени ключа, потому что индекс уже существует. Изучите существующие индексы перед решением сохранить или заменить созданный в рамках курса кандидат; не удаляйте исходные заказы и не ожидайте, что этот скрипт является идемпотентной миграцией.

Здесь -e выполняет полную строку SQL в кавычках и завершает работу. Действие plan программы выполняет EXPLAIN ANALYZE для фактического сохранённого запроса. --from и --until выбирают другую начальную и исключённую конечную границы для этого запроса; значения только с датой означают полночь.

./db -e "SHOW INDEX FROM orders;"
./desk page queue
./desk plan queue
./desk page queue --from 2025-01-05 --until 2025-01-12

Теперь путь доступа должен использовать узкий индексированный диапазон, а исходная страница 10 мая по-прежнему должна содержать те же семь ID заказов и точные поля. Проверьте оба запроса на соответствие ограничению в 64 обращения за строками. SHOW INDEX доказывает существование; фактический план, возвращённые строки и число обращений подтверждают полезное поведение. Точные имена индексов, стоимости и миллисекунды могут отличаться.

При опечатке в команде или имени файла прочитайте ошибку, используйте ls, чтобы изучить рабочий каталог, и повторите с исправленным именем. Order desk error означает ошибку файла, SQL или соединения; это не успешный пустой результат. Исправьте сохранённый SQL или индекс-кандидат, затем снова выполните настоящий запрос. Запустите проверку шага после подтверждения и правильности, и наблюдаемого объёма работы.

Выделите индексы для истории покупателя в рамках общего лимита

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

Оставайтесь во вкладке Terminal и в /home/labex/project/order-desk. Сохраните исходные таблицы и queue.sql. История выбирает одного покупателя независимо от состояния выдачи или магазина и показывает сначала новые заказы. DESC обращает порядок; используйте его и для временной метки, и для ID, чтобы заказы с одинаковым временем имели определённый порядок от новых к старым. Параметр сеанса @actor_id идентифицирует покупателя. Не заменяйте его фиксированным покупателем и не добавляйте ограничение состояния из очереди.

Запишите полный запрос первой страницы истории. Он использует тот же интерфейс из шести столбцов и полуоткрытый диапазон, что и запрос выдачи:

cat > history.sql <<'SQL'
SELECT order_id,shop_id AS group_id,customer_id AS actor_id,
       state,placed_at AS event_at,total_cents AS amount_cents
FROM orders
WHERE customer_id=@actor_id
  AND placed_at>=@from_at AND placed_at<@until_at
ORDER BY placed_at DESC,order_id DESC
LIMIT 7;
SQL
./desk page history
./desk plan history

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

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

cat >> indexes.sql добавляет новую инструкцию в существующий исходный файл; > заменил бы его. После добавления выполните только новую инструкцию CREATE ниже, потому что индекс очереди уже существует в этой VM. В новой базе сохранённый файл из двух инструкций является полным исходным кодом индексов.

cat >> indexes.sql <<'SQL'
CREATE INDEX actor_time ON orders(customer_id,placed_at,order_id);
SQL
./db -e "CREATE INDEX actor_time ON orders(customer_id,placed_at,order_id);"
./db -e "SHOW INDEX FROM orders;"
./desk page history
./desk plan history

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

./desk page history --from 2025-01-01 --until 2025-05-01
./desk page history --actor 78
./desk page history --from 2027-01-01 --until 2027-01-02
./desk page queue

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

Если вы меняете кандидат, изучите SHOW INDEX, удалите только заменяемый индекс, созданный в рамках курса, обновите indexes.sql и примените новое определение. Никогда не удаляйте исходный первичный ключ, столбцы или строки. Ошибка дублирования имени при повторном CREATE — повод изучить текущее определение, а не сбрасывать VM.

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

Резюме

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