Làm thế nào để UPSERT (MERGE, INSERT Ngày TRÊN CẬP NHẬT DUPLICATE) trong PostgreQuery?


268

Một câu hỏi rất thường gặp ở đây là làm thế nào để tăng tốc, đó là điều mà MySQL gọi INSERT ... ON DUPLICATE UPDATEvà tiêu chuẩn hỗ trợ như một phần của MERGEhoạt động.

Cho rằng PostgreSQL không hỗ trợ trực tiếp (trước trang 9,5), bạn làm điều này như thế nào? Hãy xem xét những điều sau đây:

CREATE TABLE testtable (
    id integer PRIMARY KEY,
    somedata text NOT NULL
);

INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');

Bây giờ tưởng tượng rằng bạn muốn "upsert" các tuples (2, 'Joe'), (3, 'Alan'), vì vậy nội dung bảng mới sẽ là:

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing tuple
(3, 'Alan')    -- Added new tuple

Đó là những gì mọi người đang nói về khi thảo luận về một upsert. Điều quan trọng, bất kỳ cách tiếp cận nào cũng phải an toàn khi có nhiều giao dịch hoạt động trên cùng một bảng - bằng cách sử dụng khóa rõ ràng hoặc bảo vệ chống lại các điều kiện cuộc đua kết quả.

Chủ đề này được thảo luận rộng rãi tại Chèn, về cập nhật trùng lặp trong PostgreSQL? , nhưng đó là về các lựa chọn thay thế cho cú pháp MySQL và nó đã phát triển một chút chi tiết không liên quan theo thời gian. Tôi đang làm việc trên các câu trả lời dứt khoát.

Các kỹ thuật này cũng hữu ích cho "chèn nếu không tồn tại, nếu không thì không làm gì", tức là "chèn ... vào bỏ qua khóa trùng lặp".



8
@MichaelHampton mục tiêu ở đây là tạo ra một phiên bản dứt khoát không bị nhầm lẫn bởi nhiều câu trả lời lỗi thời - và bị khóa, vì vậy không ai có thể làm bất cứ điều gì về nó. Tôi không đồng ý với các closevote.
Craig Ringer

Tại sao, sau đó điều này sẽ sớm trở nên lỗi thời - và bị khóa, vì vậy không ai có thể làm bất cứ điều gì về nó.
Michael Hampton

2
@MichaelHampton Nếu bạn lo lắng, có lẽ bạn có thể gắn cờ người bạn đã liên kết và yêu cầu mở khóa để có thể dọn sạch, sau đó chúng tôi có thể hợp nhất điều này. as-dup cho upert là một mớ hỗn độn và sai lầm như vậy.
Craig Ringer

1
Q & A đó không bị khóa!
Michael Hampton

Câu trả lời:


396

9,5 và mới hơn:

PostgreQuery 9.5 và hỗ trợ mới hơn INSERT ... ON CONFLICT UPDATE(và ON CONFLICT DO NOTHING), tức là upert.

So sánh vớiON DUPLICATE KEY UPDATE .

Giải thích nhanh .

Để sử dụng, hãy xem hướng dẫn - cụ thể là mệnh đề mâu thuẫn trong sơ đồ cú pháp và văn bản giải thích .

Không giống như các giải pháp cho 9,4 trở lên được đưa ra dưới đây, tính năng này hoạt động với nhiều hàng xung đột và nó không yêu cầu khóa độc quyền hoặc vòng lặp thử lại.

Cam kết thêm tính năng là ở đâycác cuộc thảo luận xung quanh sự phát triển của nó là ở đây .


Nếu bạn đang ở trên 9.5 và không cần phải tương thích ngược, bạn có thể ngừng đọc ngay bây giờ .


9,4 trở lên:

PostgreSQL không có bất kỳ tiện ích tích hợp UPSERT(hoặc MERGE) nào và việc thực hiện nó một cách hiệu quả khi đối mặt với việc sử dụng đồng thời là rất khó khăn.

Bài viết này thảo luận về vấn đề chi tiết hữu ích .

Nói chung, bạn phải chọn giữa hai tùy chọn:

  • Các hoạt động chèn / cập nhật riêng lẻ trong một vòng lặp thử lại; hoặc là
  • Khóa bảng và thực hiện hợp nhất hàng loạt

Vòng lặp thử lại hàng riêng lẻ

Sử dụng các đảo ngược hàng riêng lẻ trong một vòng thử lại là tùy chọn hợp lý nếu bạn muốn nhiều kết nối đồng thời cố gắng thực hiện chèn.

Tài liệu PostgreSQL chứa một quy trình hữu ích sẽ cho phép bạn thực hiện việc này trong một vòng lặp bên trong cơ sở dữ liệu . Nó bảo vệ chống lại các bản cập nhật bị mất và chèn các cuộc đua, không giống như hầu hết các giải pháp ngây thơ. Nó sẽ chỉ hoạt động trong READ COMMITTEDchế độ và chỉ an toàn nếu đó là điều duy nhất bạn làm trong giao dịch. Chức năng sẽ không hoạt động chính xác nếu kích hoạt hoặc khóa duy nhất thứ cấp gây ra vi phạm duy nhất.

Chiến lược này rất không hiệu quả. Bất cứ khi nào thực tế, bạn nên xếp hàng làm việc và thay thế hàng loạt như mô tả dưới đây.

Nhiều giải pháp đã cố gắng cho vấn đề này không xem xét các dự phòng, do đó chúng dẫn đến các bản cập nhật không đầy đủ. Hai giao dịch chạy đua với nhau; một trong số họ thành công INSERTs; cái còn lại bị lỗi khóa trùng lặp và UPDATEthay vào đó. Các UPDATEkhối đang chờ INSERTđể rollback hoặc cam kết. Khi nó quay trở lại, UPDATEđiều kiện kiểm tra lại khớp với các hàng bằng 0, do đó, mặc dù các UPDATEcam kết nó thực sự không thực hiện được như bạn mong đợi. Bạn phải kiểm tra số lượng hàng kết quả và thử lại khi cần thiết.

Một số giải pháp đã cố gắng cũng không xem xét các cuộc đua CHỌN. Nếu bạn thử rõ ràng và đơn giản:

-- THIS IS WRONG. DO NOT COPY IT. It's an EXAMPLE.

BEGIN;

UPDATE testtable
SET somedata = 'blah'
WHERE id = 2;

-- Remember, this is WRONG. Do NOT COPY IT.

INSERT INTO testtable (id, somedata)
SELECT 2, 'blah'
WHERE NOT EXISTS (SELECT 1 FROM testtable WHERE testtable.id = 2);

COMMIT;

sau đó khi hai chạy cùng một lúc có một vài chế độ thất bại. Một là vấn đề đã được thảo luận với kiểm tra lại cập nhật. Một cái khác là cả hai UPDATEcùng một lúc, khớp các hàng 0 và tiếp tục. Sau đó, cả hai đều làm EXISTSbài kiểm tra, trong đó xảy ra trước khi các INSERT. Cả hai đều nhận được hàng không, vì vậy cả hai đều làm INSERT. Một lỗi với một lỗi chính trùng lặp.

Đây là lý do tại sao bạn cần một vòng lặp thử lại. Bạn có thể nghĩ rằng bạn có thể ngăn các lỗi khóa trùng lặp hoặc mất cập nhật với SQL thông minh, nhưng bạn không thể. Bạn cần kiểm tra số lượng hàng hoặc xử lý các lỗi chính trùng lặp (tùy thuộc vào cách tiếp cận đã chọn) và thử lại.

Xin đừng cuộn giải pháp của riêng bạn cho việc này. Giống như với việc xếp hàng tin nhắn, có lẽ sai.

Số lượng lớn có khóa

Đôi khi bạn muốn thực hiện một số lượng lớn, trong đó bạn có một bộ dữ liệu mới mà bạn muốn hợp nhất thành một bộ dữ liệu cũ hơn. Đây là bao la hiệu quả hơn upserts hàng cá nhân và nên được ưa thích bất cứ khi nào thực tế.

Trong trường hợp này, bạn thường làm theo quy trình sau:

  • CREATEmột cái TEMPORARYbàn

  • COPY hoặc chèn số lượng lớn dữ liệu mới vào bảng tạm thời

  • LOCKbảng mục tiêu IN EXCLUSIVE MODE. Điều này cho phép các giao dịch khác SELECT, nhưng không thực hiện bất kỳ thay đổi nào đối với bảng.

  • Thực hiện một UPDATE ... FROMtrong các bản ghi hiện có bằng cách sử dụng các giá trị trong bảng tạm thời;

  • Thực hiện một INSERThàng không tồn tại trong bảng đích;

  • COMMIT, phát hành khóa.

Ví dụ: đối với ví dụ được đưa ra trong câu hỏi, sử dụng đa giá trị INSERTđể điền vào bảng tạm thời:

BEGIN;

CREATE TEMPORARY TABLE newvals(id integer, somedata text);

INSERT INTO newvals(id, somedata) VALUES (2, 'Joe'), (3, 'Alan');

LOCK TABLE testtable IN EXCLUSIVE MODE;

UPDATE testtable
SET somedata = newvals.somedata
FROM newvals
WHERE newvals.id = testtable.id;

INSERT INTO testtable
SELECT newvals.id, newvals.somedata
FROM newvals
LEFT OUTER JOIN testtable ON (testtable.id = newvals.id)
WHERE testtable.id IS NULL;

COMMIT;

Đọc liên quan

Thế còn MERGE?

Tiêu chuẩn SQL MERGEthực sự có ngữ nghĩa đồng thời được xác định kém và không phù hợp để nâng cấp mà không khóa bảng trước.

Đó là một tuyên bố OLAP thực sự hữu ích cho việc hợp nhất dữ liệu, nhưng thực sự nó không phải là một giải pháp hữu ích cho sự tăng cường an toàn đồng thời. Có rất nhiều lời khuyên cho những người sử dụng các DBMS khác để sử dụng MERGEcho uperts, nhưng thực sự nó đã sai.

Các DB khác:


Trong upert số lượng lớn, có thể có giá trị trong việc xóa khỏi các khoảng mới thay vì lọc INSERT không? Ví dụ: VỚI Cập nhật NHƯ (CẬP NHẬT ... ĐỔI MỚI newid.id) XÓA TỪ các khoảng thời gian mới SỬ DỤNG cập nhật WHERE new đạn.id = upd.id, theo sau là CHỌN CHỈ VÀO TỪ CHỌN CÓ THỂ kiểm tra * TỪ các khoảng mới? Ý tưởng của tôi với điều này: thay vì lọc hai lần trong INSERT (cho THAM GIA / WHERE và cho ràng buộc duy nhất), hãy sử dụng lại kết quả kiểm tra sự tồn tại từ CẬP NHẬT, đã có trong RAM và có thể nhỏ hơn nhiều. Đây có thể là một chiến thắng nếu một vài hàng khớp và / hoặc số mới nhỏ hơn nhiều so với testtable.
Gunnlaugur Briem

1
Vẫn còn những vấn đề chưa được giải quyết và đối với các nhà cung cấp khác thì không rõ cái gì hoạt động và cái gì không. 1. Giải pháp lặp Postgres như đã lưu ý không hoạt động trong trường hợp có nhiều khóa duy nhất. 2. Khóa trùng lặp cho mysql cũng không hoạt động đối với nhiều khóa duy nhất. 3. Các giải pháp khác cho MySQL, SQL Server và Oracle được đăng ở trên có hoạt động không? Là ngoại lệ có thể trong những trường hợp đó và chúng ta phải lặp lại?
dan b

@danb Đây chỉ là thực sự về PostgreSQL. Không có giải pháp nhà cung cấp chéo. Giải pháp cho PostgreSQL không hoạt động cho nhiều hàng, bạn không may phải thực hiện một giao dịch mỗi hàng. Các "giải pháp" sử dụng MERGEcho SQL Server và Oracle không chính xác và dễ xảy ra tình trạng chủng tộc, như đã lưu ý ở trên. Bạn sẽ cần xem xét từng DBMS cụ thể để tìm ra cách xử lý chúng, tôi thực sự chỉ có thể đưa ra lời khuyên về PostgreQuery. Cách duy nhất để thực hiện một upert nhiều hàng an toàn trên PostgreSQL sẽ là nếu hỗ trợ cho upert gốc được thêm vào máy chủ lõi.
Craig Ringer

Ngay cả đối với PostGresQL, giải pháp không hoạt động trong trường hợp bảng có nhiều khóa duy nhất (chỉ cập nhật một hàng). Trong trường hợp đó, bạn cần xác định khóa nào đang được cập nhật. Có thể có một giải pháp nhà cung cấp chéo sử dụng jdbc chẳng hạn.
dan b

2
Postgres hiện hỗ trợ UPSERT - git.postgresql.org/gitweb/ Kẻ
Chris

32

Tôi đang cố gắng đóng góp với một giải pháp khác cho vấn đề chèn đơn với các phiên bản trước 9.5 của PostgreQuery. Ý tưởng chỉ đơn giản là cố gắng thực hiện trước khi chèn và trong trường hợp bản ghi đã có sẵn, để cập nhật nó:

do $$
begin 
  insert into testtable(id, somedata) values(2,'Joe');
exception when unique_violation then
  update testtable set somedata = 'Joe' where id = 2;
end $$;

Lưu ý rằng giải pháp này chỉ có thể được áp dụng nếu không có việc xóa các hàng của bảng .

Tôi không biết về hiệu quả của giải pháp này, nhưng dường như tôi đủ hợp lý.


3
Cảm ơn bạn, đó chính xác là những gì tôi đang tìm kiếm. Không thể hiểu tại sao nó rất khó tìm.
isapir

4
Vâng. Đơn giản hóa này hoạt động khi và chỉ khi không có xóa.
Craig Ringer

@CraigRinger Bạn có thể giải thích chính xác điều gì sẽ xảy ra nếu có xóa không?
turbanoff

@turbanoff Chèn có thể thất bại vì bản ghi đã có sẵn, sau đó nó bị xóa đồng thời và bản cập nhật sau đó ảnh hưởng đến các hàng bằng 0 vì hàng đã bị xóa.
Craig Ringer

@CraigRinger Vậy. Xóa được xảy ra đồng thời . Outways có thể là gì nếu điều này hoạt động tốt? Nếu việc xóa đang hoạt động đồng thời - thì nó có thể được thực hiện ngay sau khối của chúng tôi. Những gì tôi đang cố gắng nói - nếu chúng tôi xóa đồng thời - thì mã này sẽ hoạt động theo cách tương tựinsert on update
turbanoff

29

Dưới đây là một số ví dụ cho insert ... on conflict ...( pg 9.5+ ):

  • Chèn, vào xung đột - không làm gì cả .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict do nothing;`  
    
  • Chèn, vào xung đột - thực hiện cập nhật , chỉ định mục tiêu xung đột thông qua cột .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict(id)
    do update set name = 'new_name', size = 3;  
    
  • Chèn, vào xung đột - thực hiện cập nhật , chỉ định mục tiêu xung đột thông qua tên ràng buộc .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict on constraint dummy_pkey
    do update set name = 'new_name', size = 4;
    

câu trả lời tuyệt vời - câu hỏi: tại sao hoặc trong tình huống nào người ta nên sử dụng đặc tả mục tiêu thông qua cột hoặc tên ràng buộc? Có một lợi thế / bất lợi cho các trường hợp sử dụng khác nhau?
Nathan Benton

1
@NathanBenton Tôi nghĩ có ít nhất 2 điểm khác biệt: (1) tên cột được chỉ định bởi lập trình viên, trong khi tên ràng buộc có thể được chỉ định bởi lập trình viên hoặc được tạo bởi cơ sở dữ liệu theo tên bảng / cột. (2) mỗi cột có thể có nhiều ràng buộc. Điều đó nói rằng, nó phụ thuộc vào trường hợp của bạn để chọn cái nào sẽ sử dụng.
Eric Wang

8

SQLAlchemy nâng cấp cho Postgres> = 9.5

Vì bài đăng lớn ở trên bao gồm nhiều cách tiếp cận SQL khác nhau cho các phiên bản Postgres (không chỉ không phải là 9.5 như trong câu hỏi), tôi muốn thêm cách thực hiện trong SQLAlchemy nếu bạn đang sử dụng Postgres 9.5. Thay vì thực hiện upert của riêng bạn, bạn cũng có thể sử dụng các hàm của SQLAlchemy (đã được thêm vào trong SQLAlchemy 1.1). Cá nhân, tôi khuyên bạn nên sử dụng những thứ này, nếu có thể. Không chỉ vì sự tiện lợi, mà còn vì nó cho phép PostgreSQL xử lý mọi điều kiện cuộc đua có thể xảy ra.

Đăng chéo từ một câu trả lời khác tôi đã đưa ra ngày hôm qua ( https://stackoverflow.com/a/44395983/2156909 )

SQLAlchemy hỗ trợ ON CONFLICTngay bây giờ với hai phương thức on_conflict_do_update()on_conflict_do_nothing():

Sao chép từ tài liệu:

from sqlalchemy.dialects.postgresql import insert

stmt = insert(my_table).values(user_email='a@b.com', data='inserted data')
stmt = stmt.on_conflict_do_update(
    index_elements=[my_table.c.user_email],
    index_where=my_table.c.user_email.like('%@gmail.com'),
    set_=dict(data=stmt.excluded.data)
    )
conn.execute(stmt)

http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert


4
Python và SQLAlchemy không được đề cập trong câu hỏi.
Alexander Emelianov

Tôi thường sử dụng Python trong các giải pháp tôi viết. Nhưng tôi đã không nhìn vào SQLAlchemy (hoặc đã biết về nó). Đây có vẻ là một lựa chọn thanh lịch. Cảm ơn bạn. Nếu nó kiểm tra, tôi sẽ trình bày điều này cho tổ chức của tôi.
Robert

3
WITH UPD AS (UPDATE TEST_TABLE SET SOME_DATA = 'Joe' WHERE ID = 2 
RETURNING ID),
INS AS (SELECT '2', 'Joe' WHERE NOT EXISTS (SELECT * FROM UPD))
INSERT INTO TEST_TABLE(ID, SOME_DATA) SELECT * FROM INS

Đã thử nghiệm trên Postgresql 9.3


@CraigRinger: bạn có thể giải thích về điều này? không phải là nguyên tử cte?
parisni

2
@parisni Không. Mỗi thuật ngữ CTE sẽ có ảnh chụp nhanh nếu nó thực hiện ghi. Ngoài ra, không có loại khóa vị ngữ nào được thực hiện trên các hàng không được tìm thấy để chúng vẫn có thể được tạo đồng thời bởi một phiên khác. Nếu bạn đã sử dụng SERIALIZABLEcách ly, bạn sẽ bị hủy bỏ với lỗi nối tiếp, nếu không, bạn có thể bị vi phạm duy nhất. Đừng phát minh lại, việc tái phát minh sẽ sai. Sử dụng INSERT ... ON CONFLICT .... Nếu PostgreSQL của bạn quá cũ, hãy cập nhật nó.
Craig Ringer

@CraigRinger INSERT ... ON CLONFLICT ...không dành cho tải số lượng lớn. Từ bài đăng của bạn, LOCK TABLE testtable IN EXCLUSIVE MODE;trong CTE là một cách giải quyết để có được những thứ nguyên tử. Không
parisni

@parisni Nó không dành cho tải số lượng lớn? Nói ai? postgresql.org/docs/civerse/sql-insert.html#Query-ON-CONFLICT . Chắc chắn, nó chậm hơn nhiều so với tải hàng loạt mà không có hành vi giống như, nhưng đó là điều hiển nhiên và sẽ là trường hợp bất kể bạn làm gì. Đó là cách nhanh hơn so với việc sử dụng các giao dịch con, đó là điều chắc chắn. Cách tiếp cận nhanh nhất là khóa bảng mục tiêu, sau đó thực hiện một insert ... where not exists ...hoặc tương tự.
Craig Ringer

1

câu hỏi này đã bị đóng, tôi sẽ đăng bài ở đây để biết cách bạn thực hiện bằng SQLAlchemy. Thông qua đệ quy, nó thử lại một bản chèn hoặc cập nhật hàng loạt để chống lại các điều kiện cuộc đua và lỗi xác nhận.

Đầu tiên là hàng nhập khẩu

import itertools as it

from functools import partial
from operator import itemgetter

from sqlalchemy.exc import IntegrityError
from app import session
from models import Posts

Bây giờ một vài chức năng trợ giúp

def chunk(content, chunksize=None):
    """Groups data into chunks each with (at most) `chunksize` items.
    https://stackoverflow.com/a/22919323/408556
    """
    if chunksize:
        i = iter(content)
        generator = (list(it.islice(i, chunksize)) for _ in it.count())
    else:
        generator = iter([content])

    return it.takewhile(bool, generator)


def gen_resources(records):
    """Yields a dictionary if the record's id already exists, a row object 
    otherwise.
    """
    ids = {item[0] for item in session.query(Posts.id)}

    for record in records:
        is_row = hasattr(record, 'to_dict')

        if is_row and record.id in ids:
            # It's a row but the id already exists, so we need to convert it 
            # to a dict that updates the existing record. Since it is duplicate,
            # also yield True
            yield record.to_dict(), True
        elif is_row:
            # It's a row and the id doesn't exist, so no conversion needed. 
            # Since it's not a duplicate, also yield False
            yield record, False
        elif record['id'] in ids:
            # It's a dict and the id already exists, so no conversion needed. 
            # Since it is duplicate, also yield True
            yield record, True
        else:
            # It's a dict and the id doesn't exist, so we need to convert it. 
            # Since it's not a duplicate, also yield False
            yield Posts(**record), False

Và cuối cùng là chức năng upsert

def upsert(data, chunksize=None):
    for records in chunk(data, chunksize):
        resources = gen_resources(records)
        sorted_resources = sorted(resources, key=itemgetter(1))

        for dupe, group in it.groupby(sorted_resources, itemgetter(1)):
            items = [g[0] for g in group]

            if dupe:
                _upsert = partial(session.bulk_update_mappings, Posts)
            else:
                _upsert = session.add_all

            try:
                _upsert(items)
                session.commit()
            except IntegrityError:
                # A record was added or deleted after we checked, so retry
                # 
                # modify accordingly by adding additional exceptions, e.g.,
                # except (IntegrityError, ValidationError, ValueError)
                db.session.rollback()
                upsert(items)
            except Exception as e:
                # Some other error occurred so reduce chunksize to isolate the 
                # offending row(s)
                db.session.rollback()
                num_items = len(items)

                if num_items > 1:
                    upsert(items, num_items // 2)
                else:
                    print('Error adding record {}'.format(items[0]))

Đây là cách bạn sử dụng nó

>>> data = [
...     {'id': 1, 'text': 'updated post1'}, 
...     {'id': 5, 'text': 'updated post5'}, 
...     {'id': 1000, 'text': 'new post1000'}]
... 
>>> upsert(data)

Ưu điểm này có bulk_save_objectsđược là nó có thể xử lý các mối quan hệ, kiểm tra lỗi, v.v. khi chèn (không giống như các hoạt động hàng loạt ).


Nó cũng có vẻ sai với tôi. Điều gì xảy ra nếu một phiên đồng thời chèn một hàng sau khi bạn thu thập danh sách ID của mình? Hoặc xóa một?
Craig Ringer

điểm tốt @CraigRinger Tôi làm điều gì đó tương tự nhưng chỉ có 1 phiên thực hiện công việc. Cách tốt nhất để xử lý nhiều phiên sau đó là gì? Một giao dịch có lẽ?
reubano

Giao dịch không phải là giải pháp kỳ diệu cho tất cả các vấn đề tương tranh. Bạn có thể sử dụng SERIALIZABLE các giao dịch và xử lý các lỗi nối tiếp nhưng nó chậm. Bạn cần xử lý lỗi và một vòng lặp thử lại. Xem câu trả lời của tôi và phần "đọc liên quan" trong đó.
Craig Ringer

@CraigRinger đã nhận được. Tôi thực sự đã thực hiện một vòng lặp thử lại trong trường hợp của riêng tôi do các lỗi xác nhận khác. Tôi sẽ cập nhật câu trả lời này cho phù hợp.
reubano
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.