Qué vas a conseguir
Resolver el 80 % de los tickets de base de datos que llegan a operaciones sin esperar al DBA: "la aplicación va lenta", "hay que dar acceso a X", "¿tenemos copia de ayer?", "el disco del servidor de base de datos está al 92 %". Los tres motores, el mismo gesto, con el comando de cada uno.
Antes de empezar
- Acceso al servidor de cada motor (SSH o RDP) y una cuenta con permisos de administración de la base de datos, usada solo para estas tareas y con la contraseña en el gestor del equipo.
- Los clientes:
psql,mysqlysqlcmd(en Windows viene con SQL Server; en Linux, el paquetemssql-tools18). - Un directorio de copias con espacio igual al tamaño de la base de datos más grande, y el ticket de a quién avisar antes de restaurar nada.
1. Conectar y ver qué hay
psql -h localhost -U postgres -c "\l+" | awk -F'|' 'NR>3 {print $1, $7}' | head -5 appdb | 2412 MB postgres | 7501 kBmysql -u root -p -e "SELECT table_schema AS db, ROUND(SUM(data_length+index_length)/1024/1024) AS mb FROM information_schema.tables GROUP BY table_schema ORDER BY mb DESC;"+--------------------+------+| db | mb |+--------------------+------+| intranet | 1870 || mysql | 3 |+--------------------+------+sqlcmd -S srv-sql-01 -E -Q "SELECT name, (SELECT SUM(size)*8/1024 FROM sys.master_files mf WHERE mf.database_id=d.database_id) AS mb FROM sys.databases d ORDER BY mb DESC" -Wname mbERP 38210tempdb 4096master 7Antes de tocar nada: qué bases de datos hay, cuánto ocupan y, en el ticket, cuál es la de la aplicación que falla. -E en sqlcmd usa tu cuenta de Windows; en PostgreSQL y MySQL la cuenta de administración va en ~/.pgpass y ~/.my.cnf con permisos 600, nunca en la línea de comandos de un script.
2. Copia y restauración (probada)
# PostgreSQL: copia lógica comprimida y restauración en una base de datos de pruebapg_dump -h localhost -U postgres -Fc appdb -f /backup/appdb-$(date +%F).dumpcreatedb -h localhost -U postgres appdb_restore_test && pg_restore -h localhost -U postgres -d appdb_restore_test -j 4 /backup/appdb-$(date +%F).dumppsql -h localhost -U postgres -d appdb_restore_test -tAc "SELECT count(*) FROM orders;"184233# MySQLmysqldump --single-transaction --routines --triggers intranet | gzip > /backup/intranet-$(date +%F).sql.gzmysql -e "CREATE DATABASE intranet_restore_test" && zcat /backup/intranet-$(date +%F).sql.gz | mysql intranet_restore_testsqlcmd -S srv-sql-01 -E -Q "BACKUP DATABASE ERP TO DISK='E:\Backup\ERP_2026-10-22.bak' WITH COMPRESSION, CHECKSUM, STATS=10"10 percent processed. ... 100 percent processed.BACKUP DATABASE successfully processed 4891264 pages in 98.412 seconds (388.3 MB/sec).sqlcmd -S srv-sql-01 -E -Q "RESTORE VERIFYONLY FROM DISK='E:\Backup\ERP_2026-10-22.bak' WITH CHECKSUM"The backup set on file 1 is valid.sqlcmd -S srv-sql-01 -E -Q "RESTORE DATABASE ERP_restore_test FROM DISK='E:\Backup\ERP_2026-10-22.bak' WITH MOVE 'ERP' TO 'E:\Restore\ERP_test.mdf', MOVE 'ERP_log' TO 'E:\Restore\ERP_test.ldf', STATS=25"Una copia que no se ha restaurado nunca es una esperanza, no una copia. La restauración de prueba a otra base de datos y un count(*) de la tabla principal es lo que apuntas en el ticket mensual de copias (y lo que el repo 33 de mi GitHub, backup-restore-test-tracker, sigue en el tiempo). Si el servidor ya tiene Veeam o un agente de copias (Nivel 2), estos comandos son la verificación, no el sustituto. Borra las bases de datos *_restore_test al terminar.
3. Espacio y crecimiento
psql -h localhost -U postgres -d appdb -c "SELECT relname, pg_size_pretty(pg_total_relation_size(oid)) AS size, n_dead_tup FROM pg_stat_user_tables t JOIN pg_class c ON c.oid=t.relid ORDER BY pg_total_relation_size(oid) DESC LIMIT 3;" relname | size | n_dead_tup--------------+---------+------------ audit_log | 1412 MB | 2203110 orders | 611 MB | 8120psql -h localhost -U postgres -d appdb -c "VACUUM (VERBOSE, ANALYZE) audit_log;" 2>&1 | tail -1du -sh /var/lib/pgsql/16/data/pg_wal3.1G /var/lib/pgsql/16/data/pg_walsqlcmd -S srv-sql-01 -E -d ERP -Q "SELECT name, size*8/1024 AS mb, CAST(FILEPROPERTY(name,'SpaceUsed') AS int)*8/1024 AS used_mb FROM sys.database_files" -Wname mb used_mbERP 36120 29877ERP_log 2090 1912sqlcmd -S srv-sql-01 -E -Q "SELECT name, recovery_model_desc, log_reuse_wait_desc FROM sys.databases WHERE name='ERP'" -Wname recovery_model_desc log_reuse_wait_descERP FULL LOG_BACKUPLos tres culpables habituales del disco al 92 %: en PostgreSQL, una tabla con millones de tuplas muertas sin VACUUM (y pg_wal creciendo porque un slot de replicación quedó huérfano); en MySQL, los binlogs sin expire_logs_days/binlog_expire_logs_seconds; en SQL Server, el log de transacciones en modo FULL sin copias de log: log_reuse_wait_desc = LOG_BACKUP lo dice, y la solución es programar BACKUP LOG, no SHRINKFILE cada semana.
4. Altas y bajas de usuarios
# PostgreSQL: rol de solo lectura para el analistapsql -h localhost -U postgres -d appdb -c "CREATE ROLE laura_prado LOGIN PASSWORD 'cambiar-en-el-primer-login'; GRANT CONNECT ON DATABASE appdb TO laura_prado; GRANT USAGE ON SCHEMA public TO laura_prado; GRANT SELECT ON ALL TABLES IN SCHEMA public TO laura_prado; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO laura_prado;"# MySQLmysql -e "CREATE USER 'laura_prado'@'10.20.%' IDENTIFIED BY 'cambiar-en-el-primer-login' PASSWORD EXPIRE; GRANT SELECT ON intranet.* TO 'laura_prado'@'10.20.%';"# baja: revocar y, si no tiene objetos, eliminarpsql -h localhost -U postgres -d appdb -c "REASSIGN OWNED BY j_navarro TO postgres; DROP OWNED BY j_navarro; DROP ROLE j_navarro;"sqlcmd -S srv-sql-01 -E -Q "CREATE LOGIN [CONTOSO\sg-erp-readonly] FROM WINDOWS; USE ERP; CREATE USER [CONTOSO\sg-erp-readonly] FOR LOGIN [CONTOSO\sg-erp-readonly]; ALTER ROLE db_datareader ADD MEMBER [CONTOSO\sg-erp-readonly];"La regla que vale para los tres: grupo, no persona, siempre que el motor lo permita. SQL Server con un grupo de AD es el ideal: la baja se hace en Active Directory (Nivel 1) y no hay que tocar la base de datos. En PostgreSQL y MySQL, un rol de lectura con GRANT a usuarios nominales, y la lista de usuarios en la revisión trimestral de accesos.
5. "La aplicación va lenta": qué está pasando ahora
psql -h localhost -U postgres -d appdb -c "SELECT pid, now()-query_start AS runtime, state, wait_event_type, left(query,60) AS query FROM pg_stat_activity WHERE state <> 'idle' AND pid <> pg_backend_pid() ORDER BY runtime DESC LIMIT 5;" pid | runtime | state | wait_event_type | query-------+-----------------+--------+-----------------+---------------------------------------------- 18211 | 00:14:02.118 | active | Lock | UPDATE orders SET status='X' WHERE customer_id=… 18240 | 00:13:58.004 | active | Lock | SELECT * FROM orders WHERE customer_id=… 17990 | 00:14:40.771 | idle in transaction | | BEGIN; UPDATE orders SET …psql -h localhost -U postgres -d appdb -c "SELECT pg_terminate_backend(17990);"mysql -e "SELECT id, user, time, state, LEFT(info,60) FROM information_schema.processlist WHERE command <> 'Sleep' ORDER BY time DESC LIMIT 5;"sqlcmd -S srv-sql-01 -E -Q "SELECT r.session_id, r.blocking_session_id, r.wait_type, r.wait_time/1000 AS wait_s, LEFT(t.text,60) AS sql_text FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE r.session_id > 50 ORDER BY r.wait_time DESC" -Wsession_id blocking_session_id wait_type wait_s sql_text71 58 LCK_M_U 812 UPDATE Invoices SET Status = …58 0 NULL 0 (sesión inactiva con transacción abierta)El patrón es idéntico en los tres: una sesión inactiva con una transacción abierta (alguien abrió una herramienta, hizo un BEGIN y se fue a comer) bloquea a todas las demás. pg_stat_activity, processlist y dm_exec_requests te dan el pid de quién bloquea; matar esa sesión (pg_terminate_backend, KILL id, KILL session_id) devuelve la aplicación a la vida, y el ticket al desarrollador dice qué consulta y desde qué cliente. Si no hay bloqueos y es una consulta pesada, EXPLAIN ANALYZE en PostgreSQL/MySQL o el plan de ejecución en SSMS es el siguiente paso, y ese sí es del DBA o del desarrollador.
6. Lo que revisas cada mes
- Restauración de prueba de cada motor, con el
count(*)apuntado. - Espacio: tablas que más crecen,
pg_wal/binlogs/log de transacciones, y la previsión a seis meses con la tendencia. - Usuarios: cuentas nominales que ya no están en Active Directory; contraseñas de las cuentas de aplicación en el gestor y con fecha.
- Parches del motor: PostgreSQL y MySQL con el parcheo mensual de Ansible; SQL Server con su CU trimestral en la ventana de cambios, con copia antes.
Siguientes pasos
- Monitorización:
postgres_exporter,mysqld_exportery el de SQL Server en el Prometheus de la guía de Grafana; el panel de "sesiones bloqueadas > 0 durante 5 minutos" te avisa antes que el usuario. - Alta disponibilidad: réplica en streaming (PostgreSQL), replicación (MySQL) y Always On (SQL Server) cuando el negocio no acepte una hora de parada; antes de eso, la restauración probada ya cubre lo importante.
- Autenticación integrada en PostgreSQL con Kerberos (guía de Linux + AD): la misma baja de AD cierra también la base de datos.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
What you'll get
Solve 80% of the database tickets that reach operations without waiting for the DBA: "the application is slow", "we need to give X access", "do we have yesterday's backup?", "the database server disk is at 92%". Three engines, the same move, with each one's command.
Before you start
- Access to each engine's server (SSH or RDP) and an account with database administration rights, used only for these tasks and with its password in the team's password manager.
- The clients:
psql,mysqlandsqlcmd(on Windows it ships with SQL Server; on Linux, themssql-tools18package). - A backup directory with space equal to the largest database, and the ticket saying whom to warn before restoring anything.
1. Connect and see what is there
psql -h localhost -U postgres -c "\l+" | awk -F'|' 'NR>3 {print $1, $7}' | head -5 appdb | 2412 MB postgres | 7501 kBmysql -u root -p -e "SELECT table_schema AS db, ROUND(SUM(data_length+index_length)/1024/1024) AS mb FROM information_schema.tables GROUP BY table_schema ORDER BY mb DESC;"+--------------------+------+| db | mb |+--------------------+------+| intranet | 1870 || mysql | 3 |+--------------------+------+sqlcmd -S srv-sql-01 -E -Q "SELECT name, (SELECT SUM(size)*8/1024 FROM sys.master_files mf WHERE mf.database_id=d.database_id) AS mb FROM sys.databases d ORDER BY mb DESC" -Wname mbERP 38210tempdb 4096master 7Before touching anything: which databases exist, how big they are and, in the ticket, which one belongs to the failing application. -E in sqlcmd uses your Windows account; in PostgreSQL and MySQL the admin account goes in ~/.pgpass and ~/.my.cnf with 600 permissions, never on a script's command line.
2. Backup and (tested) restore
# PostgreSQL: compressed logical backup and restore into a test databasepg_dump -h localhost -U postgres -Fc appdb -f /backup/appdb-$(date +%F).dumpcreatedb -h localhost -U postgres appdb_restore_test && pg_restore -h localhost -U postgres -d appdb_restore_test -j 4 /backup/appdb-$(date +%F).dumppsql -h localhost -U postgres -d appdb_restore_test -tAc "SELECT count(*) FROM orders;"184233# MySQLmysqldump --single-transaction --routines --triggers intranet | gzip > /backup/intranet-$(date +%F).sql.gzmysql -e "CREATE DATABASE intranet_restore_test" && zcat /backup/intranet-$(date +%F).sql.gz | mysql intranet_restore_testsqlcmd -S srv-sql-01 -E -Q "BACKUP DATABASE ERP TO DISK='E:\Backup\ERP_2026-10-22.bak' WITH COMPRESSION, CHECKSUM, STATS=10"10 percent processed. ... 100 percent processed.BACKUP DATABASE successfully processed 4891264 pages in 98.412 seconds (388.3 MB/sec).sqlcmd -S srv-sql-01 -E -Q "RESTORE VERIFYONLY FROM DISK='E:\Backup\ERP_2026-10-22.bak' WITH CHECKSUM"The backup set on file 1 is valid.sqlcmd -S srv-sql-01 -E -Q "RESTORE DATABASE ERP_restore_test FROM DISK='E:\Backup\ERP_2026-10-22.bak' WITH MOVE 'ERP' TO 'E:\Restore\ERP_test.mdf', MOVE 'ERP_log' TO 'E:\Restore\ERP_test.ldf', STATS=25"A backup that has never been restored is a hope, not a backup. The test restore into another database and a count(*) of the main table is what you note in the monthly backup ticket (and what repo 33 on my GitHub, backup-restore-test-tracker, follows over time). If the server already has Veeam or a backup agent (Level 2), these commands are the verification, not the substitute. Drop the *_restore_test databases when done.
3. Disk space and growth
psql -h localhost -U postgres -d appdb -c "SELECT relname, pg_size_pretty(pg_total_relation_size(oid)) AS size, n_dead_tup FROM pg_stat_user_tables t JOIN pg_class c ON c.oid=t.relid ORDER BY pg_total_relation_size(oid) DESC LIMIT 3;" relname | size | n_dead_tup--------------+---------+------------ audit_log | 1412 MB | 2203110 orders | 611 MB | 8120psql -h localhost -U postgres -d appdb -c "VACUUM (VERBOSE, ANALYZE) audit_log;" 2>&1 | tail -1du -sh /var/lib/pgsql/16/data/pg_wal3.1G /var/lib/pgsql/16/data/pg_walsqlcmd -S srv-sql-01 -E -d ERP -Q "SELECT name, size*8/1024 AS mb, CAST(FILEPROPERTY(name,'SpaceUsed') AS int)*8/1024 AS used_mb FROM sys.database_files" -Wname mb used_mbERP 36120 29877ERP_log 2090 1912sqlcmd -S srv-sql-01 -E -Q "SELECT name, recovery_model_desc, log_reuse_wait_desc FROM sys.databases WHERE name='ERP'" -Wname recovery_model_desc log_reuse_wait_descERP FULL LOG_BACKUPThe three usual culprits for a disk at 92%: in PostgreSQL, a table with millions of dead tuples without VACUUM (and pg_wal growing because a replication slot was orphaned); in MySQL, binlogs without expire_logs_days/binlog_expire_logs_seconds; in SQL Server, the transaction log in FULL recovery with no log backups: log_reuse_wait_desc = LOG_BACKUP says so, and the fix is scheduling BACKUP LOG, not SHRINKFILE every week.
4. User joiners and leavers
# PostgreSQL: read-only role for the analystpsql -h localhost -U postgres -d appdb -c "CREATE ROLE laura_prado LOGIN PASSWORD 'change-on-first-login'; GRANT CONNECT ON DATABASE appdb TO laura_prado; GRANT USAGE ON SCHEMA public TO laura_prado; GRANT SELECT ON ALL TABLES IN SCHEMA public TO laura_prado; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO laura_prado;"# MySQLmysql -e "CREATE USER 'laura_prado'@'10.20.%' IDENTIFIED BY 'change-on-first-login' PASSWORD EXPIRE; GRANT SELECT ON intranet.* TO 'laura_prado'@'10.20.%';"# leaver: revoke and, if they own no objects, droppsql -h localhost -U postgres -d appdb -c "REASSIGN OWNED BY j_navarro TO postgres; DROP OWNED BY j_navarro; DROP ROLE j_navarro;"sqlcmd -S srv-sql-01 -E -Q "CREATE LOGIN [CONTOSO\sg-erp-readonly] FROM WINDOWS; USE ERP; CREATE USER [CONTOSO\sg-erp-readonly] FOR LOGIN [CONTOSO\sg-erp-readonly]; ALTER ROLE db_datareader ADD MEMBER [CONTOSO\sg-erp-readonly];"The rule that holds for all three: group, not person, whenever the engine allows it. SQL Server with an AD group is the ideal: the leaver is handled in Active Directory (Level 1) and the database is never touched. In PostgreSQL and MySQL, a read role with GRANT to named users, and the user list in the quarterly access review.
5. "The application is slow": what is happening right now
psql -h localhost -U postgres -d appdb -c "SELECT pid, now()-query_start AS runtime, state, wait_event_type, left(query,60) AS query FROM pg_stat_activity WHERE state <> 'idle' AND pid <> pg_backend_pid() ORDER BY runtime DESC LIMIT 5;" pid | runtime | state | wait_event_type | query-------+-----------------+--------+-----------------+---------------------------------------------- 18211 | 00:14:02.118 | active | Lock | UPDATE orders SET status='X' WHERE customer_id=… 18240 | 00:13:58.004 | active | Lock | SELECT * FROM orders WHERE customer_id=… 17990 | 00:14:40.771 | idle in transaction | | BEGIN; UPDATE orders SET …psql -h localhost -U postgres -d appdb -c "SELECT pg_terminate_backend(17990);"mysql -e "SELECT id, user, time, state, LEFT(info,60) FROM information_schema.processlist WHERE command <> 'Sleep' ORDER BY time DESC LIMIT 5;"sqlcmd -S srv-sql-01 -E -Q "SELECT r.session_id, r.blocking_session_id, r.wait_type, r.wait_time/1000 AS wait_s, LEFT(t.text,60) AS sql_text FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE r.session_id > 50 ORDER BY r.wait_time DESC" -Wsession_id blocking_session_id wait_type wait_s sql_text71 58 LCK_M_U 812 UPDATE Invoices SET Status = …58 0 NULL 0 (idle session with an open transaction)The pattern is identical in all three: an idle session with an open transaction (someone opened a tool, ran BEGIN and went to lunch) blocks everyone else. pg_stat_activity, processlist and dm_exec_requests give you the pid of whoever is blocking; killing that session (pg_terminate_backend, KILL id, KILL session_id) brings the application back to life, and the ticket to the developer says which query and from which client. If there are no locks and it is a heavy query, EXPLAIN ANALYZE in PostgreSQL/MySQL or the execution plan in SSMS is the next step, and that one does belong to the DBA or the developer.
6. What you review each month
- Test restore of each engine, with the
count(*)noted. - Space: fastest-growing tables,
pg_wal/binlogs/transaction log, and the six-month forecast from the trend. - Users: named accounts no longer in Active Directory; application account passwords in the manager with a date.
- Engine patches: PostgreSQL and MySQL with the monthly Ansible patching; SQL Server with its quarterly CU in the change window, with a backup first.
Next steps
- Monitoring:
postgres_exporter,mysqld_exporterand the SQL Server one in the Prometheus from the Grafana guide; the "blocked sessions > 0 for 5 minutes" panel warns you before the user does. - High availability: streaming replica (PostgreSQL), replication (MySQL) and Always On (SQL Server) when the business won't accept an hour of downtime; before that, the tested restore already covers what matters.
- Integrated authentication in PostgreSQL with Kerberos (Linux + AD guide): the same AD leaver closes the database too.
Is something missing, or different in your environment? Get in touch and I'll fix it.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.
Apúntate y llévate las 33 ideasSign up and get the 33 ideas →