如何存储每个产品的版本序列

Jyr*_*kka 0 database-design sql-server t-sql identity ddl

我需要创建一个如下所示的表:

+----+---------+---------+
| Id | Product | Version |
+----+---------+---------+
|  1 |       1 |       1 |
|  2 |       1 |       2 |
|  3 |       2 |       1 |
|  4 |       1 |       3 |
|  5 |       2 |       2 |
+----+---------+---------+
Run Code Online (Sandbox Code Playgroud)

哪里IDIdentity(1,1)我需要的Version是自动填充这取决于插入Product

SELECT使用ROW_NUMBER()with可以达到类似的效果PARTITION

SELECT ID, Product,
ROW_NUMBER() OVER(PARTITION BY Product ORDER BY ID) AS Version
FROM Products
Run Code Online (Sandbox Code Playgroud)

我有哪些选择?

Aar*_*and 5

如果您可以相信该IDENTITY列会不断增加,那么我认为没有理由存储此信息。它变得多余,因为它是您可以从已存储在表中的数据中获取的信息(如您已经展示的)。存储它在以下方面是浪费的:

  • 额外的磁盘空间
  • 额外的内存,因为数据希望大部分时间都在内存中
  • 计算每个插入的版本号
  • 每当删除任何行时,重新计算所有相关行的版本 #

现在,权衡是可以衡量的,因为数据可能很少更新,但在更新之间会查询数百万次。在这种情况下,预先计算数据并存储它可能是有意义的。但是不要过早地为此进行优化;当您可以证明在运行时计算它会给您的工作负载增加可衡量的影响时,请做出该决定。

如果您不能相信该IDENTITY列会不断增加(毕竟,有人可以返回并使用 填充删除留下的空白IDENTITY_INSERT),那么您可以添加一个默认为的SMALLDATETIME列,成本为每行 4 个字节(或者根据精度不同的成本,如果您需要比分钟更多的粒度)。然后,您仍然应该在查询运行时计算版本 #,恕我直言,使用与您建议的查询类似的查询:NOT NULLCURRENT_TIMESTAMPDATETIME2

SELECT Id, Product,
ROW_NUMBER() OVER (PARTITION BY Product ORDER BY inserted_date)
FROM dbo.Products; --*
Run Code Online (Sandbox Code Playgroud)

*请始终使用架构前缀

如果您真的、真的、真的、真的、真的认为这是个好主意,那么您可以通过后触发器实现这一点。请注意,即使使用默认的非常细粒度的日期时间,您也不能依赖于插入顺序语义(想想像下面这样的多行插入)。同样,假设您可以依靠该IDENTITY列不断增加并且永不回填,您可以使用该列来打破任何联系。

USE tempdb;
GO

CREATE TABLE dbo.Products
(
  Id INT IDENTITY(1,1) PRIMARY KEY,
  Product INT,
  inserted_date DATETIME2(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  [Version] INT
);
GO

CREATE TRIGGER dbo.AddRedundantInfoToProducts
ON dbo.Products
AFTER INSERT, DELETE
AS
BEGIN
  SET NOCOUNT ON;

  ;WITH src AS 
  (
    SELECT Id, [Version], rn = ROW_NUMBER() OVER 
      (PARTITION BY Product ORDER BY inserted_date, Id)
    FROM dbo.Products
    WHERE Product IN
    (
      SELECT Product FROM inserted
      UNION ALL
      SELECT Product FROM deleted
    )
  )
  UPDATE src SET [Version] = rn;
END
GO
Run Code Online (Sandbox Code Playgroud)

现在我们可以测试一下:

INSERT dbo.Products(Product) VALUES(1),(1),(2),(1),(2);

SELECT Id, Product, inserted_date, Version FROM dbo.Products;
Run Code Online (Sandbox Code Playgroud)

结果:

1   1   2015-05-27 16:18:19.723 1
2   1   2015-05-27 16:18:19.723 2
3   2   2015-05-27 16:18:19.723 1
4   1   2015-05-27 16:18:19.723 3
5   2   2015-05-27 16:18:19.723 2
Run Code Online (Sandbox Code Playgroud)

现在让我们删除几行:

DELETE dbo.Products WHERE Id = 2;

SELECT Id, Product, inserted_date, Version FROM dbo.Products;
Run Code Online (Sandbox Code Playgroud)

结果:

1   1   2015-05-27 16:18:19.723 1 
3   2   2015-05-27 16:18:19.723 1
4   1   2015-05-27 16:18:19.723 2
5   2   2015-05-27 16:18:19.723 2
Run Code Online (Sandbox Code Playgroud)

现在我们将插入一些新行:

INSERT dbo.Products(Product) VALUES(2),(2),(3);

SELECT Id, Product, inserted_date, Version FROM dbo.Products;
Run Code Online (Sandbox Code Playgroud)

结果:

1   1   2015-05-27 16:18:19.723 1
3   2   2015-05-27 16:18:19.723 1
4   1   2015-05-27 16:18:19.723 2
5   2   2015-05-27 16:18:19.723 2
6   2   2015-05-27 16:19:00.773 3
7   3   2015-05-27 16:19:00.773 1
8   2   2015-05-27 16:19:00.773 4
Run Code Online (Sandbox Code Playgroud)

清理:

DROP TABLE dbo.Products;
Run Code Online (Sandbox Code Playgroud)

当然,如果您通过存储过程控制对表的 DML,这会更容易;您可以在不需要触发器的情况下计算新版本。但是,我觉得即使给你看也是浪费精力;我根本不推荐这种方法,因为您永远无法真正确保所有数据操作都通过存储过程,这意味着您的数据无论如何很容易不同步(而在查询运行时计算的排名始终保证为当前的)。鉴于上表:

CREATE PROCEDURE dbo.InsertProductRow
  @Product INT
AS
BEGIN
  SET NOCOUNT ON;
  SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  BEGIN TRANSACTION;
  DECLARE @rn INT;
  SELECT @rn = COALESCE(MAX([Version]),0) + 1
    FROM dbo.Products
    WHERE Product = @Product;
  INSERT dbo.Products(Product, [Version]) VALUES(@Product,@rn);
  COMMIT TRANSACTION;
END
GO

CREATE PROCEDURE dbo.DeleteProductRow
  @Id INT
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @Product INT;
  SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  BEGIN TRANSACTION;
  SELECT @Product = Product FROM dbo.Products WHERE Id = @Id;
  DELETE dbo.Products WHERE Id = @Id;
  ;WITH src AS 
  (
    SELECT Id, [Version], rn = ROW_NUMBER() OVER 
      (ORDER BY inserted_date, Id)
    FROM dbo.Products
    WHERE Product = @Product
  )
  UPDATE src SET [Version] = rn;
  COMMIT TRANSACTION;
END
GO
Run Code Online (Sandbox Code Playgroud)

以下批次将在每个步骤产生与上述相同的结果:

EXEC dbo.InsertProductRow @Product = 1;
EXEC dbo.InsertProductRow @Product = 1;
EXEC dbo.InsertProductRow @Product = 2;
EXEC dbo.InsertProductRow @Product = 1;
EXEC dbo.InsertProductRow @Product = 2;

SELECT Id, Product, inserted_date, [Version] FROM dbo.Products;

EXEC dbo.DeleteProductRow @Id = 2;

SELECT Id, Product, inserted_date, [Version] FROM dbo.Products;

EXEC dbo.InsertProductRow @Product = 2;
EXEC dbo.InsertProductRow @Product = 3;
EXEC dbo.InsertProductRow @Product = 2;

SELECT Id, Product, inserted_date, [Version] FROM dbo.Products;
Run Code Online (Sandbox Code Playgroud)

不要忘记清理:

DROP TABLE dbo.Products;
DROP PROCEDURE dbo.InsertProductRow, dbo.DeleteProductRow;
Run Code Online (Sandbox Code Playgroud)