想象一下这个场景:周一早上,你刚泡好咖啡准备开始一周的工作,突然接到业务部门的紧急电话——"系统里的客户订单数据乱了!上周五还正常的报表今天显示完全对不上!"你心跳加速,额头冒汗,赶紧登录SQLServer检查,发现某些表里的数值莫名其妙变成了负数,日期字段里混入了无效值,甚至有些关键字段干脆是NULL...
这种情况在数据存储不规范的环境中时有发生,今天我们就来聊聊如何用SQLServer实现数据的正确存储,避免这种"数据造反"的尴尬局面。
很多开发者习惯性地对所有数字字段使用INT,这其实是个坏习惯,SQLServer提供了丰富的数值类型:
精确数值:适合财务等需要精确计算的场景
近似数值:适合科学计算,但可能有舍入误差
-- 正确的数值类型选择示例 CREATE TABLE ProductPrices ( ProductID INT PRIMARY KEY, -- 价格需要精确到分,使用DECIMAL UnitPrice DECIMAL(10, 2) NOT NULL, -- 库存量不会超过3万,用SMALLINT足够 StockQuantity SMALLINT NOT NULL CHECK (StockQuantity >= 0), -- 产品重量可能有小数,但不需要极高精度 WeightKg FLOAT NULL );
-- 字符串类型选择示例 CREATE TABLE Customers ( CustomerID INT PRIMARY KEY, -- 中文姓名需要用NVARCHAR CustomerName NVARCHAR(50) NOT NULL, -- 身份证号固定18位,用CHAR最合适 IDNumber CHAR(18) NOT NULL, -- 地址可能较长但不确定长度 Address NVARCHAR(200) NULL, -- 备注可能很长,用MAX类型 Notes NVARCHAR(MAX) NULL );
NULL在SQL中是个特殊存在,它不等于空字符串或0,而是"未知",允许NULL值会增加查询复杂度并可能引发错误。
-- 不好的设计:允许NULL CREATE TABLE Orders ( OrderID INT PRIMARY KEY, OrderDate DATETIME NULL, -- 订单日期怎么能为空? CustomerID INT NULL -- 匿名订单?真的需要吗? ); -- 改进后的设计 CREATE TABLE Orders ( OrderID INT PRIMARY KEY, OrderDate DATETIME NOT NULL DEFAULT GETDATE(), CustomerID INT NOT NULL, -- 如果真的需要匿名订单,应该用特殊值如-1表示 IsAnonymous BIT NOT NULL DEFAULT 0 );
CREATE TABLE Employees ( EmployeeID INT PRIMARY KEY, EmployeeName NVARCHAR(50) NOT NULL, -- 年龄应该在合理范围内 Age INT NOT NULL CHECK (Age >= 18 AND Age <= 70), -- 邮箱格式验证 Email VARCHAR(100) NULL CHECK ( Email IS NULL OR Email LIKE '%@%.%' AND Email NOT LIKE '@%' AND Email NOT LIKE '%@%@%' ), -- 工资不能为负 Salary DECIMAL(10, 2) NOT NULL CHECK (Salary >= 0) );
-- 不规范的设计:订单表中直接存储产品信息 CREATE TABLE BadOrders ( OrderID INT PRIMARY KEY, ProductName NVARCHAR(100) NOT NULL, ProductPrice DECIMAL(10, 2) NOT NULL, Quantity INT NOT NULL -- 如果产品价格变化,历史订单会显示错误价格 ); -- 规范化的设计 CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName NVARCHAR(100) NOT NULL, CurrentPrice DECIMAL(10, 2) NOT NULL ); CREATE TABLE OrderDetails ( OrderDetailID INT PRIMARY KEY, OrderID INT NOT NULL, ProductID INT NOT NULL, -- 下单时的实际价格,不受后续价格变化影响 UnitPriceAtOrder DECIMAL(10, 2) NOT NULL, Quantity INT NOT NULL, FOREIGN KEY (ProductID) REFERENCES Products(ProductID) );
CREATE TABLE Departments ( DeptID INT PRIMARY KEY, DeptName NVARCHAR(50) NOT NULL ); CREATE TABLE Employees ( EmployeeID INT PRIMARY KEY, EmployeeName NVARCHAR(50) NOT NULL, DeptID INT NOT NULL, -- 级联删除要慎重考虑业务需求 FOREIGN KEY (DeptID) REFERENCES Departments(DeptID) ON DELETE NO ACTION -- 部门有员工时不允许删除部门 ON UPDATE CASCADE -- 部门ID更新时自动更新员工记录 );
CREATE TABLE OrderDetails ( OrderDetailID INT PRIMARY KEY, UnitPrice DECIMAL(10, 2) NOT NULL, Quantity INT NOT NULL, Discount DECIMAL(4, 2) NOT NULL DEFAULT 0, -- 自动计算的实际价格 ActualPrice AS (UnitPrice * Quantity * (1 - Discount)), -- 持久化计算列,提高查询性能 TotalPrice AS (UnitPrice * Quantity * (1 - Discount)) PERSISTED );
-- 创建序列 CREATE SEQUENCE OrderNumberSeq START WITH 1000 INCREMENT BY 1 MINVALUE 1000 NO MAXVALUE NO CYCLE; -- 使用序列 CREATE TABLE Orders ( OrderID INT PRIMARY KEY DEFAULT (NEXT VALUE FOR OrderNumberSeq), OrderDate DATETIME NOT NULL DEFAULT GETDATE() );
让我们看一个电商系统的核心表设计示例:
-- 产品分类 CREATE TABLE Categories ( CategoryID INT PRIMARY KEY, CategoryName NVARCHAR(50) NOT NULL, ParentCategoryID INT NULL, FOREIGN KEY (ParentCategoryID) REFERENCES Categories(CategoryID) ); -- 产品表 CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName NVARCHAR(100) NOT NULL, CategoryID INT NOT NULL, UnitPrice DECIMAL(10, 2) NOT NULL CHECK (UnitPrice > 0), Discontinued BIT NOT NULL DEFAULT 0, LastStockUpdate DATETIME2 NOT NULL DEFAULT SYSDATETIME(), FOREIGN KEY (CategoryID) REFERENCES Categories(CategoryID) ); -- 客户表 CREATE TABLE Customers ( CustomerID INT PRIMARY KEY, CustomerName NVARCHAR(50) NOT NULL, Email NVARCHAR(100) NOT NULL CHECK (Email LIKE '%@%.%'), JoinDate DATETIME2 NOT NULL DEFAULT SYSDATETIME(), CreditLimit DECIMAL(12, 2) NOT NULL DEFAULT 0 CHECK (CreditLimit >= 0) ); -- 订单表 CREATE TABLE Orders ( OrderID INT PRIMARY KEY, CustomerID INT NOT NULL, OrderDate DATETIME2 NOT NULL DEFAULT SYSDATETIME(), RequiredDate DATETIME2 NULL CHECK (RequiredDate > OrderDate), ShippedDate DATETIME2 NULL, Status TINYINT NOT NULL DEFAULT 0, -- 0=新订单,1=处理中,2=已发货,3=已完成 FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ); -- 订单明细 CREATE TABLE OrderDetails ( OrderID INT NOT NULL, ProductID INT NOT NULL, UnitPrice DECIMAL(10, 2) NOT NULL CHECK (UnitPrice > 0), Quantity SMALLINT NOT NULL CHECK (Quantity > 0), Discount DECIMAL(4, 2) NOT NULL DEFAULT 0 CHECK (Discount BETWEEN 0 AND 1), PRIMARY KEY (OrderID, ProductID), FOREIGN KEY (OrderID) REFERENCES Orders(OrderID), FOREIGN KEY (ProductID) REFERENCES Products(ProductID) );
过度使用VARCHAR:能用更具体类型时就不要用VARCHAR,比如日期用DATETIME2而不是VARCHAR存储
忽略字段大小:不要对所有字符串字段都用NVARCHAR(MAX),这会影响性能
缺少默认值:为常用字段设置合理的默认值,减少应用层代码复杂度
滥用触发器:触发器难以调试和维护,优先考虑约束和存储过程
忽视索引设计:良好的索引设计是性能关键,但不在本文讨论范围内
正确的数据存储设计就像建筑的地基,虽然用户看不见,但决定了整个系统的稳定性和可靠性,通过合理选择数据类型、设置约束、规范化设计和利用SQLServer特有功能,你可以构建出既健壮又高效的数据存储结构,今天多花一小时设计表结构,未来可能节省上百小时的故障排查和数据修复时间。
下次当你设计新的SQLServer表时,不妨问问自己:这个设计能防止数据"造反"吗?如果答案是肯定的,那么你已经走在正确的道路上了。
本文由 哀大 于2025-08-03发表在【云服务器提供商】,文中图片由(哀大)上传,本平台仅提供信息存储服务;作者观点、意见不代表本站立场,如有侵权,请联系我们删除;若有图片侵权,请您准备原始证明材料和公证书后联系我方删除!
本文链接:https://vps.7tqx.com/wenda/525149.html
发表评论