El martes de T-SQL de este mes está siendo presentado por Mike Fal (blog | twitter), y el tema es Trick Shots, donde estamos invitados a contarle a la comunidad sobre alguna solución que usamos en SQL Server que nos pareció, al menos a nosotros, como una especie de "tiro con truco", algo similar al uso de massé, "inglés" o tiros de banco complicados en el billar o el snooker. Después de trabajar con SQL Server durante unos 15 años, he tenido la oportunidad de idear trucos para resolver algunos problemas bastante interesantes, pero uno que parece ser bastante reutilizable, se adapta fácilmente a muchas situaciones y es simple de implementar, es algo que llamo "cambio de esquema".
Supongamos que tiene un escenario en el que tiene una gran tabla de búsqueda que necesita actualizarse periódicamente. Esta tabla de búsqueda es necesaria en muchos servidores y puede contener datos que se completan desde una fuente externa o de terceros, p. IP o datos de dominio, o puede representar datos desde dentro de su propio entorno.
El primer par de escenarios en los que necesitaba una solución para esto fue hacer que los metadatos y los datos desnormalizados estuvieran disponibles para "cachés de datos" de solo lectura, en realidad solo instancias de MSDE de SQL Server (y más tarde Express) instaladas en varios servidores web, por lo que estos datos almacenados en caché localmente en lugar de molestar al sistema OLTP principal. Esto puede parecer redundante, pero descargar la actividad de lectura fuera del sistema OLTP principal y poder eliminar la conexión de red de la ecuación por completo, condujo a un aumento real en el rendimiento general y, sobre todo, para los usuarios finales. .
Estos servidores no necesitaban copias actualizadas de los datos; de hecho, muchas de las tablas de caché solo se actualizaban diariamente. Pero dado que los sistemas funcionaban las 24 horas del día, los 7 días de la semana, y algunas de estas actualizaciones podían tardar varios minutos, a menudo se interponían en el camino de los clientes reales que hacían cosas reales en el sistema.
El(los) enfoque(s) original(es)
Al principio, el código era bastante simplista:eliminamos las filas que se habían eliminado de la fuente, actualizamos todas las filas que sabíamos que habían cambiado e insertamos todas las filas nuevas. Se parecía a esto (manejo de errores, etc. eliminado por brevedad):
BEGIN TRANSACTION; DELETE dbo.Lookup WHERE [key] NOT IN (SELECT [key] FROM [source]); UPDATE d SET [col] = s.[col] FROM dbo.Lookup AS d INNER JOIN [source] AS s ON d.[key] = s.[key] -- AND [condition to detect change]; INSERT dbo.Lookup([cols]) SELECT [cols] FROM [source] WHERE [key] NOT IN (SELECT [key] FROM dbo.Lookup); COMMIT TRANSACTION;
No hace falta decir que esta transacción podría causar algunos problemas reales de rendimiento cuando el sistema estaba en uso. Seguramente había otras formas de hacer esto, pero todos los métodos que probamos fueron igualmente lentos y costosos. ¿Qué tan lento y costoso? "Déjame contar los escaneos..."
Dado que este MERGE es anterior a la fecha, y ya habíamos descartado enfoques "externos" como DTS, a través de algunas pruebas determinamos que sería más eficiente simplemente borrar la tabla y volver a llenarla, en lugar de intentar sincronizar con la fuente. :
BEGIN TRANSACTION; TRUNCATE TABLE dbo.Lookup; INSERT dbo.Lookup([cols]) SELECT [cols] FROM [source]; COMMIT TRANSACTION;
Ahora, como expliqué, esta consulta de [fuente] podría demorar un par de minutos, especialmente si todos los servidores web se actualizaran en paralelo (tratamos de escalonar donde pudimos). Y si un cliente estaba en el sitio e intentaba ejecutar una consulta relacionada con la tabla de búsqueda, tenía que esperar a que finalizara la transacción. En la mayoría de los casos, si ejecutan esta consulta a medianoche, realmente no importaría si obtuvieron la copia de los datos de búsqueda de ayer o de hoy; por lo tanto, hacerlos esperar a que se actualizara parecía una tontería y, de hecho, generó una serie de llamadas de soporte.
Entonces, si bien esto era mejor, ciertamente estaba lejos de ser perfecto.
Mi solución inicial:sp_rename
Mi solución inicial, cuando SQL Server 2000 estaba de moda, fue crear una tabla "sombra":
CREATE TABLE dbo.Lookup_Shadow([cols]);
De esta manera, podía completar la tabla oculta sin interrumpir a los usuarios en absoluto y luego realizar un cambio de nombre de tres vías, una operación rápida solo de metadatos, solo después de que se completara el llenado. Algo como esto (de nuevo, muy simplificado):
TRUNCATE TABLE dbo.Lookup_Shadow; INSERT dbo.Lookup_Shadow([cols]) SELECT [cols] FROM [source]; BEGIN TRANSACTION; EXEC sp_rename N'dbo.Lookup', N'dbo.Lookup_Fake'; EXEC sp_rename N'dbo.Lookup_Shadow', N'dbo.Lookup'; COMMIT TRANSACTION; -- if successful: EXEC sp_rename N'dbo.Lookup_Fake', N'dbo.Lookup_Shadow';
La desventaja de este enfoque inicial fue que sp_rename tiene un mensaje de salida no suprimible que le advierte sobre los peligros de cambiar el nombre de los objetos. En nuestro caso, realizamos esta tarea a través de trabajos del Agente SQL Server y manejamos una gran cantidad de metadatos y otras tablas de caché, por lo que el historial de trabajos se inundó con todos estos mensajes inútiles y, de hecho, provocó que los errores reales se truncaran de los detalles del historial. (Me quejé de esto en 2007, pero mi sugerencia finalmente fue descartada y cerrada como "No se solucionará".)
Una mejor solución:esquemas
Una vez que actualizamos a SQL Server 2005, descubrí este fantástico comando llamado CREATE SCHEMA. Era trivial implementar el mismo tipo de solución utilizando esquemas en lugar de cambiar el nombre de las tablas, y ahora el historial del Agente no estaría contaminado con todos estos mensajes inútiles. Básicamente creé dos nuevos esquemas:
CREATE SCHEMA fake AUTHORIZATION dbo; CREATE SCHEMA shadow AUTHORIZATION dbo;
Luego moví la tabla Lookup_Shadow al esquema de caché y le cambié el nombre:
ALTER SCHEMA shadow TRANSFER dbo.Lookup_Shadow; EXEC sp_rename N'shadow.Lookup_Shadow', N'Lookup';
(Si solo está implementando esta solución, estaría creando una nueva copia de la tabla en el esquema, sin mover la tabla existente allí y renombrándola).
Con esos dos esquemas en su lugar y una copia de la tabla de búsqueda en el esquema de sombra, mi cambio de nombre de tres vías se convirtió en una transferencia de esquema de tres vías:
TRUNCATE TABLE shadow.Lookup; INSERT shadow.Lookup([cols]) SELECT [cols] FROM [source]; -- perhaps an explicit statistics update here BEGIN TRANSACTION; ALTER SCHEMA fake TRANSFER dbo.Lookup; ALTER SCHEMA dbo TRANSFER shadow.Lookup; COMMIT TRANSACTION; ALTER SCHEMA shadow TRANSFER fake.Lookup;
En este punto, por supuesto, puede vaciar la instantánea de la tabla, sin embargo, en algunos casos me resultó útil dejar la copia "antigua" de los datos para solucionar problemas:
TRUNCATE TABLE shadow.Lookup;
Cualquier otra cosa que haga con la instantánea, querrá asegurarse de hacerlo fuera de la transacción:las dos operaciones de transferencia deben ser lo más concisas y rápidas posible.
Algunas advertencias
- Claves foráneas
Esto no funcionará de inmediato si la tabla de búsqueda está referenciada por claves foráneas. En nuestro caso, no señalamos ninguna restricción en estas tablas de caché, pero si lo hace, es posible que deba seguir con métodos intrusivos como MERGE. O use métodos de solo agregar y deshabilite o elimine las claves externas antes de realizar cualquier modificación de datos (luego vuelva a crearlas o volver a habilitarlas después). Si se apega a las técnicas MERGE/UPSERT y está haciendo esto entre servidores o, peor aún, desde un sistema remoto, le recomiendo obtener los datos sin procesar localmente en lugar de intentar usar estos métodos entre servidores.
- Estadísticas
Cambiar las tablas (mediante el cambio de nombre o la transferencia de esquemas) hará que las estadísticas cambien de una a otra entre las dos copias de la tabla, y esto obviamente puede ser un problema para los planes. Por lo tanto, puede considerar agregar actualizaciones de estadísticas explícitas como parte de este proceso.
- Otros enfoques
Por supuesto, hay otras formas de hacer esto que simplemente no he tenido la oportunidad de probar. El cambio de partición y el uso de una vista + sinónimo son dos enfoques que puedo investigar en el futuro para un tratamiento más completo del tema. Me interesaría escuchar sus experiencias y cómo ha resuelto este problema en su entorno. Y sí, me doy cuenta de que este problema se resuelve en gran medida con los grupos de disponibilidad y los secundarios legibles en SQL Server 2012, pero lo considero un "truco" si puede resolver el problema sin arrojar licencias de alto nivel al problema o replicar un toda la base de datos para hacer algunas tablas redundantes. :-)
Conclusión
Si puede vivir con las limitaciones aquí, este enfoque puede tener un mejor desempeño que un escenario en el que esencialmente desconecta una tabla usando SSIS o su propia rutina MERGE / UPSERT, pero asegúrese de probar ambas técnicas. El punto más significativo es que el usuario final que acceda a la tabla debe tener exactamente la misma experiencia, en cualquier momento del día, incluso si llega a la mesa en medio de su actualización periódica.