需求分析把「谁要干什么」写成一张矩阵;可行性分析把「能不能扛」折算成行数和毫秒。 「自己的」那两格是权限设计的全部难点:同一个功能,三种角色看三份数据。 物流网的瓶颈从来不是数据量,是「调度员愿不愿意用」——这一点技术解决不了。
教材 22.2 讲系统分析,分需求分析和可行性分析两块。
这两块在很多教材里写得比较虚,这一篇把它们落到能验证的东西上:一张角色矩阵、一张容量表、
一组实测耗时。
需求分析的价值在于「把含糊的话逼成明确的条件」。
「司机只能看自己的单」这句话,翻译成 SQL 就是 WHERE vehicle_id = 当前司机的车。
可行性分析的价值在于「用数字否掉不切实际的设想」。
这个项目的量级一年两万多单,谈分库分表就是浪费。
演示里用 200601 行数据实测了三种查询的耗时,
其中「没索引的列」这一条是后面第 22.4 节建索引的直接依据。
一、需求分析:把话逼成条件
教材列了一堆功能点,但真正决定代码结构的只有几行「限定语」。 比如这几句:
| 需求原话 | 翻译成条件 |
|---|---|
| 客户能查自己发的货 | WHERE client_id = 当前用户 |
| 司机能查自己车上的单 | WHERE vehicle_id = 当前司机的车 |
| 调度员能查全部 | 不加条件 |
| 已验收的单不能再改 | WHERE state <> '已验收' |
右边这四条,就是权限系统最朴素的形态:不是「能不能进这个页面」,
是「进了这个页面能看见哪几行」。
这两件事在代码里的差别很大:前者一个 if 就够,后者必须落到每一条 SQL 上。
而漏掉右边任何一条的后果,都不是「报错」,是「静默地多看见了一些数据」。
这也是为什么权限问题往往在上线很久之后才被发现——因为页面上什么都不异常。
二、角色矩阵:一张表看清全部难点
| 功能 | 调度员 | 司机 | 客户 | 管理员 |
|---|---|---|---|---|
| 车源查询 | 全部 | — | — | 全部 |
| 发货单填单 | 全部 | — | — | 全部 |
| 发货单查询 | 全部 | 自己的 | 自己的 | 全部 |
| 回执验收 | — | 自己的 | — | 全部 |
| 基础信息维护 | — | — | — | 全部 |
看这张表的一个规律:只有第三行出现了两种「自己的」。 也就是说,同一个功能要按两种不同的归属过滤——司机按车、客户按人。 这在实现上意味着同一个查询要准备两个版本,或者一个查询带一个可变的过滤字段。 教材的做法是「按角色分支写不同的 SQL」。 这最直接,但改起来最痛:加一个角色就要改一遍所有查询。 更常见的做法是「查询函数接收一个归属条件」,调用方按角色传不同的值。 两种都能用,选哪种取决于角色会不会再增加。
三、可行性分析:先算行数,再谈架构
教材的可行性分析讲了技术、经济、操作三条线。 技术这条线最容易被写成空话(「本系统采用成熟技术,技术可行」),其实它可以算得很具体。
| 算什么 | 怎么算 | 结论 |
|---|---|---|
| 一年多少行 | 每天单量 × 365 | 约 2.2 万单 |
| 一年多大 | 每行字节 × 行数 | 个位数兆 |
| 查询多慢 | 真跑一遍 | 索引命中不到一毫秒 |
三个数一出来,「要不要分库分表」这个问题就自动消失了。 因为分库分表解决的是「单表上亿行」的问题,而这个项目三年才六万多行。 提前上分库分表,得到的不是「更强的扩展性」,是「一个没人会维护的复杂系统」。 经济这条线也值得算一下:一台普通云服务器加一块盘就够, 真正的成本是「有人维护」而不是「机器贵」。 所以教材说「经济可行」是对的,但理由不是「硬件便宜」,是「运维简单」。
四、操作可行性:这条最容易被忽略
三条可行性里,操作可行性最不受重视,但它决定项目生死。 系统做得再好,调度员还是打开 Excel,那这个项目就是失败的。
| 阻力 | 真实原因 | 对策 |
|---|---|---|
| 「录单太慢」 | 字段太多、必填太多 | 能自动带出的别让人填 |
| 「还不如 Excel 快」 | 要开浏览器、要登录 | 常做的动作放到一屏内 |
| 「数据不对」 | 别人改了他不知道 | 关键状态变化要有提示 |
第一行是最高频的抱怨。
「必填项」每多一个,录入速度就慢一点,而慢到某个临界点,人就回去用 Excel 了。
所以设计表的时候要分清「NOT NULL」和「必填」——数据库上可以是 NOT NULL,
但界面上未必要逼着人当场填。
这三条技术上都解决不了,只能靠「少要一个字段」「少点一次」。
这就是前面说的:物流网的瓶颈不是数据量,是人愿不愿意用。
五、动手 Demo:从需求到容量:一年的数据量能不能扛住
这一段把可行性量化:先用角色矩阵把「谁看什么」列清,再用真实页数外推一年的数据量, 最后灌到二十万行,实测三种查询的耗时。
import sqlite3, time
c = sqlite3.connect(u"ana.db", isolation_level=None)
c.executescript(u"""
CREATE TABLE tb_client (id INTEGER PRIMARY KEY, cname TEXT);
CREATE TABLE tb_vehicle (id INTEGER PRIMARY KEY, plate TEXT, state TEXT);
CREATE TABLE tb_order (id INTEGER PRIMARY KEY, order_no TEXT UNIQUE,
client_id INTEGER, vehicle_id INTEGER, freight REAL, state TEXT,
send_date TEXT);
""")
# ① 先灌 600 单,量「每行多少字节」
c.execute(u"BEGIN")
c.executemany(u"INSERT INTO tb_order VALUES (?,?,?,?,?,?,?,?)",
[(i, u"WL2026%06d" % i, i % 120 + 1, i % 60 + 1,
200.0 + (i % 40) * 25.0,
[u"待发车", u"在途", u"已到达", u"已验收"][i % 4],
u"2026-09-%02d" % (i % 28 + 1)) for i in range(1, 601)])
c.execute(u"COMMIT")
n = c.execute(u"SELECT COUNT(*) FROM tb_order").fetchone()[0]
b = c.execute(u"SELECT SUM(pgsize) FROM dbstat WHERE name='tb_order'").fetchone()[0]
per_row = b / n
print(u"%d 单占 %d 字节,每行约 %.0f 字节" % (n, b, per_row))
# ② 外推:每天 60 单
for days, label in ((30, u"1 个月"), (365, u"1 年"), (365 * 3, u"3 年")):
rows = 60 * days
print(u"%-8s %7d 行 %6.1f MB" % (label, rows, rows * per_row / 1024 / 1024))
# ③ 灌到二十万行,测三种查询
c.execute(u"BEGIN")
c.executemany(u"INSERT INTO tb_order VALUES (?,?,?,?,?,?,?,?)",
[(i, u"WLX%08d" % i, i % 120 + 1, i % 60 + 1,
200.0 + (i % 40) * 25.0, u"待发车", u"2026-09-01")
for i in range(600, 200601)])
c.execute(u"COMMIT")
c.execute(u"ANALYZE")
print(u"总行数 %d" % c.execute(u"SELECT COUNT(*) FROM tb_order").fetchone()[0])
def timed(sql, args=(), n=5):
best = 1e9
for _ in range(n):
t = time.perf_counter()
c.execute(sql, args).fetchall()
best = min(best, time.perf_counter() - t)
return best * 1000
print(u"COUNT(*) %.2f ms" % timed(u"SELECT COUNT(*) FROM tb_order"))
print(u"order_no 等值 %.3f ms" % timed(
u"SELECT * FROM tb_order WHERE order_no = ?", (u"WLX00100000",)))
print(u"state 统计 %.2f ms" % timed(
u"SELECT COUNT(*) FROM tb_order WHERE state = '待发车'"))
c.close()
实跑输出:
--------------------------------------------------------------------
从需求到容量:一年的数据量能不能扛住
--------------------------------------------------------------------
**教材 22.2 讲系统分析:需求分析和可行性分析。**
**需求分析定「做什么」,可行性分析定「能不能做」。**
**这一节把后者量化:一年多少行、多大、查多慢。**
① 角色与功能的对应矩阵
--------------------------------------------------------------------
功能 调度员 司机 客户 管理员
--------------------------------------------
车源查询 ✓ — — ✓
发货单填单 ✓ — — ✓
发货单查询 ✓ 自己的 自己的 ✓
回执验收 — ✓ — ✓
基础信息维护 — — — ✓
**这张表看着简单,但它是「权限设计」的起点。**
**「自己的」那两格是全部难点:同一个「查询发货单」,调度员看全部、司机只看自己车上的、**
**客户只看自己发的。**
**这三条在 SQL 上的差别就是 `WHERE` 后面多一个条件——而漏掉这个条件,就是一次越权。**
② 一年多少行:用真实页数外推
--------------------------------------------------------------------
现在 600 单,49152 字节
每行 82 字节(含索引页)
假设 每天 60 单
时间跨度 订单行数 tb_order 约
-------------------------------
1 个月 1800 0.1 MB
1 年 21900 1.7 MB
3 年 65700 5.1 MB
**结论很清楚:一年 2.2 万单、1.7 兆,单机 MySQL 完全不需要考虑分库分表。**
**这个量级连「大数据」的门槛都够不上。**
**所以「可行性分析」里最该写清楚的不是「用什么高并发架构」,而是「这个系统真正的瓶颈在哪」。**
**对物流网来说,瓶颈从来不是数据量,是「调度员愿不愿意用」——这一点技术解决不了。**
③ 数据涨到二十万行,查询会慢成什么样
--------------------------------------------------------------------
总行数 200601
查询 耗时
----------------------------------------
`COUNT(*)` 全表 4.49 ms
按 `order_no` 等值(唯一索引) 0.045 ms
按 `state` 统计(无索引) 40.92 ms
**第二行和第三行的差说明一件事:二十万行也不慢,慢的是「没索引的列」。**
**`order_no` 有唯一约束(等于有索引),所以是零点几毫秒;**
**`state` 只有四个取值、没建索引,就得扫全表。**
**顺便看一眼第一行:`COUNT(*)` 在 SQLite 上要真扫,4.49 毫秒;MySQL 的 InnoDB 也得扫索引。**
**所以「首页显示总单量」这种需求,不要每次都去 `COUNT(*)`——数据量一大,它比你想的贵。**
第一段量出「每行多少字节」这件事,是容量估算的关键。
不量就只能拍脑袋说「大概几十兆」,量了之后「一年几万行、个位数兆」是有依据的。
第三段三个数是这一篇最实用的部分:二十万行的表上,有唯一索引的等值查询是零点几毫秒,
没索引的列统计是几十毫秒——差两个数量级。
这说明「可行性」不是一句「技术可行」,而是「哪些查询快、哪些查询会慢下来」。
还有个容易被忽略的:COUNT(*) 也不是免费的。
它要真扫一遍,二十万行就得好几毫秒。
所以「首页显示总单量」这种需求,不该每次都去数——第 22.5 节会给出缓存的做法。
30 秒实验
- 把「每天 60 单」改成「每天 600 单」,看三年后的数据量对结论有没有影响。
- 在
tb_order.state上建索引再跑第三段,看第三条查询降到多少。 - 把第二段的外推改成「按年增长 20%」算,看第三年是多少行。
常见误区
- 把可行性分析写成「技术可行、经济可行、操作可行」三句空话。它应该给出行数、字节数、 耗时三个具体的数,否则它挡不住任何不切实际的设想。
- 以为「数据量不大」就不用管索引。二十万行也能差出两个数量级。 重点是「哪种查询有索引」,不是「总量大不大」。
- 只算技术可行性,不算操作可行性。系统再好,调度员还是用 Excel,项目就是失败的。 这类失败比性能问题常见得多。
自测三问(点开对答案)
1. 「司机只能看自己的单」这句话,翻译成 SQL 是什么? —— `WHERE vehicle_id = 当前司机的车`。关键在于它是「能看见哪几行」,不是「能不能进页面」。 漏掉它不会报错,只会静默地多看见数据。 2. 为什么说这个项目不需要考虑分库分表? —— 因为一年两万多行、个位数兆。分库分表解决的是单表上亿行的问题。 提前上,得到的不是扩展性,是一个没人会维护的复杂系统。 3. 为什么 `COUNT(*)` 不能随便调? —— 它要真扫一遍表和索引,二十万行就得好几毫秒。 在「每次请求都要算」的首页上,这个开销会放大成实际的响应时间。分析和容量都算清了,下一篇进系统设计:功能结构图、目录组织、界面预览这三件套, 以及「模块和表」的对应关系怎么用来做改动评估。
- ☐ (本篇完,下一篇 22.3 系统设计)