¿Por qué puede tomar hasta 30 segundos crear un simple grupo de filas CCI?


20

Estaba trabajando en una demostración con CCI cuando noté que algunos de mis insertos estaban tardando más de lo esperado. Definiciones de tabla para reproducir:

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);

Para las pruebas, estoy insertando todas las filas 1048576 de la tabla de etapas. Eso es suficiente para llenar exactamente un grupo de filas comprimido siempre que no se recorte por alguna razón.

Si inserto todos los números enteros mod 17000, lleva menos de un segundo:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 17000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

Tiempos de ejecución de SQL Server: tiempo de CPU = 359 ms, tiempo transcurrido = 364 ms.

Sin embargo, si inserto los mismos números enteros mod 16000, a veces lleva más de 30 segundos:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

Tiempos de ejecución de SQL Server: tiempo de CPU = 32062 ms, tiempo transcurrido = 32511 ms.

Esta es una prueba repetible que se ha realizado en varias máquinas. Parece que hay un patrón claro en el tiempo transcurrido a medida que cambia el valor del 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

Si desea ejecutar pruebas usted mismo, puede modificar el código de prueba que escribí aquí .

No pude encontrar nada interesante en sys.dm_os_wait_stats para el mod 16000 insert:

╔════════════════════════════════════╦══════════════╗
             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 
╚════════════════════════════════════╩══════════════╝

¿Por qué el inserto de ID % 16000toma mucho más tiempo que el inserto ID % 17000?

Respuestas:


12

En muchos aspectos, este es un comportamiento esperado. Cualquier conjunto de rutinas de compresión tendrá un rendimiento muy amplio según la distribución de datos de entrada. Esperamos cambiar la velocidad de carga de datos por el tamaño de almacenamiento y el rendimiento de las consultas en tiempo de ejecución.

Hay un límite definido para la respuesta detallada que obtendrá aquí, ya que VertiPaq es una implementación patentada, y los detalles son un secreto muy bien guardado. Aun así, sabemos que VertiPaq contiene rutinas para:

  • Codificación de valor (escalado y / o traducción de valores para caber en un pequeño número de bits)
  • Codificación de diccionario (referencias enteras a valores únicos)
  • Ejecución de codificación de longitud (almacenamiento de ejecuciones de valores repetidos como pares [valor, recuento])
  • Empaquetado de bits (almacenamiento de la secuencia en la menor cantidad de bits posible)

Típicamente, los datos serán codificados por valor o diccionario, luego se aplicará RLE o empaquetado de bits (o un híbrido de RLE y empaquetado de bits utilizado en diferentes subsecciones de los datos del segmento). El proceso de decidir qué técnicas aplicar puede implicar la generación de un histograma para ayudar a determinar cómo se puede lograr el máximo ahorro de bits.

Capturando el caso lento con Windows Performance Recorder y analizando el resultado con Windows Performance Analyzer, podemos ver que la gran mayoría del tiempo de ejecución se consume al observar la agrupación de datos, construir histogramas y decidir cómo dividirlos mejor ahorros:

Análisis WPA

El procesamiento más costoso ocurre para valores que aparecen al menos 64 veces en el segmento. Esta es una heurística para determinar cuándo es probable que RLE puro sea ​​beneficioso. Los casos más rápidos dan como resultado un almacenamiento impuro , por ejemplo, una representación empaquetada en bits, con un tamaño de almacenamiento final más grande. En los casos híbridos, los valores con 64 o más repeticiones están codificados con RLE, y el resto está empaquetado en bits.

La mayor duración se produce cuando el número máximo de valores distintos con 64 repeticiones aparece en el segmento más grande posible, es decir, 1.048.576 filas con 16.384 conjuntos de valores con 64 entradas cada uno. La inspección del código revela un límite de tiempo codificado para el costoso procesamiento. Esto se puede configurar en otras implementaciones de VertiPaq, por ejemplo, SSAS, pero no en SQL Server por lo que puedo decir.

Se puede obtener una idea de la disposición de almacenamiento final utilizando el comando no documentadoDBCC CSINDEX . Esto muestra el encabezado RLE y las entradas de la matriz, los marcadores en los datos RLE y un breve resumen de los datos del paquete de bits (si corresponde).

Para más información, ver:


9

No puedo decir exactamente por qué ocurre este comportamiento, pero creo que he desarrollado un buen modelo del comportamiento a través de pruebas de fuerza bruta. Las siguientes conclusiones solo se aplican cuando se cargan datos en una sola columna y con enteros que están muy bien distribuidos.

Primero intenté variar el número de filas insertadas en el CCI usando TOP. Solía ID % 16000para todas las pruebas. A continuación se muestra un gráfico que compara las filas insertadas en el tamaño del segmento del grupo de filas comprimido:

gráfico de top vs tamaño

A continuación se muestra un gráfico de filas insertadas al tiempo de CPU en ms. Tenga en cuenta que el eje X tiene un punto de partida diferente:

top vs cpu

Podemos ver que el tamaño del segmento del grupo de filas crece a una velocidad lineal y utiliza una pequeña cantidad de CPU hasta alrededor de 1 M de filas. En ese punto, el tamaño del grupo de filas disminuye drásticamente y el uso de la CPU aumenta drásticamente. Parece que pagamos un alto precio en la CPU por esa compresión.

Al insertar menos de 1024000 filas, terminé con un grupo de filas abierto en el CCI. Sin embargo, forzar la compresión usandoREORGANIZE o REBUILDno tuvo un efecto en el tamaño. Como TOPcomentario aparte, me pareció interesante que cuando utilicé una variable para terminé con un grupo de filas abierto pero RECOMPILEcon un grupo de filas cerrado.

Luego probé variando el valor del módulo mientras mantenía el mismo número de filas. Aquí hay una muestra de los datos al insertar 102400 filas:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 TOP_VALUE  MOD_NUM  SIZE_IN_BYTES  CPU_TIME_MS 
╠═══════════╬═════════╬═══════════════╬═════════════╣
    102400     1580          13504          352 
    102400     1590          13584          316 
    102400     1600          13664          317 
    102400     1601          19624          270 
    102400     1602          25568          283 
    102400     1603          31520          286 
    102400     1604          37464          288 
    102400     1605          43408          273 
    102400     1606          49360          269 
    102400     1607          55304          265 
    102400     1608          61256          262 
    102400     1609          67200          255 
    102400     1610          73144          265 
    102400     1620         132616          132 
    102400     1621         138568          100 
    102400     1622         144512           91 
    102400     1623         150464           75 
    102400     1624         156408           60 
    102400     1625         162352           47 
    102400     1626         164712           41 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Hasta un valor de mod de 1600, el tamaño del segmento del grupo de filas aumenta linealmente en 80 bytes por cada 10 valores únicos adicionales. Es una coincidencia interesante que BIGINTtradicionalmente ocupe 8 bytes y que el tamaño del segmento aumente en 8 bytes por cada valor único adicional. Más allá de un valor de mod de 1600, el tamaño del segmento aumenta rápidamente hasta que se estabiliza.

También es útil mirar los datos al dejar el valor del módulo igual y cambiar el número de filas insertadas:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 TOP_VALUE  MOD_NUM  SIZE_IN_BYTES  CPU_TIME_MS 
╠═══════════╬═════════╬═══════════════╬═════════════╣
    300000     5000         600656          131 
    305000     5000         610664          124 
    310000     5000         620672          127 
    315000     5000         630680          132 
    320000     5000          40688         2344 
    325000     5000          40696         2577 
    330000     5000          40704         2589 
    335000     5000          40712         2673 
    340000     5000          40728         2715 
    345000     5000          40736         2744 
    350000     5000          40744         2157 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Parece que cuando el número insertado de filas <~ 64 * el número de valores únicos vemos una compresión relativamente pobre (2 bytes por fila para mod <= 65000) y un bajo uso lineal de la CPU. Cuando el número insertado de filas> ~ 64 * el número de valores únicos, vemos una compresión mucho mejor y un uso de CPU aún más lineal. Hay una transición entre los dos estados que no es fácil para mí modelar, pero se puede ver en el gráfico. No parece ser cierto que veamos el uso máximo de CPU al insertar exactamente 64 filas para cada valor único. En cambio, solo podemos insertar un máximo de 1048576 filas en un grupo de filas y vemos un uso y compresión de CPU mucho mayor una vez que hay más de 64 filas por valor único.

A continuación se muestra un diagrama de contorno de cómo cambia el tiempo de la CPU a medida que cambia el número de filas insertadas y el número de filas únicas. Podemos ver los patrones descritos anteriormente:

CPU de contorno

A continuación se muestra un diagrama de contorno del espacio utilizado por el segmento. Después de cierto punto, comenzamos a ver una compresión mucho mejor, como se describió anteriormente:

tamaño del contorno

Parece que hay al menos dos algoritmos de compresión diferentes en funcionamiento aquí. Dado lo anterior, tiene sentido que veamos el uso máximo de CPU al insertar 1048576 filas. También tiene sentido que veamos el mayor uso de CPU en ese punto al insertar alrededor de 16000 filas. 1048576/64 = 16384.

Subí todos mis datos en bruto aquí en caso de que alguien quiera analizarlos.

Vale la pena mencionar lo que sucede con los planes paralelos. Solo observé este comportamiento con valores distribuidos uniformemente. Al hacer una inserción paralela, a menudo hay un elemento de aleatoriedad y los hilos generalmente no están equilibrados.

Ponga 2097152 filas en la tabla de etapas:

DROP TABLE IF EXISTS STG_2097152;
CREATE TABLE dbo.STG_2097152 (ID BIGINT NOT NULL);
INSERT INTO dbo.STG_2097152 WITH (TABLOCK)
SELECT TOP (2097152) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Este inserto termina en menos de un segundo y tiene una compresión deficiente:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_2097152 
OPTION (MAXDOP 2);

Podemos ver el efecto de los hilos no balanceados:

╔════════════╦════════════╦══════════════╦═══════════════╗
 state_desc  total_rows  deleted_rows  size_in_bytes 
╠════════════╬════════════╬══════════════╬═══════════════╣
 OPEN             13540             0         311296 
 COMPRESSED     1048576             0        2095872 
 COMPRESSED     1035036             0        2070784 
╚════════════╩════════════╩══════════════╩═══════════════╝

Hay varios trucos que podemos hacer para forzar el equilibrio de los hilos y tener la misma distribución de filas. Aqui esta uno de ellos:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT FLOOR(0.5 * ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))  % 15999
FROM dbo.STG_2097152
OPTION (MAXDOP 2)

Elegir un número impar para el módulo es importante aquí. SQL Server escanea la tabla de etapas en serie, calcula el número de fila y luego usa la distribución por turnos para colocar las filas en subprocesos paralelos. Eso significa que terminaremos con hilos perfectamente equilibrados.

balance 1

El inserto toma alrededor de 40 segundos, que es similar al inserto en serie. Obtenemos grupos de filas bien comprimidos:

╔════════════╦════════════╦══════════════╦═══════════════╗
 state_desc  total_rows  deleted_rows  size_in_bytes 
╠════════════╬════════════╬══════════════╬═══════════════╣
 COMPRESSED     1048576             0         128568 
 COMPRESSED     1048576             0         128568 
╚════════════╩════════════╩══════════════╩═══════════════╝

Podemos obtener los mismos resultados insertando datos de la tabla de etapas original:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT t.ID % 16000 ID
FROM  (
    SELECT TOP (2) ID 
    FROM (SELECT 1 ID UNION ALL SELECT 2 ) r
) s
CROSS JOIN dbo.STG_1048576 t
OPTION (MAXDOP 2, NO_PERFORMANCE_SPOOL);

Aquí se usa la distribución round robin para la tabla derivada, spor lo que se realiza un escaneo de la tabla en cada subproceso paralelo:

equilibrado 2

En conclusión, al insertar enteros distribuidos uniformemente puede ver una compresión muy alta cuando cada entero único aparece más de 64 veces. Esto puede deberse a que se utiliza un algoritmo de compresión diferente. Puede haber un alto costo en la CPU para lograr esta compresión. Pequeños cambios en los datos pueden conducir a diferencias dramáticas en el tamaño del segmento de grupo de filas comprimido. Sospecho que ver el peor de los casos (desde la perspectiva de la CPU) será poco común en la naturaleza, al menos para este conjunto de datos. Es aún más difícil de ver cuando se realizan inserciones paralelas.


8

Creo que esto tiene que ver con las optimizaciones internas de la compresión para las tablas de una sola columna y el número mágico de los 64 KB ocupados por el diccionario.

Ejemplo: si ejecuta MOD 16600 , el resultado final del tamaño del Grupo de filas será 1.683 MB , mientras que si ejecuta MOD 17000 obtendrá un Grupo de filas con un tamaño de 2.001 MB .

Ahora, eche un vistazo a los diccionarios creados (puede usar mi biblioteca CISL para eso, necesitará la función cstore_GetDictionaries, o alternativamente ir y consultar sys.column_store_dictionaries DMV):

(MOD 16600) 61 KB

ingrese la descripción de la imagen aquí

(MOD 17000) 65 KB

ingrese la descripción de la imagen aquí

Lo gracioso, si va a agregar otra columna a su tabla, y llamémosla REALID:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, REALID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

Vuelva a cargar los datos para el MOD 16600:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16600, ID
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

Esta vez la ejecución será rápida, porque el optimizador decidirá no trabajar demasiado y comprimirla demasiado:

select column_id, segment_id, cast(sum(seg.on_disk_size) / 1024. / 1024 as Decimal(8,3) ) as SizeInMB
    from sys.column_store_segments seg
        inner join sys.partitions part
            on seg.hobt_id = part.hobt_id 
    where object_id = object_id('dbo.CCI_BIGINT')
    group by column_id, segment_id;

Aunque habrá una pequeña diferencia entre los tamaños de los Grupos de filas, será insignificante (2.000 (MOD 16600) frente a 2.001 (MOD 17000))

Para este escenario, el diccionario para el MOD 16000 será más grande que para el primer escenario con 1 columna (0.63 vs 0.61).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.