2015-02-05 10 views
8

Sto eseguendo una migrazione di dati tra due tabelle (suddividendo una tabella correlata). La tabella esistente è reminders e ha una colonna start e una colonna dateset_id appena aggiunta che punta a una nuova tabella dateset, che ha anche una colonna start. Per ogni riga in reminders, voglio INSERT una nuova riga in dateset con il valore start copiato e UPDATE la riga corrispondente in reminders con l'ID dateset appena inserito.PostgreSQL: inserisce le righe in base alla selezione da un'altra tabella e aggiorna un FK in quella tabella con le righe appena inserite

Ecco lo SQL ho provato:

WITH inserted_datesets AS (
    INSERT INTO dateset (start) 
    SELECT start FROM reminder 
    RETURNING reminder.id AS reminder_id, id AS dateset_id 
) 
UPDATE reminder 
SET dateset_id = ids.dateset_id 
FROM inserted_datesets AS ids 
WHERE reminder.id = ids.reminder_id 

ottengo un errore missing FROM-clause entry for table "reminder", perché sto tra cui la colonna reminder.id nella clausola RETURNING, ma in realtà non selezionandolo per l'inserto. Questo ha senso, ma non riesco a capire come modificare la query per fare ciò di cui ho bisogno. C'è un approccio totalmente diverso che mi manca?

risposta

9

Ci sono diversi modi per risolvere il problema.

1. aggiungere temporaneamente una colonna

Come altri hanno detto, il modo in cui straight-forward è di aggiungere temporaneamente una colonna reminder_id al dateset. Compilalo con il numero IDs originale dal tavolo reminder. Usalo per unire reminder con la tabella dateset. Elimina la colonna temporanea.

2. quando l'inizio è unico

Se i valori della colonna start è unico, è possibile farlo senza colonna in più unendo reminder tabella con la tabella dateset sulla colonna start.

INSERT INTO dateset (start) 
SELECT start FROM reminder; 

WITH 
CTE_Joined 
AS 
(
    SELECT 
     reminder.id AS reminder_id 
     ,reminder.dateset_id AS old_dateset_id 
     ,dateset.id AS new_dateset_id 
    FROM 
     reminder 
     INNER JOIN dateset ON dateset.start = reminder.start 
) 
UPDATE CTE_Joined 
SET old_dateset_id = new_dateset_id 
; 

3. quando inizio non riservate

E 'possibile farlo senza colonna temporanea anche in questo caso. L'idea principale è la seguente. Diamo uno sguardo a questo esempio:

abbiamo due righe in reminder con lo stesso valore di start e ID 3 e 7:

reminder 
id start   dateset_id 
3  2015-01-01 NULL 
7  2015-01-01 NULL 

Dopo che li inseriamo nella dateset, ci saranno nuovi ID generati , ad esempio, 1 e 2:

dateset 
id start 
1  2015-01-01 
2  2015-01-01 

Non importa in realtà come colleghiamo queste due righe. Il risultato finale potrebbe essere

reminder 
id start   dateset_id 
3  2015-01-01 1 
7  2015-01-01 2 

o

reminder 
id start   dateset_id 
3  2015-01-01 2 
7  2015-01-01 1 

Entrambe queste varianti sono corrette. Il che ci porta alla seguente soluzione.

È sufficiente inserire prima tutte le righe.

INSERT INTO dateset (start) 
SELECT start FROM reminder; 

Partita/join due tabelle su start colonna sapendo che non è unico. "Rendilo" unico aggiungendo ROW_NUMBER e unendo due colonne. E 'possibile rendere la query più breve, ma ho spiegato ogni passo in modo esplicito:

WITH 
CTE_reminder_rn 
AS 
(
    SELECT 
     id 
     ,start 
     ,dateset_id 
     ,ROW_NUMBER() OVER (PARTITION BY start ORDER BY id) AS rn 
    FROM reminder 
) 
,CTE_dateset_rn 
AS 
(
    SELECT 
     id 
     ,start 
     ,ROW_NUMBER() OVER (PARTITION BY start ORDER BY id) AS rn 
    FROM dateset 
) 
,CTE_Joined 
AS 
(
    SELECT 
     CTE_reminder_rn.id AS reminder_id 
     ,CTE_reminder_rn.dateset_id AS old_dateset_id 
     ,CTE_dateset_rn.id AS new_dateset_id 
    FROM 
     CTE_reminder_rn 
     INNER JOIN CTE_dateset_rn ON 
      CTE_dateset_rn.start = CTE_reminder_rn.start AND 
      CTE_dateset_rn.rn = CTE_reminder_rn.rn 
) 
UPDATE CTE_Joined 
SET old_dateset_id = new_dateset_id 
; 

Spero sia chiaro dal codice quello che fa, soprattutto quando si confronta con la versione più semplice, senza ROW_NUMBER. Ovviamente, la soluzione complessa funzionerà anche se start è univoco, ma non è così efficiente come una soluzione semplice.

Questa soluzione presuppone che dateset sia vuoto prima di questo processo.

+0

Come funziona '2.'? Sembra che questa variante possa funzionare solo se le CTE possono essere considerate come viste e aggiornate.Penso che al momento questo non sia possibile nei postgres. –

+0

@matthiaskrull, sembra che tu abbia ragione. Stavo usando la sintassi di SQL Server e non ho Postgres a portata di mano per controllare. In Postgres è necessario utilizzare la clausola 'FROM' nell'istruzione [' UPDATE'] (http://www.postgresql.org/docs/9.4/static/sql-update.html) per unire le tabelle. –

3

Il problema è che è possibile restituire solo le colonne presenti nella tabella in cui si inserisce. Potresti risolverlo dando al set di dati della tabella una colonna aggiuntiva in cui inserisci reminder.id in modo da poterlo restituire.

Quindi dopo la migrazione è possibile rilasciare quella colonna.

4

È possibile restituire solo le colonne utilizzando RESTITUZIONE dalla parte INSERT, non dalla tabella selezionata. Quindi, se siete disposti ad aggiungere una colonna reminder_id al vostro dateset-tavolo,

ALTER TABLE dateset ADD COLUMN reminder_id integer; 

la seguente dichiarazione funzionerà:

WITH inserted_datesets AS (
    INSERT INTO dateset (start, reminder_id) 
    SELECT start, id FROM reminder 
    RETURNING reminder_id, id AS dateset_id 
) 
UPDATE reminder 
SET dateset_id = ids.dateset_id 
FROM inserted_datesets AS ids 
WHERE id = reminder_id 

Solo se i valori della colonna start in promemoriasono tutti unici, le seguenti 2 affermazioni funzioneranno anche:

INSERT INTO dateset(start) SELECT start FROM reminder; 
UPDATE reminder SET dateset_id = (SELECT id FROM dateset WHERE start=reminder.start); 
+0

Risposta molto bella, grazie! Vorrei poter assegnare la taglia a più risposte, ma dal momento che devo sceglierne una, ho intenzione di andare con Vladimir, per avere la più ampia gamma di opzioni, tra cui quella che funziona quando l'avvio non è unico, senza bisogno di una colonna temporanea . –

6

Ecco un altro modo di farlo, distinto dai 3 modi che Vladimir ha suggerito finora.

Una funzione temporanea vi permetterà di leggere l'ID delle nuove righe create così come altri valori nella query:

--minimal demonstration schema 
CREATE TABLE dateset (
    id SERIAL PRIMARY KEY, 
    start TIMESTAMP 
    -- other things here... 
); 

CREATE TABLE reminder (
    id SERIAL PRIMARY KEY, 
    start TIMESTAMP, 
    dateset_id INTEGER REFERENCES dateset(id) 
    -- other things here... 
); 

--pre-migration data 
INSERT INTO reminder (start) VALUES ('2014-02-14'), ('2014-09-06'), ('1984-01-01'), ('2014-02-14'); 

--all at once 
BEGIN; 

CREATE FUNCTION insertreturning(ts TIMESTAMP) RETURNS INTEGER AS $$ 
    INSERT INTO dateset (start) 
    VALUES (ts) 
    RETURNING dateset.id; 
    $$ LANGUAGE SQL; 

UPDATE reminder SET dateset_id = insertreturning(reminder.start); 

DROP FUNCTION insertreturning(TIMESTAMP); 

ALTER TABLE reminder DROP COLUMN start; 

END; 

Questo approccio al problema stesso ha suggerito dopo mi sono reso conto che scrivere INSERT ... RETURNING come una sottoquery risolverebbe il problema; anche se INSERT s non sono consentiti come subquery, le chiamate alle funzioni lo sono certamente.

Intrigante, questo suggerisce che le subquery DML che restituiscono valori potrebbero essere ampiamente utili. Se fossero possibili, scriveremmo semplicemente:

UPDATE reminder SET dateset_id = (
    INSERT INTO dateset (start) 
    VALUES (reminder.start) 
    RETURNING dateset.id)); 
+0

Oh, molto bello! Ok, a questo punto non ho idea di chi dare il premio a - Questa è la quarta buona risposta. Vorrei poterlo dare a tutti quelli che hanno risposto. –

+0

Penso che lo darò a Vladimir, solo per suggerire la più ampia gamma di opzioni, incluse molte che non richiedono l'aggiunta/eliminazione di una colonna o funzione temporanea. Ma questa è una risposta molto bella - grazie! –

Problemi correlati