Docente: Ing. María Eugenia Weibel
Una transacción es una unidad de trabajo lógica que agrupa una o más operaciones de base de datos que deben ejecutarse como un todo indivisible. En PostgreSQL, las transacciones garantizan que los datos permanezcan consistentes incluso cuando múltiples usuarios acceden simultáneamente a la base de datos.
-- Inicio de transacción
BEGIN;
-- Operaciones de la transacción
INSERT INTO accounts (id, balance, name) VALUES (1, 1000, 'Juan');
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Finalización exitosa
COMMIT;
-- O cancelación (rollback)
ROLLBACK;
Concepto: “Todo o nada” - Una transacción se ejecuta completamente o no se ejecuta en absoluto.
Ejemplo:
BEGIN;
-- Transferencia bancaria: debe ser atómica
UPDATE accounts SET balance = balance - 500 WHERE id = 1; -- Débito
UPDATE accounts SET balance = balance + 500 WHERE id = 2; -- Crédito
COMMIT; -- Ambas operaciones o ninguna
Implementación en PostgreSQL: - WAL (Write-Ahead Logging) - Rollback automático en caso de error - Puntos de guardado (savepoints)
Concepto: Los datos deben mantener todas las reglas de integridad antes y después de la transacción.
Tipos de Consistencia: - Consistencia de Entidad: Claves primarias únicas - Consistencia Referencial: Claves foráneas válidas - Consistencia de Dominio: Tipos de datos y restricciones - Consistencia Definida por Usuario: Triggers y checks
Ejemplo:
-- Restricción que mantiene consistencia
ALTER TABLE accounts ADD CONSTRAINT positive_balance
CHECK (balance >= 0);
BEGIN;
UPDATE accounts SET balance = balance - 1500 WHERE id = 1;
-- Si balance < 0, la transacción falla manteniendo consistencia
COMMIT;
Concepto: Las transacciones concurrentes no deben interferir entre sí.
Implementación: - Niveles de aislamiento configurables - Sistema de locks multinivel - MVCC (MultiVersion Concurrency Control)
Concepto: Una vez confirmada, los cambios persisten incluso ante fallos del sistema.
Mecanismos: - WAL (Write-Ahead Logging) - Checkpoints periódicos - Sincronización a disco
Características: - Nivel más bajo de aislamiento - Permite lecturas sucias (dirty reads) - Raramente usado en producción
Permite: - ✅ Dirty Reads - ✅ Non-Repeatable
Reads
- ✅ Phantom Reads
Configuración:
BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM accounts; -- Puede leer datos no confirmados
COMMIT;
Características: - Nivel por defecto en PostgreSQL - Previene lecturas sucias - Balance entre consistencia y rendimiento
Permite: - ❌ Dirty Reads - ✅ Non-Repeatable Reads - ✅ Phantom Reads
Ejemplo:
-- Sesión 1
BEGIN;
UPDATE accounts SET balance = 1500 WHERE id = 1;
-- No hace COMMIT todavía
-- Sesión 2 (READ COMMITTED)
SELECT balance FROM accounts WHERE id = 1;
-- Verá el valor original (1000), no 1500
Características: - Garantiza lecturas repetibles dentro de la transacción - Utiliza snapshot de inicio de transacción - Previene dirty reads y non-repeatable reads
Permite: - ❌ Dirty Reads - ❌ Non-Repeatable Reads - ✅ Phantom Reads
Ejemplo:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- Resultado: 10
-- Otra sesión inserta un registro
-- INSERT INTO accounts VALUES (99, 600, 'Ana');
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- Resultado: 10 (igual)
COMMIT;
Características: - Nivel más alto de aislamiento - Emula ejecución serial de transacciones - Previene todos los problemas de concurrencia
Permite: - ❌ Dirty Reads - ❌ Non-Repeatable Reads - ❌ Phantom Reads
Implementación:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- PostgreSQL usa Serializable Snapshot Isolation (SSI)
SELECT * FROM accounts WHERE balance > 1000;
UPDATE accounts SET balance = balance * 1.1 WHERE balance > 1000;
COMMIT;
Definición: Una transacción lee datos modificados por otra transacción que aún no ha hecho commit.
Problema:
-- Transacción A
BEGIN;
UPDATE accounts SET balance = 2000 WHERE id = 1;
-- No hace COMMIT
-- Transacción B (READ UNCOMMITTED)
SELECT balance FROM accounts WHERE id = 1; -- Lee 2000
-- Transacción A
ROLLBACK; -- Los cambios se pierden, B leyó datos "sucios"
Prevención: Usar READ COMMITTED o superior.
Definición: Una transacción lee el mismo dato dos veces y obtiene valores diferentes.
Problema:
-- Transacción A (READ COMMITTED)
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Lee 1000
-- Transacción B
UPDATE accounts SET balance = 1500 WHERE id = 1;
COMMIT;
-- Transacción A (continuando)
SELECT balance FROM accounts WHERE id = 1; -- Ahora lee 1500
COMMIT;
Prevención: Usar REPEATABLE READ o SERIALIZABLE.
Definición: Una consulta ejecutada dos veces retorna conjuntos de registros diferentes.
Problema:
-- Transacción A (REPEATABLE READ)
BEGIN;
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- Resultado: 5
-- Transacción B
INSERT INTO accounts VALUES (100, 600, 'Carlos');
COMMIT;
-- Transacción A (continuando)
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- Resultado: 6
COMMIT;
Prevención: Usar SERIALIZABLE.
Definición: Dos transacciones actualizan el mismo dato y una sobrescribe a la otra.
Problema y Solución:
-- Problema: Actualización perdida
-- T1: balance = 1000, lee valor
-- T2: balance = 1000, lee valor
-- T1: actualiza a 1100 (1000 + 100)
-- T2: actualiza a 1200 (1000 + 200) -- ¡Perdió el cambio de T1!
-- Solución: Usar SELECT FOR UPDATE
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- Esto bloquea el registro para otras transacciones
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;
FOR UPDATE:
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- Bloqueo exclusivo: nadie más puede modificar esta fila
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;
FOR SHARE:
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- Bloqueo compartido: otros pueden leer, pero no modificar
-- Operaciones de lectura...
COMMIT;
FOR NO KEY UPDATE:
-- Permite actualizaciones concurrentes que no afecten claves
SELECT * FROM accounts WHERE id = 1 FOR NO KEY UPDATE;
FOR KEY SHARE:
-- Bloqueo más débil, solo previene cambios en claves
SELECT * FROM accounts WHERE id = 1 FOR KEY SHARE;
-- Bloqueo explícito de tabla
BEGIN;
LOCK TABLE accounts IN SHARE MODE;
-- Solo permite lecturas concurrentes
SELECT * FROM accounts;
COMMIT;
-- Otros modos de bloqueo de tabla:
LOCK TABLE accounts IN ACCESS SHARE MODE; -- Más permisivo
LOCK TABLE accounts IN ROW SHARE MODE;
LOCK TABLE accounts IN ROW EXCLUSIVE MODE;
LOCK TABLE accounts IN SHARE UPDATE EXCLUSIVE MODE;
LOCK TABLE accounts IN SHARE MODE;
LOCK TABLE accounts IN SHARE ROW EXCLUSIVE MODE;
LOCK TABLE accounts IN EXCLUSIVE MODE;
LOCK TABLE accounts IN ACCESS EXCLUSIVE MODE; -- Más restrictivo
-- Bloqueos definidos por la aplicación
SELECT pg_advisory_lock(123456);
-- Lógica de aplicación que requiere sincronización
SELECT pg_advisory_unlock(123456);
-- Versión que no bloquea
SELECT pg_try_advisory_lock(123456); -- Retorna true/false
PostgreSQL usa MVCC para permitir alta concurrencia:
Funcionamiento: - Cada fila tiene información de versión (xmin, xmax) - Las transacciones ven snapshots consistentes - No hay bloqueos de lectura tradicionales - Los writers no bloquean a los readers
Ejemplo de Versionado:
-- Estado inicial: (xmin=100, xmax=null, data='Juan, 1000')
-- Transacción 101 actualiza:
UPDATE accounts SET balance = 1100 WHERE id = 1;
-- Nuevo registro: (xmin=101, xmax=null, data='Juan, 1100')
-- Registro anterior: (xmin=100, xmax=101, data='Juan, 1000')
-- Las transacciones ven la versión apropiada según su snapshot
Un deadlock ocurre cuando dos o más transacciones se bloquean mutuamente, esperando recursos que la otra tiene.
-- Sesión 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- Lock en fila 1
-- Esperando...
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- Intenta lock en fila 2
-- Sesión 2 (ejecutándose simultáneamente)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- Lock en fila 2
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- Intenta lock en fila 1
-- ¡DEADLOCK!
PostgreSQL automáticamente: 1. Detecta deadlocks usando un detector de ciclos 2. Elige una víctima (transacción más barata de abortar) 3. Aborta la transacción víctima con error 4. Permite que la otra transacción continúe
Configuración:
-- Tiempo antes de detectar deadlock (por defecto: 1 segundo)
SET deadlock_timeout = '2s';
-- Ver deadlocks en el log
SET log_lock_waits = on;
-- Siempre acceder a tablas en el mismo orden
-- Mal: tabla A, luego tabla B en una transacción; tabla B, luego A en otra
-- Bien: siempre tabla A, luego tabla B
-- Minimizar el tiempo de bloqueo
BEGIN;
-- Operaciones rápidas y específicas
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT; -- Confirmar rápidamente
-- Configurar timeout de transacción
SET statement_timeout = '30s';
-- Iniciar transacción
BEGIN;
START TRANSACTION;
-- Confirmar cambios
COMMIT;
-- Cancelar cambios
ROLLBACK;
-- Transacción con nivel de aislamiento específico
BEGIN ISOLATION LEVEL SERIALIZABLE;
BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
-- Transacción de solo lectura
BEGIN READ ONLY;
-- Transacción con características múltiples
BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
BEGIN;
INSERT INTO accounts VALUES (1, 1000, 'Juan');
SAVEPOINT sp1;
INSERT INTO accounts VALUES (2, 500, 'María');
-- Error en esta operación
ROLLBACK TO SAVEPOINT sp1;
-- Solo revierte hasta el savepoint, mantiene la primera inserción
INSERT INTO accounts VALUES (2, 600, 'María'); -- Corregido
COMMIT;
-- A nivel de sesión
SET default_transaction_isolation = 'repeatable read';
SET default_transaction_read_only = on;
SET default_transaction_deferrable = on;
-- A nivel de base de datos
ALTER DATABASE mydb SET default_transaction_isolation = 'serializable';
-- A nivel de usuario
ALTER ROLE myuser SET default_transaction_isolation = 'repeatable read';
-- ✅ BIEN: Transacciones cortas y específicas
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- ❌ MAL: Transacciones largas con lógica de aplicación
BEGIN;
SELECT * FROM accounts WHERE id = 1;
-- ... lógica compleja en la aplicación ...
-- ... espera por input del usuario ...
UPDATE accounts SET balance = new_balance WHERE id = 1;
COMMIT;
-- Estructura recomendada para manejo de errores
DO $$
DECLARE
account_id INTEGER := 1;
transfer_amount DECIMAL := 100;
BEGIN
-- Iniciar transacción implícita
UPDATE accounts SET balance = balance - transfer_amount
WHERE id = account_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Cuenta % no encontrada', account_id;
END IF;
UPDATE accounts SET balance = balance + transfer_amount
WHERE id = 2;
-- Si todo va bien, confirma automáticamente
EXCEPTION
WHEN OTHERS THEN
-- Rollback automático en caso de error
RAISE NOTICE 'Error en transferencia: %', SQLERRM;
RAISE; -- Re-lanza el error
END $$;
-- Usar SELECT FOR UPDATE solo cuando sea necesario
-- ✅ BIEN: Solo cuando vas a actualizar
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- ❌ MAL: Bloquear innecesariamente
SELECT * FROM accounts FOR UPDATE; -- Bloquea toda la tabla
-- Crear índices apropiados para reducir bloqueos
CREATE INDEX CONCURRENTLY idx_accounts_balance ON accounts(balance);
CREATE INDEX CONCURRENTLY idx_accounts_status ON accounts(status)
WHERE status = 'active';
-- Transferencia bancaria segura
CREATE OR REPLACE FUNCTION transfer_funds(
from_account INTEGER,
to_account INTEGER,
amount DECIMAL
) RETURNS BOOLEAN AS $$
DECLARE
from_balance DECIMAL;
BEGIN
-- Iniciar transacción implícita
-- Bloquear cuenta origen y verificar saldo
SELECT balance INTO from_balance
FROM accounts
WHERE id = from_account
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Cuenta origen % no existe', from_account;
END IF;
IF from_balance < amount THEN
RAISE EXCEPTION 'Saldo insuficiente: % disponible, % requerido',
from_balance, amount;
END IF;
-- Realizar transferencia
UPDATE accounts SET balance = balance - amount
WHERE id = from_account;
UPDATE accounts SET balance = balance + amount
WHERE id = to_account;
IF NOT FOUND THEN
RAISE EXCEPTION 'Cuenta destino % no existe', to_account;
END IF;
-- Registrar transacción
INSERT INTO transaction_log (from_account, to_account, amount, timestamp)
VALUES (from_account, to_account, amount, NOW());
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
-- El rollback es automático
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
-- Uso
SELECT transfer_funds(1, 2, 150.00);
-- Reserva de productos con control de stock
CREATE OR REPLACE FUNCTION reserve_product(
product_id INTEGER,
quantity INTEGER,
customer_id INTEGER
) RETURNS INTEGER AS $$
DECLARE
current_stock INTEGER;
reservation_id INTEGER;
BEGIN
-- Bloquear producto para verificar stock
SELECT stock INTO current_stock
FROM products
WHERE id = product_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Producto % no existe', product_id;
END IF;
IF current_stock < quantity THEN
RAISE EXCEPTION 'Stock insuficiente: % disponible, % solicitado',
current_stock, quantity;
END IF;
-- Crear reserva
INSERT INTO reservations (product_id, customer_id, quantity, created_at)
VALUES (product_id, customer_id, quantity, NOW())
RETURNING id INTO reservation_id;
-- Reducir stock
UPDATE products
SET stock = stock - quantity
WHERE id = product_id;
RETURN reservation_id;
END;
$$ LANGUAGE plpgsql;
-- Sistema de votación con prevención de votos duplicados
CREATE OR REPLACE FUNCTION cast_vote(
voter_id INTEGER,
candidate_id INTEGER
) RETURNS BOOLEAN AS $$
BEGIN
-- Verificar si ya votó (usando constraint único)
INSERT INTO votes (voter_id, candidate_id, voted_at)
VALUES (voter_id, candidate_id, NOW());
-- Actualizar contador de candidato
UPDATE candidates
SET vote_count = vote_count + 1
WHERE id = candidate_id;
RETURN TRUE;
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'El votante % ya emitió su voto', voter_id;
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
-- Constraint para prevenir votos duplicados
ALTER TABLE votes ADD CONSTRAINT unique_voter
UNIQUE (voter_id);
-- Ver transacciones activas
SELECT
pid,
usename,
application_name,
client_addr,
backend_start,
xact_start,
query_start,
state,
query
FROM pg_stat_activity
WHERE state IN ('active', 'idle in transaction')
ORDER BY xact_start;
-- Ver bloqueos activos
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity
ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
-- Ver estadísticas de deadlocks
SELECT
datname,
deadlocks,
temp_files,
temp_bytes
FROM pg_stat_database
WHERE datname = current_database();
-- Encontrar transacciones que llevan mucho tiempo
SELECT
pid,
now() - xact_start AS transaction_duration,
query,
state
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND now() - xact_start > interval '5 minutes'
ORDER BY transaction_duration DESC;
-- Configurar logging para debugging
ALTER SYSTEM SET log_lock_waits = on;
ALTER SYSTEM SET log_statement = 'all';
ALTER SYSTEM SET log_duration = on;
ALTER SYSTEM SET log_min_duration_statement = 1000; -- ms
ALTER SYSTEM SET deadlock_timeout = '1s';
-- Recargar configuración
SELECT pg_reload_conf();
-- Extensión pg_stat_statements para análisis de consultas
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Ver consultas que más tiempo consumen
SELECT
query,
calls,
total_time,
mean_time,
stddev_time,
rows
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;
Las transacciones garantizan ACID: Atomicidad, Consistencia, Aislamiento y Durabilidad
PostgreSQL usa MVCC: Permite alta concurrencia sin bloquear lecturas
Niveles de aislamiento:
Mantén transacciones cortas: Reduce bloqueos y mejora concurrencia
Maneja errores apropiadamente: Usa bloques de excepción y rollbacks
Monitorea regularmente: Usa vistas del sistema para identificar problemas
-- Control de transacciones
BEGIN; COMMIT; ROLLBACK;
-- Bloqueos explícitos
SELECT ... FOR UPDATE;
SELECT ... FOR SHARE;
-- Configuración de aislamiento
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- Savepoints
SAVEPOINT sp1; ROLLBACK TO sp1;
-- Monitoreo
SELECT * FROM pg_stat_activity;
SELECT * FROM pg_locks;
Este material de estudio cubre todos los aspectos fundamentales de las transacciones en PostgreSQL, desde conceptos básicos hasta técnicas avanzadas de optimización y monitoreo. Úsalo como referencia y guía práctica para implementar transacciones robustas y eficientes en tus aplicaciones.