一尘不染

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

sql-server

当我注意到我的一些插入花费的时间比预期的要长时,我正在制作一个涉及 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 ║
╚════════════════════════════════════╩══════════════╝

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


阅读 100

收藏
2022-11-23

共1个答案

一尘不染

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

由于 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 数据中的任何书签以及位包数据(如果有)的简要摘要。

2022-11-23