¿Cómo inserto una fila que contiene una clave foránea?

54

Usando PostgreSQL v9.1. Tengo las siguientes tablas:

CREATE TABLE foo
(
    id BIGSERIAL     NOT NULL UNIQUE PRIMARY KEY,
    type VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE bar
(
    id BIGSERIAL NOT NULL UNIQUE PRIMARY KEY,
    description VARCHAR(40) NOT NULL UNIQUE,
    foo_id BIGINT NOT NULL REFERENCES foo ON DELETE RESTRICT
);

Digamos que la primera tabla foose rellena así:

INSERT INTO foo (type) VALUES
    ( 'red' ),
    ( 'green' ),
    ( 'blue' );

¿Hay alguna forma de insertar filas barfácilmente haciendo referencia a la footabla? ¿O debo hacerlo en dos pasos, primero buscando el footipo que quiero y luego insertando una nueva fila bar?

Aquí hay un ejemplo de pseudocódigo que muestra lo que esperaba que se pudiera hacer:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     SELECT id from foo WHERE type='blue' ),
    ( 'another row', SELECT id from foo WHERE type='red'  );
Stéphane
fuente

Respuestas:

67

Su sintaxis es casi buena, necesita algunos paréntesis alrededor de las subconsultas y funcionará:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     (SELECT id from foo WHERE type='blue') ),
    ( 'another row', (SELECT id from foo WHERE type='red' ) );

Probado en SQL-Fiddle

Otra forma, con una sintaxis más corta si tiene muchos valores para insertar:

WITH ins (description, type) AS
( VALUES
    ( 'more testing',   'blue') ,
    ( 'yet another row', 'green' )
)  
INSERT INTO bar
   (description, foo_id) 
SELECT 
    ins.description, foo.id
FROM 
  foo JOIN ins
    ON ins.type = foo.type ;
ypercubeᵀᴹ
fuente
Tomó leerlo varias veces, pero ahora entiendo esa segunda solución que proporcionó. Me gusta. Utilizándolo ahora para arrancar mi base de datos con un puñado de valores conocidos cuando el sistema aparece por primera vez.
Stéphane
37

INSERTAR llano

INSERT INTO bar (description, foo_id)
SELECT val.description, f.id
FROM  (
   VALUES
      (text 'testing', text 'blue')  -- explicit type declaration; see below
    , ('another row', 'red' )
    , ('new row1'   , 'purple')      -- purple does not exist in foo, yet
    , ('new row2'   , 'purple')
   ) val (description, type)
LEFT   JOIN foo f USING (type);
  • El uso de a en LEFT [OUTER] JOINlugar de [INNER] JOINsignifica que las filas de val no se descartan cuando no se encuentra ninguna coincidencia foo. En cambio, NULLse ingresa para foo_id.

  • La VALUESexpresión en la subconsulta hace lo mismo que el CTE de @ ypercube . Las expresiones de tabla comunes ofrecen características adicionales y son más fáciles de leer en grandes consultas, pero también se presentan como barreras de optimización. Por lo tanto, las subconsultas suelen ser un poco más rápidas cuando no se necesita ninguna de las anteriores.

  • idcomo nombre de columna es un antipatrón generalizado. En caso de ser foo_idy bar_iddescriptiva o nada. Al unir un montón de tablas, terminas con múltiples columnas todas llamadas id...

  • Considere simple texto en varcharlugar de varchar(n). Si realmente necesita imponer una restricción de longitud, agregue una CHECKrestricción:

  • Es posible que deba agregar conversiones de tipo explícito. Dado que la VALUESexpresión no se adjunta directamente a una tabla (como en INSERT ... VALUES ...), los tipos no se pueden derivar y los tipos de datos predeterminados se usan sin una declaración de tipo explícita, que puede no funcionar en todos los casos. Es suficiente hacerlo en la primera fila, el resto se alineará.

INSERTAR filas FK faltantes al mismo tiempo

Si desea crear entradas inexistentes foosobre la marcha, en una sola instrucción SQL , los CTE son instrumentales:

WITH sel AS (
   SELECT val.description, val.type, f.id AS foo_id
   FROM  (
      VALUES
         (text 'testing', text 'blue')
       , ('another row', 'red'   )
       , ('new row1'   , 'purple')
       , ('new row2'   , 'purple')
      ) val (description, type)
   LEFT   JOIN foo f USING (type)
   )
, ins AS ( 
   INSERT INTO foo (type)
   SELECT DISTINCT type FROM sel WHERE foo_id IS NULL
   RETURNING id AS foo_id, type
   )
INSERT INTO bar (description, foo_id)
SELECT sel.description, COALESCE(sel.foo_id, ins.foo_id)
FROM   sel
LEFT   JOIN ins USING (type);

Tenga en cuenta las dos nuevas filas ficticias para insertar. Ambos son de color púrpura , que aún no existe foo. Dos filas para ilustrar la necesidad DISTINCTen la primera INSERTdeclaración.

Explicación paso a paso

  1. El primer CTE selproporciona múltiples filas de datos de entrada. La subconsulta valcon la VALUESexpresión se puede reemplazar con una tabla o subconsulta como fuente. Inmediatamente LEFT JOINpara fooagregar foo_idlas typefilas preexistentes . Todas las demás filas se ponen de foo_id IS NULLesta manera.

  2. El segundo CTE insinserta nuevos tipos distintos ( foo_id IS NULL) fooy devuelve el recién generado foo_id, junto con el typepara volver a unir e insertar filas.

  3. El exterior final INSERTahora puede insertar un foo.id para cada fila: ya sea el tipo preexistente o se insertó en el paso 2.

Estrictamente hablando, ambas inserciones ocurren "en paralelo", pero dado que esta es una declaración única , las FOREIGN KEYrestricciones predeterminadas no se quejarán. La integridad referencial se aplica al final de la declaración por defecto.

SQL Fiddle para Postgres 9.3. (Funciona igual en 9.1.)

Hay una pequeña condición de carrera si ejecuta varias de estas consultas al mismo tiempo. Lea más en preguntas relacionadas aquí y aquí y aquí . Realmente solo ocurre bajo una gran carga concurrente, si alguna vez. En comparación con las soluciones de almacenamiento en caché como se anuncia en otra respuesta, la posibilidad es muy pequeña .

Función para uso repetido

Para uso repetido, crearía una función SQL que toma una matriz de registros como parámetro y la usa unnest(param)en lugar de la VALUESexpresión.

O, si la sintaxis para las matrices de registros es demasiado complicada para usted, use una cadena separada por comas como parámetro _param. Por ejemplo de la forma:

'description1,type1;description2,type2;description3,type3'

Luego use esto para reemplazar la VALUESexpresión en la declaración anterior:

SELECT split_part(x, ',', 1) AS description
       split_part(x, ',', 2) AS type
FROM unnest(string_to_array(_param, ';')) x;


Función con UPSERT en Postgres 9.5

Cree un tipo de fila personalizado para pasar parámetros. Podríamos prescindir de él, pero es más simple:

CREATE TYPE foobar AS (description text, type text);

Función:

CREATE OR REPLACE FUNCTION f_insert_foobar(VARIADIC _val foobar[])
  RETURNS void AS
$func$
   WITH val AS (SELECT * FROM unnest(_val))    -- well-known row type
   ,    ins AS ( 
      INSERT INTO foo AS f (type)
      SELECT DISTINCT v.type                   -- DISTINCT!
      FROM   val v
      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows
      RETURNING f.type, f.id
      )
   INSERT INTO bar AS b (description, foo_id)
   SELECT v.description, COALESCE(f.id, i.id)  -- assuming most types pre-exist
   FROM        val v
   LEFT   JOIN foo f USING (type)              -- already existed
   LEFT   JOIN ins i USING (type)              -- newly inserted
   ON     CONFLICT (description) DO UPDATE     -- description already exists
   SET    foo_id = excluded.foo_id             -- real UPSERT this time
   WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed
$func$  LANGUAGE sql;

Llamada:

SELECT f_insert_foobar(
     '(testing,blue)'
   , '(another row,red)'
   , '(new row1,purple)'
   , '(new row2,purple)'
   , '("with,comma",green)'  -- added to demonstrate row syntax
   );

Rápido y sólido como una roca para entornos con transacciones concurrentes.

Además de las consultas anteriores, esto ...

  • ... aplica SELECTo INSERTactiva foo: typese inserta todo lo que no existe en la tabla FK. Asumiendo que la mayoría de los tipos preexisten. Para estar absolutamente seguro y descartar las condiciones de carrera, las filas existentes que necesitamos están bloqueadas (para que las transacciones concurrentes no puedan interferir). Si eso es demasiado paranoico para su caso, puede reemplazar:

      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows

    con

      ON     CONFLICT(type) DO NOTHING
  • ... aplica INSERTo UPDATE(verdadero "UPSERT") en bar: Si el descriptionya existe, typese actualiza:

      ON     CONFLICT (description) DO UPDATE     -- description already exists
      SET    foo_id = excluded.foo_id             -- real UPSERT this time
      WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed

    Pero solo si typerealmente cambia:

  • ... pasa valores como tipos de fila bien conocidos con un VARIADICparámetro. ¡Tenga en cuenta el máximo predeterminado de 100 parámetros! Comparar:

    Hay muchas otras formas de pasar varias filas ...

Relacionado:

Erwin Brandstetter
fuente
En su INSERT missing FK rows at the same timeejemplo, ¿poner esto en una transacción reduciría el riesgo de condiciones de carrera en SQL Server?
element11
1
@ element11: La respuesta es para Postgres, pero como estamos hablando de un solo comando SQL, en cualquier caso es una transacción única. Ejecutarlo dentro de una transacción más grande solo aumentaría la ventana de tiempo para posibles condiciones de carrera. En cuanto a SQL Server: los CTE modificadores de datos no son compatibles (solo SELECTdentro de una WITHcláusula). Fuente: documentación de la EM.
Erwin Brandstetter
1
También puede hacer esto con INSERT ... RETURNING \gsety psqlluego usar los valores devueltos como psql :'variables', pero esto solo funciona para inserciones de una sola fila.
Craig Ringer
@ErwinBrandstetter esto es genial, pero soy demasiado nuevo para sql para entenderlo todo, ¿podría agregar algunos comentarios a "INSERTAR filas FK faltantes al mismo tiempo" explicando cómo funciona? Además, gracias por los ejemplos de trabajo de SQLFiddle.
glallen
@glallen: agregué una explicación paso a paso. También hay muchos enlaces a respuestas relacionadas y al manual con más explicaciones. Usted necesita entender lo que hace la consulta o puede ser adentro sobre su cabeza.
Erwin Brandstetter
4

Buscar. Básicamente, necesitas los identificadores para insertarlos en la barra.

No postgres específico, por cierto. (y no lo etiquetó así): en general, así es como funciona SQL. No hay atajos aquí.

Sin embargo, en cuanto a la aplicación, es posible que tenga una memoria caché de elementos foo en la memoria. Mis tablas a menudo tienen hasta 3 campos únicos:

  • Id (entero o algo) que es la clave primaria de nivel de tabla.
  • Identificador, que es un GUID que se usa como nivel de aplicación de ID estable (y puede estar expuesto al cliente en URL, etc.)
  • Código: una cadena que puede estar allí y debe ser única si está allí (servidor sql: índice único filtrado en no nulo). Ese es un identificador de conjunto de clientes.

Ejemplo:

  • Cuenta (en una aplicación comercial) -> Id es un int usado para claves foráneas. -> El identificador es un Guid y se utiliza en los portales web, etc., siempre aceptado. -> El código se configura manualmente. Regla: una vez establecida, no cambia.

Obviamente, cuando desea vincular algo a una cuenta, primero debe obtener técnicamente el Id, pero dado que tanto el Identificador como el Código nunca cambian una vez que están allí, un caché positivo en la memoria puede evitar que la mayoría de las búsquedas lleguen a la base de datos.

TomTom
fuente
10
¿Sabe que puede dejar que el RDBMS haga la búsqueda por usted, en una sola declaración SQL, evitando la memoria caché propensa a errores?
Erwin Brandstetter
¿Sabe que buscar elementos que no cambian no es propenso a errores? Además, normalmente, el RDBMS no es escalable y es el elemento más caro del juego, debido a los costos de licencia. Tomar tanta carga como sea posible no es exactamente malo. Además, no muchos ORM admiten eso para empezar.
TomTom
14
Elementos que no cambian? ¿El elemento más caro? Costos de licencia (para PostgreSQL)? ¿ORMs que definen lo que está sano? No, no estaba al tanto de todo eso.
Erwin Brandstetter