为什么创建一个简单的 CCI 行组最多需要 30 秒?

Joe*_*ish 20 sql-server columnstore sql-server-2016

当我注意到我的一些插入花费的时间比预期的要长时,我正在做一个涉及 CCI 的演示。要重现的表定义:

DROP TABLE IF EXISTS dbo.STG_1048576;
CREATE TABLE dbo.STG_1048576 (ID BIGINT NOT NULL);
INSERT INTO dbo.STG_1048576
SELECT TOP (1048576) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);
Run Code Online (Sandbox Code Playgroud)

对于测试,我将从临时表中插入所有 1048576 行。只要它没有因某种原因被修剪,这就足以填充一个压缩的行组。

如果我插入所有整数 mod 17000,它需要不到一秒钟的时间:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 17000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

SQL Server 执行时间:CPU 时间 = 359 毫秒,已用时间 = 364 毫秒。

但是,如果我插入相同的整数 mod 16000,它有时需要超过 30 秒:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

SQL Server 执行时间:CPU 时间 = 32062 毫秒,已用时间 = 32511 毫秒。

这是在多台机器上完成的可重复测试。随着 mod 值的变化,经过的时间似乎有一个清晰的模式:

MOD_NUM TIME_IN_MS
1000    2036
2000    3857
3000    5463
4000    6930
5000    8414
6000    10270
7000    12350
8000    13936
9000    17470
10000   19946
11000   21373
12000   24950
13000   28677
14000   31030
15000   34040
16000   37000
17000   563
18000   583
19000   576
20000   584
Run Code Online (Sandbox Code Playgroud)

如果您想自己运行测试,请随时修改我在此处编写的测试代码。

对于 mod 16000 插入,我在 sys.dm_os_wait_stats 中找不到任何有趣的东西:

?????????????????????????????????????????????????????
?             wait_type              ? diff_wait_ms ?
?????????????????????????????????????????????????????
? XE_DISPATCHER_WAIT                 ?       164406 ?
? QDS_PERSIST_TASK_MAIN_LOOP_SLEEP   ?       120002 ?
? LAZYWRITER_SLEEP                   ?        97718 ?
? LOGMGR_QUEUE                       ?        97298 ?
? DIRTY_PAGE_POLL                    ?        97254 ?
? HADR_FILESTREAM_IOMGR_IOCOMPLETION ?        97111 ?
? SQLTRACE_INCREMENTAL_FLUSH_SLEEP   ?        96008 ?
? REQUEST_FOR_DEADLOCK_SEARCH        ?        95001 ?
? XE_TIMER_EVENT                     ?        94689 ?
? SLEEP_TASK                         ?        48308 ?
? BROKER_TO_FLUSH                    ?        48264 ?
? CHECKPOINT_QUEUE                   ?        35589 ?
? SOS_SCHEDULER_YIELD                ?           13 ?
?????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

为什么插入 for 的ID % 16000时间比插入 for 的时间长ID % 17000

Pau*_*ite 12

在许多方面,这是预期的行为。根据输入数据分布,任何一组压缩例程都将具有广泛的性能。我们希望用数据加载速度来换取存储大小和运行时查询性能。

由于 VertiPaq 是专有实现,并且细节是一个严密保密的秘密,因此您在此处获得的答案的详细程度有一定的限制。即便如此,我们确实知道 VertiPaq 包含以下例程:

  • 值编码(缩放和/或转换值以适应少量位)
  • 字典编码(对唯一值的整数引用)
  • 运行长度编码(将重复值的运行存储为 [value, count] 对)
  • 位打包(以尽可能少的位存储流)

通常,数据将是值或字典编码,然后将应用 RLE 或位打包(或在段数据的不同子部分使用 RLE 和位打包的混合)。决定应用哪些技术的过程可能涉及生成直方图以帮助确定如何实现最大的位节省。

使用 Windows Performance Recorder 捕获慢速情况并使用 Windows Performance Analyzer 分析结果,我们可以看到绝大多数执行时间都消耗在查看数据的聚类、构建直方图以及决定如何对其进行最佳分区节省:

WPA分析

最昂贵的处理发生在段中至少出现 64 次的值。这是一种确定RLE何时可能有益的启发式方法。较快的情况会导致不纯的存储,例如位打包表示,具有更大的最终存储大小。在混合情况下,具有 64 次或更多重复的值是 RLE 编码的,其余部分是位打包的。

当具有 64 次重复的最大数量的不同值出现在最大可能的段中时出现最长持续时间,即 1,048,576 行,16,384 组值,每组 64 个条目。检查代码揭示了昂贵处理的硬编码时间限制。这可以在其他 VertiPaq 实现中配置,例如 SSAS,但据我所知不能在 SQL Server 中配置。

可以使用未记录的DBCC CSINDEX命令获得对最终存储安排的一些了解。这显示了 RLE 标头和数组条目、RLE 数据中的任何书签以及位包数据(如果有)的简要摘要。

有关更多信息,请参阅:


Joe*_*ish 9

我不能确切地说为什么会发生这种行为,但我相信我已经通过蛮力测试开发了一个很好的行为模型。以下结论仅适用于将数据加载到单个列中且整数分布非常好的情况。

首先,我尝试使用TOP. 我用于ID % 16000所有测试。下面的图表比较了插入到压缩行组段大小的行:

顶部与尺寸图

下面是插入到 CPU 时间(以毫秒为单位)的行的图表。请注意,X 轴具有不同的起点:

顶级vs CPU

我们可以看到行组段大小以线性速率增长,并使用少量 CPU,直到大约 1 M 行。在这一点上,行组大小显着减少,CPU 使用率显着增加。看起来我们为这种压缩在 CPU 上付出了沉重的代价。

当插入少于 1024000 行时,我最终在 CCI 中打开了一个行组。但是,使用REORGANIZE或强制压缩REBUILD对大小没有影响。TOP顺便说一句,我发现有趣的是,当我使用一个变量 for我结束了一个开放的行组但RECOMPILE我结束了一个封闭的行组。

接下来,我通过在保持行数不变的情况下改变模量值来进行测试。以下是插入 102400 行时的数据示例:

?????????????????????????????????????????????????????
? TOP_VALUE ? MOD_NUM ? SIZE_IN_BYTES ? CPU_TIME_MS ?
?????????????????????????????????????????????????????
?    102400 ?    1580 ?         13504 ?         352 ?
?    102400 ?    1590 ?         13584 ?         316 ?
?    102400 ?    1600 ?         13664 ?         317 ?
?    102400 ?    1601 ?         19624 ?         270 ?
?    102400 ?    1602 ?         25568 ?         283 ?
?    102400 ?    1603 ?         31520 ?         286 ?
?    102400 ?    1604 ?         37464 ?         288 ?
?    102400 ?    1605 ?         43408 ?         273 ?
?    102400 ?    1606 ?         49360 ?         269 ?
?    102400 ?    1607 ?         55304 ?         265 ?
?    102400 ?    1608 ?         61256 ?         262 ?
?    102400 ?    1609 ?         67200 ?         255 ?
?    102400 ?    1610 ?         73144 ?         265 ?
?    102400 ?    1620 ?        132616 ?         132 ?
?    102400 ?    1621 ?        138568 ?         100 ?
?    102400 ?    1622 ?        144512 ?          91 ?
?    102400 ?    1623 ?        150464 ?          75 ?
?    102400 ?    1624 ?        156408 ?          60 ?
?    102400 ?    1625 ?        162352 ?          47 ?
?    102400 ?    1626 ?        164712 ?          41 ?
?????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

直到 mod 值为 1600,每增加 10 个唯一值,行组段大小线性增加 80 个字节。一个有趣的巧合是,aBIGINT传统上占用 8 个字节,而每增加一个唯一值,段大小就增加 8 个字节。超过 1600 的 mod 值,段大小迅速增加,直到稳定。

在保持模数值相同并更改插入的行数时查看数据也很有帮助:

?????????????????????????????????????????????????????
? TOP_VALUE ? MOD_NUM ? SIZE_IN_BYTES ? CPU_TIME_MS ?
?????????????????????????????????????????????????????
?    300000 ?    5000 ?        600656 ?         131 ?
?    305000 ?    5000 ?        610664 ?         124 ?
?    310000 ?    5000 ?        620672 ?         127 ?
?    315000 ?    5000 ?        630680 ?         132 ?
?    320000 ?    5000 ?         40688 ?        2344 ?
?    325000 ?    5000 ?         40696 ?        2577 ?
?    330000 ?    5000 ?         40704 ?        2589 ?
?    335000 ?    5000 ?         40712 ?        2673 ?
?    340000 ?    5000 ?         40728 ?        2715 ?
?    345000 ?    5000 ?         40736 ?        2744 ?
?    350000 ?    5000 ?         40744 ?        2157 ?
?????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

看起来当插入的行数 < ~64 * 唯一值的数量时,我们看到压缩率相对较差(mod <= 65000 每行 2 个字节)和低线性 CPU 使用率。当插入的行数 > ~64 * 唯一值的数量时,我们看到更好的压缩和更高的,仍然是线性的 CPU 使用率。两种状态之间存在过渡,这对我来说不容易建模,但可以在图中看到。当为每个唯一值插入 64 行时,我们看到最大 CPU 使用率似乎并不正确。相反,我们最多只能将 1048576 行插入到一个行组中,一旦每个唯一值超过 64 行,我们就会看到更高的 CPU 使用率和压缩率。

下面是 cpu 时间如何随着插入行数和唯一行数的变化而变化的等高线图。我们可以看到上面描述的模式:

轮廓CPU

下面是该段所用空间的等高线图。在某一点之后,我们开始看到更好的压缩,如上所述:

轮廓尺寸

似乎这里至少有两种不同的压缩算法在起作用。鉴于上述情况,我们在插入 1048576 行时会看到最大 CPU 使用率是有道理的。当插入大约 16000 行时,我们看到此时 CPU 使用率最高,这也是有道理的。1048576 / 64 = 16384。

在这里上传了我所有的原始数据,以防有人想对其进行分析。

值得一提的是并行计划会发生什么。我只观察到具有均匀分布值的这种行为。在进行并行插入时,通常会有随机因素,线程通常是不平衡的。

将 2097152 行放入临时表:

DROP TABLE IF EXISTS STG_2097152;
CREATE TABLE dbo.STG_2097152 (ID BIGINT NOT NULL);
INSERT INTO dbo.STG_2097152 WITH (TABLOCK)
SELECT TOP (2097152) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
Run Code Online (Sandbox Code Playgroud)

此刀片可在不到一秒的时间内完成且压缩性较差:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_2097152 
OPTION (MAXDOP 2);
Run Code Online (Sandbox Code Playgroud)

我们可以看到不平衡线程的效果:

??????????????????????????????????????????????????????????
? state_desc ? total_rows ? deleted_rows ? size_in_bytes ?
??????????????????????????????????????????????????????????
? OPEN       ?      13540 ?            0 ?        311296 ?
? COMPRESSED ?    1048576 ?            0 ?       2095872 ?
? COMPRESSED ?    1035036 ?            0 ?       2070784 ?
??????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

我们可以使用各种技巧来强制线程平衡并具有相同的行分布。这是其中之一:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT FLOOR(0.5 * ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))  % 15999
FROM dbo.STG_2097152
OPTION (MAXDOP 2)
Run Code Online (Sandbox Code Playgroud)

为模数选择奇数在这里很重要。SQL Server 串行扫描临时表,计算行号,然后使用循环分配将行放在并行线程上。这意味着我们最终会得到完美平衡的线程。

余额 1

插入大约需要 40 秒,这与串行插入相似。我们得到了很好的压缩行组:

??????????????????????????????????????????????????????????
? state_desc ? total_rows ? deleted_rows ? size_in_bytes ?
??????????????????????????????????????????????????????????
? COMPRESSED ?    1048576 ?            0 ?        128568 ?
? COMPRESSED ?    1048576 ?            0 ?        128568 ?
??????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

我们可以通过从原始临时表中插入数据来获得相同的结果:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT t.ID % 16000 ID
FROM  (
    SELECT TOP (2) ID 
    FROM (SELECT 1 ID UNION ALL SELECT 2 ) r
) s
CROSS JOIN dbo.STG_1048576 t
OPTION (MAXDOP 2, NO_PERFORMANCE_SPOOL);
Run Code Online (Sandbox Code Playgroud)

这里轮转分配用于派生表,s因此在每个并行线程上对表进行一次扫描:

平衡 2

总之,当插入均匀分布的整数时,当每个唯一整数出现超过 64 次时,您会看到非常高的压缩率。这可能是由于使用了不同的压缩算法。实现这种压缩的 CPU 成本可能很高。数据的微小变化可能会导致压缩行组段的大小出现巨大差异。我怀疑看到最坏的情况(从 CPU 的角度)在野外并不常见,至少对于这个数据集。在进行平行插入时更难看到。


小智 8

我相信,这与单列表压缩的内部优化以及字典占用的 64 KB 的幻数有关。

示例:如果您使用MOD 16600运行,行组大小的最终结果将为1.683 MB,而运行MOD 17000将为您提供一个大小为2.001 MB的行组。

现在,看看创建的字典(你可以使用我的CISL 库,你需要函数 cstore_GetDictionaries,或者去查询 sys.column_store_dictionaries DMV):

(MOD 16600) 61 KB

在此处输入图片说明

(MOD 17000) 65 KB

在此处输入图片说明

有趣的是,如果您将另一列添加到您的表中,我们称之为 REALID :

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, REALID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);
Run Code Online (Sandbox Code Playgroud)

重新加载 MOD 16600 的数据:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16600, ID
FROM dbo.STG_1048576
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

这次执行会很快,因为优化器会决定不过度工作并将其压缩得太远:

select column_id, segment_id, cast(sum(seg.on_disk_size) / 1024. / 1024 as Decimal(8,3) ) as SizeInMB
    from sys.column_store_segments seg
        inner join sys.partitions part
            on seg.hobt_id = part.hobt_id 
    where object_id = object_id('dbo.CCI_BIGINT')
    group by column_id, segment_id;
Run Code Online (Sandbox Code Playgroud)

即使行组大小之间会有细微差别,但可以忽略不计(2.000 (MOD 16600) 与 2.001 (MOD 17000))

对于这种情况,MOD 16000 的字典将大于具有 1 列的第一种情况(0.63 对 0.61)。