A maneira mais rápida de dividir / armazenar uma string longa para a função charindex

8

Eu tenho uma sequência de dígitos de 1 TB. Dada uma sequência de dígitos de 12 caracteres, quero obter a posição inicial dessa sequência na string original ( charindexfunção).

Eu testei isso com uma string de 1 GB e uma substring de 9 dígitos usando o SQL Server, armazenando a string como a varchar(max). Charindexleva 10 segundos. Quebrando a sequência de 1 GB em pedaços sobrepostos de 900 bytes e criando uma tabela (StartPositionOfChunk, Chunkofstring) com chunkofstring em agrupamento binário, o tempo de indexação é de menos de 1 segundo. O método mais recente para substratos de 10 GB e 10 dígitos aumenta o índice para 1,5 min. Gostaria de encontrar um método de armazenamento mais rápido.

Exemplo

sequência de dígitos: 0123456789 - a substring para pesquisar 345
charindex ('345', '0123456789') fornece 4

Método 1 : Agora eu posso armazenar isso em uma tabela do SQL Server que consiste em uma coluna colstre executar:

select charindex('345',colstr) from strtable

Método 2 : ou eu posso criar uma tabela strtable2 (pos, colstr1) dividindo a seqüência original: 1; 012 | 2; 123 | 3; 234 aso e, em seguida, podemos ter a consulta

select pos from strtable2 where colstr1='345'

Método 3 : Eu posso criar uma tabela strtable2 (pos2, colstr2) dividindo a string original em pedaços maiores 1; 01234 | 4; 34567 | 7; 6789 e depois

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

O primeiro método é o mais lento.

O segundo método explode o tamanho do armazenamento do banco de dados!

Método 3 : Definir o comprimento de colstr2 para 900 bytes no agrupamento binário, criando um índice nesta coluna leva 1 segundo para pesquisa de seqüência de caracteres de 1 GB e substring de 9 dígitos. Para string de 10 GB e substring de 10 dígitos, leva 90 segundos.

Alguma outra idéia de como tornar isso mais rápido (talvez utilizando a string consiste em Digits, com inteiros longos, ....)?

A pesquisa é sempre para uma substring de 12 dígitos em uma seqüência de caracteres de 1 TB do SQL Server 2017 Developer Edition, 16 núcleos e 16 GB de RAM. O objetivo principal é a velocidade de pesquisa! 10 dígitos em uma sequência de 10 GB (para teste de desempenho).

Werner Aumayr
fonte

Respostas:

6

Eu recomendo usar o método 2 e dividir o intervalo de pesquisa em várias tabelas de destino. 10000 tabelas é uma boa primeira tentativa. Por exemplo, se você procurar "012345678901", sua consulta examinará a tabela associada aos dados que começam com "0123". Você ainda teria cerca de um trilhão de linhas no total, mas dividir os dados em muitas tabelas tem as seguintes qualidades positivas:

  1. Todas as seqüências de 12 dígitos possíveis agora podem caber em uma INT.
  2. Construir uma representação mais eficiente da pesquisa de sua cadeia de 1 TB provavelmente será caro, não importa o quê. Com muitas tabelas, você pode facilmente paralelizar o trabalho e até solicitar temporariamente que mais CPUs sejam alocadas à sua VM.
  3. Você pode criar uma única tabela como prova de conceito para determinar o tempo de consulta e os requisitos de espaço total para a sequência completa.
  4. Se você precisar fazer algum tipo de manutenção no banco de dados, ficará feliz por não ter criado uma tabela enorme.

Neste ponto, a principal questão para mim é se você usa o rowstore compactado ou o columnstore. O código abaixo cria uma tabela rowstore para o espaço de pesquisa "0123" e insere 100 milhões de linhas nele. Se sua sequência for aleatória o suficiente, você também poderá ver cerca de 100 milhões de linhas por tabela.

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;

A má notícia é que, para o conjunto de dados completo, você provavelmente precisará de cerca de 15,4 TB. A boa notícia é que as consultas levam apenas 1 ms para mim, mesmo quando não há dados relevantes no cache do buffer, o que quase sempre será o caso de um conjunto de dados tão grande quanto o seu.

-- 1 ms
CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Provavelmente, você pode lançar esses dados no armazenamento mais barato que você possui e ainda obter bons tempos de resposta, pois a consulta faz poucas leituras lógicas.

Para o columnstore, você não pode procurar os dados necessários e ainda é extremamente improvável que encaixe todos os dados na memória; portanto, é importante ler o mínimo possível de dados compactados com suas consultas. Eu recomendo particionar suas tabelas. Uma abordagem simples que funciona bem é usar os quatro primeiros dígitos da sua sequência de pesquisa para encontrar o nome da tabela e os próximos dois dígitos como a partição. Usando "012345678901" novamente, você iria para a partição 45 da tabela que contém dados para "0123". 100 partições é um bom número para evitar problemas causados ​​por muitas partições e você terá, em média, cerca de 1 milhão de linhas para cada partição. O número máximo de linhas que podem caber em um único grupo de linhas é 1048576, portanto, com essa abordagem, você fará o mínimo de IO possível.

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

Com essa abordagem, o conjunto completo de dados exigiria cerca de 10,9 TB. Não está claro para mim como fazer isso menor. A consulta de pesquisa é um pouco mais lenta nesse caso. Na minha máquina, são necessários cerca de 25 ms, mas isso depende principalmente do IO:

CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Uma observação importante sobre a abordagem columnstore é que o valor de 10,9 TB refere-se a 100% de dados compactados. Será um desafio preencher essa tabela com eficiência, evitando as lojas delta. É provável que você acabe com dados não compactados em lojas delta em algum momento do processo, o que poderia facilmente exigir mais do que os 15,4 TB usados ​​na abordagem de armazenamento de linhas.

Joe Obbish
fonte
6

Armazenar e processar 1 TB de dados com apenas 16 GB de RAM disponível pode ser um desafio. 1 GB por núcleo é bastante desequilibrado, especialmente para esse tipo de carga de trabalho. 8 GB por núcleo seria um ponto de partida muito melhor, com mais desejável.

Dito isto, eu ainda ficaria tentado a experimentar uma variação do método 2:

Armazene todas as possíveis substrings de 12 caracteres, como bigintem uma tabela columnstore clusterizada (com compactação de archive, se isso for útil):

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

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

Você provavelmente terá que implementar alguma maneira de carregar de forma incremental os dados de origem nessa tabela. Certifique-se de terminar com grupos de linhas de tamanho máximo (1.048.576 linhas) na estrutura de armazenamento de colunas concluída . Consulte as orientações de carregamento de dados .

Você pode preparar linhas em múltiplos de 1048576 em uma tabela de rowstore não indexada antes de criar um índice columnstore clusterizado e, em seguida, alternar o resultado diretamente para uma tabela principal particionada. A abordagem exata depende de como você pretende carregar os dados, se eles serão adicionados e como você está familiarizado com o SQL Server em geral.

É possível um desempenho muito bom com esse método, mas como tantas vezes com o columnstore, você precisaria obter uma eliminação eficaz da partição e do segmento. Particionar na fragmentcoluna e criar o índice columnstore em série enquanto substitui um índice clusterizado de rowstore com chave fragmenté a maneira de conseguir isso, conforme observado na documentação vinculada acima. Isso também minimizará as necessidades de armazenamento, pois fragmentvalores no mesmo intervalo serão armazenados no mesmo segmento. Isso permite rebasing de valor efetivo e empacotamento de bits.

Durante o carregamento, tente limitar as seqüências com as quais você está trabalhando no SQL Server para tipos não-LOB (máx.). Se você acha que trabalhar com LOBs é melhor para taxa de transferência, geralmente há um ponto ideal para o comprimento dos dados, acima do qual o desempenho diminui significativamente.

Dependendo do tamanho final da estrutura e da velocidade do seu subsistema de E / S, você pode achar que essa abordagem oferece desempenho suficientemente bom o suficiente. Aumentar a memória disponível melhoraria bastante as coisas.

Paul White 9
fonte