我在SQL Server数据库中有几个带有可为空列的表。这些可为空的属性要么在行创建时未提供,要么在某些情况下不适用。
我意识到NULL在某些情况下我不能使用相同的值来表示“未提供”,而在其他情况下则表示“不适用”。这些可选列的类型的int,datetime和string。即使我标记string为强制性(列NOT NULL),要么设置的属性为“未提供”和“不适用”合适,怎么做int和datetime列?我从书中阅读了第 23 章:
数据库探索:CJ Data 和 Hugh Darwen 关于第三宣言的论文
看起来状态列只有数值,但由于作者设置了 'n/a' 和 'd/k'(不知道),不确定他们是否将状态创建为字符串列。将所有可选列创建为字符串列并在属性没有值时捕获属性中的原因是否是个好主意?
在每一列中保留正确的数据类型。使用所谓的幻数来表示“不适用”或“未知”。
例如,对于一int列,您可以-2147483647用来表示“不适用”和-2147483646“未知”,假设这两个值没有出现在“正常”数据中。对于日期,请使用N'1900-01-01T00:00:00'和这样的值N'1900-01-01T00:00:01'。当然,在执行统计分析或报告时,您需要以编程方式排除这些值,因为它们会扭曲平均值和总和等内容。
包含int使用幻数的列的示例表:
CREATE TABLE dbo.Items
(
SomeValue int NOT NULL
, CONSTRAINT CK_SomeValue
CHECK (
SomeValue = -2147483647 /* Not Applicable */
OR SomeValue = -2147483646 /* Unknown */
OR (SomeValue > 0 AND SomeValue < 2147483648)
)
);
Run Code Online (Sandbox Code Playgroud)
请注意,“记录”魔法/金丝雀值的检查约束。正如Erik Darling 在博客文章中详细说明的那样,金丝雀值消除了列允许 NULL 的需要,这反过来可以减轻使用NOT IN (...)andJOIN ON ISNULL(...) = ISNULL(...)等时的一些令人讨厌的意外。
一个例子可能是展示与使用魔法/金丝雀值相关的问题的最佳方式,而是使用 NULL 值。我们将在 tempdb 中完成这项工作,并创建两个几乎相同的表。一种使用magic/canary 值,另一种使用NULL。
USE tempdb;
IF OBJECT_ID(N'dbo.ItemsNoNulls', N'U') IS NOT NULL
DROP TABLE dbo.ItemsNoNulls;
IF OBJECT_ID(N'dbo.ItemsNulls', N'U') IS NOT NULL
DROP TABLE dbo.ItemsNulls;
CREATE TABLE dbo.ItemsNoNulls
(
SomeValue int NOT NULL
, CONSTRAINT CK_ItemsNoNulls_SomeValue
CHECK (
SomeValue = -2147483647 /* Unknown */
OR SomeValue = -2147483646 /* Not Applicable */
OR (SomeValue > 0 AND SomeValue < 2147483648)
)
);
CREATE TABLE dbo.ItemsNulls
(
SomeValue int NULL
, CONSTRAINT CK_ItemsNulls_SomeValue
CHECK (
SomeValue IS NULL
OR (SomeValue > 0 AND SomeValue < 2147483648)
)
);
Run Code Online (Sandbox Code Playgroud)
在这里,我们将插入大量行,其中 2% 的行是 -2147483647 或 -2147483646 的“魔术”值:
;WITH src AS (
SELECT val = ABS(CONVERT(int, CRYPT_GEN_RANDOM(4)))
FROM sys.columns c1
CROSS JOIN sys.columns c2
)
INSERT INTO dbo.ItemsNoNulls(SomeValue)
SELECT CASE src.val % 50
WHEN 1 THEN -2147483647
WHEN 2 THEN -2147483646
ELSE src.val
END
FROM src;
Run Code Online (Sandbox Code Playgroud)
这会将那些先前生成的行插入到表中,其中 NULL 表示未知或不可用:
INSERT INTO dbo.ItemsNulls (SomeValue)
SELECT CASE inn.SomeValue
WHEN -2147483647 THEN NULL
WHEN -2147483646 THEN NULL
ELSE inn.SomeValue
END
FROM dbo.ItemsNoNulls inn
Run Code Online (Sandbox Code Playgroud)
比较这两个简单查询的输出:
SELECT [Avg] = AVG(CONVERT(bigint, SomeValue))
, [Count] = COUNT(SomeValue)
FROM dbo.ItemsNoNulls;
Run Code Online (Sandbox Code Playgroud)
???????????????????????? ? 平均 ? 数数 ? ???????????????????????? ? 944303347 ? 549081? ????????????????????????
SELECT [Avg] = AVG(CONVERT(bigint, SomeValue))
, [Count] = COUNT(SomeValue)
, [CountNull] = COUNT(COALESCE(SomeValue, 0))
FROM dbo.ItemsNulls
Run Code Online (Sandbox Code Playgroud)
??????????????????????????????????????? ? 平均 ? 数数 ?计数空? ??????????????????????????????????????? ? 1073162998 ? 527112?549081? ???????????????????????????????????????
由于金丝雀值的存在,第一个结果集中的“平均值”人为地偏低。第二个查询中的“计数”低于预期,因为聚合消除了 NULL 值,如警告所示:
警告:空值被聚合或其他 SET 操作消除。
如果我们修改第一个查询以消除金丝雀值:
SELECT [Avg] = AVG(CONVERT(bigint, SomeValue))
, [Count] = COUNT(SomeValue)
FROM dbo.ItemsNoNulls
WHERE SomeValue <> -2147483647
AND SomeValue <> -2147483646;
Run Code Online (Sandbox Code Playgroud)
我们得到了正确的平均值,但是计数不再反映表中的实际行数:
??????????????????????????? ? 平均 ? 数数 ? ??????????????????????????? ? 1073162998 ? 527112? ???????????????????????????
要获得准确的平均值和准确的计数,我们需要执行以下操作:
SELECT [Avg] = AVG(CONVERT(bigint, SomeValue))
, [Count] = (SELECT COUNT(SomeValue) FROM dbo.ItemsNoNulls)
FROM dbo.ItemsNoNulls
WHERE SomeValue <> -2147483647
AND SomeValue <> -2147483646;
Run Code Online (Sandbox Code Playgroud)
??????????????????????????? ? 平均 ? 数数 ? ??????????????????????????? ? 1073162998 ? 549081? ???????????????????????????
如您所见,NULL 列和魔法/金丝雀值都会引入一些对报告不利的意外问题。确保您和其他团队成员完全理解其含义对于准确性非常重要。
记录“未知”或“不适用”值的另一种方法是允许NULL这些列上的值与指示值状态的单独列相结合。例如:
USE tempdb;
IF OBJECT_ID(N'dbo.Items', N'U') IS NOT NULL
DROP TABLE dbo.Items;
IF OBJECT_ID(N'dbo.StatusValues', N'U') IS NOT NULL
DROP TABLE dbo.StatusValues;
CREATE TABLE dbo.StatusValues
(
StatusValue int NOT NULL
CONSTRAINT PK_StatusValues
PRIMARY KEY CLUSTERED
, StatusDescription varchar(30) NOT NULL
);
INSERT INTO dbo.StatusValues (StatusValue, StatusDescription)
VALUES (0, 'Normal')
, (1, 'Unknown')
, (2, 'Not Applicable');
CREATE TABLE dbo.Items
(
SomeValue int NULL
, SomeValueStatus int NOT NULL
CONSTRAINT FK_StatusValue
FOREIGN KEY REFERENCES dbo.StatusValues(StatusValue)
, CONSTRAINT CK_ValueStatus
CHECK ((SomeValue IS NULL AND (SomeValueStatus = 1 OR SomeValueStatus = 2))
OR (SomeValue IS NOT NULL AND (SomeValueStatus = 0)))
);
Run Code Online (Sandbox Code Playgroud)
这一系列语句展示了检查约束如何防止无效插入:
--this fails, since 0 indicates a status of "normal"
INSERT INTO dbo.Items (SomeValue, SomeValueStatus)
VALUES (NULL, 0);
--this succeeds since status "1" is Unknown
INSERT INTO dbo.Items (SomeValue, SomeValueStatus)
VALUES (NULL, 1);
--this succeeds since status "2" is Not Applicable
INSERT INTO dbo.Items (SomeValue, SomeValueStatus)
VALUES (NULL, 2);
--this succeeds since the SomeValue column contains a non-null value
INSERT INTO dbo.Items (SomeValue, SomeValueStatus)
VALUES (1, 0);
--this fails since "Unknown" is incompatible with a non-null SomeValue
INSERT INTO dbo.Items (SomeValue, SomeValueStatus)
VALUES (1, 1);
--this fails since "Not Applicable" is incompatible with a non-null SomeValue
INSERT INTO dbo.Items (SomeValue, SomeValueStatus)
VALUES (1, 2);
Run Code Online (Sandbox Code Playgroud)
表格内容:
SELECT i.SomeValue
, sv.StatusDescription
FROM dbo.Items i
INNER JOIN dbo.StatusValues sv ON i.SomeValueStatus = sv.StatusValue;
Run Code Online (Sandbox Code Playgroud)
???????????????????????????????????? ? 某个值?状态说明 ? ???????????????????????????????????? ? 空值 ?未知? ? 空值 ?不适用 ? ? 1 ? 普通的 ? ????????????????????????????????????
上面无效的insert会导致客户端返回如下错误:
消息 547,级别 16,状态 0,第 30 行
INSERT 语句与 CHECK 约束“CK_ValueStatus”冲突。冲突发生在数据库“tempdb”、表“dbo.Items”中。