Si dos personas compran la última unidad, leer el stock y descontarlo más tarde permite que ambas crean que hay disponibilidad. Vamos a reservar producto y registrar el pedido en una misma operación de PostgreSQL. El caso sirve para entradas, artículos virtuales o cupos; no incluye pagos ni la entrega del producto.
Última revisión: 2 de octubre de 2026. Probado con: PostgreSQL 17.6, en una transacción con tablas temporales. Se verificaron la función, el descuento de stock, el rechazo por falta de unidades y el rollback. Referencia: documentación de PostgreSQL 18. La prueba de concurrencia con dos sesiones se describe para reproducirla; no se ejecutó durante esta revisión.
Crea un laboratorio separado
Usa una base de pruebas vacía y una cuenta con permisos para crear tablas y funciones. No ejecutes estas sentencias sobre tablas reales de tu tienda. Puedes abrirla con psql o con el editor SQL de tu proveedor. Comprueba SELECT version(); para registrar tu motor.
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
stock integer NOT NULL CHECK (stock >= 0)
);
CREATE TABLE reservations (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_id bigint NOT NULL REFERENCES products(id),
buyer_id bigint NOT NULL,
quantity integer NOT NULL CHECK (quantity > 0),
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO products (name, stock) VALUES ('Entrada al evento', 1);La clave primaria crea el índice que localizará el producto. CHECK impide stock negativo y cantidades inválidas incluso si otro código escribe directamente. La referencia de reserva exige un producto existente. buyer_id es un identificador de ejemplo: en tu aplicación debe venir de la sesión autorizada y referenciar la tabla real de usuarios.

Descuenta y registra juntos
Crea esta función en la base de laboratorio:
CREATE FUNCTION reserve_product(p_product bigint, p_buyer bigint, p_quantity integer)
RETURNS bigint
LANGUAGE plpgsql
AS $$
DECLARE
v_product bigint;
v_reservation bigint;
BEGIN
IF p_quantity IS NULL OR p_quantity <= 0 OR p_buyer IS NULL THEN
RAISE EXCEPTION 'Comprador o cantidad inválidos';
END IF;
UPDATE products
SET stock = stock - p_quantity
WHERE id = p_product AND stock >= p_quantity
RETURNING id INTO v_product;
IF NOT FOUND THEN
RAISE EXCEPTION 'Producto inexistente o stock insuficiente';
END IF;
INSERT INTO reservations (product_id, buyer_id, quantity)
VALUES (v_product, p_buyer, p_quantity)
RETURNING id INTO v_reservation;
RETURN v_reservation;
END;
$$;La condición stock >= p_quantity está en el UPDATE que descuenta, no en una consulta anterior. En Read Committed, el segundo UPDATE que compite por la fila espera; al confirmarse el primero, PostgreSQL vuelve a evaluar la condición sobre la fila actualizada.
RETURNING entrega el producto que realmente se modificó. NOT FOUND detecta que no se descontó ninguna fila. La excepción aborta la operación: si el INSERT falla después del descuento, el descuento tampoco queda confirmado. No añadimos SECURITY DEFINER: la función conserva los permisos del llamador.
Reproduce la competencia
Abre dos sesiones de la misma base. En la primera ejecuta:
BEGIN;
SELECT reserve_product(1, 101, 1);
-- Mantén esta transacción abierta mientras pruebas la segunda sesión.En la segunda ejecuta SELECT reserve_product(1, 202, 1);. Debe esperar. Vuelve a la primera y ejecuta COMMIT;. La segunda debe responder con stock insuficiente. Si hubieras ejecutado ROLLBACK; en la primera, la segunda podría reservar la unidad.
Consulta el resultado:
SELECT id, stock FROM products;
SELECT product_id, buyer_id, quantity FROM reservations;Debe quedar stock cero y una sola reserva. Estos ids corresponden a una base nueva; si ya insertaste filas, utiliza el id devuelto por INSERT ... RETURNING id.
Para comprobar rollback sin otra sesión, inicia una transacción, reserva y ejecuta ROLLBACK; las consultas deben mostrar el stock original y ninguna reserva de esa ejecución. Una secuencia puede dejar huecos tras un rollback: el número de id no cuenta pedidos confirmados.
Lleva la regla al backend
Llama a la función con parámetros del driver, nunca concatenando texto del usuario en SQL. El backend debe comprobar la identidad, autorización y cantidad antes de llamar; las restricciones de la base son una segunda defensa. Devuelve un error de negocio claro cuando no hay stock y no repitas una compra automáticamente sin una clave de idempotencia.
No mantengas esta transacción abierta mientras consultas una pasarela de pago. Una reserva necesita caducidad y una operación de liberación diseñada por separado. Si reservas varios productos, bloquea en orden consistente y maneja deadlocks; esta solución de una fila no garantiza reglas que involucran varias filas bajo todos los niveles de aislamiento.
Si ves «current transaction is aborted» después de una excepción dentro de BEGIN, ejecuta ROLLBACK antes de continuar. Si la operación espera demasiado, revisa la sesión que mantiene la fila bloqueada. El ejemplo deliberadamente reúne toda la lógica en una sola llamada para evitar que la aplicación confirme el descuento antes del pedido.
Fuentes y documentación
Proyecto y desarrollador: PostgreSQL, PostgreSQL Global Development Group. Descarga: postgresql.org. Licencia: PostgreSQL License. Código y gráficos originales de The Silencers.






Comentarios 0
Únete a la conversación con tu cuenta.
Inicia sesiónCargando comentarios…