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


20

当我发现一些插入内容花费的时间比预期的长时,我正在进行一个涉及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);

对于测试,我将从登台表中插入所有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);

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);

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

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

我在sys.dm_os_wait_stats中找不到mod 16000插入的任何有趣内容:

╔════════════════════════════════════╦══════════════╗
             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 
╚════════════════════════════════════╩══════════════╝

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

Answers:


12

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

由于VertiPaq是专有实现,因此答案的详细程度有一定的限制,而细节是一个严密保护的秘密。即使这样,我们也知道VertiPaq包含以下例程:

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

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

使用Windows Performance Recorder捕获慢速情况并使用Windows Performance Analyzer分析结果,我们可以看到,大部分执行时间都花在了查看数据的群集,构建直方图以及决定如何对其进行最佳划分方面节省:

WPA分析

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

最长的持续时间发生在最大可能的段中,即具有64个重复的最大不重复值出现时,即1,048,576行,每个行有16384个值集,每个值有64个条目。对代码的检查揭示了对昂贵处理的硬编码时间限制。可以在其他VertiPaq实现中(例如SSAS)进行配置,但据我所知,不能在SQL Server中进行配置。

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

有关更多信息,请参见:


9

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

首先,我尝试使用更改插入CCI的行数TOP。我用于ID % 16000所有测试。下图是将插入的行与压缩的行组段大小进行比较的图形:

顶部与尺寸的关系图

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

top vs cpu

我们可以看到,行组段的大小以线性速率增长,并且使用少量的CPU直到大约1 M行。那时,行组的大小显着减小,CPU使用率显着增加。似乎我们为此压缩在CPU上付出了沉重的代价。

当插入少于1024000行时,我最终在CCI中得到一个开放的行组。但是,使用REORGANIZE或强制压缩REBUILD对大小没有影响。TOP顺便说一句,我发现有趣的是,当我使用变量时,我最终得到一个开放的行组,但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 
╚═══════════╩═════════╩═══════════════╩═════════════╝

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

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

╔═══════════╦═════════╦═══════════════╦═════════════╗
 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 
╚═══════════╩═════════╩═══════════════╩═════════════╝

看起来当插入的行数<〜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;

该插入片段在不到一秒钟的时间内完成,压缩效果较差:

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);

我们可以看到不平衡线程的影响:

╔════════════╦════════════╦══════════════╦═══════════════╗
 state_desc  total_rows  deleted_rows  size_in_bytes 
╠════════════╬════════════╬══════════════╬═══════════════╣
 OPEN             13540             0         311296 
 COMPRESSED     1048576             0        2095872 
 COMPRESSED     1035036             0        2070784 
╚════════════╩════════════╩══════════════╩═══════════════╝

我们可以采取各种技巧来迫使线程保持平衡并具有相同的行分布。这是其中之一:

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)

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

平衡1

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

╔════════════╦════════════╦══════════════╦═══════════════╗
 state_desc  total_rows  deleted_rows  size_in_bytes 
╠════════════╬════════════╬══════════════╬═══════════════╣
 COMPRESSED     1048576             0         128568 
 COMPRESSED     1048576             0         128568 
╚════════════╩════════════╩══════════════╩═══════════════╝

通过从原始登台表插入数据,我们可以获得相同的结果:

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);

这里,循环分布用于派生表,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);

重新加载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);

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

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;

即使行组大小之间的差异很小,也可以忽略不计(2.000(MOD 16600)与2.001(MOD 17000))

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.