charindex関数の長い文字列を分割/保存する最速の方法


8

1 TBの数字列があります。12文字の数字のシーケンスが与えられた場合、元の文字列(charindex関数)でこのシーケンスの開始位置を取得します。

SQL Serverを使用して1GBの文字列と9桁の部分文字列でこれをテストし、文字列をとして保存しましたvarchar(max)Charindex10秒かかります。1 GBの文字列を900バイトのオーバーラップチャンクに分割し、バイナリ照合でchunkofstringを使用してテーブル(StartPositionOfChunk、Chunkofstring)を作成すると、インデックス作成に1秒未満かかります。10GB、10桁の部分文字列の後者の方法では、charindexが1.5分に上昇します。より高速な保存方法を見つけたいのですが。

数字列:0123456789-検索する部分文字列345
charindex( '345'、 '0123456789')は4を与えます

方法1:これを、1つの列で構成されるSQL Serverテーブルstrtableに格納しcolstr、実行できます。

select charindex('345',colstr) from strtable

方法2:または、元の文字列を分割することによりテーブルstrtable2(pos、colstr1)を作成できます。2; 123 | 3; 234 asoそして、クエリを

select pos from strtable2 where colstr1='345'

方法3:元の文字列をより大きなチャンクに分割することで、テーブルstrtable2(pos2、colstr2)を作成できます1; 01234 | 4; 34567 | 7、6789、次いで

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

最初の方法が最も遅いです。

2番目の方法では、データベースのストレージサイズが大きくなります。

方法3:バイナリ照合でcolstr2の長さを900バイトに設定し、この列にインデックスを作成すると、1GBの文字列と9桁の部分文字列の検索に1秒かかります。10GBの文字列と10桁の部分文字列の場合、istには90秒かかります。

これをより速くする他のアイデア(おそらく、文字列を使用することによって、文字列は長整数の数字で構成されます...)?

検索では常に、1 TBの数字列の12桁の部分文字列が検索されます。SQLServer 2017 Developer Edition、16コア、16 GB RAM。主な目標は検索速度です!10GB文字列の10桁(パフォーマンステスト用)。

回答:


6

方法2のフレーバーを使用し、検索範囲を多数のターゲットテーブルに分割することをお勧めします。最初の試みとしては、10000テーブルが適切です。たとえば、「012345678901」を検索すると、クエリは「0123」で始まるデータに関連付けられたテーブルを調べます。合計で約1兆行になりますが、データを多くのテーブルに分割すると、次のようなメリットがあります。

  1. 可能なすべての12桁の文字列がINTに収まるようになりました。
  2. 1 TB文字列の検索効率の高い表現を作成すると、何があってもコストがかかる可能性があります。多くのテーブルを使用すると、ジョブを簡単に並列化でき、VMに割り当てるCPUを一時的に増やすこともできます。
  3. 完全な文字列のクエリ時間と合計スペース要件を決定するための概念実証として、単一のテーブルを構築できます。
  4. なんらかのデータベースメンテナンスを行う必要がある場合は、巨大なテーブルを1つも作成していなくても大丈夫です。

この時点での主な質問は、圧縮された行ストアと列ストアのどちらを使用するかです。以下のコードは、「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'

クエリで論理的な読み取りがほとんど行われないため、このデータを最も安価なストレージに投入し、応答時間を改善することができます。

列ストアの場合、必要なデータを探すことができず、すべてのデータをメモリに収めることが非常に困難であるため、クエリでできるだけ少ない圧縮データを読み取ることが重要です。テーブルを分割することを強くお勧めします。うまく機能する簡単な方法の1つは、検索文字列の最初の4桁を使用してテーブル名と次の2桁をパーティションとして見つけることです。もう一度「012345678901」を使用すると、「0123」のデータを保持するテーブルのパーティション45に移動します。100パーティションは、パーティションが多すぎるために発生する問題を回避するのに適した数であり、各パーティションの平均で約100万行になります。1つの行グループに収まる行の最大数は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'

列ストアアプローチに関する重要な注意点の1つは、10.9 TBの数値は100%圧縮されたデータに対するものであるということです。デルタストアを回避しながら、このようなテーブルに効率的にデータを投入することは困難です。プロセスのある時点で、デルタストアに圧縮されていないデータができる可能性があります。これは、行ストアアプローチに使用される15.4 TB以上を簡単に必要とする可能性があります。


6

16 GBのRAMしか利用できない状態で1 TBのデータを保存および処理することは、困難な場合があります。コアあたり1 GBは、特にこの種のワークロードの場合、かなり不均衡です。コアあたり8GBは、より望ましい出発点であり、はるかに優れています。

それでも、方法2のバリエーションを試してみたくなります。

bigintクラスター化された列ストアテーブルのように、考えられるすべての12文字の部分文字列を格納します(有用であることが判明した場合は、アーカイブ圧縮を使用します)。

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

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

おそらく、ソースデータをこのテーブルに段階的に読み込む何らかの方法を実装する必要があります。最終的な列ストア構造で最大サイズの行グループ(1,048,576行)になることを確認してください。データ読み込みのガイダンスをご覧ください。

クラスター化列ストアインデックスを作成する前に、インデックス付けされていない行ストアテーブルに1048576の倍数の行をステージングし、その結果をパーティション分割されたメインテーブルに直接切り替えることができます。正確なアプローチは、データをロードする方法、データが追加されるかどうか、およびSQL Serverの一般的な知識に依存します。

この方法では非常に優れたパフォーマンスが得られますが、列ストアの場合と同じように、パーティションとセグメントを効果的に削除する必要があります。fragment列にパーティションを作成し、キーストアされた行ストアクラスター化インデックスを置き換えながら列ストアインデックスをシリアルに構築するfragmentことは、上記のリンク先のドキュメントに記載されているように、これを実現する方法です。fragment同じ範囲の値は同じセグメントに保存されるため、これによりストレージの必要性も最小限になります。これにより、効果的な値のリベースとビットパッキングが可能になります。

ロード中は、SQL Server内で使用している文字列を非LOB(最大)型に制限してください。スループットに最適なLOBの操作を見つけた場合、データ長のスイートスポットが見つかることがよくあります。これを超えると、パフォーマンスが大幅に低下します。

構造の最終的なサイズとI / Oサブシステムの速度によっては、このアプローチが常に十分に優れたパフォーマンスを提供する場合があります。利用可能なメモリを増やすと、物事が著しく改善されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.