Enormes datos y rendimiento en SQL Server

20

He escrito una aplicación con un servidor SQL Server que recopila y almacena una gran cantidad de registros. He calculado que, en el pico, la cantidad promedio de registros está en algún lugar en la avenida de 3-4 mil millones por día (20 horas de operación).

Mi solución original (antes de hacer el cálculo real de los datos) era que mi aplicación insertara registros en la misma tabla que mis clientes consultaban. Eso se estrelló y se quemó bastante rápido, obviamente, porque es imposible consultar una tabla que tiene tantos registros insertados.

Mi segunda solución fue usar 2 bases de datos, una para los datos recibidos por la aplicación y otra para los datos listos para el cliente.

Mi aplicación recibiría datos, los dividiría en lotes de ~ 100k registros y los insertaría en masa en la tabla de etapas. Después de ~ 100k registros, la aplicación crearía sobre la marcha otra tabla de etapas con el mismo esquema que antes y comenzaría a insertarse en esa tabla. Crearía un registro en una tabla de trabajos con el nombre de la tabla que tiene 100k registros y un procedimiento almacenado en el lado del Servidor SQL movería los datos de la (s) tabla (s) provisional a la tabla de producción lista para el cliente, y luego soltaría el tabla tabla temporal creada por mi aplicación.

Ambas bases de datos tienen el mismo conjunto de 5 tablas con el mismo esquema, excepto la base de datos provisional que tiene la tabla de trabajos. La base de datos provisional no tiene restricciones de integridad, clave, índices, etc. en la tabla donde residirá la mayor parte de los registros. A continuación se muestra el nombre de la tabla SignalValues_staging. El objetivo era que mi aplicación bloqueara los datos en SQL Server lo más rápido posible. El flujo de trabajo de crear tablas sobre la marcha para que puedan migrarse fácilmente funciona bastante bien.

Las siguientes son las 5 tablas relevantes de mi base de datos provisional, más mi tabla de trabajos:

Tablas de montaje El procedimiento almacenado que he escrito maneja el movimiento de los datos de todas las tablas de ensayo y su inserción en producción. A continuación se muestra la parte de mi procedimiento almacenado que se inserta en la producción desde las tablas de preparación:

-- Signalvalues jobs table.
SELECT *
      ,ROW_NUMBER() OVER (ORDER BY JobId) AS 'RowIndex'
INTO #JobsToProcess
FROM 
(
    SELECT JobId 
           ,ProcessingComplete  
           ,SignalValueStagingTableName AS 'TableName'
           ,(DATEDIFF(SECOND, (SELECT last_user_update
                              FROM sys.dm_db_index_usage_stats
                              WHERE database_id = DB_ID(DB_NAME())
                                AND OBJECT_ID = OBJECT_ID(SignalValueStagingTableName))
                     ,GETUTCDATE())) SecondsSinceLastUpdate
    FROM SignalValueJobs
) cte
WHERE cte.ProcessingComplete = 1
   OR cte.SecondsSinceLastUpdate >= 120

DECLARE @i INT = (SELECT COUNT(*) FROM #JobsToProcess)

DECLARE @jobParam UNIQUEIDENTIFIER
DECLARE @currentTable NVARCHAR(128) 
DECLARE @processingParam BIT
DECLARE @sqlStatement NVARCHAR(2048)
DECLARE @paramDefinitions NVARCHAR(500) = N'@currentJob UNIQUEIDENTIFIER, @processingComplete BIT'
DECLARE @qualifiedTableName NVARCHAR(128)

WHILE @i > 0
BEGIN

    SELECT @jobParam = JobId, @currentTable = TableName, @processingParam = ProcessingComplete
    FROM #JobsToProcess 
    WHERE RowIndex = @i 

    SET @qualifiedTableName = '[Database_Staging].[dbo].['+@currentTable+']'

    SET @sqlStatement = N'

        --Signal values staging table.
        SELECT svs.* INTO #sValues
        FROM '+ @qualifiedTableName +' svs
        INNER JOIN SignalMetaData smd
            ON smd.SignalId = svs.SignalId  


        INSERT INTO SignalValues SELECT * FROM #sValues

        SELECT DISTINCT SignalId INTO #uniqueIdentifiers FROM #sValues

        DELETE c FROM '+ @qualifiedTableName +' c INNER JOIN #uniqueIdentifiers u ON c.SignalId = u.SignalId

        DROP TABLE #sValues
        DROP TABLE #uniqueIdentifiers

        IF NOT EXISTS (SELECT TOP 1 1 FROM '+ @qualifiedTableName +') --table is empty
        BEGIN
            -- processing is completed so drop the table and remvoe the entry
            IF @processingComplete = 1 
            BEGIN 
                DELETE FROM SignalValueJobs WHERE JobId = @currentJob

                IF '''+@currentTable+''' <> ''SignalValues_staging''
                BEGIN
                    DROP TABLE '+ @qualifiedTableName +'
                END
            END
        END 
    '

    EXEC sp_executesql @sqlStatement, @paramDefinitions, @currentJob = @jobParam, @processingComplete = @processingParam;

    SET @i = @i - 1
END

DROP TABLE #JobsToProcess

Lo uso sp_executesqlporque los nombres de las tablas para las tablas de preparación vienen como texto de los registros en la tabla de trabajos.

Este procedimiento almacenado se ejecuta cada 2 segundos utilizando el truco que aprendí de esta publicación de dba.stackexchange.com .

El problema que no puedo resolver durante toda mi vida es la velocidad a la que se realizan las inserciones en la producción. Mi aplicación crea tablas temporales y las llena de registros increíblemente rápido. La inserción en producción no puede mantenerse al día con la cantidad de tablas y eventualmente hay un excedente de tablas en miles. La única forma en que he podido mantenerme al día con los datos entrantes es eliminar todas las claves, índices, restricciones, etc. de la SignalValuestabla de producción . El problema que enfrento es que la tabla termina con tantos registros que resulta imposible consultar.

He intentado particionar la tabla usando la [Timestamp]columna como partición en vano. Cualquier forma de indexación ralentiza tanto las inserciones que no pueden seguir el ritmo. Además, necesitaría crear miles de particiones (una por minuto, ¿hora?) Con años de anticipación. No pude descubrir cómo crearlos sobre la marcha

He intentado crear la partición mediante la adición de una columna calculada a la tabla llamada TimestampMinutecuyo valor era, en INSERT, DATEPART(MINUTE, GETUTCDATE()). Todavía muy lento.

Intenté convertirlo en una tabla con memoria optimizada según este artículo de Microsoft . Tal vez no entiendo cómo hacerlo, pero el MOT hizo que los insertos fueran más lentos de alguna manera.

Revisé el plan de ejecución del procedimiento almacenado y descubrí que (¿creo?) La operación más intensiva es

SELECT svs.* INTO #sValues
FROM '+ @qualifiedTableName +' svs
INNER JOIN SignalMetaData smd
    ON smd.SignalId = svs.SignalId

Para mí esto no tiene sentido: he agregado el registro de reloj de pared al procedimiento almacenado que demostró lo contrario.

En términos de registro de tiempo, esa declaración particular anterior se ejecuta en ~ 300 ms en 100k registros.

La declaración

INSERT INTO SignalValues SELECT * FROM #sValues

Se ejecuta en 2500-3000ms en 100k registros. Eliminar de la tabla los registros afectados, por:

DELETE c FROM '+ @qualifiedTableName +' c INNER JOIN #uniqueIdentifiers u ON c.SignalId = u.SignalId

Toma otros 300ms.

¿Cómo puedo hacer esto más rápido? ¿Puede SQL Server manejar miles de millones de registros por día?

Si es relevante, este es SQL Server 2014 Enterprise x64.

Configuración de hardware:

Olvidé incluir hardware en el primer paso de esta pregunta. Culpa mía.

Prefacio esto con estas declaraciones: que estoy perdiendo algo de rendimiento debido a mi configuración de hardware. Lo he intentado muchas veces, pero debido al presupuesto, el nivel C, la alineación de los planetas, etc., desafortunadamente no hay nada que pueda hacer para obtener una mejor configuración. El servidor se ejecuta en una máquina virtual y ni siquiera puedo aumentar la memoria porque simplemente no tenemos más.

Aquí está la información de mi sistema:

Información del sistema

El almacenamiento está conectado al servidor VM a través de la interfaz iSCSI a una caja NAS (esto degradará el rendimiento). La caja NAS tiene 4 unidades en una configuración RAID 10. Son unidades de disco giratorio de 4 TB WD WD4000FYYZ con interfaz SATA de 6 GB / s. El servidor solo tiene un almacén de datos configurado, por lo que tempdb y mi base de datos están en el mismo almacén de datos.

Max DOP es cero. ¿Debo cambiar esto a un valor constante o simplemente dejar que SQL Server lo maneje? Leí sobre RCSI: ¿Estoy en lo cierto al suponer que el único beneficio de RCSI viene con las actualizaciones de fila? Nunca habrá actualizaciones para ninguno de estos registros particulares, serán INSERTeditados y SELECTeditados. ¿RCSI todavía me beneficiará?

Mi tempdb es de 8mb. Basado en la respuesta a continuación de jyao, cambié los #sValues ​​a una tabla regular para evitar tempdb por completo. Sin embargo, el rendimiento fue casi el mismo. Intentaré aumentar el tamaño y el crecimiento de tempdb, pero dado que el tamaño de #sValues ​​será más o menos siempre el mismo tamaño, no preveo mucha ganancia.

He tomado un plan de ejecución que adjunto a continuación. Este plan de ejecución es una iteración de una tabla de etapas: 100k registros. La ejecución de la consulta fue bastante rápida, alrededor de 2 segundos, pero tenga en cuenta que esto no tiene índices en la SignalValuestabla y la SignalValuestabla, el objetivo de la INSERT, no tiene registros.

Plan de ejecución

Brandon
fuente
3
¿Ya experimentaste con durabilidad retrasada?
Martin Smith
2
¿Qué índices estaban en su lugar con inserciones de producción lenta?
paparazzo
Hasta ahora, no creo que haya suficientes datos aquí para descubrir qué es lo que realmente consume tanto tiempo. ¿Es CPU? ¿Es IO? Como parece que obtienes 30k filas por segundo, no me parece que sea IO. ¿Entiendo bien que estás muy cerca de lograr tu objetivo de rendimiento? Necesita 50k filas por segundo, por lo que un lote de 100k cada 2 segundos debería ser suficiente. En este momento, un lote parece tomar 3 segundos. Publique el plan de ejecución real de una ejecución representativa. Cualquier sugerencia que no ataque las operaciones que consumen más tiempo es discutible.
usr
He publicado el plan de ejecución.
Brandon

Respuestas:

7

He calculado que, en el pico, la cantidad promedio de registros está en algún lugar en la avenida de 3-4 mil millones por día (20 horas de operación).

De su captura de pantalla, SOLO tiene 8 GB de memoria RAM total y 6 GB asignados a SQL Server. Esto es demasiado bajo para lo que está tratando de lograr.

Le sugiero que actualice la memoria a un valor más alto: 256 GB y aumente también sus CPU VM.

En este punto, debe invertir en hardware para su carga de trabajo.

Consulte también la guía de rendimiento de carga de datos : describe formas inteligentes de cargar eficientemente los datos.

Mi tempdb es de 8mb.

Según su edición ... debe tener un tempdb sensible, preferiblemente varios archivos de datos tempdb del mismo tamaño junto con TF 1117 y 1118 habilitados para toda la instancia.

Le sugiero que obtenga un chequeo de salud profesional y comience desde allí.

Altamente recomendado

  1. Aumente las especificaciones de su servidor.

  2. Pídale a una persona profesional * que realice un control de salud de la instancia del servidor de su base de datos y siga las recomendaciones.

  3. Una vez. y B. terminados, luego sumérgete en el ajuste de consultas y otras optimizaciones como mirar estadísticas de espera, planes de consulta, etc.

Nota: Soy un experto profesional en servidores sql en hackhands.com , una compañía plural, pero de ninguna manera le sugiero que me contrate para obtener ayuda. Simplemente le sugiero que busque ayuda profesional basada solo en sus ediciones.

HTH.

Kin Shah
fuente
Estoy tratando de armar una propuesta (léase: rogando) más hardware para esto. Con eso en mente y su respuesta aquí, ¿no hay nada más desde la configuración de SQL Server o el punto de vista de optimización de consultas que sugiera para hacer esto más rápido?
Brandon
1

Consejos generales para tales problemas con big-data, cuando se enfrenta a una pared y nada funciona:

Un huevo se va a cocinar 5 minutos aproximadamente. Se cocinarán 10 huevos al mismo tiempo si hay suficiente electricidad y agua.

O, en otras palabras:

Primero, mira el hardware; segundo, separe la lógica del proceso (remodelación de datos) y hágalo en paralelo.

Es muy posible crear particiones verticales personalizadas de forma dinámica y automatizada, por cuenta de tabla y por tamaño de tabla; Si tengo Quarter_1_2017, Quarter_2_2017, Quarter_3_2017, Quarter_4_2017, Quarter_1_2018 ... y no sé dónde están mis registros y cuántas particiones tengo, ejecute las mismas consultas en todas las particiones personalizadas al mismo tiempo, sesiones separadas y ensamblaje El resultado se procesará para mi lógica.

Goran Mancevski
fuente
El problema del OP parece ser manejar la inserción y el acceso a los datos recién ingresados, más que procesar datos de semanas o meses atrás. OP menciona la partición de datos por minuto en su marca de tiempo (60 particiones, dividiendo los datos actuales en cubos separados); dividir por trimestre no sería una gran ayuda. Su punto está bien tomado en general, pero es poco probable que ayude a alguien en esta situación específica.
RDFozz
-1

Haré la siguiente verificación / optimización:

  1. Asegúrese de que los archivos de datos y registros de la base de datos de producción no crezcan durante la operación de inserción (pre-crezca si es necesario)

  2. No utilice

    select * into [dest table] from [source table];

    pero en su lugar, predefinir la [tabla dest]. Además, en lugar de soltar la [tabla dest] y volver a crearla, truncaré la tabla. De esta manera, si fuera necesario, en lugar de usar la tabla temporal, usaría la tabla regular. (También puedo crear el índice en [tabla dest] para facilitar el rendimiento de la consulta de combinación)

  3. En lugar de usar sql dinámico, prefiero usar nombres de tabla codificados con alguna lógica de codificación para elegir qué tabla operar.

  4. También supervisaré el rendimiento de E / S de la memoria, la CPU y el disco para ver si hay inanición de recursos durante una gran carga de trabajo.

  5. Como mencionó que puede manejar la inserción soltando los índices en el lado de producción, comprobaría si hay muchas divisiones de página, de ser así, disminuiría el factor de relleno de los índices y reconstruiría los índices antes de considerar la posibilidad de soltarlos. Los índices.

Buena suerte y me encanta tu pregunta.

jyao
fuente
Gracias por la respuesta. Establecí el tamaño de la base de datos en 1 gb y crecí en 1 gb anticipando que las operaciones de crecimiento tomarían algo de tiempo, eso ayudó con la velocidad inicialmente. Intentaré implementar el pre-crecimiento hoy. Implementé la tabla [dest] como una tabla normal pero no vi mucho aumento de rendimiento. No he tenido mucho tiempo en los últimos días, pero trataré de llegar a los otros hoy.
Brandon