一百万个项目的写作和阅读日期范围

Eri*_*ikE 5 sql-server sql-server-2012 merge date

我有一张每天排一排的桌子,可以存放大约一百万个缓慢变化的物品。

查找这些项目Item, Date以查看它们是否存在于表中。对于涉及具有不同日期的许多项目的查找,这有点慢——我的表现在有大约 2.6 亿行。但是,能够快速插入数据以及快速查询数据对我来说很有价值。

我想将其转换为一个表格,该表格通常每个项目有一行,并带有日期范围。(如果日期范围不连续,则一个项目可能有两行。)一个原始导入文件可用于按日期显示每天的项目,与现有架构匹配。每次有新的导入文件可用时,我都会更新现有行的日期范围以涵盖新的导入数据。我还需要能够重新导入文件,以便适当更新信息。在这样的文件中,可能会添加或删除某个项目以前存在的日期。

虽然这有点烦人的任务,但我知道我可以想出正确的MERGE语句来适当地更新数据,但是需要花几个小时的时间来确保我做对了,而且可能不是一个非常快的导入过程。

所以我在玩一种有点不寻常的不同模式设计,想知道是否有人可以评论它 - 好主意,坏主意,如何使用它,性能特征,陷阱等?

当前数据布局示例

Item   Date 
----   ----------
SLAM   2016-11-01
SLAM   2016-11-02
SLAM   2016-11-03
SLAM   2016-11-05
GULP   2016-11-01
GULP   2016-11-02
GULP   2016-11-03
GULP   2016-11-05
Run Code Online (Sandbox Code Playgroud)

请注意,2016-11-04 到目前为止已被跳过,需要尽快加载。

这使得插入和删除数据变得非常容易,但是,数据库增长非常快——这不是最大的表,但已经接近 8 GB,它以每月 1 GB 的速度增长(实际上有两个不同的项目不同分类系统下的代码,因此第二个项目代码的索引将使用的空间增加一倍)。此外,查询花费的时间比我想容忍的要长。

预期的新数据布局

Item   FromDate     ToDateExclusive
----   ----------   ---------------
SLAM   2016-11-01   2016-11-04
SLAM   2016-11-05   2016-11-06
GULP   2016-11-01   2016-11-04
GULP   2016-11-05   2016-11-06
Run Code Online (Sandbox Code Playgroud)

虽然这对于空间使用和查询非常有用,但是当 2016-11-04 的新导入文件同时包含SLAMGULP时,合并过程必须确定新日期是否连续连接到两个现有日期范围,然后更新一行以包含整个范围并删除另一行。这很棘手。

如果稍后缺少 2016-11-04 的新导入文件,则GULP必须再次将单行拆分为两行,就像上面一样。这种情况很少见,但也并非不可能,尤其是考虑到文件可能会乱序导入——它们只能按需获取——并且对多个历史文件进行更正可能会产生这种情况。

不寻常的想法数据布局

Item   DateBitPattern (varbinary)
----   --------------
SLAM   0xE8    -- bits: 11101000
GULP   0xE8    -- bits: 11101000
Run Code Online (Sandbox Code Playgroud)

这种不寻常布局的想法是每个项目只有一行。选择一个锚定日期,在该日期之前不会收集任何数据(此处为 2016-11-01)。对于项目有效的每个日期,将为表示该日期的项目设置一个位(由锚日期起的天数索引)。更新很容易,至于打开或关闭项目的日期,我们可以只用一些位算术调整一个字节,而不必担心其他字节和位是什么。在查找时,我的应用程序可以获取每个所需项目的所有日期位模式,并自行计算以确定该日期是否存在该项目。插入日期、删除日期和查找都很快——代价是非规范化和不得不处理一些笨拙的问题(数据含义的不透明性,

随着时间的推移,varbinary 值会越来越长。20 年后,假设每天导入,则长度约为 913 字节。这对我来说是可以接受的,因为一次查找的最多项目数以万计。或者,我可以添加一Year列,允许每一行精确地包含binary(46). 这可以防止表增长如此之快。

你怎么认为?替代策略?关于如何使合并更容易而不是使用这种非规范化模式的想法?

附加信息

  1. 这是现在的桌子。

    CREATE TABLE dbo.ItemDate (
       RecordId int identity(1, 1) NOT NULL,
       ReferenceDate date NOT NULL,
       ReferenceType bit NOT NULL,
       ItemOfType1Code char(12) NOT NULL,
       ItemOfType2Code char(7) NOT NULL
    );
    
    Run Code Online (Sandbox Code Playgroud)

    这两个项目代码总是正好分别是 12 个字符和 7 个字符,因此为什么char在表中使用。

  2. 数据实际上存在于两个文件中,每个引用类型一个,我必须记录它来自哪个文件,以便我可以跟踪哪些数据已加载或未加载。我掩盖了这一点,因为这对我目前的问题并不重要。该ItemOfType1CodeItemOfType2Code总是完全意味着ReferenceType-there与相同的,行ItemOfType1Code的是有不同的ReferenceType

  3. RecordId需要死。我最初是在时间压力下添加它的,因此覆盖的非聚集索引ItemOfType2Code不必ItemOfType1Code在每一行中重复 12 个字节。我现在意识到我的错误,并将这张表分成两个,每个项目类型一个。在任何情况下,这都有助于提高性能。

  4. 还有一个表,我记录了什么已经加载,什么没有,它看起来像这样:

    CREATE TABLE [dbo].[ItemDatePresence](
       [ReferenceDate] [date] NOT NULL,
       [ReferenceType] [bit] NOT NULL,
       CONSTRAINT [PK_ItemDatePresence] PRIMARY KEY CLUSTERED (
          [ReferenceDate] ASC,
          [ReferenceType] ASC
       )
    );
    
    Run Code Online (Sandbox Code Playgroud)

    我目前没有计划摆脱这张表,因为它对我非常有用并且可以帮助我了解加载的内容。虽然,正如我所想的那样,也许我会这样做,但是在检查是否为某个日期加载数据时,我不想去数百万行表,而是能够快速检查是否存在已加载的数据在这个只有 170 x 2 行的表中,加载了 170 天的数据,跳过周末。我掩盖了周末的事情,因为它无关紧要,我可以自己处理。

  5. 目前指标如下:

    CREATE UNIQUE CLUSTERED INDEX [CI_ItemDate] ON [dbo].[ItemDate] (
       [ReferenceDate] ASC,
       [ReferenceType] ASC,
       [ItemOfType1Code] ASC,
       [ItemOfType2Code] ASC,
       [RecordId] ASC
    );
    
    CREATE NONCLUSTERED INDEX [IX_ItemDate_ItemOfType2Code] ON [dbo].[ItemDate] (
       [ReferenceDate] ASC,
       [ItemOfType2Code] ASC
    );
    
    Run Code Online (Sandbox Code Playgroud)

    此表上没有其他索引或键。

    我立即发现我的聚集索引是假的。我最初选择它是因为我需要能够在与ReferenceType特定输入文件相对应的段中加载数据,这些输入文件可以在不同的时间到达,也可以独立地成功或失败。但是,整个的原因加入RecordId的第一个地方是不重复的ItemOfType1Code非聚集索引,这是我结束了反正这样做,每一行中加上RecordId

  6. 当前数据的问题之一是可能有多个ItemOfType2Code对应于单个ItemOfType1Code. ItemOfType1Code即使我的索引是正确的(我完全承认它们不是),这也会使表查找效率低下。它们甚至可以是空白的。例如:

    ItemOfType1Code  ItemOfType2Code
    -----------  -----------
    ABC          ZORKMID
    ABC          VERMICIOUS
    ABC          BOROGOVE
                 XYZZY
    DEF
    
    Run Code Online (Sandbox Code Playgroud)

    因此,在新模式中,我将有两个表,每个类型一个表,我将在其中插入不同的代码列表。将有第三个表,它只包含代码类型 1 和 2 之间每个有效关系的每个特定实例——但我不需要帮助。

  7. 到目前为止,我的新日期范围表可能看起来很接近:

    CREATE TABLE dbo.ItemOfType1DateRanges (
       ItemOfType1Code char(12) NOT NULL,
       ReferenceType bit NOT NULL,
       FromBusinessDate date NOT NULL,
       ToBusinessDateExclusive date NOT NULL,
       PRIMARY KEY CLUSTERED (ItemOfType1Code, FromBusinessDate)
    );
    
    CREATE TABLE dbo.ItemOfType2DateRanges (
       ItemOfType2Code char(7) NOT NULL,
       ReferenceType bit NOT NULL,
       FromBusinessDate date NOT NULL,
       ToBusinessDateExclusive date NOT NULL,
       PRIMARY KEY CLUSTERED (ItemOfType2Code, FromBusinessDate)
    );
    
    Run Code Online (Sandbox Code Playgroud)

    这些隐含地表明代码ReferenceType完全隐含了is Item- 我已经在数据中验证了这一点,并且从逻辑上讲,了解业务,代码似乎不可能有两种不同的引用类型,因为每个代码都指代一种特定的事物那只能是一种类型。

现在,上面提到的索引和数据问题很容易解决,我相信我可以调整这些方面——请关注实际提出的问题,除非这些附加信息可能有助于提出建议。

为了具体回答@mendosi 的问题,空间是一个问题。我更愿意使用一种不会为每个项目代码 + 天添加另一行的策略,除非它确实表现良好且易于维护,并且每月占用的数据明显少于当前 1 GB。在运行时,我只有一个配对值列表,Date + ItemOfType1Code或者Date + ItemOfType2Code正在检查每个值是否在表中。我不知道ReferenceType——至少在这一点上,我不确定我是否可以可靠地为每个项目生成或计算它,尽管将来我可能能够从其他元数据中进行计算。

另一个奇怪的想法

SQL Server 2016 可更新列存储索引,项目代码作为 PK,日期作为列存储列?

当然,要等到 2016 年可能还需要几年时间。今年只将所有剩余的实例升级到 SQL Server 2012。

Sql*_*Zim 1

看来您走在正确的道路上。您的日期位模式想法是一个有趣的选择,我想听听您在该设计方面是否取得了更多进展。

我的建议是一个非常简单的建议。为什么不将 ItemOfType 拆分到它们自己的表中并使用代理 id 引用它们?我知道,如果您有良好的自然键可用,这并不总是一个流行的想法,但就节省空间而言,它可以稍微减少您的存储需求。

这对于评论来说太过分了,请不要钉死我。

create table dbo.ItemOfType1 (
    ItemOfType1Id int identity(1,1) not null
  , ItemOfType1Code char(12) not null
  , ReferenceType bit not null
  , constraint pkc_ItemOfType1 primary key clustered (ItemOfType1Id)
  , constraint uq_ItemOfType1_ItemOfType1Code_ReferenceType unique nonclustered (ItemOfType1Code, ReferenceType)
);
create table dbo.ItemOfType2 (
    ItemOfType2Id int identity(1,1) not null
  , ItemOfType2Code char(7) not null
  , ReferenceType bit not null
  , constraint pkc_ItemOfType2 primary key clustered (ItemOfType2Id)
  , constraint uq_ItemOfType2_ItemOfType2Code_ReferenceType unique nonclustered (ItemOfType2Code, ReferenceType)
);
create table dbo.ItemOfType1Dates (
    ItemOfType1Id int  not null
  , ReferenceDate date not null
  , constraint pkc_ItemOfType1Dates primary key clustered (ReferenceDate,ItemOfType1Id)
  , constraint fk_ItemOfType1Dates_ItemOfType1_ItemOfType1Id foreign key (ItemOfType1Id) references dbo.ItemOfType1 (ItemOfType1Id)
);
create table dbo.ItemOfType2Dates (
    ItemOfType2Id int  not null
  , ReferenceDate date not null
  , constraint pkc_ItemOfType2Dates primary key clustered (ReferenceDate,ItemOfType2Id)
  , constraint fk_ItemOfType2Dates_ItemOfType2_ItemOfType2Id foreign key (ItemOfType2Id) references dbo.ItemOfType2 (ItemOfType2Id)
);
/*  -- date ranges alternate
create table dbo.ItemOfType1DateRanges (
    ItemOfType1Id int  not null
  , FromBusinessDate date not null
  , ToBusinessDate date not null     
  , constraint pkc_ItemOfType1DateRanges primary key clustered (FromBusinessDate,ItemOfType1Id)
  , constraint fk_ItemOfType1DateRanges_ItemOfType1_ItemOfType1Id foreign key (ItemOfType1Id) references dbo.ItemOfType1 (ItemOfType1Id)
);
create table dbo.ItemOfType2DateRanges (
    ItemOfType2Id int  not null
  , FromBusinessDate date not null
  , ToBusinessDate date not null     
  , constraint pkc_ItemOfType2DateRanges primary key clustered (FromBusinessDate,ItemOfType2Id)
  , constraint fk_ItemOfType2DateRanges_ItemOfType2_ItemOfType2Id foreign key (ItemOfType2Id) references dbo.ItemOfType2 (ItemOfType2Id)
);    
*/
Run Code Online (Sandbox Code Playgroud)