2010-01-10 12 views
6

En una base de datos PostgreSQL tengo una tabla con una clave principal y otro campo que debe ser único.¿hay alguna manera de evitar llamar a nextval() si la inserción falla en PostgreSQL?

CREATE TABLE users (
    id  INTEGER PRIMARY KEY DEFAULT nextval('groups_id_seq'::regclass), 
    name VARCHAR(255) UNIQUE NOT NULL 
); 

    INSERT users (name) VALUES ('foo'); 
    INSERT users (name) VALUES ('foo'); 
    INSERT users (name) VALUES ('bar'); 

El segundo inserto falla pero la groups_id_seq secuencia ya se incrementa de modo que cuando 'bar' se añade que deja un hueco en los números de ID.

¿Hay alguna manera de decirle a PostgreSQL que obtenga el siguiente valor solo si se cumplen otras restricciones o debo verificar primero con SELECT si el nombre no está duplicado? Esto aún no garantizaría la ausencia de lagunas, pero al menos reduciría su número a los raros casos en que hay otro proceso que intenta insertar el mismo nombre al mismo tiempo

+0

Ver también: http://stackoverflow.com/a/9985219/398670 –

+0

Posible duplicado de [Huecos entre la identificación de la clave principal en la tabla sql] (http://stackoverflow.com/questions/39099905/gaps-between- primary-key-id-in-sql-table) – e4c5

Respuesta

12

No lo creo: una característica básica de secuencias es que las lagunas son posibles (piense en dos transacciones simultáneas, con una realización de un ROLLBACK). Debes ignorar las lagunas. ¿Por qué son un problema en tu caso?

+0

No es realmente un problema, simplemente me sorprendió verlo y si se puede evitar en su mayoría, ¿por qué no invertir la energía para aprender cómo? Sé que no puedo evitarlo al 100%. – szabgab

+5

No, no se pueden evitar. Hay otros casos que también pueden causar lagunas bastante importantes en las secuencias: simplemente diseñe su aplicación en torno al hecho de que las secuencias tienen la garantía de seguir incrementándose (hasta que llegue al máximo para su tipo de datos, 32 o 64 bits), pero no está garantizado. gapless –

+1

@Magnus: se pueden evitar, pero a un gran costo en términos de complejidad (ver mi respuesta). Estoy totalmente de acuerdo en que es mejor diseñar para permitir las brechas si es posible. –

6

Si necesita secuencias sin intervalos, hay formas de hacerlo, pero no es trivial, y definitivamente mucho más lento.

Además, si le preocupa "usar demasiados identificadores", simplemente defina id como bigserial.

5

Es posible, aunque engorroso, hacer esto. Como bortzmeyer says, es peligroso confiar en los valores de las secuencias que son contiguas, por lo que es mejor dejar las cosas como están si es posible.

Si no puede:

Cada acceso a la tabla que podría causar una fila para tener un cierto nombre (es decir, todos los INSERT a la mesa, y si usted lo permite (aunque es una mala práctica) cada UPDATE que podría cambiar el campo name) debe hacerlo dentro de una transacción que bloquee primero todo. La opción más simple y menos eficiente es simplemente bloquear toda la tabla usando LOCK users IN EXCLUSIVE MODE (agregar las últimas 3 palabras permite el acceso de lectura concurrente por otros procesos, lo cual es seguro).

Sin embargo, es un bloqueo muy grueso que ralentizará el rendimiento si hay muchas modificaciones simultáneas en users; una mejor opción sería bloquear una única fila correspondiente en otra tabla que ya debe existir. Esta fila se puede bloquear con SELECT ... FOR UPDATE. Esto tiene sentido solo cuando se trabaja con una tabla "secundaria" que tiene una dependencia FK en otra tabla "principal".

Por ejemplo, imagina por el momento que estamos tratando de crear de forma segura orders para un customer, y que estas órdenes de alguna manera tienen 'nombres' identificativos. (Lo sé, pobre ejemplo ...) orders tiene una dependencia de FK en customers. A continuación, para evitar que alguna vez la creación de dos órdenes con el mismo nombre para un cliente determinado, se puede hacer lo siguiente:

BEGIN; 

-- Customer 'jbloggs' must exist for this to work. 
SELECT 1 FROM customers 
WHERE id = 'jbloggs' 
FOR UPDATE 

-- Provided every attempt to create an order performs the above step first, 
-- at this point, we will have exclusive access to all orders for jbloggs. 
SELECT 1 FROM orders 
WHERE id = 'jbloggs' 
AND order_name = 'foo' 

-- Determine if the preceding query returned a row or not. 
-- If it did not: 
INSERT orders (id, name) VALUES ('jbloggs', 'foo'); 

-- Regardless, end the transaction: 
END; 

en cuenta que es no suficiente para simplemente bloquear la fila correspondiente en users con SELECT ... FOR UPDATE - si la fila aún no existe, varios procesos concurrentes pueden informar simultáneamente que la fila no existe, y luego intentar inserciones simultáneas, lo que da como resultado transacciones fallidas y, por lo tanto, espacios de secuencia.

Cualquiera de los dos esquemas de bloqueo funcionará; lo importante es que cualquiera que intente crear una fila con el mismo nombre debe intentar bloquear el mismo objeto.

Cuestiones relacionadas