阅读时间约 16 分钟

仓储管理系统数据库优化文档

索引优化

Posted by LuckyE on January 1, 2025

仓储管理系统流水表优化方案

原始表设计

库存流水表 (InventoryTransaction)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE InventoryTransaction (
    TransactionID INT PRIMARY KEY IDENTITY(1,1),  -- 流水ID
    ItemID INT NOT NULL,                          -- 物料ID
    WarehouseID INT NOT NULL,                     -- 仓库ID
    TransactionType TINYINT NOT NULL,             -- 交易类型(1:入库, 2:出库, 3:调拨, 4:盘点)
    Quantity DECIMAL(10,2) NOT NULL,              -- 数量
    UnitPrice DECIMAL(10,2),                      -- 单价
    TotalAmount DECIMAL(12,2),                    -- 总金额
    RelatedOrderID VARCHAR(50),                   -- 关联订单号
    OperatorID INT NOT NULL,                      -- 操作人员ID
    DepartmentID INT,                             -- 部门ID
    TransactionTime DATETIME NOT NULL,            -- 交易时间
    RecordTime DATETIME NOT NULL DEFAULT GETDATE(), -- 记录时间
    Notes VARCHAR(500),                           -- 备注
    Status TINYINT NOT NULL DEFAULT 2             -- 状态(0(无效)、1(有效/已审批)、2(待审批)、3(已拒绝))
)

问题分析

发现问题:在系统中对特定时间区间内的特定仓库物料流水查询响应缓慢,执行时间超过5秒。

查询:

1
2
3
4
5
6
7
-- 查询某时间段某仓库的物料流水统计
SELECT ItemID, TransactionType, SUM(Quantity) AS TotalQuantity, SUM(TotalAmount) AS TotalAmount
FROM InventoryTransaction
WHERE TransactionTime BETWEEN '2023-01-01' AND '2023-06-30'
  AND WarehouseID = 5
GROUP BY ItemID, TransactionType
ORDER BY ItemID;

原因分析:

  • 表中已有主键索引(TransactionID)
  • 但查询主要基于TransactionTime、WarehouseID和ItemID字段组合
  • 查询需要扫描整表(几万条记录)才能筛选出符合时间和仓库条件的记录

原索引:

1
2
3
4
-- SQL Server语法
CREATE NONCLUSTERED INDEX IX_InvTrans_TimeWarehouseItem
ON InventoryTransaction (TransactionTime, WarehouseID, ItemID)
INCLUDE (TransactionType, Quantity, TotalAmount);

:::info 创建的是一个非聚集索引,

名为IX_InvTrans_TimeWarehouseItem,在InventoryTransaction表上,

涉及三个列:TransactionTime、WarehouseID和ItemID。

还包括了TransactionType、Quantity、TotalAmount这三个列作为包含列。

非聚集索引和聚集索引的区别

非聚集索引不影响数据的物理顺序,而是单独存储,所以一个表可以有多个非聚集索引。

索引的键列是TransactionTime、WarehouseID和ItemID

INCLUDE子句的作用是包含额外的列,这些列不是索引键的一部分,但可以被查询直接访问,避免回表操作,提高性能。

1. 索引名称IX_InvTrans_TimeWarehouseItem

  • 这是一个自定义的索引名称,遵循了常见命名规范:
    • IX 表示索引(Index)
    • _TimeWarehouseItem 表示索引基于的字段(TransactionTime、WarehouseID、ItemID)

2. 索引类型NONCLUSTERED INDEX

  • 非聚集索引:索引结构与实际数据物理存储顺序无关,类似书籍的目录(目录顺序≠正文顺序)。
  • 一个表可以有多个非聚集索引(但最多 999 个),而聚集索引只能有 1 个。

3. 索引键列(Key Columns)

ON InventoryTransaction (TransactionTime, WarehouseID, ItemID)

  • 索引基于三个字段的组合:
    • TransactionTime:交易时间(常用于范围查询,如 BETWEEN
    • WarehouseID:仓库编号(常用于等值查询,如 WHERE WarehouseID = 123
    • ItemID:商品编号(常用于等值查询或 JOIN 操作)
  • 顺序很重要:索引会按字段顺序(TransactionTime → WarehouseID → ItemID)组织数据,优化以下场景:
    • 同时使用这三个字段的查询(如 WHERE TransactionTime > '2023' AND WarehouseID = 1 AND ItemID = 99
    • 仅使用前缀字段的查询(如 WHERE TransactionTime > '2023'

4. 包含列(INCLUDE)

INCLUDE (TransactionType, Quantity, TotalAmount)

  • 包含列:这些字段的值会被存储在索引中,但不会参与索引的排序或查找。
  • 作用:当查询只需要索引键列和包含列时,无需回表查询原始数据页(Covering Index 覆盖索引),显著提升性能。
  • 典型场景:如果查询是:数据库可以直接从索引中获取所有数据,无需访问原始表。

:::

:::danger 问题:

  • 范围查询导致过滤效率低TransactionTime BETWEEN 是范围查询,作为索引首列时,后续的 WarehouseID=5 无法通过索引快速定位(需要逐行筛选)。
  • 分组排序未优化GROUP BY ItemID, TransactionType 的分组操作需要额外排序,当前索引无法避免临时表或文件排序。

:::

优化方案

1
2
3
CREATE NONCLUSTERED INDEX IX_Warehouse_Time_Item_Type
ON InventoryTransaction (WarehouseID, TransactionTime, ItemID, TransactionType)
INCLUDE (Quantity, TotalAmount)

设计原理:

  1. 等值条件优先
    • WarehouseID 作为索引首列,直接定位 WarehouseID=5 的所有数据,大幅减少扫描范围。
  2. 范围查询后置
    • TransactionTime BETWEEN 作为第二列,在 WarehouseID=5 的范围内快速筛选时间区间。
  3. 覆盖分组字段
    • ItemIDTransactionType 加入索引键列,使索引数据物理上按这些字段顺序存储,直接支持 GROUP BY ItemID, TransactionType 操作,避免额外排序。
  4. 包含计算列
    • INCLUDE (Quantity, TotalAmount) 保证索引覆盖查询所需全部字段,无需回表查数据页。

性能提升点

  • 减少数据扫描量
    首列 WarehouseID=5 直接过滤掉其他仓库数据,相比原索引减少 90% 以上的扫描量(假设仓库数据分布均匀)。
  • 消除排序操作
    索引已按 ItemID, TransactionType 顺序存储,GROUP BY 可直接利用索引顺序完成分组,无需临时表或文件排序。
  • 覆盖索引优势
    所有字段均从索引中读取,I/O 开销降低到最小。

优化后执行计划预期

  1. 索引查找(Index Seek)
    快速定位 WarehouseID=5TransactionTime 在指定范围内的数据。
  2. 流式聚合(Stream Aggregate)
    直接按索引顺序(ItemID → TransactionType)完成分组求和,无需显式排序。
  3. 零回表(Key Lookup 消除)
    所有字段从索引中读取,执行计划中不会出现 Key Lookup
  4. 通过将 等值条件字段前置 + 覆盖分组字段 + 包含计算列,将该查询性能提升。

SQL Server与MySQL的索引差异

  1. 语法差异:
1
2
3
-- MySQL语法
CREATE INDEX IX_InvTrans_TimeWarehouseItem 
ON InventoryTransaction (TransactionTime, WarehouseID, ItemID, TransactionType, Quantity, TotalAmount);
1
2
3
4
5
6
7
8
- SQL Server使用INCLUDE子句创建包含列
- MySQL中对应的语法是直接将列加入索引 2. 索引类型:
- SQL Server区分聚集索引(CLUSTERED)和非聚集索引(NONCLUSTERED)
- MySQL的InnoDB有主键聚集索引(PRIMARY KEY)和二级索引(普通索引) 3. 优化器差异:
- SQL Server优化器可能会选择索引交叉或合并
- MySQL优化器在老版本中通常只选择一个索引 4. 查询提示:
- SQL Server使用INDEX提示强制使用特定索引
- MySQL使用FORCE INDEX或USE INDEX