Inserte datos en 3 tablas a la vez usando Postgres

81

Quiero insertar datos en 3 tablas con una sola consulta.
Mis tablas se ven a continuación:

CREATE TABLE sample (
   id        bigserial PRIMARY KEY,
   lastname  varchar(20),
   firstname varchar(20)
);

CREATE TABLE sample1(
   user_id    bigserial PRIMARY KEY,
   sample_id  bigint REFERENCES sample,
   adddetails varchar(20)
);

CREATE TABLE sample2(
   id      bigserial PRIMARY KEY,
   user_id bigint REFERENCES sample1,
   value   varchar(10)
);

Recibiré una clave a cambio de cada inserción y necesito insertar esa clave en la siguiente tabla.
Mi consulta es:

insert into sample(firstname,lastname) values('fai55','shaggk') RETURNING id;
insert into sample1(sample_id, adddetails) values($id,'ss') RETURNING user_id;
insert into sample2(user_id, value) values($id,'ss') RETURNING id;

Pero si ejecuto consultas únicas, simplemente me devuelven valores y no puedo reutilizarlos en la siguiente consulta inmediatamente.

¿Cómo lograrlo?

Faisal
fuente

Respuestas:

131

Utilice CTE de modificación de datos :

WITH ins1 AS (
   INSERT INTO sample(firstname, lastname)
   VALUES ('fai55', 'shaggk')
-- ON     CONFLICT DO NOTHING         -- optional addition in Postgres 9.5+
   RETURNING id AS sample_id
   )
, ins2 AS (
   INSERT INTO sample1 (sample_id, adddetails)
   SELECT sample_id, 'ss' FROM ins1
   RETURNING user_id
   )
INSERT INTO sample2 (user_id, value)
SELECT user_id, 'ss2' FROM ins2;

Cada uno INSERTdepende del anterior. SELECTen lugar de, VALUESse asegura de que no se inserte nada en las tablas subsidiarias si no se devuelve ninguna fila de una anterior INSERT. (Desde Postgres 9.5+, puede agregar un ON CONFLICT.)
También es un poco más corto y rápido de esta manera.

Por lo general, es más conveniente proporcionar filas de datos completas en un solo lugar :

WITH data(firstname, lastname, adddetails, value) AS (
   VALUES                              -- provide data here
      ('fai55', 'shaggk', 'ss', 'ss2') -- see below
    , ('fai56', 'XXaggk', 'xx', 'xx2') -- works for multiple input rows
       --  more?                      
   )
, ins1 AS (
   INSERT INTO sample (firstname, lastname)
   SELECT firstname, lastname          -- DISTINCT? see below
   FROM   data
   -- ON     CONFLICT DO NOTHING       -- UNIQUE constraint? see below
   RETURNING firstname, lastname, id AS sample_id
   )
, ins2 AS (
   INSERT INTO sample1 (sample_id, adddetails)
   SELECT ins1.sample_id, d.adddetails
   FROM   data d
   JOIN   ins1 USING (firstname, lastname)
   RETURNING sample_id, user_id
   )
INSERT INTO sample2 (user_id, value)
SELECT ins2.user_id, d.value
FROM   data d
JOIN   ins1 USING (firstname, lastname)
JOIN   ins2 USING (sample_id);

db <> violín aquí

Es posible que necesite conversiones de tipos explícitas en una VALUESexpresión independiente , a diferencia de una VALUESexpresión adjunta a una INSERTdonde los tipos de datos se derivan de la tabla de destino. Ver:

Si varias filas pueden tener idénticas (firstname, lastname), es posible que deba doblar duplicados para la primera INSERT:

...
INSERT INTO sample (firstname, lastname)
SELECT DISTINCT firstname, lastname FROM data
...

Puede utilizar una tabla (temporal) como fuente de datos en lugar del CTE data.

Probablemente tendría sentido combinar esto con una restricción ÚNICA (firstname, lastname)en la tabla y una ON CONFLICTcláusula en la consulta.

Relacionado:

Erwin Brandstetter
fuente
1
gracias por la repetición, ¿puedo agregar el despliegue de la transacción si se produce alguna inserción fallida? Sí, ¿cómo puedo?
Faisal
3
Esta es una sola declaración SQL. Se pueden agrupar varias declaraciones en una sola transacción, pero no se puede dividir esta. Además, lo que dice Denis en su comentario. Y agregué algunos enlaces a mi respuesta.
Erwin Brandstetter
2
@mmcrae: Sí, puedes. Relacionado: dba.stackexchange.com/questions/151199/…
Erwin Brandstetter
1
@No_name: claro, de varias formas. Le sugiero que haga una pregunta con detalles definitorios. siempre puede vincular aquí para el contexto. o dejar un comentario aquí enlazando para llamar mi atención.
Erwin Brandstetter
1
¿Es esto un error tipográfico? En tu respuesta INSERT INTO sample1 (user_id, adddetails), ¿no debería serlo (sample_id, addetails)?
Adam Hughes
17

Algo como esto

with first_insert as (
   insert into sample(firstname,lastname) 
   values('fai55','shaggk') 
   RETURNING id
), 
second_insert as (
  insert into sample1( id ,adddetails) 
  values
  ( (select id from first_insert), 'ss')
  RETURNING user_id
)
insert into sample2 ( id ,adddetails) 
values 
( (select user_id from first_insert), 'ss');

Como sample2no se necesita la identificación generada del inserto en , eliminé la returningcláusula del último inserto.

un caballo sin nombre
fuente
Me gusta este enfoque con valores internos seleccionados. Es más consistente y también puede colocar los alias de retorno dentro de las declaraciones with
mattdlockyer
6

Normalmente, usaría una transacción para evitar escribir consultas complicadas.

http://www.postgresql.org/docs/current/static/sql-begin.html

http://dev.mysql.com/doc/refman/5.7/en/commit.html

También puede usar un CTE, asumiendo que su etiqueta de Postgres es correcta. Por ejemplo:

with sample_ids as (
  insert into sample(firstname, lastname)
  values('fai55','shaggk')
  RETURNING id
), sample1_ids as (
  insert into sample1(id, adddetails)
  select id,'ss'
  from sample_ids
  RETURNING id, user_id
)
insert into sample2(id, user_id, value)
select id, user_id, 'val'
from sample1_ids
RETURNING id, user_id;
Denis de Bernardy
fuente
1
gracias, ¿cómo podría lograr la transacción en esta consulta si falla alguna inserción? Podría hacer una reversión
Faisal
1
Luego, comienza todo de nuevo, después de corregir las consultas, por supuesto, ya que toda la transacción (o el cte) se revertiría. Por cierto, si sus inserciones fallan ocasionalmente, probablemente esté haciendo algo mal. El único caso en el que es razonable que un inserto falle es en un escenario de upsert que se encuentra con claves únicas duplicadas durante transacciones simultáneas, e incluso entonces podría obtener un bloqueo de aviso o un bloqueo de tabla si necesita hacer las cosas a prueba de balas.
Denis de Bernardy
3

Puede crear un desencadenador de inserción posterior en la tabla Sample para insertarlo en las otras dos tablas.

El único problema que veo al hacer esto es que no tendrá una forma de insertar detalles adicionales, siempre estará vacío o, en este caso, ss. No hay forma de insertar una columna en una muestra que no esté actualmente en la tabla de muestras, por lo que no puede enviarla junto con la inserción inicial.

Otra opción sería crear un procedimiento almacenado para ejecutar sus inserciones.

Tiene la pregunta taged mysql y postgressql ¿de qué base de datos estamos hablando aquí?

DaImTo
fuente