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:
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_executesql
porque 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 SignalValues
tabla 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 TimestampMinute
cuyo 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: Sé 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:
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 INSERT
editados y SELECT
editados. ¿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 SignalValues
tabla y la SignalValues
tabla, el objetivo de la INSERT
, no tiene registros.