La forma más rápida de dividir / almacenar una cadena larga para la función charindex

8

Tengo una cadena de 1 TB de dígitos. Dada una secuencia de dígitos de 12 caracteres, quiero obtener la posición de inicio de esta secuencia en la cadena ( charindexfunción) original.

He probado esto con una cadena de 1GB y una subcadena de 9 dígitos usando SQL Server, almacenando la cadena como a varchar(max). Charindextoma 10 segundos Romper la cadena de 1GB en fragmentos superpuestos de 900 bytes y crear una tabla (StartPositionOfChunk, Chunkofstring) con chunkofstring en intercalación binaria, indexado toma menos de 1 segundo. El último método para 10GB, subcadena de 10 dígitos aumenta el índice de charin a 1,5 min. Me gustaría encontrar un método de almacenamiento más rápido.

Ejemplo

cadena de dígitos: 0123456789 - la subcadena para buscar 345
charindex ('345', '0123456789') da 4

Método 1 : ahora puedo almacenar esto en una tabla de SQL Server iniciable que consta de una columna colstry realizar:

select charindex('345',colstr) from strtable

Método 2 : o puedo hacer una tabla strtable2 (pos, colstr1) dividiendo la cadena original: 1; 012 | 2; 123 | 3; 234 aso y luego podemos tener la consulta

select pos from strtable2 where colstr1='345'

Método 3 : puedo hacer una tabla strtable2 (pos2, colstr2) dividiendo la cadena original en trozos más grandes 1; 01234 | 4; 34567 | 7; 6789 y luego

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

El primer método es el más lento.

¡El segundo método explota el tamaño de almacenamiento de la base de datos!

Método 3 : establecer la longitud de colstr2 en 900 bytes en intercalación binaria, crear un índice en esta columna lleva 1 segundo para la búsqueda de cadenas de 1GB y subcadenas de 9 dígitos. Para una cadena de 10 GB y una subcadena de 10 dígitos, se necesitan 90 segundos.

¿Alguna otra idea de cómo hacer esto más rápido (tal vez mediante la utilización de la cadena consta de dígitos, con enteros largos, ...)?

La búsqueda es siempre para una subcadena de 12 dígitos en una cadena de 1TB de dígitos SQL Server 2017 Developer Edition, 16 núcleos, 16GB de RAM. ¡El objetivo principal es la velocidad de búsqueda! 10 dígitos en una cadena de 10 GB (para pruebas de rendimiento).

Werner Aumayr
fuente

Respuestas:

6

Recomiendo usar una muestra del método 2 y dividir el rango de búsqueda en muchas tablas de destino. 10000 mesas es un buen primer intento. Por ejemplo, si busca "012345678901", su consulta vería la tabla asociada con los datos que comienzan con "0123". Todavía tendría aproximadamente un billón de filas en total, pero dividir los datos en muchas tablas tiene las siguientes cualidades positivas:

  1. Todas las cadenas de 12 dígitos posibles ahora pueden caber en un INT.
  2. Construir una representación más eficiente en la búsqueda de su cadena de 1 TB probablemente sea costoso, pase lo que pase. Con muchas tablas, puede paralelizar fácilmente el trabajo e incluso solicitar temporalmente que se asignen más CPU a su VM.
  3. Puede crear una sola tabla como prueba de concepto para determinar el tiempo de consulta y los requisitos de espacio total para la cadena completa.
  4. Si alguna vez necesita realizar algún tipo de mantenimiento de la base de datos, se alegrará de no haber creado una tabla enorme.

En este punto, la pregunta principal para mí es si usa el almacén de filas comprimido o el almacén de columnas. El siguiente código crea una tabla de almacén de filas para el espacio de búsqueda "0123" e inserta 100 millones de filas en él. Si su cadena es lo suficientemente aleatoria, también podría esperar ver aproximadamente 100 millones de filas por tabla.

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;

La mala noticia es que para el conjunto de datos completo probablemente necesitará alrededor de 15.4 TB. La buena noticia es que las consultas solo toman 1 ms para mí, incluso cuando no hay datos relevantes en la memoria caché del búfer, que casi siempre será el caso para un conjunto de datos tan grande como el suyo.

-- 1 ms
CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Probablemente pueda arrojar estos datos en el almacenamiento más barato que tenga y aún así ver buenos tiempos de respuesta ya que la consulta realiza muy pocas lecturas lógicas.

Para el almacén de columnas, no puede buscar los datos que necesita y aún es extremadamente improbable que guarde todos sus datos en la memoria, por lo que es importante leer la menor cantidad de datos comprimidos posible con sus consultas. Recomiendo particionar sus tablas. Un enfoque simple que funciona bien es usar los primeros cuatro dígitos de su cadena de búsqueda para encontrar el nombre de la tabla y los siguientes dos dígitos como partición. Usando "012345678901" nuevamente, iría a la partición 45 de la tabla que contiene datos para "0123". 100 particiones es un buen número para evitar problemas causados ​​por demasiadas particiones y terminará con aproximadamente 1 millón de filas para cada partición en promedio. El número máximo de filas que pueden caber en un solo grupo de filas es 1048576, por lo que con este enfoque, hará la menor cantidad de E / S posible.

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

Con este enfoque, el conjunto de datos completo requeriría aproximadamente 10,9 TB. No me queda claro cómo hacerlo más pequeño. La consulta de búsqueda es un poco más lenta en este caso. En mi máquina tarda unos 25 ms, pero esto dependerá principalmente de IO:

CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Una nota importante sobre el enfoque de almacén de columnas es que la cifra de 10,9 TB es para datos 100% comprimidos. Será un desafío poblar dicha tabla de manera eficiente mientras se evitan las tiendas delta. Es probable que termine con datos sin comprimir en almacenes delta en algún momento del proceso que fácilmente podría requerir más de los 15.4 TB utilizados para el enfoque de almacén de filas.

Joe Obbish
fuente
6

Almacenar y procesar 1 TB de datos con solo 16 GB de RAM disponible puede ser un desafío. 1 GB por núcleo está bastante desequilibrado, especialmente para este tipo de carga de trabajo. 8 GB por núcleo sería un punto de partida mucho mejor, con más deseable.

Dicho esto, aún estaría tentado a probar una variación del método 2:

Almacene todas las posibles subcadenas de 12 caracteres como biginten una tabla de almacén de columnas en clúster (con compresión de archivo si eso resulta útil):

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

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

Probablemente tendrá que implementar alguna forma de cargar de forma incremental los datos de origen en esta tabla. Asegúrese de terminar con grupos de filas de tamaño máximo (1,048,576 filas) en la estructura de almacén de columnas terminada . Consulte la guía de carga de datos .

Puede organizar filas en múltiplos de 1048576 en una tabla de almacén de filas no indexada antes de crear un índice de almacén de columnas agrupado en eso, y luego cambiar el resultado directamente a una tabla principal particionada. El enfoque exacto depende de cómo pretende cargar los datos, si se agregarán y qué tan familiarizado está con SQL Server en general.

Con este método es posible obtener un rendimiento muy bueno, pero como ocurre con frecuencia con el almacén de columnas, necesitaría lograr una eliminación efectiva de la partición y el segmento. Particionar en la fragmentcolumna y construir el índice del almacén de columnas en serie mientras se reemplaza un índice agrupado del almacén de filas con clave fragmentes la forma de lograr esto, como se señala en la documentación vinculada anteriormente. Esto también minimizará las necesidades de almacenamiento, ya que los fragmentvalores en el mismo rango se almacenarán en el mismo segmento. Esto permite un rebase de valor efectivo y un poco de empaque.

Mientras se carga, intente limitar las cadenas con las que está trabajando dentro de SQL Server a tipos que no sean LOB (max). Si encuentra que trabajar con LOB es mejor para el rendimiento, a menudo hay un punto óptimo de longitud de datos que se encuentra, por encima del cual el rendimiento disminuye significativamente.

Dependiendo del tamaño final de la estructura y la velocidad de su subsistema de E / S, es posible que este enfoque ofrezca un rendimiento suficientemente bueno. Aumentar la memoria disponible mejoraría notablemente las cosas.

Paul White 9
fuente