Làm cách nào để 'chèn nếu không tồn tại' trong MySQL?


838

Tôi bắt đầu bằng cách googling, và tìm thấy bài viết này nói về các bảng mutex.

Tôi có một bảng với ~ 14 triệu hồ sơ. Nếu tôi muốn thêm nhiều dữ liệu ở cùng định dạng, có cách nào để đảm bảo bản ghi tôi muốn chèn không tồn tại mà không sử dụng một cặp truy vấn (nghĩa là một truy vấn để kiểm tra và một truy vấn để chèn là tập kết quả là trống)?

Liệu một uniqueràng buộc trên một lĩnh vực đảm bảo insertsẽ thất bại nếu nó đã ở đó?

Có vẻ như chỉ với một ràng buộc, khi tôi phát hành phần chèn qua php, tập lệnh sẽ bị khóa.



Xem stackoverflow.com/questions/44550788/ cho cuộc thảo luận về việc không ghi giá trị auto_inc.
Rick James

@RickJames - đó là một q thú vị .. nhưng không chắc nó liên quan trực tiếp đến q này :)
warren

1
Nó đã được đề cập trong một bình luận, và Câu hỏi khác khẳng định Câu hỏi này là "bản sao chính xác". Vì vậy, tôi cảm thấy rằng nên liên kết các câu hỏi với nhau vì lợi ích của người khác.
Rick James

1
Ồ, tôi không bao giờ nghĩ rằng nhìn vào thanh bên.
Rick James

Câu trả lời:


807

sử dụng INSERT IGNORE INTO table

xem http://bogdan.org.ua/2007/10/18/mysql-insert-if-not-exists-syntax.html

Ngoài ra còn có INSERT … ON DUPLICATE KEY UPDATEcú pháp, bạn có thể tìm thấy lời giải thích trên dev.mysql.com


Bài đăng từ bogdan.org.ua theo webcache của Google :

Ngày 18 tháng 10 năm 2007

Để bắt đầu: kể từ MySQL mới nhất, cú pháp được trình bày trong tiêu đề là không thể. Nhưng có một số cách rất dễ dàng để thực hiện những gì được mong đợi bằng cách sử dụng chức năng hiện có.

Có 3 giải pháp khả thi: sử dụng CHỨNG CHỈ IGNORE, THAY THẾ hoặc CHỨNG MINH TRÊN CẬP NHẬT KHÓA NGHIÊM TRỌNG.

Hãy tưởng tượng chúng ta có một bảng:

CREATE TABLE `transcripts` (
`ensembl_transcript_id` varchar(20) NOT NULL,
`transcript_chrom_start` int(10) unsigned NOT NULL,
`transcript_chrom_end` int(10) unsigned NOT NULL,
PRIMARY KEY (`ensembl_transcript_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Bây giờ hãy tưởng tượng rằng chúng ta có một đường ống tự động nhập dữ liệu siêu dữ liệu từ siêu dữ liệu từ Makeembl và do nhiều lý do khác nhau, đường ống có thể bị phá vỡ ở bất kỳ bước thực hiện nào. Vì vậy, chúng ta cần đảm bảo hai điều:

  1. thực hiện lặp lại các đường ống sẽ không phá hủy cơ sở dữ liệu của chúng tôi

  2. thực thi lặp đi lặp lại sẽ không chết do lỗi 'trùng lặp khóa chính'.

Phương pháp 1: sử dụng REPLACE

Nó rất đơn giản:

REPLACE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Nếu hồ sơ tồn tại, nó sẽ bị ghi đè; nếu nó chưa tồn tại, nó sẽ được tạo ra. Tuy nhiên, sử dụng phương pháp này không hiệu quả đối với trường hợp của chúng tôi: chúng tôi không cần ghi đè lên các bản ghi hiện có, chỉ cần bỏ qua chúng.

Phương pháp 2: sử dụng INSERT IGNORE Cũng rất đơn giản:

INSERT IGNORE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Ở đây, nếu 'oblembl_transcript_id' đã có trong cơ sở dữ liệu, nó sẽ bị bỏ qua trong âm thầm (bỏ qua). (Nói chính xác hơn, đây là trích dẫn từ tài liệu tham khảo MySQL: Từ Nếu bạn sử dụng từ khóa IGNORE, các lỗi xảy ra trong khi thực hiện câu lệnh INSERT được coi là cảnh báo thay vào đó. Ví dụ, không có IGNORE, một hàng trùng lặp chỉ mục UNIITE hiện có hoặc giá trị PRIMARY KEY trong bảng gây ra lỗi khóa trùng lặp và câu lệnh bị hủy bỏ. nghiêm trọng.) Nếu bản ghi chưa tồn tại, nó sẽ được tạo.

Phương pháp thứ hai này có một số điểm yếu tiềm ẩn, bao gồm cả việc không hủy bỏ truy vấn trong trường hợp có bất kỳ vấn đề nào khác xảy ra (xem hướng dẫn). Do đó, nó nên được sử dụng nếu được kiểm tra trước đó mà không có từ khóa IGNORE.

Phương pháp 3: sử dụng INSERT trên ON CẬP NHẬT KHÓA HỌC:

Tùy chọn thứ ba là sử dụng INSERT … ON DUPLICATE KEY UPDATE cú pháp và trong phần CẬP NHẬT, không cần thực hiện một số thao tác vô nghĩa (trống), như tính toán 0 + 0 (Geoffray đề nghị thực hiện gán id = id cho công cụ tối ưu hóa MySQL để bỏ qua thao tác này). Ưu điểm của phương pháp này là nó chỉ bỏ qua các sự kiện quan trọng trùng lặp và vẫn hủy bỏ các lỗi khác.

Như một thông báo cuối cùng: bài đăng này được lấy cảm hứng từ Xaprb. Tôi cũng khuyên bạn nên tham khảo bài đăng khác của mình về cách viết các truy vấn SQL linh hoạt.


3
và tôi có thể kết hợp điều đó với "trì hoãn" để tăng tốc kịch bản không?
warren

3
vâng, chèn chậm trễ có thể tăng tốc mọi thứ cho bạn. dùng thử
knittl


10
INSERT … ON DUPLICATE KEY UPDATElà tốt hơn vì nó không xóa hàng, bảo quản bất kỳ auto_incrementcột và dữ liệu khác.
hương thơm

15
Chỉ để thông báo cho mọi người. Việc sử dụng INSERT … ON DUPLICATE KEY UPDATEphương pháp sẽ làm tăng bất kỳ cột AUTO_INCREMENT nào khi chèn không thành công. Có lẽ bởi vì nó không thực sự thất bại, nhưng CẬP NHẬT.
not2qubit

216

Giải pháp:

INSERT INTO `table` (`value1`, `value2`) 
SELECT 'stuff for value1', 'stuff for value2' FROM DUAL 
WHERE NOT EXISTS (SELECT * FROM `table` 
      WHERE `value1`='stuff for value1' AND `value2`='stuff for value2' LIMIT 1) 

Giải trình:

Truy vấn trong cùng

SELECT * FROM `table` 
      WHERE `value1`='stuff for value1' AND `value2`='stuff for value2' LIMIT 1

được sử dụng như WHERE NOT EXISTS-condition phát hiện nếu đã tồn tại một hàng với dữ liệu được chèn. Sau khi tìm thấy một hàng loại này, truy vấn có thể dừng lại, do đó LIMIT 1(tối ưu hóa vi mô, có thể bị bỏ qua).

Truy vấn trung gian

SELECT 'stuff for value1', 'stuff for value2' FROM DUAL

đại diện cho các giá trị được chèn. DUALđề cập đến một hàng đặc biệt, một bảng cột được trình bày theo mặc định trong tất cả các cơ sở dữ liệu của Oracle (xem https://en.wikipedia.org/wiki/DUAL_table ). Trên phiên bản MySQL-Server 5.7.26 tôi đã nhận được một truy vấn hợp lệ khi bỏ qua FROM DUAL, nhưng các phiên bản cũ hơn (như 5.5.60) dường như yêu cầu FROMthông tin. Bằng cách sử dụngWHERE NOT EXISTS truy vấn trung gian trả về một tập kết quả trống nếu truy vấn trong cùng tìm thấy dữ liệu khớp.

Truy vấn bên ngoài

INSERT INTO `table` (`value1`, `value2`) 

chèn dữ liệu, nếu có được trả về bởi truy vấn trung gian.


4
bạn có thể cung cấp thêm một số thông tin về cách sử dụng này?
Alex V

36
Biến thể này phù hợp nếu không tồn tại khóa duy nhất trên bàn ( INSERT IGNOREINSERT ON DUPLICATE KEYyêu cầu các ràng buộc khóa duy nhất)
rabudde

2
Nếu bạn sử dụng "từ kép" trên dòng 2 thay vì "từ bảng", thì bạn không cần mệnh đề "giới hạn 1".
Giàu

6
Điều gì nếu stuff for value1stuff for value2giống hệt nhau? Điều này sẽ ném mộtDuplicate column name
Robin

1
Tôi cũng thích nhiều hơn SELECT 1thay vì SELECT *trong các truy vấn con. Nhiều khả năng điều này có thể được thỏa mãn bởi một chỉ số.
Arth

58

trên bản cập nhật khóa trùng lặp hoặc bỏ qua chèn có thể là giải pháp khả thi với MySQL.


Ví dụ về cập nhật cập nhật khóa trùng lặp dựa trên mysql.com

INSERT INTO table (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

UPDATE table SET c=c+1 WHERE a=1;

Ví dụ về bỏ qua chèn dựa trên mysql.com

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name [(col_name,...)]
    {VALUES | VALUE} ({expr | DEFAULT},...),(...),...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

Hoặc là:

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name
    SET col_name={expr | DEFAULT}, ...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

Hoặc là:

INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name [(col_name,...)]
    SELECT ...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

24

Bất kỳ ràng buộc đơn giản nào cũng nên thực hiện công việc, nếu một ngoại lệ được chấp nhận. Ví dụ:

  • khóa chính nếu không thay thế
  • ràng buộc duy nhất trên một cột
  • ràng buộc nhiều cột duy nhất

Xin lỗi là điều này có vẻ đơn giản. Tôi biết có vẻ xấu khi đối mặt với liên kết bạn chia sẻ với chúng tôi. ;-(

Nhưng tôi không bao giờ đưa ra câu trả lời này, bởi vì nó dường như đáp ứng nhu cầu của bạn. (Nếu không, nó có thể kích hoạt việc cập nhật các yêu cầu của bạn, đó cũng sẽ là "Điều tốt" (TM)).

Đã chỉnh sửa : Nếu một phần chèn sẽ phá vỡ ràng buộc duy nhất của cơ sở dữ liệu, một ngoại lệ được ném ở cấp cơ sở dữ liệu, được trình điều khiển chuyển tiếp. Nó chắc chắn sẽ dừng kịch bản của bạn, với một thất bại. PHP phải có khả năng xử lý trường hợp đó ...


1
tôi đã thêm một sự làm rõ cho câu hỏi - câu trả lời của bạn vẫn còn áp dụng chứ?
warren

2
Tôi tin là có. Một ràng buộc duy nhất sẽ gây ra sự thất bại của các chèn không chính xác. Lưu ý: bạn phải đối phó với lỗi này trong mã của mình, nhưng điều này khá chuẩn.
KLE

1
bây giờ tôi sẽ gắn bó với giải pháp mà tôi đã chấp nhận - nhưng sẽ tiếp tục xem xét xử lý các lỗi INSERT, v.v. khi ứng dụng phát triển
warren

3
INSERT IGNOREvề cơ bản thay đổi tất cả các lỗi thành cảnh báo để tập lệnh của bạn không bị gián đoạn. Sau đó, bạn có thể xem bất kỳ cảnh báo với lệnh SHOW WARNINGS. Và một lưu ý quan trọng khác : các ràng buộc UNIITE không hoạt động với các giá trị NULL, tức là. row1 (1, NULL) và row2 (1, NULL) đều sẽ được chèn (trừ khi một ràng buộc khác như khóa chính bị hỏng). Thật không may.
Simon East

18

Đây là một hàm PHP sẽ chỉ chèn một hàng nếu tất cả các giá trị cột được chỉ định không tồn tại trong bảng.

  • Nếu một trong các cột khác nhau, hàng sẽ được thêm vào.

  • Nếu bảng trống, hàng sẽ được thêm vào.

  • Nếu một hàng tồn tại trong đó tất cả các cột được chỉ định có các giá trị được chỉ định, hàng sẽ không được thêm vào.

    function insert_unique($table, $vars)
    {
      if (count($vars)) {
        $table = mysql_real_escape_string($table);
        $vars = array_map('mysql_real_escape_string', $vars);
    
        $req = "INSERT INTO `$table` (`". join('`, `', array_keys($vars)) ."`) ";
        $req .= "SELECT '". join("', '", $vars) ."' FROM DUAL ";
        $req .= "WHERE NOT EXISTS (SELECT 1 FROM `$table` WHERE ";
    
        foreach ($vars AS $col => $val)
          $req .= "`$col`='$val' AND ";
    
        $req = substr($req, 0, -5) . ") LIMIT 1";
    
        $res = mysql_query($req) OR die();
        return mysql_insert_id();
      }
    
      return False;
    }

Ví dụ sử dụng:

<?php
insert_unique('mytable', array(
  'mycolumn1' => 'myvalue1',
  'mycolumn2' => 'myvalue2',
  'mycolumn3' => 'myvalue3'
  )
);
?>

5
Khá đắt nếu bạn có một lượng lớn các phần chèn thêm.
Эџad đã được đăng ký vào

đúng, nhưng hiệu quả nếu bạn cần thêm kiểm tra cụ thể
Charles Forest

1
Cảnh báo: mysql_* tiện ích mở rộng không được chấp nhận kể từ PHP 5.5.0 và đã bị xóa kể từ phiên bản PHP 7.0.0. Thay vào đó, nên sử dụng phần mở rộng mysqli hoặc PDO_MyQuery . Xem thêm Tổng quan về API của MySQL để được trợ giúp thêm trong khi chọn API MySQL.
Dharman

17
REPLACE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Nếu hồ sơ tồn tại, nó sẽ bị ghi đè; nếu nó chưa tồn tại, nó sẽ được tạo ra.


10
REPLACEcó thể xóa hàng và sau đó chèn thay vì cập nhật. Tác dụng phụ là các ràng buộc có thể xóa các đối tượng khác và xóa các kích hoạt được kích hoạt.
xmedeko

1
Từ hướng dẫn sử dụng MySQL: "REPLACE chỉ có ý nghĩa nếu một bảng có chỉ số PRIMARY KEY hoặc UNIQUE. Mặt khác, nó trở nên tương đương với INSERT, bởi vì không có chỉ mục nào được sử dụng để xác định xem một hàng mới có nhân đôi một hàng khác hay không."
BurninLeo

16

Hãy thử như sau:

IF (SELECT COUNT(*) FROM beta WHERE name = 'John' > 0)
  UPDATE alfa SET c1=(SELECT id FROM beta WHERE name = 'John')
ELSE
BEGIN
  INSERT INTO beta (name) VALUES ('John')
  INSERT INTO alfa (c1) VALUES (LAST_INSERT_ID())
END

5
Hãy thử Câu trả lời này có giá trị thấp trên StackOverflow vì chúng làm rất ít để giáo dục OP và hàng ngàn nhà nghiên cứu trong tương lai. Vui lòng chỉnh sửa câu trả lời này để bao gồm cách giải pháp hoạt động và tại sao đó là một ý tưởng tốt.
mickmackusa

1
Giải pháp hoàn hảo trong trường hợp các trường tương ứng không có khóa ..!
Leo

6

Có một số câu trả lời bao gồm cách giải quyết vấn đề này nếu bạn có một UNIQUEchỉ mục mà bạn có thể kiểm tra với ON DUPLICATE KEYhoặc INSERT IGNORE. Điều đó không phải lúc nào cũng đúng, và như UNIQUEcó một ràng buộc về độ dài (1000 byte), bạn có thể không thể thay đổi điều đó. Ví dụ: tôi đã phải làm việc với siêu dữ liệu trong WordPress ( wp_postmeta).

Cuối cùng tôi đã giải quyết nó bằng hai truy vấn:

UPDATE wp_postmeta SET meta_value = ? WHERE meta_key = ? AND post_id = ?;
INSERT INTO wp_postmeta (post_id, meta_key, meta_value) SELECT DISTINCT ?, ?, ? FROM wp_postmeta WHERE NOT EXISTS(SELECT * FROM wp_postmeta WHERE meta_key = ? AND post_id = ?);

Truy vấn 1 là một UPDATEtruy vấn thông thường không có hiệu lực khi không có bộ dữ liệu trong câu hỏi. Truy vấn 2 là một INSERTphụ thuộc vào a NOT EXISTS, tức INSERTlà chỉ được thực hiện khi tập dữ liệu không tồn tại.


2

Một điều đáng chú ý là INSERT IGNORE vẫn sẽ tăng khóa chính cho dù tuyên bố đó có thành công hay không giống như một INSERT bình thường.

Điều này sẽ gây ra những khoảng trống trong các khóa chính của bạn có thể khiến lập trình viên không ổn định về mặt tinh thần. Hoặc nếu ứng dụng của bạn được thiết kế kém và phụ thuộc vào các khóa chính gia tăng hoàn hảo, nó có thể trở thành vấn đề đau đầu.

Nhìn vào innodb_autoinc_lock_mode = 0(cài đặt máy chủ và đi kèm với một lần nhấn hiệu suất nhẹ) hoặc sử dụng CHỌN trước để đảm bảo truy vấn của bạn sẽ không bị lỗi (cũng đi kèm với lần nhấn hiệu suất và mã bổ sung).


Tại sao "lỗ hổng trong khóa chính của bạn" - thậm chí có khả năng - "làm cho lập trình viên không ổn định về mặt tinh thần"? Các khoảng trống xảy ra mọi lúc trong các khóa chính - chẳng hạn như mỗi lần bạn xóa một bản ghi.
warren

Bắt đầu với một SELECTthất bại toàn bộ mục đích chỉ là bàn giao một lô lớn INSERTvà không muốn lo lắng về các bản sao.
warren

2

Cập nhật hoặc chèn mà không biết khóa chính

Nếu bạn đã có khóa duy nhất hoặc khóa chính, các câu trả lời khác có INSERT INTO ... ON DUPLICATE KEY UPDATE ...hoặcREPLACE INTO ... sẽ hoạt động tốt (lưu ý thay thế thành xóa nếu tồn tại và sau đó chèn - do đó không cập nhật một phần giá trị hiện tại).

Nhưng nếu bạn có các giá trị cho some_column_idsome_type, sự kết hợp của chúng được biết là duy nhất. Và bạn muốn cập nhật some_valuenếu tồn tại, hoặc chèn nếu không tồn tại. Và bạn muốn thực hiện nó chỉ trong một truy vấn (để tránh sử dụng giao dịch). Đây có thể là một giải pháp:

INSERT INTO my_table (id, some_column_id, some_type, some_value)
SELECT t.id, t.some_column_id, t.some_type, t.some_value
FROM (
    SELECT id, some_column_id, some_type, some_value
    FROM my_table
    WHERE some_column_id = ? AND some_type = ?
    UNION ALL
    SELECT s.id, s.some_column_id, s.some_type, s.some_value
    FROM (SELECT NULL AS id, ? AS some_column_id, ? AS some_type, ? AS some_value) AS s
) AS t
LIMIT 1
ON DUPLICATE KEY UPDATE
some_value = ?

Về cơ bản, truy vấn thực hiện theo cách này (ít phức tạp hơn vẻ ngoài của nó):

  • Chọn một hàng hiện có thông qua WHEREkhớp mệnh đề.
  • Liên kết kết quả với một hàng mới tiềm năng (bảng s), trong đó các giá trị cột được đưa ra rõ ràng (s.id là NULL, do đó, nó sẽ tạo ra một định danh tăng tự động mới).
  • Nếu một hàng hiện có được tìm thấy, sau đó hàng tiềm năng mới từ bảng sbị loại bỏ (do LIMIT 1 trên bảng t), và nó sẽ luôn kích hoạt ON DUPLICATE KEYmà sẽ UPDATEcác some_valuecột.
  • Nếu không tìm thấy một hàng hiện có, thì hàng mới tiềm năng được chèn vào (như được đưa ra bởi bảng s).

Lưu ý: Mỗi bảng trong cơ sở dữ liệu quan hệ nên có ít nhất một idcột tăng tự động chính . Nếu bạn không có thứ này, hãy thêm nó, ngay cả khi bạn không cần nó từ cái nhìn đầu tiên. Nó chắc chắn là cần thiết cho "mẹo" này.


Một số người trả lời khác đã nhận được một INSERT INTO ... SELECT FROMđịnh dạng. Tại sao bạn cũng vậy?
warren

2
@warren Hoặc bạn không đọc câu trả lời của tôi, bạn không hiểu nó hoặc tôi đã không giải thích nó một cách chính xác. Trong mọi trường hợp, hãy để tôi nhấn mạnh những điều sau: đây không chỉ là một INSERT INTO... SELECT FROM...giải pháp thông thường . Vui lòng tham khảo cho tôi một liên kết đến một câu trả lời giống nhau, nếu bạn có thể tìm thấy nó, tôi sẽ xóa câu trả lời này, nếu không bạn nêu lên câu trả lời của tôi (thỏa thuận?). Hãy chắc chắn xác minh rằng câu trả lời bạn sẽ liên kết chỉ sử dụng 1 truy vấn (để cập nhật + chèn), không có giao dịch và có thể nhắm mục tiêu bất kỳ tổ hợp cột nào được biết là duy nhất (vì vậy riêng biệt các cột không cần phải là duy nhất).
Yeti
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.