Resumen interactivo del libro de Peter A. Carter (Manning, 2025)
para desarrolladores, DBA y arquitectos de datos.
Si alguna vez has escuchado "es solo una base de datos, ¿no?", prepárate: esa mentalidad es la raíz del "Error #0" que origina casi todos los problemas en entornos SQL Server. En su obra 100 SQL Server Mistakes and How to Avoid Them, Peter A. Carter recopila dos décadas de experiencia real trabajando en proyectos FTSE 100 en Londres, exponiendo los tropiezos más comunes que cometen desde desarrolladores hasta DBA accidentales.
Este resumen interactivo está diseñado para que, en menos de 15 minutos, absorbas los pilares fundamentales del libro y puedas aplicarlos inmediatamente. Lo organizamos en 4 ejes estratégicos:
El código bien escrito y un esquema bien pensado son la base de cualquier sistema escalable. Carter insiste en que la arquitectura y los estándares no son opcionales, incluso en metodologías ágiles donde la prisa por entregar suele sacrificar calidad.
sp_El prefijo sp_ está reservado para procedimientos del sistema. Cuando llamas a un procedimiento con ese prefijo, SQL Server busca primero en la base de datos master, generando latencia innecesaria y, peor aún, ejecutando el procedimiento equivocado si existe colisión de nombres.
-- ❌ SQL Server buscará primero en master
CREATE PROCEDURE sp_addUser
@UserDetails XML
AS
BEGIN
INSERT INTO dbo.customers (...)
VALUES (...);
END;
-- ✅ Sin prefijo reservado, claro y rápido
CREATE PROCEDURE dbo.AddCustomer
@UserDetails XML
AS
BEGIN
INSERT INTO dbo.customers (...)
VALUES (...);
END;
Usar INT o NVARCHAR(MAX) "por defecto" desperdicia memoria y degrada el rendimiento del buffer pool. En tablas con millones de filas la diferencia es gigabytes de RAM y disco.
-- ❌ Antipatrón: todo NVARCHAR(MAX), todo INT
CREATE TABLE dbo.Employees (
EmployeeID NVARCHAR(MAX) NOT NULL,
FirstName NVARCHAR(MAX) NOT NULL,
Salary NVARCHAR(MAX) NOT NULL,
IsActive NVARCHAR(MAX) NOT NULL -- "0" o "1" como texto 😱
);
-- ✅ Tipos ajustados al dominio real
CREATE TABLE dbo.Employees (
EmployeeID SMALLINT NOT NULL PRIMARY KEY, -- hasta 32K empleados
FirstName NVARCHAR(32) NOT NULL,
LastName NVARCHAR(32) NOT NULL,
DateOfBirth DATE NOT NULL,
Salary MONEY NOT NULL,
DepartmentCode NCHAR(2) NOT NULL, -- siempre 2 chars
IsActive BIT NOT NULL -- 8 BITs = 1 byte
);
TINYINT (0-255), SMALLINT (±32K), INT (±2.1B) o BIGINT según el rango real. Para textos de longitud fija (códigos ISO, country codes), prefiere CHAR/NCHAR: ahorras 2 bytes por fila del column offset array.El tipo HIERARCHYID incluye métodos nativos que simplifican enormemente la lógica jerárquica (organigramas, categorías de productos, BOMs).
-- ✅ Encontrar todos los reportes directos e indirectos de un manager
DECLARE @Manager HIERARCHYID;
SELECT @Manager = ManagerHierarchyID
FROM dbo.Employees
WHERE EmployeeID = 2;
SELECT EmployeeID, FirstName, LastName, ManagerHierarchyID.ToString()
FROM dbo.Employees
WHERE ManagerHierarchyID.IsDescendantOf(@Manager) = 1;
-- ✅ Encontrar al jefe del jefe (escalación)
SELECT FirstName, LastName, Role
FROM dbo.Employees
WHERE ManagerHierarchyID = (
SELECT ManagerHierarchyID.GetAncestor(2)
FROM dbo.Employees
WHERE EmployeeID = 22
);
NULLSQL Server usa lógica de tres valores. NULL significa "desconocido", por lo que NULL = NULL devuelve UNKNOWN, no TRUE.
SELECT COUNT(*)
FROM dbo.ProductSubcategories
WHERE Description = NULL;
-- Resultado: 0 (siempre)
SELECT COUNT(*)
FROM dbo.ProductSubcategories
WHERE Description <> NULL;
-- Resultado: 0 (también)
SELECT COUNT(*)
FROM dbo.ProductSubcategories
WHERE Description IS NULL;
SELECT COUNT(*)
FROM dbo.ProductSubcategories
WHERE Description IS NOT NULL;
-- Para reemplazar NULL en proyecciones:
SELECT ISNULL(Description, 'Sin descripción')
FROM dbo.ProductSubcategories;
"El tipo de dato es la primera línea de defensa para la integridad. UnBITpara estados booleanos o unDATEpara fechas no es solo ahorro de espacio: es documentación implícita."
La infraestructura define los límites del rendimiento y la estabilidad. Muchos errores nacen de instalaciones manuales, configuraciones por defecto y falta de automatización.
Cada servicio (SSAS, SSRS, PolyBase, Master Data Services) consume RAM/CPU y aumenta la superficie de ataque. Solo instala lo que realmente vas a usar.
# ✅ Instalación silenciosa solo con Database Engine
.\setup.exe /ACTION=Install `
/FEATURES=SQLENGINE `
/INSTANCENAME=MSSQLSERVER `
/SQLSVCACCOUNT="DOMAIN\sqlservice" `
/SQLSVCPASSWORD="********" `
/SQLSYSADMINACCOUNTS="DOMAIN\sqladmins" `
/SECURITYMODE=SQL `
/SAPWD="********" `
/PID="22222-00000-00000-00000-00000" `
/IACCEPTSQLSERVERLICENSETERMS `
/Q
# Después: deshabilitar acceso remoto y renombrar 'sa'
Invoke-Sqlcmd -Query "EXEC sp_configure 'remote access', 0; RECONFIGURE;"
Invoke-Sqlcmd -Query "ALTER LOGIN sa WITH NAME = SQLAdmin;"
AUTO_SHRINKGenera fragmentación cercana al 99% en todos los índices y bloquea constantemente. El shrink manual solo debe usarse en casos excepcionales, seguido obligatoriamente de un rebuild de índices.
-- ❌ Ver si tienes AUTO_SHRINK activado (¡no debería!)
SELECT name, is_auto_shrink_on
FROM sys.databases
WHERE is_auto_shrink_on = 1;
-- ✅ Desactivarlo en todas las BBDD
ALTER DATABASE Marketing SET AUTO_SHRINK OFF WITH NO_WAIT;
-- Si DEBES hacer shrink manual (caso excepcional):
DBCC SHRINKFILE('MarketingArchive', 838) WITH WAIT_AT_LOW_PRIORITY;
-- ⚠️ Y SIEMPRE reconstruir índices después:
EXEC sp_msforeachtable 'ALTER INDEX ALL ON ? REBUILD';
Es mucho más doloroso enterarse de la corrupción a las 3 AM por un usuario que en una revisión proactiva. Automatiza DBCC CHECKDB semanalmente.
-- ✅ Verificación completa de integridad
DBCC CHECKDB('MarketingArchive') WITH NO_INFOMSGS, ALL_ERRORMSGS;
-- ✅ Verificación más rápida (solo estructura física)
DBCC CHECKDB('MarketingArchive') WITH PHYSICAL_ONLY;
-- ⚠️ ÚLTIMO RECURSO si no hay backups (puede haber pérdida de datos)
ALTER DATABASE MarketingArchive SET SINGLE_USER;
DBCC CHECKDB('MarketingArchive', REPAIR_ALLOW_DATA_LOSS);
ALTER DATABASE MarketingArchive SET MULTI_USER;
"Es 24/7, no podemos parar" no es excusa. Usa Availability Groups para hacer rolling updates con apenas segundos de interrupción durante el failover.
-- ✅ Rolling patch con Availability Groups:
-- 1. Patcha el secundario (sin afectar producción)
-- 2. Failover manual al secundario
ALTER AVAILABILITY GROUP [AG_Marketing] FAILOVER;
-- 3. Patcha el antiguo primario (ahora secundario)
-- 4. Failover de vuelta si lo deseas
-- Resultado: ~30 segundos de "blip" en lugar de 2h de downtime
-- ❌ Default: crece en 64 MB cada vez (fragmentación masiva)
-- ✅ Configurar crecimiento predecible según uso real
ALTER DATABASE MarketingArchive
MODIFY FILE (
NAME = 'MarketingArchive',
SIZE = 200GB, -- Pre-dimensionar
FILEGROWTH = 5GB -- Crecimientos grandes y predecibles
);
-- Para múltiples archivos del filegroup, crecer todos juntos
ALTER DATABASE MarketingArchive
MODIFY FILEGROUP [PRIMARY] AUTOGROW_ALL_FILES;
AUTOGROW_ALL_FILES y MIXED_PAGE_ALLOCATION en su lugar. Si los ves en tu instancia, ¡quítalos!"Tirar más hardware" es la solución más cara y temporal. La optimización inteligente reduce costos, mejora la experiencia y alarga la vida útil de tu infraestructura.
SELECT *: el enemigo silenciosoConsume red innecesaria, rompe índices covering y crea dependencias frágiles. Solo úsalo en queries ad-hoc o exploratorias, nunca en código de producción.
-- Trae 60+ bytes/fila cuando solo necesitas 9
SELECT *
FROM dbo.SalesOrderHeaders
WHERE SalesOrderDate = '2025-06-16';
-- ✅ Crea índice covering
CREATE NONCLUSTERED INDEX IX_OrderDate_DueActual
ON dbo.SalesOrderHeaders (SalesOrderDate)
INCLUDE (DeliveryDueDate, DeliveryActualDate);
-- ✅ Solo las columnas necesarias
SELECT
SalesOrderDate,
DeliveryDueDate,
DeliveryActualDate
FROM dbo.SalesOrderHeaders
WHERE SalesOrderDate = '2025-06-16';
NOLOCK vs concurrencia optimistaNOLOCK permite dirty reads: puedes leer datos que nunca llegaron a confirmarse. Es como ver el saldo de tu cuenta justo cuando un retiro está siendo revertido.
NOLOCK mientras un admin actualiza una dirección. El admin hace ROLLBACK, pero la vendedora ya envió el pedido a una dirección que nunca existió.-- ✅ Mejor solución: Read Committed Snapshot
ALTER DATABASE Marketing
SET READ_COMMITTED_SNAPSHOT ON WITH NO_WAIT;
-- ✅ O para casos específicos: Snapshot Isolation
ALTER DATABASE Marketing SET ALLOW_SNAPSHOT_ISOLATION ON;
-- En la sesión:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRAN;
SELECT * FROM dbo.Addresses WHERE CustomerID = @id;
-- Lectura consistente sin bloqueos ni dirty reads
COMMIT;
PIVOTCaso real del libro: un proceso ETL con cursor que tardaba 3 horas se redujo a 48 segundos al reescribirlo con PIVOT.
-- ✅ PIVOT en una sola consulta set-based
SELECT
[Raw Ingredients],
[Machine Parts],
[Misc],
[Confectionary Products],
[Non-confectionary Products]
FROM (
SELECT
pc.ProductCategoryName,
ISNULL(p.ProductStockLevel, 0) AS Stock
FROM dbo.ProductCategories pc
INNER JOIN dbo.ProductSubcategories ps
ON pc.ProductCategoryID = ps.ProductCategoryID
LEFT JOIN dbo.Products p
ON ps.ProductSubcategoryID = p.ProductSubcategoryID
) AS WorkingTable
PIVOT (
SUM(Stock)
FOR ProductCategoryName IN (
[Raw Ingredients], [Machine Parts], [Misc],
[Confectionary Products], [Non-confectionary Products]
)
) AS PivotTable;
Borrar millones de filas en una sola transacción llenará el transaction log y disparará un error 9002. Hazlo en lotes.
-- ✅ Borrado por lotes de 250K filas
DECLARE @RowCounter BIGINT = 1;
WHILE @RowCounter > 0
BEGIN
DELETE TOP (250000)
FROM dbo.VeryLargeTable
WHERE FechaCreacion < DATEADD(YEAR, -3, GETDATE());
SET @RowCounter = @@ROWCOUNT;
-- Pequeña pausa para permitir checkpoint
WAITFOR DELAY '00:00:01';
END;
Para tablas grandes (>100M filas), particiona por fecha y aplica ROW o PAGE compression. Reduce I/O hasta 70% y permite eliminación de particiones en queries con WHERE en la clave de partición.
-- ✅ 1. Crear función y esquema de partición
CREATE PARTITION FUNCTION ImpressionDatesPF (DATETIME)
AS RANGE RIGHT FOR VALUES (
'20230101', '20240101', '20250101', '20260101'
);
CREATE PARTITION SCHEME ImpressionDatesPS
AS PARTITION ImpressionDatesPF
ALL TO ([PRIMARY]);
-- ✅ 2. Estimar el ahorro ANTES de aplicar compresión
EXEC sp_estimate_data_compression_savings
@schema_name = 'dbo',
@object_name = 'ImpressionsArchive',
@index_id = 1,
@partition_number = NULL,
@data_compression = 'PAGE';
-- ✅ 3. Aplicar compresión si vale la pena
ALTER TABLE dbo.ImpressionsArchive
REBUILD WITH (DATA_COMPRESSION = PAGE);
-- ✅ Para data warehouses: clustered columnstore
CREATE CLUSTERED COLUMNSTORE INDEX CCI_Impressions
ON dbo.ImpressionsArchive;
-- ✅ Para OLTP con analytics ocasional: nonclustered columnstore
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Sales
ON dbo.Sales (SalesDate, ProductID, Quantity, Amount);
-- Comparación real del libro en una agregación:
-- B-tree clásico: 1067 ms
-- Columnstore: 45 ms (casi 24x más rápido)
"El optimizador es brillante, pero no adivina tu negocio. Ayúdalo con buenos índices, estadísticas frescas y consultas que expresen claramente qué necesitas, no cómo obtenerlo."
Los datos son el activo más valioso. Protegerlos requiere estrategia, no solo herramientas. Un backup sin probar no es un backup; una réplica sin failover probado no es alta disponibilidad.
Más del 70% de los ataques son internos. Crea roles personalizados en lugar de regalar sysadmin.
-- ✅ Rol custom para failover de Availability Groups
CREATE SERVER ROLE AvailabilityGroupsFailover;
ALTER SERVER ROLE AvailabilityGroupsFailover
ADD MEMBER [NT AUTHORITY\SYSTEM];
GRANT ALTER ANY AVAILABILITY GROUP TO AvailabilityGroupsFailover;
GRANT CONNECT SQL TO AvailabilityGroupsFailover;
GRANT VIEW SERVER STATE TO AvailabilityGroupsFailover;
-- ✅ Rol de DB para ejecutar SPs (no existe out-of-the-box)
USE model; -- aplicará a todas las nuevas BBDD
GO
CREATE ROLE ExecSP;
GRANT EXECUTE TO ExecSP;
sa: deshabilitar o renombrar-- ✅ Opción A: deshabilitar (recomendado)
ALTER LOGIN sa DISABLE;
-- ✅ Opción B: renombrar (si por compatibilidad debe existir)
ALTER LOGIN sa WITH NAME = SQLAdmin_2025;
-- ✅ Y siempre forzar políticas de contraseña
ALTER LOGIN SQLAdmin_2025
WITH CHECK_POLICY = ON,
CHECK_EXPIRATION = ON;
xp_cmdshellNo basta con desactivarlo: pon una alarma para detectar intentos de activación.
-- ✅ Mensaje custom para alertar
EXEC sp_addmessage 50001, 16,
'Intento de activar xp_cmdshell detectado.';
-- ✅ Trigger DDL que bloquea + alerta
CREATE TRIGGER prevent_xp_cmdshell ON ALL SERVER
FOR ALTER_INSTANCE
AS
BEGIN
DECLARE @Statement NVARCHAR(4000);
SET @Statement = (
SELECT EVENTDATA().value(
'(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]',
'nvarchar(4000)')
);
IF (CHARINDEX('sp_configure', @Statement) > 0)
AND (CHARINDEX('xp_cmdshell', @Statement) > 0)
BEGIN
RAISERROR(50001, 16, 1) WITH LOG; -- Alerta al event log
ROLLBACK; -- Bloquea la activación
END
END;
COPY_ONLY + cifradoUn backup manual no debe romper la cadena diferencial. Y siempre cifrado.
-- ✅ 1. Crear infraestructura de cifrado (una sola vez)
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = '********';
CREATE CERTIFICATE BackupCert
WITH SUBJECT = 'Backup Encryption Certificate';
-- ⚠️ ¡BACKUP DEL CERTIFICADO! (sin esto, los backups son inútiles)
BACKUP CERTIFICATE BackupCert
TO FILE = 'C:\Certs\BackupCert.cer'
WITH PRIVATE KEY (
FILE = 'C:\Certs\BackupCertPK.pvk',
ENCRYPTION BY PASSWORD = '********'
);
-- ✅ 2. Backup ad-hoc seguro
BACKUP DATABASE Marketing
TO DISK = 'D:\Backups\Marketing_Adhoc.bak'
WITH
COPY_ONLY, -- No rompe la cadena diferencial
CHECKSUM, -- Verifica integridad
COMPRESSION, -- Reduce tamaño 50-80%
ENCRYPTION (
ALGORITHM = AES_256,
SERVER CERTIFICATE = BackupCert
);
-- ✅ 3. Verificar sin restaurar
RESTORE VERIFYONLY
FROM DISK = 'D:\Backups\Marketing_Adhoc.bak'
WITH CHECKSUM;
Para: Bases de datos OLTP en producción.
Permite: Backups full + diferenciales + log (point-in-time recovery).
Trade-off: Logging completo (más I/O), pero RPO de minutos.
ALTER DATABASE Sales SET RECOVERY FULL;
-- ⚠️ Después del cambio: full backup OBLIGATORIO
BACKUP DATABASE Sales TO DISK = 'D:\Backups\Sales_Init.bak';
-- A partir de aquí, los log backups funcionarán
Para: Data warehouses, BBDD de desarrollo, staging.
Permite: Solo full + diferenciales. NO point-in-time.
Ventaja: Logging mínimo (mejor rendimiento ETL), log se trunca automáticamente.
ALTER DATABASE MarketingArchive SET RECOVERY SIMPLE;
-- Sin necesidad de log backups
-- El log se trunca en cada CHECKPOINT
Para: Ventanas temporales de carga masiva (ETL nocturno).
Permite: Operaciones bulk minimamente logged.
Cuidado: No permite point-in-time durante el bulk.
-- Patrón típico de ETL nocturno:
-- 1. Antes del ETL
ALTER DATABASE DataWarehouse SET RECOVERY BULK_LOGGED;
-- 2. Ejecutar bulk inserts, indexes, etc.
-- 3. Volver a FULL al terminar
ALTER DATABASE DataWarehouse SET RECOVERY FULL;
-- 4. Backup de log para cerrar la ventana
BACKUP LOG DataWarehouse TO DISK = '...';
Sin autenticador, un atacante con permisos UPDATE puede copiar el valor cifrado de otro registro (ataque de sustitución de valor completo). El autenticador vincula el dato cifrado a una columna específica.
-- ✅ Setup de cifrado
CREATE MASTER KEY ENCRYPTION BY PASSWORD = '********';
CREATE CERTIFICATE SalaryCert
WITH SUBJECT = 'Salarios encriptados';
CREATE SYMMETRIC KEY SalaryKey
WITH ALGORITHM = AES_256
ENCRYPTION BY CERTIFICATE SalaryCert;
-- ✅ Cifrado CON autenticador (EmployeeID como vínculo)
OPEN SYMMETRIC KEY SalaryKey
DECRYPTION BY CERTIFICATE SalaryCert;
UPDATE dbo.Employees
SET SalarioCifrado = ENCRYPTBYKEY(
KEY_GUID('SalaryKey'),
CAST(Salario AS VARBINARY),
1, -- ✅ Habilitar autenticador
CAST(EmployeeID AS VARBINARY(8000)) -- ✅ Vínculo único
);
-- ✅ Descifrado: si alguien copió el valor, devuelve NULL
SELECT
EmployeeID,
CAST(DECRYPTBYKEY(
SalarioCifrado,
1,
CAST(EmployeeID AS VARBINARY(8000))
) AS MONEY) AS Salario
FROM dbo.Employees;
CLOSE SYMMETRIC KEY SalaryKey;
-- ✅ Plantilla recomendada para SPs en producción
CREATE PROCEDURE dbo.RegisterTransaction
@Amount DECIMAL(10,2),
@AccountID INT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- Rollback automático en errores graves
BEGIN TRANSACTION;
BEGIN TRY
UPDATE dbo.Accounts
SET Balance = Balance - @Amount
WHERE ID = @AccountID;
INSERT INTO dbo.Transactions (AccountID, Amount, CreatedAt)
VALUES (@AccountID, @Amount, GETDATE());
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- Log a tabla de errores (opcional)
INSERT INTO dbo.ErrorLog (ErrorMessage, ErrorNumber, ErrorSeverity)
VALUES (ERROR_MESSAGE(), ERROR_NUMBER(), ERROR_SEVERITY());
-- Re-lanzar el error para que la app lo capture
THROW;
END CATCH
END;
Aunque el desarrollador frontend "valide" inputs, el DBA debe asumir lo peor. Fuerza el uso de stored procedures con parámetros tipados.
// C#: SQL dinámico concatenado
string qry = "SELECT * FROM Users " +
"WHERE UserName='" + userInput + "' " +
"AND Pwd='" + pwdInput + "'";
// Si userInput = "' OR 1=1--"
// La query se convierte en:
// SELECT * FROM Users
// WHERE UserName='' OR 1=1--' AND Pwd='...'
// → Devuelve TODOS los usuarios 💀
CREATE PROCEDURE dbo.LoginUser
@UserName NVARCHAR(128),
@Password NVARCHAR(128)
AS
BEGIN
SET NOCOUNT ON;
SELECT UserID, UserName
FROM dbo.Users
WHERE UserName = @UserName
AND UserPassword = @Password;
END;
-- Los parámetros se tratan como datos,
-- nunca como código SQL.
Busca, expande y copia los patrones que necesites. Más de 15 ejemplos categorizados.
-- ✅ Índice que "cubre" toda la query (sin lookups)
CREATE NONCLUSTERED INDEX IX_Email_IncludeNames
ON dbo.CustomerContacts (CustomerContactEmail)
INCLUDE (CustomerContactFirstName, CustomerContactLastName);
-- Esta query usará el índice sin tocar la tabla base
SELECT
CustomerContactFirstName,
CustomerContactLastName,
CustomerContactEmail
FROM dbo.CustomerContacts
ORDER BY CustomerContactEmail;
-- ✅ Solo reconstruye lo necesario (no todo indiscriminadamente)
DECLARE @SQL NVARCHAR(MAX);
SET @SQL = (
SELECT
'ALTER INDEX ' + i.name +
' ON ' + s.name + '.' + o.name +
' REBUILD WITH (ONLINE = ON, MAXDOP = 4); '
FROM sys.dm_db_index_physical_stats(
DB_ID(), NULL, NULL, NULL, 'DETAILED'
) ips
INNER JOIN sys.indexes i
ON i.object_id = ips.object_id
AND i.index_id = ips.index_id
INNER JOIN sys.objects o
ON i.object_id = o.object_id
INNER JOIN sys.schemas s
ON s.schema_id = o.schema_id
WHERE i.type_desc <> 'HEAP'
AND o.type_desc = 'USER_TABLE'
AND ips.avg_fragmentation_in_percent > 20
AND ips.page_count > 1000
AND ips.index_level = 0
FOR XML PATH('')
);
EXEC sp_executesql @SQL;
-- ❌ Antipatrón: cursor para iterar tablas
DECLARE Tables CURSOR READ_ONLY FOR
SELECT name FROM sys.tables;
-- ... 20+ líneas de FETCH/EXEC ...
-- ✅ Set-based con XML PATH (mucho más rápido)
DECLARE @SQL NVARCHAR(MAX);
SET @SQL = (
SELECT 'ALTER INDEX ALL ON ' + QUOTENAME(name) + ' REBUILD; '
FROM sys.tables
FOR XML PATH('')
);
EXEC sp_executesql @SQL;
-- ✅ Snapshot ANTES de un cambio crítico
CREATE DATABASE Marketing_Snapshot ON
(
NAME = Marketing,
FILENAME = 'D:\Snapshots\Marketing_PreDeploy.ss'
)
AS SNAPSHOT OF Marketing;
-- Si algo sale mal, revertir es casi instantáneo:
USE master;
RESTORE DATABASE Marketing
FROM DATABASE_SNAPSHOT = 'Marketing_Snapshot';
-- ⚠️ Recuerda: NO sustituye a un backup
-- (depende de la BD origen)
DROP DATABASE Marketing_Snapshot;
-- ✅ Insertar o actualizar agregaciones diarias
MERGE INTO reporting.ImpressionAggregates AS Target
USING (
SELECT
CampaignID,
CountryCode,
AVG(BidPrice) AS AvgBid,
AVG(CostPerMille) AS AvgCPM,
CAST(EventTime AS DATE) AS EventDate
FROM marketing.Impressions
WHERE EventTime >= DATEADD(DAY, -1, GETDATE())
GROUP BY CampaignID, CountryCode, CAST(EventTime AS DATE)
) AS Source
ON Source.CampaignID = Target.CampaignID
AND Source.CountryCode = Target.CountryCode
AND Source.EventDate = Target.EventDate
WHEN MATCHED THEN
UPDATE SET
AvgBidPrice = Source.AvgBid,
AvgCostPerMille = Source.AvgCPM
WHEN NOT MATCHED THEN
INSERT (CampaignID, CountryCode, EventDate, AvgBidPrice, AvgCostPerMille)
VALUES (Source.CampaignID, Source.CountryCode, Source.EventDate,
Source.AvgBid, Source.AvgCPM);
DECLARE @json NVARCHAR(MAX) = N'{
"EmployeeAddress": [{
"EmployeeID": 1,
"Address": {
"Line1": "5331 Rexford Court",
"City": "Montgomery",
"State": "AL",
"ZipCode": "36116"
}
}]
}';
-- ✅ Extraer un valor escalar
SELECT JSON_VALUE(@json, '$.EmployeeAddress[0].EmployeeID');
-- ✅ Convertir JSON → tabla relacional
SELECT *
FROM OPENJSON(@json, '$.EmployeeAddress[0]')
WITH (
EmployeeID SMALLINT '$.EmployeeID',
Line1 NVARCHAR(64) '$.Address.Line1',
City NVARCHAR(64) '$.Address.City',
[State] NCHAR(2) '$.Address.State',
ZipCode NVARCHAR(10) '$.Address.ZipCode'
);
-- ✅ Generar JSON desde resultado
SELECT
1 AS 'EmployeeID',
'5331 Rexford Court' AS 'Address.Line1',
'Montgomery' AS 'Address.City'
FOR JSON PATH, ROOT('EmployeeAddress');
-- ✅ 1. Estimar antes de aplicar
EXEC sp_estimate_data_compression_savings
@schema_name = 'dbo',
@object_name = 'ImpressionsArchive',
@index_id = NULL, -- Todos los índices
@partition_number = NULL,
@data_compression = 'PAGE'; -- Probar también 'ROW'
-- ✅ 2. Aplicar si la ratio compensa la CPU extra
ALTER TABLE dbo.ImpressionsArchive
REBUILD WITH (
DATA_COMPRESSION = PAGE,
ONLINE = ON,
MAXDOP = 2
);
-- ✅ 3. Verificar el ahorro real
SELECT
OBJECT_NAME(object_id) AS Tabla,
in_row_used_page_count AS Paginas,
(in_row_used_page_count * 8) / 1024 AS MB
FROM sys.dm_db_partition_stats
WHERE object_id = OBJECT_ID('dbo.ImpressionsArchive')
AND index_id = 1;
-- ✅ 1. Crear el audit (escribe al Security Log de Windows)
USE master;
CREATE SERVER AUDIT AdminActivityAudit
TO SECURITY_LOG
WITH (ON_FAILURE = CONTINUE);
-- ✅ 2. Especificar QUÉ auditar
CREATE SERVER AUDIT SPECIFICATION AdminActivitySpec
FOR SERVER AUDIT AdminActivityAudit
ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP),
ADD (AUDIT_CHANGE_GROUP),
ADD (DBCC_GROUP),
ADD (SERVER_PERMISSION_CHANGE_GROUP),
ADD (SERVER_PRINCIPAL_CHANGE_GROUP),
ADD (SERVER_PRINCIPAL_IMPERSONATION_GROUP),
ADD (SERVER_OBJECT_CHANGE_GROUP),
ADD (SERVER_STATE_CHANGE_GROUP);
-- ✅ 3. Activar
ALTER SERVER AUDIT AdminActivityAudit
WITH (STATE = ON);
ALTER SERVER AUDIT SPECIFICATION AdminActivitySpec
WITH (STATE = ON);
-- ✅ Tamaño actual y espacio libre por archivo
SELECT
DB_NAME(database_id) AS DataBase_Name,
Name AS Logical_Name,
Physical_Name AS Physical_Name,
(size * 8) / 1024 AS TotalSpace_MB,
(FILEPROPERTY(name, 'SpaceUsed') * 8) / 1024 AS UsedSpace_MB,
((size - FILEPROPERTY(name, 'SpaceUsed')) * 8) / 1024 AS FreeSpace_MB,
growth AS Growth_Setting,
is_percent_growth AS Is_Percent_Growth
FROM sys.master_files
WHERE database_id = DB_ID()
ORDER BY DataBase_Name, Logical_Name;
-- ✅ VLFs activos en el log (detectar fragmentación)
SELECT
total_vlf_count,
active_vlf_count,
log_truncation_holdup_reason
FROM sys.dm_db_log_stats(DB_ID());
-- ✅ Activar Query Store (gratis, low-overhead)
ALTER DATABASE Marketing
SET QUERY_STORE = ON (
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
DATA_FLUSH_INTERVAL_SECONDS = 900,
MAX_STORAGE_SIZE_MB = 1000,
INTERVAL_LENGTH_MINUTES = 60
);
-- ✅ Activar feedback automático de paralelismo (SQL 2022+)
ALTER DATABASE SCOPED CONFIGURATION
SET DOP_FEEDBACK = ON;
-- ✅ Top 10 queries más costosas en CPU
SELECT TOP 10
qsq.query_id,
qsqt.query_sql_text,
SUM(qsrs.count_executions) AS executions,
AVG(qsrs.avg_duration / 1000.0) AS avg_duration_ms,
AVG(qsrs.avg_cpu_time / 1000.0) AS avg_cpu_ms
FROM sys.query_store_query qsq
INNER JOIN sys.query_store_query_text qsqt
ON qsq.query_text_id = qsqt.query_text_id
INNER JOIN sys.query_store_plan qsp
ON qsq.query_id = qsp.query_id
INNER JOIN sys.query_store_runtime_stats qsrs
ON qsp.plan_id = qsrs.plan_id
GROUP BY qsq.query_id, qsqt.query_sql_text
ORDER BY AVG(qsrs.avg_cpu_time) DESC;
-- ✅ CHECKSUM en cada página al escribirla
ALTER DATABASE Marketing SET PAGE_VERIFY CHECKSUM;
-- Verificar la configuración actual
SELECT name, page_verify_option_desc
FROM sys.databases
WHERE page_verify_option_desc <> 'CHECKSUM';
-- Cualquier resultado aquí es un problema.
Configuration InstallSql {
param (
$SqlInstanceName = 'MSSQLSERVER',
$Edition
)
Import-DscResource -ModuleName SqlServerDsc
node localhost {
SqlSetup 'InstallInstance' {
InstanceName = $SqlInstanceName
Features = 'SQLENGINE'
SourcePath = 'C:\SQL Media'
SQLSysAdminAccounts = @('Administrator')
ProductKey = $ProductKey
}
SqlConfiguration 'RemoteAccess' {
InstanceName = $SqlInstanceName
OptionName = 'remote access'
OptionValue = 0
DependsOn = '[SqlSetup]InstallInstance'
}
SqlLogin 'AddSqlAdmin' {
Ensure = 'Present'
Name = 'DOMAIN\sqladmins'
InstanceName = $SqlInstanceName
LoginType = 'WindowsGroup'
DependsOn = '[SqlSetup]InstallInstance'
}
}
}
# ✅ 1. Crear volumen Docker para persistir datos
sudo docker volume create sqldata
# ✅ 2. Levantar contenedor con volumen montado
sudo docker run \
-e "ACCEPT_EULA=Y" \
-e "MSSQL_SA_PASSWORD=Pa\$\$w0rd!" \
-p 1433:1433 \
--name production-sql \
--hostname production-sql \
-v sqldata:/var/opt/mssql \
-d mcr.microsoft.com/mssql/server:2022-latest
# ✅ 3. Conectarte
docker exec -it production-sql /opt/mssql-tools/bin/sqlcmd \
-S localhost -U sa -P 'Pa$$w0rd!'
-- ✅ 1. Crear operador (destinatario)
EXEC msdb.dbo.sp_add_operator
@name = 'DBA_Team',
@enabled = 1,
@email_address = 'dba@empresa.com';
-- ✅ 2. Alerta para errores de severidad alta (>= 19)
EXEC msdb.dbo.sp_add_alert
@name = 'High Severity Errors',
@severity = 19,
@enabled = 1,
@delay_between_responses = 60,
@include_event_description_in = 1;
EXEC msdb.dbo.sp_add_notification
@alert_name = 'High Severity Errors',
@operator_name = 'DBA_Team',
@notification_method = 1; -- email
-- ✅ 3. Alerta para xp_cmdshell (custom error 50001)
EXEC msdb.dbo.sp_add_alert
@name = 'xp_cmdshell Activation Attempt',
@message_id = 50001,
@enabled = 1;
EXEC msdb.dbo.sp_add_notification
@alert_name = 'xp_cmdshell Activation Attempt',
@operator_name = 'DBA_Team',
@notification_method = 1;
-- ✅ Sesiones bloqueadas + bloqueador + query SQL
SELECT
blocking.session_id AS BlockerSPID,
blocked.session_id AS BlockedSPID,
blocked.wait_type,
blocked.wait_time / 1000 AS WaitSeconds,
blocked.last_wait_type,
bl_text.text AS BlockerSQL,
bd_text.text AS BlockedSQL,
DB_NAME(blocked.database_id) AS DBName
FROM sys.dm_exec_requests blocked
INNER JOIN sys.dm_exec_sessions blocker_sess
ON blocked.blocking_session_id = blocker_sess.session_id
INNER JOIN sys.dm_exec_requests blocking
ON blocking.session_id = blocker_sess.session_id
CROSS APPLY sys.dm_exec_sql_text(blocking.sql_handle) bl_text
CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) bd_text
WHERE blocked.blocking_session_id <> 0;
-- ✅ Top 5 wait types desde el último restart
SELECT TOP 5
wait_type,
waiting_tasks_count,
wait_time_ms / 1000 AS wait_seconds,
(wait_time_ms / waiting_tasks_count) AS avg_wait_ms
FROM sys.dm_os_wait_stats
WHERE waiting_tasks_count > 0
AND wait_type NOT IN (
'CLR_AUTO_EVENT', 'WAITFOR', 'SLEEP_TASK',
'BROKER_TASK_STOP', 'LAZYWRITER_SLEEP'
)
ORDER BY wait_time_ms DESC;
<!-- ✅ Properties recomendadas en el Data Flow Task -->
<DataFlow>
<DefaultBufferMaxRows>50000</DefaultBufferMaxRows>
<AutoAdjustBufferSize>True</AutoAdjustBufferSize>
<EngineThreads>10</EngineThreads>
</DataFlow>
<!-- ✅ OLE DB Destination Fast Load -->
<OleDbDestination>
<FastLoadOptions>TABLOCK,CHECK_CONSTRAINTS</FastLoadOptions>
<MaximumInsertCommitSize>50000</MaximumInsertCommitSize>
<Rows per batch>50000</Rows per batch>
</OleDbDestination>
<!-- ⚠️ Antes de bulk load: deshabilitar índices NCI -->
# ✅ Generar script de deploy desde DACPAC sin aplicarlo
SqlPackage.exe `
/Action:Script `
/SourceFile:"C:\Builds\Marketing.dacpac" `
/TargetServerName:"SQLPROD01" `
/TargetDatabaseName:"Marketing" `
/OutputPath:"C:\Deploy\preview.sql" `
/p:BlockOnPossibleDataLoss=true `
/p:GenerateSmartDefaults=true
# ✅ Aplicar tras revisión humana
SqlPackage.exe `
/Action:Publish `
/SourceFile:"C:\Builds\Marketing.dacpac" `
/TargetServerName:"SQLPROD01" `
/TargetDatabaseName:"Marketing" `
/p:BlockOnPossibleDataLoss=true
-- ✅ Activar auto-update asíncrono (no bloquea queries)
ALTER DATABASE Marketing SET AUTO_UPDATE_STATISTICS ON;
ALTER DATABASE Marketing SET AUTO_UPDATE_STATISTICS_ASYNC ON;
-- ❌ NO hagas esto después de un REBUILD:
-- UPDATE STATISTICS ... -- empeora el muestreo (el rebuild ya las tiene al 100%)
-- ✅ Si necesitas actualizar manualmente, solo COLUMNS:
UPDATE STATISTICS marketing.Impressions
WITH FULLSCAN, COLUMNS;
-- ✅ Ver edad de las estadísticas
SELECT
OBJECT_NAME(s.object_id) AS TableName,
s.name AS StatName,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECTPROPERTY(s.object_id, 'IsUserTable') = 1
ORDER BY sp.last_updated;
¿No encuentras lo que buscas? Usa el buscador arriba 👆
5 preguntas rápidas para auto-evaluar tu comprensión.
sp_ en procedimientos de usuario?NOLOCK?DBCC SHRINKFILE a un archivo de datos. ¿Qué debes hacer DESPUÉS?Este resumen condensa los conceptos clave, pero el libro de Peter A. Carter incluye scripts listos para usar, diagramas C4, explicaciones paso a paso de SSIS, AlwaysOn, Columnstore, y casos reales de producción con métricas antes/después. Si tu rol toca SQL Server, 100 SQL Server Mistakes es el manual de supervivencia que evita que aprendas a base de incidentes en sábado por la noche.
📕 Comprar el libro en Manning 📦 Volver a la galería