第2章:数据库基础回顾

各位同学,咱们今天聊聊数据库基础。说实话,很多做了几年ERP开发的人,对这块的理解还是模模糊糊的。我见过太多因为基础不牢导致的性能事故了。今天咱们就把这些核心概念掰开揉碎,讲清楚。

2.1 关系型数据库核心概念

关系型数据库,说白了就是一张张表格组成的仓库。你想想看,ERP系统里的物料、订单、客户,最终都存成了一张张表。

我个人习惯把关系型数据库拆成三个层次来理解:

  • 表(Table):就是二维表格,行是记录,列是字段。比如物料表,每一行是一种物料,每一列是物料的属性。
  • 关系(Relation):表与表之间的关联。比如订单表关联客户表,通过客户ID建立联系。
  • 约束(Constraint):保证数据完整性的规则。主键、外键、唯一约束,这些都是。

我在项目中遇到过最典型的坑:有人把客户地址直接存在订单表里。结果客户搬家了,要改几百个订单的地址。这就是没理解「关系」的本质。

核心要点:关系型数据库的精髓在于「数据冗余最小化,数据一致性最大化」。每个数据只存一份,其他地方通过关系引用。

2.2 ACID特性

ACID这四个字母,做ERP的人必须刻在脑子里。为什么?因为ERP系统里全是钱、库存、订单,出一点差错就是事故。

特性 含义 ERP场景举例
原子性(Atomicity) 事务要么全做,要么全不做 转账:扣钱和加钱必须同时成功或同时失败
一致性(Consistency) 事务前后数据状态合法 库存不能为负数,订单金额必须等于明细之和
隔离性(Isolation) 并发事务互不干扰 两个人同时下单,库存扣减不能乱
持久性(Durability) 提交后数据永久保存 订单提交后,即使断电也不能丢

嗯,这里要注意。很多开发人员只记得ACID这四个字母,但实际写SQL时根本不考虑。我曾经接手过一个ERP项目,库存扣减居然没用事务。结果并发下单时,库存直接变成负数。你说这能忍吗?

避坑指南:我曾经见过一个团队,为了「性能优化」把事务隔离级别降到了READ UNCOMMITTED。结果出现了脏读,财务对账对了一个月。记住:ERP系统里,数据正确性永远优先于性能。

2.3 范式理论

范式这东西,刚学的时候觉得抽象。说白了就是一套「如何设计表才能不重复、不混乱」的规则。

咱们重点说前三范式,ERP系统里够用了:

  • 第一范式(1NF):每个字段不可再分。比如「地址」字段不能同时存省、市、区,要拆开。
  • 第二范式(2NF):非主键字段必须完全依赖于主键。比如订单明细表,主键是「订单ID+物料ID」,那「物料名称」就不该出现在这里。
  • 第三范式(3NF):非主键字段不能传递依赖于主键。比如员工表里有「部门ID」,就不该再有「部门名称」,因为部门名称依赖于部门ID。

你想想看,违反范式会怎样?数据冗余、更新异常、插入异常、删除异常。我在项目中遇到过最典型的:一个客户表里,同一个客户录了三遍,因为地址写法不一样。这就是没做好范式设计。

个人经验:我建议ERP系统设计时,先按第三范式设计。等性能确实有问题了,再考虑反范式化。别一上来就搞反范式,那是给自己挖坑。

2.4 SQL语言基础

SQL是跟数据库打交道的语言。说实话,很多干了三五年的开发,SQL水平还停留在「能跑就行」的阶段。咱们今天把核心的捋一遍。

2.4.1 DDL(数据定义语言)

建表、改表结构用的。我习惯先画好ER图,再写DDL。

-- 创建物料表
CREATE TABLE material (
    material_id   INT PRIMARY KEY,
    material_code VARCHAR(20) NOT NULL UNIQUE,
    material_name VARCHAR(100) NOT NULL,
    unit          VARCHAR(10),
    create_time   DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 添加外键约束
ALTER TABLE order_detail 
ADD CONSTRAINT fk_order_header 
FOREIGN KEY (order_id) REFERENCES order_header(order_id);

2.4.2 DML(数据操作语言)

增删改查,日常用得最多。但要注意写法,性能差别很大。

-- 查询:别用SELECT *,指定字段
SELECT material_id, material_name, unit 
FROM material 
WHERE material_code LIKE 'M%';

-- 插入:批量插入用一条语句
INSERT INTO material (material_id, material_code, material_name, unit) 
VALUES 
(1, 'M001', '螺丝', '个'),
(2, 'M002', '螺母', '个');

-- 更新:一定要加WHERE,不然全表更新
UPDATE material 
SET unit = 'KG' 
WHERE material_id = 100;

重要提醒:我曾经见过一个事故,有人执行UPDATE忘了加WHERE,把整个物料表的单位全改了。还好有备份,但恢复数据花了半天。所以,写DML语句时,先写WHERE条件,再写SET,这是保命习惯。

2.4.3 连接查询

ERP系统里,几乎没有单表查询。订单要关联客户、物料、仓库,全是多表连接。

-- 内连接:只返回匹配的数据
SELECT o.order_no, c.customer_name, m.material_name, od.quantity
FROM order_header o
JOIN customer c ON o.customer_id = c.customer_id
JOIN order_detail od ON o.order_id = od.order_id
JOIN material m ON od.material_id = m.material_id
WHERE o.order_date >= '2024-01-01';

-- 左连接:保留左表所有数据
SELECT c.customer_name, COUNT(o.order_id) AS order_count
FROM customer c
LEFT JOIN order_header o ON c.customer_id = o.customer_id
GROUP BY c.customer_id;

嗯,这里要注意。连接查询的性能瓶颈往往在JOIN条件上。如果关联字段没有索引,数据量一大就卡死。我建议所有外键字段都建索引,这是基本操作。

2.4.4 聚合与分组

报表统计离不开聚合函数。但很多人写GROUP BY时,SELECT里的字段没包含在GROUP BY里,这在严格模式下会报错。

-- 统计每个客户的订单总额
SELECT c.customer_id, c.customer_name, 
       SUM(od.quantity * od.price) AS total_amount
FROM customer c
JOIN order_header o ON c.customer_id = o.customer_id
JOIN order_detail od ON o.order_id = od.order_id
GROUP BY c.customer_id, c.customer_name
HAVING total_amount > 10000
ORDER BY total_amount DESC;

个人习惯:我写聚合查询时,习惯先用小数据量验证结果对不对。因为聚合函数容易出错,特别是NULL值的处理。比如SUM遇到NULL会忽略,但COUNT(*)会统计NULL行。这些细节不注意,报表数据就是错的。

2.5 本章小结

数据库基础这块,说白了就是三件事:理解关系模型、遵守ACID、用好范式。SQL只是工具,真正重要的是设计思想。

我见过太多ERP项目,前期设计随便搞搞,后期维护累死累活。记住:数据库设计花的时间,会在后面十倍百倍地省回来。下一章咱们讲索引优化,那才是真正考验功力的地方。