库存流水表越“全”,查询不一定越快。一次仓储系统排查中,我看到一张库存流水表已经积累了约2.8亿行,业务人员查询某个仓 SKU 的近三个月出入库记录需要等待47秒;但真正拖慢查询的,并不是数据量本身,而是流水表把当前库存、历史变更、单据状态、批次属性和报表计算全部混在了一起。经过分区、索引、汇总表和查询口径重构后,同一类查询稳定在1.6秒左右。数据库管理员真正要管理的,不只是“有没有索引”,而是库存流水如何被写入、保存、归档、聚合和读取。
本文给出一套面向库存数据库的排查清单:先判断查询慢在扫描、关联、排序还是数据模型;再区分实时库存查询、流水追溯、经营分析和对账查询;最后用分区、覆盖索引、增量汇总、冷热分层和读写隔离组合解决问题。核心观点是:库存查询性能的上限,往往由流水数据的组织方式决定,而不是由服务器配置决定。
库存系统里经常出现一个误判:只要把库存流水表的索引建得足够多,所有查询都会变快。实际并不是这样。查询“当前可用库存”和查询“某批次过去90天的所有变动”,虽然都围绕库存,但它们需要读取的数据范围、排序方式、关联对象和一致性要求完全不同。
当前库存查询通常只需要读取每个仓库、货品、批次的最新余额;流水追溯需要按时间顺序读取多条变更记录;经营分析可能还要按区域、渠道、供应商和日期进行聚合。如果所有场景都直接查询一张明细流水表,数据库就会同时承担在线交易、历史检索和分析计算三种工作,慢查询几乎不可避免。
因此,第一步不是执行“加索引”,而是把查询按业务目标分类。一个很实用的判断方法是:如果查询结果中90%以上的数据都不是当前状态,而是历史过程,那么它就不应该长期依赖当前库存表;如果查询只是为了展示当前余额,就不应该每次从数百万条流水重新计算。

我在库存系统中最常见的结构问题,是把当前状态和历史流水放在同一张表里,用一列状态字段试图满足所有业务。更稳妥的做法是至少拆成两层:库存快照表负责回答“现在有多少”,库存流水表负责回答“为什么变成这样”。
库存快照表可以按仓库、货品、批次或库位形成唯一粒度,例如一个仓库中的一个批次只有一条当前记录。每次入库、出库、调拨或盘盈盘亏完成后,事务内更新快照,同时追加一条流水。这样,实时查询不再需要扫描流水,也不会因为历史记录增长而持续恶化。
流水表则要坚持事件记录原则。一条流水应该描述一个已经发生的库存变化,包括业务单号、事件类型、发生时间、数量变化、变更前数量、变更后数量、操作来源和幂等标识。不要把“当前可用库存”反复写进每一条流水并将其当作权威状态,否则回补、冲销或并发写入时很容易出现语义冲突。
CREATE TABLE inventory_snapshot (
warehouse_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
batch_id BIGINT NOT NULL DEFAULT 0,
on_hand_qty DECIMAL(18,4) NOT NULL DEFAULT 0,
reserved_qty DECIMAL(18,4) NOT NULL DEFAULT 0,
available_qty DECIMAL(18,4) NOT NULL DEFAULT 0,
version_no BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL,
PRIMARY KEY (warehouse_id, sku_id, batch_id)
);
CREATE TABLE inventory_flow (
flow_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
batch_id BIGINT NOT NULL DEFAULT 0,
document_no VARCHAR(64) NOT NULL,
event_type VARCHAR(32) NOT NULL,
delta_qty DECIMAL(18,4) NOT NULL,
before_qty DECIMAL(18,4) NOT NULL,
after_qty DECIMAL(18,4) NOT NULL,
event_time TIMESTAMP NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
PRIMARY KEY (flow_id, event_time),
UNIQUE KEY uk_idempotency (idempotency_key, event_time)
);上面的结构只是示例,具体字段还要结合数据库类型、分区限制和业务精度调整。最重要的不是照抄建表语句,而是明确:快照表是状态,流水表是事件;两者可以互相校验,但不应互相替代。
库存流水索引不应按照字段“看起来重要”来建立,而应按照真实查询的过滤顺序设计。通常,业务查询会先按仓库、货品、批次缩小范围,再按时间范围排序或过滤。因此,常见索引会围绕“业务定位字段加时间字段”组织。
但组合索引并不是越长越好。把仓库、货品、批次、单据号、事件类型、操作人、来源系统和时间全部塞进一个索引,既会增加写入成本,也可能因为选择性不足而失去效果。应从慢查询日志中找出前几类真实 SQL,再根据过滤条件、排序条件和返回字段建立最小有效索引。
| 查询场景 | 优先过滤字段 | 推荐索引思路 | 主要风险 |
|---|---|---|---|
| 查询某货品最近流水 | warehouse_id、sku_id、event_time | 仓库、货品、时间倒序 | 时间范围过大时仍可能读取大量记录 |
| 查询某单据明细 | document_no、event_time | 单据号加时间 | 单据号重复或回补时需增加业务类型 |
| 查询某批次历史 | warehouse_id、sku_id、batch_id | 业务定位字段加时间 | 批次为空或默认值过多会降低区分度 |
| 按事件类型统计 | event_type、event_time | 只在统计频率高时单独考虑 | 事件类型通常选择性低,单独建索引收益有限 |
我的经验是,库存流水表的索引数量通常控制在3到6个核心索引更容易维护。超过这个范围后,必须用写入耗时、索引命中率、磁盘占用和慢查询改善幅度来证明新增索引的价值。
库存流水的增长速度通常会被促销、季节、门店扩张和业务系统改造放大。一个日常每天新增2万条流水的仓库,在大促期间可能每天新增80万条;如果表结构、分区和归档策略仍按日常规模设计,原本几秒的查询可能在活动期间突然超过一分钟。
更棘手的是,数据增长不仅影响总行数,还会影响索引深度、缓存命中率、统计信息准确性和备份窗口。表从500万行增长到5000万行,不一定只是查询时间增加十倍,但执行计划可能发生跳变:优化器原本选择索引回表,后来改成全表扫描;原本走嵌套循环,后来在大结果集上变成低效连接。
因此,数据库管理员需要同时观察绝对数据量和增长曲线。每周记录流水表行数、数据文件大小、索引大小、日增量、P95查询耗时和最慢SQL数量,才能提前看到拐点,而不是等业务报障后再处理。

很多运营报表需要展示每日入库、出库、调拨和盘点数据。开发人员为了快速交付,常常让报表直接查询生产库流水表,再在 SQL 中完成日期转换、条件分类、金额计算和多表关联。报表数量一多,生产库就会被大量重复扫描。
例如,十张报表分别统计入库量、出库量、负库存次数、供应商到货及时率和仓库周转,可能都重复扫描同一批日期范围的流水。即使每张报表单独执行只需要4秒,遇到整点刷新、多人同时打开和导出明细时,也会产生明显的 I/O 峰值。
这类问题不能简单归咎于报表工具。报表工具只是把查询需求暴露出来,根因是生产库没有提供适合分析读取的中间层。对于高频、固定口径的统计,应建立按日、按仓、按 SKU 或按业务单据聚合的事实表;对于临时探索,应将数据同步到分析库或数据仓库。
在一个多仓库存分析场景中,业务团队使用九数云连接订单、采购、仓储和商品数据,重点观察库存周转、入库及时率、缺货和呆滞库存。最初的做法是直接从业务数据库的明细流水读取数据,结果每天早上集中刷新时,仓储系统的数据库连接数和磁盘读取明显上升。
我没有先让团队继续堆索引,而是把指标分成三类。第一类是必须接近实时的库存余额和缺货预警;第二类是每小时更新的仓库作业看板;第三类是按天更新的周转和呆滞分析。随后分别提供快照表、小时汇总表和日粒度分析表,避免所有指标都访问原始流水。
改造后,业务数据库只承担快照读取和必要的明细追溯;分析端读取经过清洗的增量数据。一次样本观察中,早高峰刷新期间生产库磁盘读取峰值从约每秒185MB降至72MB,库存看板刷新耗时从平均26秒降至7秒,生产库连接等待也明显减少。这里的数据属于项目样本观察,不是平台官方承诺或行业统一基准,但它说明了一个关键事实:分析性能改善的主要来源,往往是读取层分工,而不是单纯增加硬件。

索引的价值取决于它能否有效缩小扫描范围,或者直接覆盖查询所需字段。库存流水中的事件类型、来源系统、操作人等字段,经常只有少量取值,区分度低。为这些字段单独建立索引,查询未必能得到足够收益,却会让每次插入、更新和批量导入都维护更多索引页。
尤其是流水表通常是高写入表。每增加一个二级索引,写入就可能增加额外的页定位、日志记录和缓存淘汰。某次测试中,流水表从3个二级索引增加到9个二级索引后,单批写入耗时增加约34%,但常用追溯查询的P95只从4.8秒下降到4.2秒,收益明显不匹配成本。
正确做法是给索引建立“使用账本”,记录索引创建原因、服务的SQL、命中次数、维护成本、存储占用和删除风险。没有稳定使用证据的索引,不应因为“以后可能有用”而长期保留。
库存查询中最常见的低效写法之一,是对时间列直接使用函数。例如将 event_time 转成日期后再比较。这样会让数据库难以直接利用时间索引,因为索引保存的是原始时间值,而不是每一行经过函数计算后的结果。
-- 容易导致索引利用率下降的写法 SELECT * FROM inventory_flow WHERE DATE(event_time) = '2026-09-18'; -- 更适合范围索引的写法 SELECT * FROM inventory_flow WHERE event_time >= '2026-09-18 00:00:00' AND event_time < '2026-09-19 00:00:00';
时间范围的右边界建议使用“左闭右开”,不要把结束时间写成当天23:59:59。前者能避免毫秒精度、微秒精度和时区换算造成的边界遗漏,也更适合连续时间窗口拼接。
流水表字段通常很多,除了数量和时间,还有单据、批次、仓位、供应商、操作员、备注和扩展属性。使用 SELECT * 会让数据库读取并传输所有列,即使页面只展示货品、数量和时间。宽表数据一旦跨页读取,缓存利用率、网络传输和回表成本都会增加。
查询列表页时,只取页面需要的列;导出明细时,单独设计导出SQL,并限制时间范围和最大行数。对于频繁访问的固定字段,可以考虑覆盖索引,但不要为了覆盖少量报表而把大文本、JSON或备注字段塞入索引。
当用户翻到第500页,传统的 LIMIT 50 OFFSET 24950 需要先定位并跳过前面的记录。页码越深,数据库需要处理的无效数据越多。库存流水通常按时间倒序展示,更适合使用基于游标的翻页:记录上一页最后一条的时间和流水 ID,下一页从该位置继续查询。
-- 深分页方式:页码越大,跳过的数据越多 SELECT flow_id, sku_id, delta_qty, event_time FROM inventory_flow WHERE warehouse_id = 10 ORDER BY event_time DESC, flow_id DESC LIMIT 50 OFFSET 25000; -- 游标翻页方式:沿索引继续向后读取 SELECT flow_id, sku_id, delta_qty, event_time FROM inventory_flow WHERE warehouse_id = 10 AND ( event_time < :last_event_time OR ( event_time = :last_event_time AND flow_id < :last_flow_id ) ) ORDER BY event_time DESC, flow_id DESC LIMIT 50;
游标翻页要求排序字段具有稳定的唯一性。仅按时间排序可能出现同一时间多条记录,从而导致重复或漏读,所以通常需要增加流水 ID 作为第二排序字段。
缓存可以降低重复读取,但不能解决库存状态本身不清晰的问题。如果缓存中的可用库存、数据库快照和流水累计结果不一致,业务人员只会更难判断哪个数字可信。库存属于强业务语义数据,缓存失效、消息重复、事务回滚和并发扣减都必须有明确处理规则。
我通常把缓存放在“已经定义清楚的读取结果”之后,而不是放在模型设计之前。先保证快照表可校验、流水可重放、扣减有幂等控制,再对高频商品或门店库存做短时缓存,收益会更稳定。

“查询很慢”只是现象,不是诊断结论。数据库管理员至少要拿到SQL文本、执行次数、平均耗时、P95或P99耗时、扫描行数、返回行数、排序方式、临时表使用情况和锁等待时间。只有把这些信息放在一起,才能判断是单条SQL低效,还是并发把资源耗尽。
例如,平均耗时2秒但P99达到40秒,往往说明存在锁等待、缓存未命中、临时空间不足或特定参数触发了异常执行计划。平均耗时8秒但P99只有10秒,则可能是稳定但需要重构的数据访问路径。两者的处理方式完全不同。
选择性可以粗略理解为某个条件筛选后剩余数据的比例。仓库加货品通常比事件类型更有选择性;仓库加货品加时间范围,通常比只按仓库查询更容易有效利用索引。真正的索引设计还要看数据分布,例如某个货品占总流水的40%,它即使是常用过滤字段,也未必足够高选择性。
我的判断顺序通常是:先看最常见的业务定位条件,再看时间范围,再看排序字段,最后考虑是否覆盖返回列。不要先从字段数量出发,而应从“查询能否尽快把候选集合缩小”出发。
| 判断问题 | 如果答案为“是” | 如果答案为“否” |
|---|---|---|
| 过滤条件能否缩小到总数据的5%以内 | 索引通常有较高收益 | 需要结合其他条件或汇总表 |
| 查询是否有固定排序要求 | 将排序字段纳入索引顺序评估 | 避免为了偶发排序建立宽索引 |
| 返回字段是否很少且稳定 | 可以评估覆盖索引 | 优先减少返回列,不要盲目覆盖 |
| SQL是否高频执行 | 小幅改善也可能值得优化 | 低频查询应优先控制资源上限 |
| 是否涉及生产写入高峰 | 谨慎增加索引和在线变更 | 可在低峰期验证和发布 |
按时间分区适合库存流水,因为流水具有明显的发生时间和归档周期。合理的分区可以让查询只读取涉及的日期分区,也便于按分区归档、备份和删除。但分区并不会自动修复低效SQL。如果查询没有包含分区键,或者时间条件写法使数据库无法识别范围,仍然可能扫描大量分区。
分区粒度要根据日增量、查询窗口和运维能力决定。日增量只有几千行时,按天分区可能产生过多小分区;日增量数百万行时,按月分区又可能过粗。常见的选择是按月或按周分区,并将超大分区继续拆分;真正的答案应来自容量测试,而不是固定模板。
还要注意分区键与业务时间的差异。入库单创建时间、实际发生时间、系统落库时间可能不一致。追溯按哪个时间查,决定了分区键应该选哪个字段。若业务经常按实际发生时间查询,却用系统落库时间分区,分区裁剪收益会被打折。

库存分析通常需要按日统计期初库存、入库量、出库量、调拨量、盘盈盘亏和期末库存。直接从流水重算虽然逻辑直观,但每次查询都重复消耗资源。汇总表能把计算前移,但也引入了延迟、补数和口径变更问题。
好的汇总表必须保留计算粒度和来源信息。例如按仓库、SKU、日期汇总时,要能追溯到对应的流水时间范围、批次版本或同步批次号。若只保存最终数字,不保存刷新批次、统计时间和数据版本,出现对账差异时很难判断是源数据变化、重复同步还是聚合逻辑问题。
我建议采用“增量计算加定期校验”的方式:正常情况下只处理新增或变更流水;每天或每周挑选部分日期和 SKU,从原始流水全量重算,与汇总结果对比。差异率超过阈值时暂停自动发布,并进入补数流程。
下面这个案例来自一个多仓、多批次的库存系统,数据已做匿名化和结构化处理。系统运行约两年,流水累计约2.8亿行,日均新增约38万行,促销日最高约170万行。业务人员最常用的查询是:选择仓库、SKU、批次和最近90天,按发生时间倒序查看流水。
原始SQL返回字段超过30列,其中包含备注、扩展属性和供应商快照。查询计划显示,数据库先根据时间范围扫描较大的索引区间,再回表取宽字段,随后进行排序。平均扫描约96万行,实际返回不到2000行;业务页面只展示50行,却把大量无关数据从存储层拉了出来。
同时,日报和看板在每天8点集中刷新,直接对流水表进行按日、按仓库聚合。在线追溯与批量分析叠加后,导致缓存命中率从平时的92%降至68%,部分扣减事务出现锁等待。

我们先没有改变数据结构,而是调整查询合同。页面默认只允许查询最近31天,超过31天必须选择仓库和SKU;超过180天则引导用户进入历史查询。列表查询只返回页面必要字段,导出和追溯详情由独立接口处理。
这个改动看起来不像数据库优化,却直接减少了无效请求。很多系统把“任意时间、任意仓库、任意SKU”的自由查询当作灵活性,结果是把最昂贵的查询入口开放给所有人。对于生产数据库,查询条件的最小约束本身就是一种容量保护。
同时,我们将日期参数从字符串转换为明确的时间边界,在应用层生成开始时间和结束时间,避免数据库对每行调用日期函数。对于没有仓库和SKU条件的请求,接口返回提示,不允许执行全历史明细查询。
根据最常用的访问路径,我们建立了以仓库、SKU、批次和发生时间为核心的组合索引,并使用流水 ID作为稳定排序的补充字段。对于按单据追溯的场景,另建单据号和发生时间组合索引。索引字段没有包含备注、扩展属性等大字段。
CREATE INDEX idx_flow_stock_time ON inventory_flow (warehouse_id, sku_id, batch_id, event_time DESC, flow_id DESC); CREATE INDEX idx_flow_document_time ON inventory_flow (document_no, event_time DESC, flow_id DESC);
索引发布后,我们用生产流量的脱敏样本回放,而不是只执行一次手工SQL。回放覆盖普通SKU、热门SKU、无批次SKU、跨月查询、空结果查询和并发翻页,避免“测试数据刚好适合索引”的假象。
对日库存分析,我们新增按日期、仓库、SKU的库存事实汇总表。表中保存期初数量、入库数量、出库数量、调拨净变化、盘点调整、期末数量和数据批次号。小时看板则采用小时粒度汇总,保留最近14天的高频数据,较早数据转为日粒度。
增量任务根据流水的落库时间读取新增数据,但统计归属使用实际发生时间。两者分离后,迟到流水可以被识别并触发受影响日期的重算,而不是被错误地计入当前小时。
对跨系统同步,我们增加幂等键和批次校验。每次同步记录源端批次号、开始时间、结束时间、读取行数、写入行数、重复行数和异常行数。只有校验通过,汇总表的批次状态才会更新为可用。
完成三步改造后,最近90天的标准追溯查询平均耗时从19秒降至0.9秒,P95从47秒降至1.6秒,扫描行数从约96万降至约2100行。库存看板刷新由平均26秒降至7秒,生产库早高峰磁盘读取峰值下降约61%。
不过,跨年度审计仍然需要约18秒,带有复杂批次关系的导出仍需要异步任务。我们没有继续追求所有查询都在1秒内完成,因为那会迫使生产库承担不适合它的历史分析和大文件导出。性能优化不是把每一个结果都压到最低延迟,而是让每类请求进入合适的执行路径。
| 指标 | 改造前 | 改造后 | 判断 |
|---|---|---|---|
| 标准追溯平均耗时 | 19秒 | 0.9秒 | 查询边界和组合索引共同改善 |
| 标准追溯P95耗时 | 47秒 | 1.6秒 | 极端慢查询显著减少 |
| 单次平均扫描行数 | 约96万行 | 约2100行 | 过滤路径从时间宽扫转为业务定位 |
| 看板刷新耗时 | 26秒 | 7秒 | 主要受益于小时汇总层 |
| 历史审计耗时 | 不可稳定预测 | 约18秒 | 仍需接受异步或分析库方案 |

如果一张流水表既没有稳定主键,也没有幂等标识,后续任何索引或分区优化都只能解决表面问题。重复写入和无法重放会让汇总结果不可信,最终导致业务不敢使用性能更高的预计算结果。
查询清单最好按业务重要性排序,而不是按开发人员提交的先后排序。扣减前的可用库存、出库拣货校验和负库存预警应属于高优先级;月度周转分析和历史导出可以采用异步或延迟刷新。

如果流水只有几十万行,查询仍然超过10秒,通常不应先考虑分库分表。优先检查是否发生全表扫描、隐式类型转换、函数包裹列、无效关联、返回字段过宽或应用层重复查询。数据量小却慢,往往说明访问路径或SQL写法有明显问题。
如果数据量已经达到千万级,且查询性能随着月份增加而持续恶化,应开始规划分区、归档和汇总层。此时最危险的做法是继续依赖“每季度加一台更大的服务器”,因为数据增长会把硬件增益逐步吃掉。
对于近30天高频查询,可以保留在线明细和热数据索引;对于超过180天的历史数据,考虑压缩、归档或迁移到分析库。涉及经营分析的SQL,应逐步改为读取按日汇总表,原始流水只承担钻取和审计。
当生产库在报表刷新时出现连接暴涨、I/O等待和锁竞争,应优先进行读写隔离。同步方式可以是日志复制、增量抽取、消息订阅或按时间窗口批量同步,选择哪一种取决于实时性、改造成本和数据一致性要求。
在线库只保留交易必须的数据访问路径,分析库负责大范围扫描和多维聚合。若业务需要回看单据明细,分析库可以保存宽表,但要保留源单号和流水 ID,以便追溯到原始记录。
秒级库存不能通过每次扫描流水实现。应采用快照表作为读取入口,使用事务内更新、版本号控制或乐观锁避免并发覆盖。对于扣减操作,必须明确“可用库存”是现货减锁定,还是还要扣除质检、冻结和调拨在途数量。
如果引入缓存,应定义失效和回源规则。库存扣减成功后何时更新缓存、消息重复如何处理、缓存丢失如何重建,都应该写进技术方案,而不是留给线上故障时临时判断。
完整追溯与秒级响应是两种不同目标。历史追溯需要保留不可变流水、冲销关系、操作人和来源单据;为了查询方便,可以建立针对单据、批次和货品的专用索引或检索表,但不能牺牲原始流水的完整性。
如果审计查询频率低,可以接受异步生成结果。用户提交查询任务后,后台在归档库执行,完成后提供结果文件和查询批次号。这样比让审计人员直接占用生产库连接更安全。
优点:改造快、风险相对可控,适合过滤条件明确、数据范围有限的高频查询。
缺点:会增加写入、存储、备份和索引维护成本;如果SQL本身存在函数转换、深分页或全量聚合,索引收益可能非常有限。
适用场景:单据追溯、指定仓库和SKU的近期流水、固定接口查询。
优点:当前库存查询可以从扫描历史流水变成点查,响应稳定,适合交易系统。
缺点:需要处理并发扣减、事务一致性、失败重试和快照重建;如果流水写入与快照更新不在同一可靠事务中,可能出现状态不一致。
适用场景:可用库存、锁定库存、仓库库存看板和拣货校验。
优点:大幅降低报表扫描量,适合趋势、周转、缺货、呆滞和仓库对比分析。
缺点:存在刷新延迟,迟到流水和历史更正需要重算;指标口径变更后,旧汇总可能需要回补。
适用场景:运营看板、管理层报表、周期性分析和多维度聚合。
优点:减少在线表规模,改善分区裁剪、备份、删除和归档效率。
缺点:分区键选错时收益不明显;跨分区查询、分区数量管理和分区索引维护会增加复杂度。
适用场景:流水持续增长、数据生命周期清晰、历史查询可分层的系统。
优点:将大范围扫描和聚合从交易库移出,能改善生产库稳定性。
缺点:引入同步延迟、数据链路监控、故障切换和口径管理;分析结果不一定代表当前秒级状态。
适用场景:报表较多、并发读取高、分析查询复杂、生产库已出现资源争用。
| 方案 | 性能收益 | 实施成本 | 一致性影响 | 优先级建议 |
|---|---|---|---|---|
| 重写SQL与限制查询范围 | 中到高 | 低 | 几乎无 | 第一优先级 |
| 组合索引 | 中到高 | 低到中 | 无 | 适合明确高频查询 |
| 库存快照表 | 高 | 中 | 需要事务保障 | 实时库存优先 |
| 小时或日汇总表 | 高 | 中 | 存在刷新延迟 | 分析场景优先 |
| 时间分区归档 | 中到高 | 中到高 | 历史访问需规划 | 规模增长后实施 |
| 分析库或读写分离 | 高 | 高 | 需要同步链路 | 生产与分析互相干扰时实施 |

第一周不要急着改表。先收集至少7天的慢查询、业务高峰、流水日增量和报表刷新记录。将查询按接口、用户类型、业务目的和数据范围分类,形成“查询,表,索引,资源”的对应关系。
基线指标至少包括平均耗时、P95耗时、扫描行数、返回行数、数据库CPU、磁盘读取、活跃连接、锁等待和刷新延迟。没有基线,就无法判断优化是有效,还是因为当天数据量较少而产生的错觉。
第二周优先修复SQL写法和接口限制,包括去除不必要的字段、改写时间范围、禁止无限导出、修复深分页和限制无条件查询。随后针对排名靠前的SQL建立少量组合索引,并通过回放验证。
发布索引时要预留回滚方案。记录索引创建前后的写入延迟、磁盘占用、缓存命中率和慢查询变化。如果读性能只改善几个百分点,却让写入高峰明显变慢,应撤销或重新设计,而不是因为索引已经创建就继续保留。
第三周处理结构性问题。实时库存使用快照表,日报和看板使用汇总表,流水保留为追溯和校验来源。每张新表都要写清楚粒度、刷新频率、延迟容忍度、重算范围和数据责任人。
汇总任务先采用小范围回放,例如选择3个仓库、100个SKU和连续14天数据,比较流水全量计算与增量计算的结果。只有差异率、重复率和迟到数据处理符合要求,才扩大到全量。
第四周不能只测试“正常查询”。需要模拟大促日增量、热门SKU集中访问、跨月查询、批量导入、盘点调整、迟到流水、重复消息、数据库主从延迟和归档失败。
验收标准要同时包含性能和正确性。例如,P95低于2秒但库存对账差异率达到0.3%,不能算成功;汇总表刷新只需要5分钟但迟到流水无法回补,也不能算完成。库存系统的性能指标必须与业务正确性指标一起验收。

第一类是流水连续性校验。对同一仓库、SKU和批次按发生顺序排列,检查后一条记录的变更前数量是否等于前一条记录的变更后数量。由于并发和补录可能存在例外,校验结果要区分正常冲销、人工调整和异常断链。
第二类是快照校验。选定时间点,将快照数量与该时间点之前的流水累计结果进行比对。若系统允许期初库存调整,必须把期初版本纳入计算,否则会把合法期初差异误判为系统错误。
第三类是汇总校验。按日汇总表与原始流水重算结果对比,检查入库、出库、调拨和盘点调整四类数量是否分别一致。不能只对比期末库存,因为不同类型的错误可能互相抵消,导致最终数字看似正确。
库存流水并不总是按业务发生顺序落库。网络中断、外部系统重试、人工补录和审批延迟都会产生迟到数据。如果汇总任务只处理“昨天新增的记录”,迟到流水可能永远不会进入原本日期的统计。
比较稳妥的办法是保留一个可回溯窗口。例如每天重算最近3天或7天的汇总;超过窗口的迟到流水进入异常队列,由任务根据受影响日期、仓库和SKU局部重算。窗口大小取决于业务延迟分布,而不是随意设成一天。
冲销数据也不能简单当作普通负数。原入库、冲销入库和重新入库可能具有不同的业务关系。流水表应保留原单据号、冲销单据号或关联事件 ID,让审计和重算能够识别它们属于同一业务链路。
只监控数据库CPU是不够的。CPU低并不代表查询快,I/O等待、锁等待、连接池排队和网络传输都可能成为瓶颈。库存系统更应该把业务接口耗时与数据库内部指标关联起来。

遇到库存查询变慢时,我建议先问四个问题。第一,用户真正要的是当前状态、历史过程还是统计结论?第二,查询是否读取了超过业务需要的数据?第三,数据是否被多个报表重复扫描?第四,性能下降来自单条SQL,还是来自并发、锁和资源竞争?
这四个问题能把“性能问题”从模糊抱怨变成可执行判断。当前状态应考虑快照,历史过程应优化业务定位和时间范围,统计结论应使用汇总或分析层,资源竞争则需要读写隔离、错峰和事务治理。
数据库管理员经常面对两种压力:业务要求实时,开发要求灵活,运维要求稳定。没有数据时,大家只能凭经验争论;有了扫描行数、P95、刷新延迟、写入成本和差异率,方案取舍就会变得清晰。
例如,某报表每天只需要更新一次,却占用生产库大量资源,那么牺牲15分钟延迟换取读写隔离通常是合理的;某个扣减接口每天调用数百万次,即使只优化100毫秒,也可能值得建立快照和专用索引;某个年度审计查询每月使用一次,则不值得为了秒级响应把生产库设计得极度复杂。
库存数据库优化最容易犯的错误,是把“查询性能”理解成一个孤立的技术指标。真正决定系统能否长期稳定运行的,是库存状态是否可快速读取,变化过程是否可完整追溯,分析数据是否与交易库分工,迟到和冲销是否可重算,以及每一种查询是否进入了合适的数据层。
我的最终判断是:库存流水不是越少越好,也不是越快扫完越好,而是要让每条流水只在它应该被读取的场景中出现。今天可以先从慢查询日志和四类查询分组开始,找出扫描行数最多、执行频率最高、对交易影响最大的前三条SQL。先修复访问边界,再决定索引、快照、汇总和归档的组合,这通常比直接进行大规模架构改造更快看到结果,也更容易证明每一步投入是否值得。
我负责维护过一个库存系统,刚上线时按 SKU 和仓库查询流水很快,但运行一段时间后,明细页开始出现明显延迟。团队最初以为是服务器配置不够,后来发现真正的问题是实时库存、历史流水和报表统计都在反复扫描同一张明细表。
库存流水变慢,通常不是因为“记录多”这么简单,而是因为查询范围、索引设计和数据职责混在了一起。当前库存回答的是“现在还有多少”,库存流水回答的是“发生过什么”,日报统计回答的是“整体变化如何”,三类问题不应该长期依赖同一张大表解决。
在一次演示环境复盘中,我用约 800 万条流水记录模拟入库、出库和调拨查询。没有时间条件、只按 SKU 查询时,数据库需要扫描大量历史记录;增加仓库和时间范围,并建立与真实过滤条件匹配的联合索引后,扫描范围明显收窄。下面是示意性对比,具体结果仍需以实际数据库、硬件和数据分布为准。
查询方式主要问题优化方向 只按 SKU 查全部历史流水扫描范围过大强制时间范围,并按访问模式设计索引 实时库存从流水现场计算每次查询都聚合明细维护独立的当前库存表 日报直接扫描全量流水与在线查询争抢资源使用汇总表、离线任务或报表库 我的判断是:先拆分“当前状态”和“历史事实”,再讨论索引,通常比一上来给流水表增加多个索引更稳妥。
索引只能缩小访问范围,不能替代合理的数据生命周期设计。
我以前踩过一个坑:看到 SKU、仓库、时间、单据号都经常出现在查询条件里,就给每个字段都建了索引。结果查询没有明显改善,写入却变慢了,索引维护和存储空间也增加了。
库存流水索引不能按“字段越多越保险”的方式设计,应该从高频 SQL、过滤条件、排序方式和返回行数倒推。先记录最常见的查询,例如“某仓库某 SKU 在一段时间内的流水”“按单据号追溯变更”“查询某 SKU 最近一笔变动”,再用执行计划验证索引是否真的减少了扫描。
以常见的明细查询为例: SELECT transaction_id, quantity, transaction_type, business_time FROM inventory_transaction WHERE warehouse_id = ?AND sku_id = ?
AND business_time >= ?AND business_time 如果仓库和 SKU 是稳定的等值过滤,时间用于范围过滤和排序,通常可以优先评估类似“仓库、SKU、业务时间”的联合索引。但这不是固定答案:如果实际查询主要按 SKU 跨仓库查询,字段顺序就可能不同;
如果某个仓库几乎占全部数据,仓库字段的区分度也可能不足。
做法看似合理的原因实际风险 每个查询字段各建一个单列索引覆盖字段更多优化器未必能高效合并,写入成本上升 所有字段都塞进一个超宽索引希望覆盖更多查询索引膨胀,维护成本高,收益不稳定 依据高频查询建立少量联合索引贴合真实访问路径需要持续监控并防止索引冗余 验收时不要只看“执行计划显示使用了索引”。
还要比较扫描行数、返回行数、排序是否落到磁盘、实际耗时和高峰期写入延迟。一个索引即使被使用,如果扫描 100 万行只返回几十行,也可能并没有解决核心问题。
我见过最危险的做法是为了让数据库变小,直接删除几年前的流水。业务人员后来要查历史退货,财务还要做跨期对账,结果只能从备份里临时恢复数据,查询和核对都变得很被动。
分区和归档解决的是不同问题。分区主要帮助数据库更有组织地管理大表,并在查询条件合适时减少不必要的数据访问;归档则是把低频历史数据迁移到独立存储或历史表,降低在线表的体积。两者都不是“加上就一定变快”的开关。库存流水适合优先评估按业务时间管理,因为大多数查询都有日期范围,历史数据也天然存在冷热差异。
实际设计时,应先确认流水是否需要按月、按季度或按财务期间查询,再决定分区粒度。分区过细会增加维护和管理复杂度,分区过粗又可能无法有效缩小扫描范围。
数据层典型内容访问策略 热数据近期出入库、待处理对账流水在线库、高频索引、严格监控 温数据仍常被业务查询的历史流水在线或近线存储,保留清晰查询入口 冷数据低频访问但需审计留存的流水归档库或对象存储,先验证恢复流程 归档前我建议做四项检查:第一,列出仍依赖历史流水的报表和接口;
第二,验证当前库存是否能与流水结余对账;第三,完成备份、抽样恢复和权限验证;第四,确认归档后的查询路径和责任人。没有完成这些检查时,宁可先优化查询和汇总,也不要贸然删除历史记录。
我曾经遇到过一次“优化成功”的报告:开发环境里的查询从 2 秒降到了 200 毫秒,但上线后高峰期仍然超时。复查后发现,测试只用了很小的数据量,也没有模拟报表查询、并发写入和锁等待。
库存性能优化必须建立可复现基线,否则任何前后对比都可能失真。至少要固定查询条件、数据规模、数据库版本、硬件配置、并发数量和测试时间段。单次执行耗时只能说明一个瞬间,不能代表库存系统在高峰期的稳定性。建议先选三类代表性 SQL:实时库存查询、历史流水明细查询和库存汇总报表。
记录平均耗时之外,还要记录 P95 或 P99 延迟、扫描行数、返回行数、CPU、磁盘 IO、锁等待和错误率。尤其要关注“平均速度变快但尾部请求变慢”的情况,这通常意味着高峰并发或资源竞争没有解决。
验证项目优化前要记录优化后要比较 执行计划访问路径、扫描方式、排序方式是否减少无效扫描和额外排序 查询延迟平均值、P95、P99是否在高峰期仍保持稳定 资源消耗CPU、IO、内存是否只是把压力转移到其他资源 并发影响写入延迟、锁等待索引或报表是否影响入库出库 我的经验判断是,库存系统不能只追求某条 SQL 的最低耗时,而要看业务链路是否更稳:实时库存是否及时、出入库写入是否正常、报表是否避开高峰、对账是否准确。
只有性能和数据一致性同时通过验证,才算真正完成优化。


读者评论
把当前库存快照和历史流水拆开这一点很实用。实时库存查余额,追溯查询变更原因,各自服务不同场景,确实比每次从大流水表重算更合理。
索引部分没有简单鼓吹“越多越好”,而是强调结合真实慢查询设计,这个判断比较客观。尤其是事件类型选择性低,单独建索引未必有效,值得在执行计划中验证。
文中的2.8亿行和47秒案例很有参考价值,但数据增长曲线属于情景推演,不能直接当作行业标准。实际落地时还应结合数据库类型、硬件和并发量测试。