Введение
Ваш коллега уже оптимизировал два запроса первых страниц магазина. Очередь выдачи идёт от старых заказов к новым, а история покупателя — от новых к старым. Теперь вызывающей стороне нужно продолжить после первых семи заказов. У нескольких настоящих заказов одна временная метка, поэтому курсор только по времени может терять или повторять записи.
Это независимое занятие предоставляет собственный снимок MySQL, работающие запросы первых страниц queue.sql и history.sql, применённые индексы и indexes.sql. Файлы из предыдущей VM не нужны. Вы отредактируете два сохранённых запроса так, чтобы каждый исходный заказ сохранился при полном обходе страниц по возрастанию и убыванию, с сохранением эффективного индексированного доступа.
Предварительно необходимы изученные в первом занятии параметры MySQL, полуоткрытые границы, сортировка по двум столбцам и диагностические планы. Курсоры с возможным NULL, их полные сравнения и проверки глубоких страниц объясняются здесь. Используйте Terminal и /home/labex/project/order-desk. Меняйте только queue.sql и history.sql; сохраните предоставленные индексы и их исходный файл, все факты о заказах, столбцы, первичный ключ и исполняемые инструменты.
Каждая страница по-прежнему содержит до семи настоящих заказов и использует не более 64 диагностических обращений за строками. Подготовленное соединение сохраняет index condition pushdown выключенным для этого контролируемого сравнения; сохраните настройку и избегайте подсказок оптимизатору. Запросы используют фиксированный снимок данных. Не заявляются ни снимок при одновременных обновлениях, ни подсчёт физических операций ввода-вывода, ни гарантия задержки в рабочей системе. Метки и вывод работающих инструментов остаются на английском.
Перенесите оба ключа в следующую страницу выдачи
На этом шаге вы продвинете очередь от старых заказов к новым, не потеряв оставшиеся заказы с той же временной меткой.
Начните во вкладке Terminal с обычной командной оболочки. Предоставленные db и desk — те же интерфейсы, которые вы отработали в первом занятии. При неверном пути или имени файла используйте ls, проверьте настоящее имя и повторите, не меняя исходные строки. Здесь cat печатает текст сохранённого запроса. Следующий запрос page читает его фактический вывод; никакой эталонный запрос не восполняет пропущенные строки.
cd /home/labex/project/order-desk
cat queue.sql
./desk page queue
Сохранённый запрос имеет полезные условия магазина, состояния и времени и порядок (placed_at,order_id), но не имеет условия курсора. Его первая страница правильно возвращает 900001–900007. Значение cursor_for_next_request программы содержит фактические время и ID последней строки. Оно определяет, откуда может начинаться следующий запрос, но не обещает ещё одну непустую страницу.
--after принимает два аргумента: последнюю фактическую временную метку и последний фактический ID заказа. Кавычки сохраняют пробел временной метки внутри одного аргумента оболочки. Запросите следующую страницу с помощью только что увиденного курсора:
./desk page queue --after "2025-05-10 09:15:00.000000" 900007
Неизменённый начальный запрос снова возвращает первые семь ID, потому что игнорирует этот курсор. Все команды выполнились успешно, но это неправильная следующая страница. Не меняйте исходные временные метки и не удаляйте повторы из отображаемого списка ради сокрытия проблемы.
Программа передаёт ещё две переменные сеанса MySQL: @after_at и @after_id. Для первой страницы обе равны NULL, что означает отсутствие предыдущего курсора. SQL проверяет такое отсутствие через IS NULL, а не = NULL. В остальных случаях следующая строка при движении от старых к новым должна иметь более позднюю временную метку или ту же временную метку и больший ID. Использование только placed_at > @after_at потеряло бы ID 900008 и 900009 с повторяющимся показанием часов.
Сохраните существующие группу, состояние и полуоткрытые границы. Поместите варианты курсора в скобки, соединив их с этими фильтрами через AND; OR не должен освобождать строку от остальных условий задачи. Порядок остаётся возрастающим по обоим ключам. cat > queue.sql перезаписывает полный запрос ниже; маркер here-document SQL в кавычках предотвращает подстановки оболочки и завершает блок на отдельной строке.
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
AND (@after_at IS NULL
OR placed_at>@after_at
OR (placed_at=@after_at AND order_id>@after_id))
ORDER BY placed_at ASC,order_id ASC
LIMIT 7;
SQL
./desk page queue
./desk page queue --after "2025-05-10 09:15:00.000000" 900007
./desk plan queue --after "2025-05-10 09:15:00.000000" 900007
Первая страница не меняется. Следующая теперь содержит 900008, 900009 и 900021 с исходными временем и центами. Точная следующая полночь по-прежнему вне запрошенного дня, а последняя микросекунда внутри. План должен обращаться к индексированному диапазону курсора вместо чтения и отбрасывания всех предыдущих заказов. Изучите фактические строки вместе с диагностическим счётчиком.
Действие walk повторно запускает этот же сохранённый запрос, передавая курсор фактической последней строки, пока не появится настоящая пустая страница. Оно точно сохраняет возвращённые порядок и поля; не сортирует, не удаляет повторы и не дополняет результаты. Если запрос повторяет свой последний курсор, инструмент сообщает об ошибке, а не представляет частичный обход как полный.
./desk walk queue
./desk page queue --after "2025-05-10 23:59:59.999999" 900021
Обход выдаёт все десять заказов ровно один раз: семь, затем три, затем пустую страницу. Явный курсор последней строки также возвращает пустой список строк. Это допустимый конец, в отличие от Order desk error. При ошибке файла или SQL используйте ls или cat для изучения, исправьте сохранённый запрос и снова выполните настоящий запрос. Не меняйте предоставленные индексы или исходные строки. Запустите проверку шага после подтверждения полноты очереди.
Продолжите историю от новых заказов к старым с ограниченным объёмом работы
На этом шаге вы завершите противоположное направление курсора и сравните глубокую страницу с чтением тех же данных через смещение.
Оставайтесь во вкладке Terminal и в /home/labex/project/order-desk. Сохраните исправленный запрос выдачи, подготовленные индексы и их исходный файл. История покупателя использует DESC и для времени, и для ID, поэтому следующая строка имеет более раннее время либо то же время и меньший ID. Фильтр покупателя и полуоткрытый диапазон сохраняют действие. Случай NULL для первой страницы остаётся тем же.
Замените полный запрос истории. Не добавляйте ограничение готового состояния: история покупателя включает все исходные состояния и магазины этого покупателя.
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
AND (@after_at IS NULL
OR placed_at<@after_at
OR (placed_at=@after_at AND order_id<@after_id))
ORDER BY placed_at DESC,order_id DESC
LIMIT 7;
SQL
./desk page history
./desk page history --after "2025-05-10 09:15:00.000000" 900004
./desk walk history
Первая страница начинается с 900021, затем идут 900009–900004 по убыванию. Следующая страница содержит 900003, 900002 и 900001. Полный обход сохраняет все десять заказов и затем достигает настоящей пустой страницы. Заказ на ноль центов остаётся на первой странице; равное время или цена никогда не объединяют идентификаторы.
Глубокий OFFSET просит MySQL отбросить предшествующие строки после сортировки. Он может вернуть семь правильных строк, прочитав гораздо больше. Курсор вместо этого может начинать рядом с последним фактическим ключом. Сравните оба способа на истории покупателя 77 с января по апрель, у которой исключённая конечная граница — 1 мая.
В штатном клиенте ниже LIMIT 1 OFFSET 199 пропускает 199 упорядоченных строк и возвращает 200-ю — последнюю перед страницей OFFSET 200. EXPLAIN ANALYZE действительно выполняет запрос со смещением и сообщает работу его итераторов. Последний SELECT показывает настоящие строки этой страницы. Here-document и оба направления сортировки такие же, как уже отработанные.
./db <<'SQL'
SELECT order_id,placed_at
FROM orders
WHERE customer_id=77
AND placed_at>='2025-01-01' AND placed_at<'2025-05-01'
ORDER BY placed_at DESC,order_id DESC
LIMIT 1 OFFSET 199;
EXPLAIN ANALYZE FORMAT=TREE
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=77
AND placed_at>='2025-01-01' AND placed_at<'2025-05-01'
ORDER BY placed_at DESC,order_id DESC
LIMIT 7 OFFSET 200;
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=77
AND placed_at>='2025-01-01' AND placed_at<'2025-05-01'
ORDER BY placed_at DESC,order_id DESC
LIMIT 7 OFFSET 200;
SQL
200-я строка — заказ 94263 от 2025-02-25 02:30:00.000000. Прочитайте фактические строки нижнего итератора: запрос со смещением должен пройти первые 200 перед выдачей семи. Теперь используйте эту фактическую строку как курсор сохранённого запроса с теми же покупателем и границами. --from и --until действуют только для этого вызова.
./desk page history --from 2025-01-01 --until 2025-05-01 --after "2025-02-25 02:30:00.000000" 94263
./desk plan history --from 2025-01-01 --until 2025-05-01 --after "2025-02-25 02:30:00.000000" 94263
Появляются те же семь ID, начиная с 3542 и 87067. Запрос с курсором должен достигать индексированных диапазонов с небольшим фактическим числом обращений за строками вместо отбрасывания 200 предыдущих строк. Сравнивайте работу на одной и той же странице, а не два несвязанных запроса; прошедшие миллисекунды по-прежнему не определяют результат приёмки.
Наконец, проверьте другого покупателя, допустимый пустой диапазон и неизменённый обход выдачи:
./desk page history --actor 78
./desk page history --from 2027-01-01 --until 2027-01-02
./desk walk queue
Сохраните оба запроса SQL для повторного использования. Предоставленные индексы и indexes.sql остаются нетронутыми. Ошибка SQL, файла или соединения запроса остаётся ошибкой; средство чтения не выдумывает отсутствующие заказы. Это стабильные обходы подготовленного фиксированного снимка, а не снимок транзакции во время одновременных изменений. Итоговая проверка охватывает точные исходные поля и порядок, небольшие полные обходы, изменённые идентификаторы, диапазоны и курсоры и диагностическую работу вместе с сохранностью источника и инструментов.
Резюме
Вы продолжили два индексированных представления заказов через полные курсоры из временной метки и ID, сохранили каждый заказ с равным временем и нулевую стоимость и проверили настоящие пустые конечные страницы. Вы сравнили одну и ту же глубокую страницу через OFFSET и запросы с индексированным курсором, не принимая миллисекунды за доказательство. Оба направления работают на фиксированном снимке, а исходные данные, инструменты и подготовленные индексы остаются нетронутыми.



