介绍
一家不断发展的商店需要两种实用订单视图:某家店最早的待取订单,以及某位顾客最新的订单历史。首页包含七笔订单。即使时间戳和价格相同,不同订单 ID 也必须分别保留;零金额的促销订单仍可能处于可取货状态。
你将保存两个可复用的 MySQL 查询和一份索引脚本,应用有效索引,让两个首页共同遵守最多两个二级索引、每页最多 64 次原生诊断行请求的预算。原始合成快照包含超过 96,000 笔订单。本单元优化首页;下一个独立单元讲解完整游标分页。
你需要掌握基本终端导航、MySQL 连接和 SELECT/WHERE。参数、半开时间范围、排序、索引和实际执行计划观察会在使用前讲解。专用 MySQL 服务器、原始数据和 Terminal 均已准备好;无需 Python 或前端开发。
在 /home/labex/project/order-desk 中工作。创建或编辑 queue.sql、history.sql 和 indexes.sql,管理最多两个二级索引。保留原始订单、列、主键、标识、状态、精确应用时间戳、以分为单位的金额、备注和可执行工具。不要删除或重写源行、修改诊断设置、添加优化器提示或使用外部缓存。索引名称和等效 SQL 可以不同。运行时标签和输出保持英文。
准备好的诊断连接禁用了索引条件下推,这是一项可将筛选隐藏在存储引擎内部的优化。禁用它使原生行请求的比较更容易解读。设置只适用于这些连接;本练习不测量物理磁盘 I/O,也不承诺生产环境的速度提升倍数。毫秒耗时和某个精确计划字符串都不是验收目标。
读取工作负载并优化取货队列
本步骤中,你将先检查有效订单,再选择有用索引。更快的首页仍必须包含相同的所需订单事实。
从 Terminal 的普通 shell 开始。cd 进入工作区;./ 运行当前目录中的程序。db 打开准备好的 MySQL 客户端,用于读取订单和管理课程索引。here-document 将带引号的 SQL 标记及其结尾 SQL 之间的行送入该客户端。结束标记独占一行。SQL 语句以分号结束,代码块结束时客户端连接关闭。如果路径或命令名输错,用 ls 检查真实名称,再重试修正命令;保持原始数据完整。
检查店铺 3 在 5 月 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 笔订单。诊断连接保持索引条件下推关闭,让筛选保持可见;保留这个预设比较条件。只返回一小页,并不能证明数据库工作量小。
保存一个可处理不同店铺、状态请求及任意半开时间范围的查询。在名称前加 @,就成为原生 MySQL 会话变量。工作台在每次请求的查询连接中提供 @group_id、@state、@from_at 和 @until_at。在某个手动 db 连接中设置的变量,不会延续到另一个连接。不要将 SET 语句放入保存的查询文件。
两个查询接口按顺序返回六列:order_id、group_id、actor_id、state、event_at、amount_cents。AS 为所选列指定接口名称:店铺对应 group,顾客对应 actor。保留真实值,不要用汇总替代。LIMIT 7 是工作台每页七笔订单的契约;降低它是在隐藏订单,而非优化页面。ASC 明确指定升序。
cat > queue.sql 将下面完整文本写入该文件,替换已有内容。带引号的 here-document 标记防止 shell 展开。粘贴整个代码块,让结束标记独占一行:
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 值统计此诊断连接中服务器与存储引擎边界上的请求,不是物理 I/O 次数。建立索引前,它仍很大。显示的下一次请求游标只来自实际最后返回行;此首页查询还不使用它。首页请求中,显示为 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
访问路径现在应使用较窄的索引范围,而原来的 5 月 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
5 月 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。
本单元终点是两个实用的首页查询。返回七行并不是完整的较长队列。下一个独立单元会预先准备这些首页查询和索引,再讲解跨页保留每笔订单的游标条件。在保留两个查询、完整索引脚本和最多两个有效二级索引的状态下,运行最终检查。
总结
你检查了真实订单事实和实际执行观察,保存了可复用的半开范围取货及顾客历史查询,并用少量有效索引支持了两个首页。原始标识、时间戳、金额和备注均保持完整。你区分了原生诊断行请求、返回行数、估计值和物理 I/O。交付完整的多页结果是下一个单元的目标。



