2015-02-05 25 views
8

Tôi đang di chuyển dữ liệu giữa hai bảng (tách ra một bảng liên quan). Bảng hiện tại là reminders và cột có cột start và cột dateset_id mới được thêm vào trỏ đến một bảng dateset mới, cũng có cột start. Đối với mỗi hàng trong reminders, tôi muốn INSERT hàng mới trong dateset với giá trị start được sao chép qua và UPDATE hàng tương ứng trong reminders với ID mới được thêm vào dateset.PostgreSQL - chèn hàng dựa trên lựa chọn từ một bảng khác, và cập nhật một FK trong bảng đó với các hàng mới được chèn

Đây là SQL tôi đã cố gắng:

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 

tôi nhận được một lỗi missing FROM-clause entry for table "reminder", vì tôi bao gồm reminder.id cột trong mệnh đề RETURNING, nhưng không thực sự chọn nó cho chèn. Điều này có ý nghĩa, nhưng tôi không thể tìm ra cách sửa đổi truy vấn để làm những gì tôi cần. Có cách tiếp cận hoàn toàn khác nào tôi đang thiếu?

Trả lời

9

Có một số cách để giải quyết vấn đề.

1. tạm thời thêm một cột

Như những người khác đã đề cập, cách thẳng về phía trước là để tạm thời thêm một cột reminder_id đến dateset. Điền nó với IDs gốc từ bảng reminder. Sử dụng nó để tham gia reminder với bảng dateset. Thả cột tạm thời.

2.khi bắt đầu là độc đáo

Nếu giá trị của cột start là duy nhất chúng ta có thể làm điều đó mà không có cột thêm bằng cách tham gia reminder bảng với bảng dateset trên cột 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. khi bắt đầu là không độc đáo

Có thể làm điều đó mà không có cột tạm thời ngay cả trong trường hợp này. Ý tưởng chính là như sau. Chúng ta hãy có một cái nhìn vào ví dụ này:

Chúng tôi có hai hàng trong reminder với cùng giá trị start và ID 3 và 7:

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

Sau khi chúng tôi chèn chúng vào dateset, sẽ có ID mới được tạo , ví dụ: 1 và 2:

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

Việc chúng tôi liên kết hai hàng này không quan trọng. Kết quả cuối cùng có thể là

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

hoặc

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

Cả hai phiên bản là chính xác. Điều này đưa chúng ta đến giải pháp sau.

Chỉ cần chèn tất cả các hàng trước.

INSERT INTO dateset (start) 
SELECT start FROM reminder; 

Khớp/nối hai bảng trên start khi biết rằng nó không phải là duy nhất. "Làm cho nó" độc đáo bằng cách thêm ROW_NUMBER và tham gia bằng hai cột. Nó có thể làm cho các truy vấn ngắn hơn, nhưng tôi nêu ra từng bước một cách rõ ràng:

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 
; 

Tôi hy vọng nó là rõ ràng từ mã những gì nó làm, đặc biệt là khi bạn so sánh nó với phiên bản đơn giản mà không ROW_NUMBER. Rõ ràng, giải pháp phức tạp sẽ hoạt động ngay cả khi start là duy nhất, nhưng nó không hiệu quả như một giải pháp đơn giản.

Giải pháp này giả định rằng dateset trống trước quá trình này.

+0

Làm cách nào để '2.' hoạt động? Có vẻ như biến thể này chỉ có thể hoạt động nếu CTE có thể được coi là lượt xem và được cập nhật.Tôi nghĩ tại thời điểm này, điều này là không thể ở bưu chính. –

+0

@matthiaskrull, có vẻ như bạn đã đúng. Tôi đã sử dụng cú pháp SQL Server và tôi không có Postgres để kiểm tra. Trong Postgres bạn sẽ cần sử dụng mệnh đề 'FROM' trong câu lệnh [' UPDATE'] (http://www.postgresql.org/docs/9.4/static/sql-update.html) để nối các bảng. –

3

Vấn đề là bạn chỉ có thể trả về các cột có trong bảng bạn chèn vào. Bạn có thể giải quyết nó bằng cách cung cấp cho bảng dữ liệu một cột bổ sung mà bạn chèn remind.id để bạn có thể trả về nó.

Sau đó, sau khi di chuyển, bạn có thể thả cột đó.

4

Bạn chỉ có thể trả về các cột bằng cách sử dụng RETURNING từ phần INSERT, chứ không phải từ bảng đã chọn. Vì vậy, nếu bạn sẵn sàng để thêm một cột reminder_id để dateset-bảng của bạn,

ALTER TABLE dateset ADD COLUMN reminder_id integer; 

tuyên bố sau sẽ làm việc:

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 

Chỉ khi các giá trị của cột bắt đầu trong các lời nhắc đều là duy nhất, 2 câu sau đây cũng sẽ hoạt động:

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

Câu trả lời rất hay, cảm ơn! Ước gì tôi có thể trao giải thưởng cho nhiều câu trả lời, nhưng vì tôi phải chọn một câu trả lời, tôi sẽ đi cùng với Vladimir, để có phạm vi tùy chọn rộng nhất, bao gồm cả tùy chọn khi bắt đầu không phải là duy nhất, mà không cần cột tạm thời . –

6

Đây là một cách khác để làm điều đó, khác với 3 cách mà Vladimir đề xuất cho đến thời điểm này.

Một chức năng tạm thời sẽ cho phép bạn đọc id của các hàng mới được tạo ra cũng như các giá trị khác trong truy vấn:

--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; 

Cách tiếp cận này cho vấn đề gợi ý riêng của mình sau khi tôi nhận ra rằng viết INSERT ... RETURNING như một subquery sẽ giải quyết vấn đề; mặc dù INSERT s không được phép làm truy vấn phụ, các cuộc gọi đến các hàm chắc chắn là.

Điều thú vị, điều này gợi ý rằng truy vấn con DML trả lại giá trị có thể hữu ích rộng rãi. Nếu có thể, chúng tôi sẽ viết:

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

Ồ, rất tuyệt! Ok, tại thời điểm này tôi không có ý tưởng ai để cung cấp cho các bounty - Đây là câu trả lời tốt thứ tư. Ước gì tôi có thể đưa nó cho tất cả những ai trả lời. –

+0

Tôi nghĩ tôi sẽ đưa nó cho Vladimir, chỉ để đề xuất phạm vi tùy chọn rộng nhất, bao gồm một số tùy chọn không yêu cầu thêm/bỏ một cột hoặc chức năng tạm thời. Nhưng đây là một câu trả lời rất hay - cảm ơn! –

Các vấn đề liên quan