为charindex函数分割/存储长字符串的最快方法


8

我有一个1 TB的数字字符串。给定一个12个字符的数字序列,我想在原始字符串(charindex函数)中获得该序列的开始位置。

我已经使用SQL Server使用1GB字符串和9位子字符串测试了此字符串,并将该字符串存储为varchar(max)Charindex需要10秒。将1GB的字符串分成900个字节的重叠块,并创建一个表(StartPositionOfChunk,Chunkofstring),该表的二进制排序规则中使用chunkofstring,建立索引所需的时间不到1秒。10GB的后一种方法是10个数字子串,将charindex升至1.5分钟。我想找到一种更快的存储方法。

字符串:0123456789-搜索345
charindex('345','0123456789')的子字符串得出4

方法1:现在,我可以将其存储在由一列组成的SQL Server表strtable中colstr并执行:

select charindex('345',colstr) from strtable

方法2:或者我可以通过拆分原始字符串来组成表strtable2(pos,colstr1)1; 012 | 2; 123 | 3; 234 aso,然后我们可以进行查询

select pos from strtable2 where colstr1='345'

方法3:我可以通过将原始字符串分成较大的块1; 01234 |来组成表strtable2(pos2,colstr2)。4; 34567 | 7; 6789然后

select pos2+charindex('345',colstr2) from strtable2 where colstr2 like '%345%'

第一种方法最慢。

第二种方法炸毁了数据库的存储容量!

方法3:在二进制排序规则中,将colstr2长度设置为900字节,在此列上创建索引需要1秒的时间来进行1GB字符串和9位数字子字符串的搜索。对于10GB的字符串和10位的子字符串,ist需要90秒。

还有其他想法如何使它更快(也许通过利用包含长整数的数字组成的字符串,....)?

SQL Server 2017 Developer Edition,16核,16GB RAM始终在1TB的数字字符串中搜索12位子字符串。主要目标是搜索速度!10GB字符串中的10位数字(用于性能测试)。

Answers:


6

我建议使用一种方法2,并将搜索范围分为许多目标表。10000张桌子是一个很好的第一次尝试。例如,如果搜索“ 012345678901”,则查询将查看与以“ 0123”开头的数据关联的表。您总共仍然会有大约一万亿行,但是将数据拆分为许多表具有以下积极效果:

  1. 现在,所有可能的12位数字字符串都可以放入INT。
  2. 无论如何,构建一个1 TB字符串的搜索效率更高的表示可能都会很昂贵。使用许多表,您可以轻松并行化作业,甚至可以临时要求将更多CPU分配给您的VM。
  3. 您可以构建一个表作为概念证明,以确定完整字符串的查询时间和总空间要求。
  4. 如果您需要进行任何类型的数据库维护,您都会很高兴没有创建一个巨大的表。

在这一点上,我的主要问题是您使用压缩的行存储还是列存储。下面的代码为“ 0123”搜索空间创建一个行存储表,并向其中插入1亿行。如果字符串足够随机,那么您还可以期望每个表看到约1亿行。

DROP TABLE IF EXISTS #t;

SELECT TOP (10000) 0 ID INTO #t
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.Q229892_RAW_100M_RANGE;

CREATE TABLE dbo.Q229892_RAW_100M_RANGE (
STRING_PIECE INT NOT NULL,
STR_POS BIGINT NOT NULL
);

INSERT INTO dbo.Q229892_RAW_100M_RANGE WITH (TABLOCK)
SELECT ABS(CHECKSUM(NEWID()) % 100000000),
TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT) * TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT)
FROM #t t1
CROSS JOIN #t t2
OPTION (MAXDOP 4);


DROP TABLE IF EXISTS dbo.T0123_Q229892_PAGE_COMPRESSION;

CREATE TABLE dbo.T0123_Q229892_PAGE_COMPRESSION (
    STRING_PIECE INT NOT NULL,
    STR_POS BIGINT NOT NULL,
    PRIMARY KEY (STRING_PIECE, STR_POS)
) WITH (DATA_COMPRESSION = PAGE);

INSERT INTO dbo.T0123_Q229892_PAGE_COMPRESSION WITH (TABLOCK)
SELECT STRING_PIECE, STR_POS
FROM dbo.Q229892_RAW_100M_RANGE;

坏消息是您可能需要大约15.4 TB的完整数据集。好消息是,即使缓冲区高速缓存中没有任何相关数据,查询也只需要1毫秒,这对于与您一样大的数据集几乎总是如此。

-- 1 ms
CHECKPOINT;
DBCC DROPCLEANBUFFERS;

SELECT MIN(STR_POS)
FROM dbo.T0123_Q229892_PAGE_COMPRESSION
WHERE STRING_PIECE = 45678901; -- searching for '012345678901'

您可以将这些数据丢到您拥有的最便宜的存储上,并且由于查询执行的逻辑读操作很少,因此仍然可以看到良好的响应时间。

对于columnstore,您无法查找所需的数据,并且仍然极不可能将所有数据放入内存中,因此,在查询中读取尽可能少的压缩数据非常重要。我强烈建议对表进行分区。一种行之有效的简单方法是使用搜索字符串的前四位数字查找表名,然后使用后两位数字作为分区。再次使用“ 012345678901”,您将转到包含“ 0123”数据的表的分区45。100个分区是一个很好的数目,可以避免由过多的分区引起的问题,平均每个分区大约有100万行。单个行组中可容纳的最大行数为1048576,因此使用这种方法,您将尽可能减少IO。

DROP TABLE IF EXISTS dbo.Q229892_RAW_1M_RANGE;

CREATE TABLE dbo.Q229892_RAW_1M_RANGE (
STRING_PIECE INT NOT NULL,
STR_POS BIGINT NOT NULL
);

INSERT INTO dbo.Q229892_RAW_1M_RANGE WITH (TABLOCK)
SELECT TOP (1000000) ABS(CHECKSUM(NEWID()) % 1000000),
TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT) * TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);



DECLARE @IntegerPartitionFunction nvarchar(max) = 
    N'CREATE PARTITION FUNCTION partition100 (tinyint) 
    AS RANGE LEFT FOR VALUES (';  
DECLARE @i int = 0;  
WHILE @i < 100
BEGIN  
SET @IntegerPartitionFunction += CAST(@i as nvarchar(10)) + N', ';  
SET @i += 1;  
END  
SET @IntegerPartitionFunction += CAST(@i as nvarchar(10)) + N');';  
EXEC sp_executesql @IntegerPartitionFunction;  
GO  

CREATE PARTITION SCHEME partition100_scheme
AS PARTITION partition100  
ALL TO ([DEFAULT]);

DROP TABLE IF EXISTS dbo.T0123_Q229892_COLUMNSTORE;

-- this table must be partitioned by PART_ID!
CREATE TABLE dbo.T0123_Q229892_COLUMNSTORE (
    PART_ID TINYINT NOT NULL,
    STRING_PIECE INT NOT NULL,
    STR_POS BIGINT NOT NULL,
    INDEX CCS CLUSTERED COLUMNSTORE
) ON partition100_scheme (PART_ID);


GO

DECLARE @part_id TINYINT = 0;
SET NOCOUNT ON;
WHILE @part_id < 100
BEGIN
    INSERT INTO dbo.T0123_Q229892_COLUMNSTORE WITH (TABLOCK)
    SELECT @part_id, STRING_PIECE, STR_POS
    FROM dbo.Q229892_RAW_1M_RANGE
    OPTION (MAXDOP 1);

    SET @part_id = @part_id + 1;
END;

GO

采用这种方法,整个数据集将需要大约10.9 TB。我不清楚如何缩小尺寸。在这种情况下,搜索查询会慢一些。在我的机器上大约需要25毫秒,但这主要取决于IO:

CHECKPOINT;
DBCC DROPCLEANBUFFERS;

SELECT MIN(STR_POS)
FROM dbo.T0123_Q229892_COLUMNSTORE
WHERE PART_ID = 45
AND STRING_PIECE = 678901; -- searching for '012345678901'

关于列存储方法的一个重要说明是10.9 TB数字是针对100%压缩数据的。在避免增量存储的同时,高效地填充此类表将具有挑战性。在此过程中的某个时刻,您最终可能会在增量存储中获得未压缩的数据,这很容易需要超过行存储方法所用的15.4 TB。


6

仅使用16GB RAM来存储和处理1TB数据可能是一个挑战。每核1GB相当不平衡,尤其是对于这种工作负载。每核8GB将是一个更好的起点,并且更加理想。

也就是说,我仍然很想尝试方法2的变体:

将所有可能的12个字符的子字符串存储bigint在集群的列存储表中(如果有用,则使用存档压缩):

CREATE TABLE dbo.Search
(
    pos bigint NOT NULL,
    fragment bigint NOT NULL,

    INDEX CCS CLUSTERED COLUMNSTORE 
        WITH (DATA_COMPRESSION = COLUMNSTORE_ARCHIVE) -- optional
);

您可能必须实现某种将源数据增量加载到该表中的方法。确保在完成的columnstore结构中最终得到最大大小的行组(1,048,576行)。请参阅数据加载指南

您可以在未索引的行存储表中以1048576的倍数分段存储行,然后在该表上创建群集的列存储索引,然后将结果直接切换到分区的主表中。确切的方法取决于您打算如何加载数据,是否将其添加到数据库以及总体上对SQL Server的熟悉程度。

使用此方法可以实现非常好的性能,但是与columnstore一样,您通常需要实现有效的分区和段消除。如上面链接的文档中所述,对fragment列进行分区并顺序构建列存储索引,同时替换键入的行存储聚簇索引fragment是实现此目的的方法。由于fragment相同范围内的值将存储在同一段中,因此这也将最小化存储需求。这样可以进行有效的值重新计算和位打包。

加载时,尝试将您在SQL Server中使用的字符串限制为非LOB(最大)类型。如果您发现使用LOB最适合吞吐量,通常会发现一个数据长度的最佳点,超过该长度,性能就会大大下降。

根据结构的最终大小和I / O子系统的速度,您可能会发现此方法始终提供足够好的性能。增加可用内存将明显改​​善性能。

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.