1. 程式人生 > >開發工作中使用過的mysql的一些總結

開發工作中使用過的mysql的一些總結

saveOrUpdate 的方法?

常見的 insert or update 場景

在平常的開發中,經常碰到這種更新資料的場景:先判斷某一資料在庫表中是否存在,存在則 update,不存在則 insert。 
如果使用Hibernate,它自帶saverOrUpdate方法,用起來很方便,但如使用 SQL 語句呢? 
最常見的寫法是,先通過 select 語句查詢記錄是否存在,存在則使用 update 語句更新,不存在則使用 insert 語句插入。

但是這樣做明顯不夠優雅,存在幾個問題:

  • 為了執行一次更新操作,卻在程式中使用了兩次 SQL 查詢,在系統併發大的情況下,效能還是會有影響的。
  • 併發量大的情況下,這裡的 select 與 insret 、update 操作必須繫結在一起作為一個原子操作,這又涉及到事務問題了。
  • 需要針對 insert 與 update 兩種情況寫不同的 SQL 語句,但是這兩個操作業務場景經常是同一個。
  • 程式碼中存在 if else 語句,明明幹了一件事,程式碼卻很長。

如何優雅的實現 saverOrUpdate?

Oracle下有 merge 的語法, MySQL 針對這種場景,在標準 SQL 下也有自己的拓展語法。

INSERT INTO IGNORE

資料不存在則插入,存在則無操作。 
在 INSERT 語句中使用 IGNORE 關鍵字實現資料不存在則插入,存在則無操作。它的實現邏輯是,當插入語句出現主鍵衝突,或者唯一鍵衝突時,不丟擲錯誤,直接忽略這條插入語句。官網上的相關介紹如下:

If you use the IGNORE keyword, errors that occur while executing the INSERT statement are ignored. For example, without IGNORE, a row that duplicates an existing UNIQUE index or PRIMARY KEY value in the table causes a duplicate-key error and the statement is aborted. With IGNORE, the row is discarded and no error occurs. Ignored errors may generate warnings instead, although duplicate-key errors do not.

MySQL 官方文件提供的標準語法:

  1. INSERT IGNORE
  2. INTO tbl_name
  3. [PARTITION (partition_name,...)]
  4. [(col_name,...)]
  5. {VALUES | VALUE}({expr | DEFAULT},...),(...),...
  6. 或者
  7. INSERT IGNORE
  8. [INTO] tbl_name
  9. [PARTITION (partition_name,...)]
  10. [(col_name,...)]
  11. SELECT ...

可見除了多了個 IGNORE 關鍵字以外,跟一般 INSERT 語句並無區別。

舉個例子:

1.建一張測試用的表

  1. CREATE TABLE `test_tab`(
  2. `name` varchar(64) NOT NULL,
  3. `age`int(11) NOT NULL,
  4. PRIMARY KEY (`name`)
  5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.插入一條資料

  1. INSERT INTO `test_tab`(`name`,`age`) values ('zhangsan',24)

當前test_tab表的資料為:

name age
zhangsan 24

3.再執行一次步驟2的插入語句,則會報異常:

  1. [Err]1062-Duplicate entry 'zhangsan'for key 'PRIMARY'

4.對步驟2的 INSERT 語句增加 IGNORE 關鍵字,則不會報異常,已存在的資料也不會被更新。

  1. INSERT IGNORE INTO `test_tab`(`name`,`age`) values ('zhangsan',24);
  2. ------
  3. 語句執行情況:
  4. 受影響的行:0
  5. 時間:0.000s

當前 test_tab 表的資料為:

name age
zhangsan 24

DUPLICATE KEY UPDATE 關鍵字

在 INSERT 語句中使用 ON DUPLICATE KEY UPDATE 關鍵字實現資料不存在則插入,存在則更新的操作。判斷資料重複的邏輯依然是主鍵衝突或者唯一鍵衝突。 
官網上的相關介紹如下:

if you specify ON DUPLICATE KEY UPDATE, and a row is inserted that would cause a duplicate value in a UNIQUE index or PRIMARY KEY, an UPDATE of the old row is performed. The affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated, and 0 if an existing row is set to its current values.

MySQL 官方文件中提供標準的語法:

  1. INSERT
  2. [INTO] tbl_name
  3. [PARTITION (partition_name,...)]
  4. [(col_name,...)]
  5. {VALUES | VALUE}({expr | DEFAULT},...),(...),...
  6. [ ON DUPLICATE KEY UPDATE
  7. col_name=expr
  8. [, col_name=expr]...]
  9. 或者:
  10. INSERT
  11. [INTO] tbl_name
  12. [PARTITION (partition_name,...)]
  13. SET col_name={expr | DEFAULT},...
  14. [ ON DUPLICATE KEY UPDATE
  15. col_name=expr
  16. [, col_name=expr]...]
  17. 或者:
  18. INSERT
  19. [INTO] tbl_name
  20. [PARTITION (partition_name,...)]
  21. [(col_name,...)]
  22. SELECT ...
  23. [ ON DUPLICATE KEY UPDATE
  24. col_name=expr
  25. [, col_name=expr]...]

可見,還是原來 INSERT 語句的寫法。

舉個例子:

1.使用剛才新建的 test_tab 表,此時表中的資料如下:

name age
zhangsan 24

2.使用主鍵相同的 insert 語句,仍然會 duplicate key 錯誤

  1. INSERT INTO `test_tab`(`name`,`age`) values ('zhangsan',50);
  2. ------------
  3. [Err]1062-Duplicate entry 'zhangsan'for key 'PRIMARY'

3.對剛才的 INSERT 語句新增 ON DUPLICATE KEY UPDATE 
… 關鍵字:

  1. INSERT INTO `test_tab`(`name`,`age`) values ('zhangsan',50)
  2. ON DUPLICATE KEY UPDATE `age`=50;
  3. ------------
  4. 受影響的行:2
  5. 時間:0.025s

4.此時主鍵為’zhangsan’的資料,age欄位已被更新:

name age
zhangsan 50

5.當然,如果主鍵不衝突,效果跟一般插入語句是一樣的:

  1. INSERT INTO `test_tab`(`name`,`age`) values ('lisi',30)
  2. ON DUPLICATE KEY UPDATE `age`=30;
  3. ------------
  4. 受影響的行:1
  5. 時間:0.009s
name age
zhangsan 50
lisi 30

REPLACE INTO

saveOrUpdate 在 MySQL 中還有另一種實現,即 REPLACE INTO 語句,它用起來有點像 Oracle 的 Merge。判斷資料重複的邏輯依然是主鍵或者唯一鍵衝突。 MySQL 官方文件中提供標準的語法:

  1. REPLACE [LOW_PRIORITY | DELAYED]
  2. [INTO] tbl_name
  3. [PARTITION (partition_name,...)]
  4. [(col_name,...)]
  5. {VALUES | VALUE}({expr | DEFAULT},...),(...),...
  6. 或:
  7. REPLACE [LOW_PRIORITY | DELAYED]
  8. [INTO] tbl_name
  9. [PARTITION (partition_name,...)]
  10. SET col_name={expr | DEFAULT},...
  11. 或:
  12. REPLACE [LOW_PRIORITY | DELAYED]
  13. [INTO] tbl_name
  14. [PARTITION (partition_name,...)]
  15. [(col_name,...)]
  16. SELECT ...
舉個例子:

1.仍然使用上面的 test_tab 表的資料,此時資料如下

name age
zhangsan 50
lisi 30

2.使用一般的insert語句插入 name=zhangsan 的資料,報主鍵衝突。但是換成 REPLACE INTO … 語句則沒問題:

  1. REPLACE INTO `test_tab`(`name`,`age`) VALUES ('zhangsan',30);
  2. ------------
  3. 受影響的行:2
  4. 時間:0.009s

3.結果如下:

name age
zhangsan 30
lisi 30

對於操作結果來說,很像是 SAVE OR UPDATE,但是實現方式與 INSERT 的“DUPLICATE KEY UPDATE”關鍵字不同。當使用 REPLACE INTO 語句時,對於重複的資料,是直接刪除,然後再插入新資料的。所以它的更新其實不是 UPDATE,而是 DELETE->INSERT 。大多數情況下,使用 REPLACE INTO 完成更新操作並無問題,但是有一種場景必須特別注意:

  • 當被更新的表,存在 INSERT,UPDATE ,和 DELETE 觸發器時,使用 REPLACE 語句必須特別小心。因為按照業務邏輯,更新完資料後,應該觸發 UPDATE 觸發器,但是使用 REPLACE 語句的話,會觸發 DELETE 和 INSERT 觸發器,如果 UPDATE 觸發器有一些特殊操作(比如記錄操作日誌)的話,使用 REPLACE 會導致業務邏輯混亂。

所以當被更新表存在觸發器的場景時,使用 INSERT 的“DUPLICATE KEY UPDATE”關鍵字更合適。

總結

  1. 不存在則插入,存在則不操作,使用 INSERT IGNORE INTO 。
  2. 不存在則插入,存在則更新,可以考慮 INSERT …… ON DUPLICATE KEY ,或者 REPLACE INTO。
  3. INSERT …… ON DUPLICATE KEY 是真正的 saveOrUpdate 操作,REPLACE 會先刪除舊資料,再插入新資料。
  4. INSERT …… ON DUPLICATE KEY 操作在 5.6.6 版本以前,MyISAM 引擎的表,UPDATE 操作會使用表級鎖。
  5. 使用單獨的 INSERT 或者 UPDATE 語句會比 INSERT …… ON DUPLICATE KEY有更好的效能,對於INSERT 或 UPDATE 明顯區分的場景或者效能要求高的情況,前者還是有一定優勢。關於它們效能的討論
  6. 有觸發器,或者主鍵使用自增 id 的資料表,慎用 REPLACE INTO 。
  7. REPLACE 返回受影響的行數是 DELETE 和 INSERT 操作的總和。
  8. 以上三種操作判斷資料是否重複,都是使用主鍵或者唯一鍵,使用自增 id 作為主鍵的情況,需要另外指定唯一鍵。
  9. 以上語法 MySQL 5.0 以上都能使用。

參考資料

MySQL 中的 COUNT 的使用經驗。

  1. COUNT(*) vs COUNT(COL): 
    他們在邏輯上是不用的。在 MySQL 中,COUNT(COL) 是不會統計 NULL 列的。例如下面這張 pet 表, COUNT(*) 與 COUNT(owner) 是不同的:
  1. select*from pet;
  2. +--------+---------+
  3. | owner | species |
  4. +--------+---------+
  5. |Benny| bird |
  6. |Diane| bird |
  7. |Gwen| cat |
  8. |Harold| cat |
  9. |Adrian| dog |
  10. | NULL | dog |
  11. +--------+---------+
  12. SELECT species, COUNT(*) FROM pet GROUP BY species;
  13. +---------+----------+
  14. | species | COUNT(*)|
  15. +---------+----------+
  16. | bird |2|
  17. | cat |2|
  18. | dog |2|
  19. +---------+----------+
  20. SELECT species, COUNT(owner) FROM pet GROUP BY species;
  21. +---------+--------------+
  22. | species | COUNT(owner)|
  23. +---------+--------------+
  24. | bird |2|
  25. | cat |2|
  26. | dog |1|
  27. +---------+--------------+
  1. COUNT(*) 在不同引擎中的差別: 
    MyISAM 儲存引擎會儲存總行數,沒有 WHERE 條件的情況,MyISAM 表效能明顯優於 INNODB 表。官方手冊。例如:
  1. //todo
  1. COUNT(*) vs COUNT(VAL): 
    他們是等價的,INNODB 中都需要進行全表掃描,並使用合適的索引。INNODB 下看下該表是否有輔助索引,如果有輔助索引的話, count(*)操作會走覆蓋索引,走覆蓋索引速度會比較快,使用 EXPLAIN可以看到 Using index 。 
    COUNT(*) 與 SELECT(*) 中的*是不同的,SELECT(*) 因為不使用覆蓋索引,所以不推薦使用。 
    count(*) 優化在5.6版本後

  2. COUNT(*) vs COUNT(COL) vs COUNT(VAL):

    • 在沒有 WHERE 條件的情況下: COUNT(*) 約等於COUNT(pk) 優於 COUNT(非主鍵有索引) 優於 COUNT(非主鍵無索引) 。
    • 除非要統計某列非空值的總數,否則任何情況一律用COUNT(*),讓查詢分析器自動選擇索引,獲得較高的效率。
    • 除非有特殊需要,否則 COUNT(*) 不要加 WHERE 條件,會嚴重影響效率,如果加了條件 COUNT(*) 和 COUNT(pk) 效率是一致的,COUNT(非主鍵)效率很低。
    • 多表查詢的情況,MySQL 不支援 COUNT(TABLENAME.*) 寫法 。

關於 count() 的一些討論

group_concat

使用場景

當兩張表 inner join ,兩張表記錄是一對多的關係時,需要把多的表的記錄合併成一行,例如:考試與考試標籤 的關係,需要通過一次查詢,獲得考試資訊,以及每個考試對應的所有標籤資訊,所有標籤合併成一個欄位,逗號分隔。 
語法:

  1. GROUP_CONCAT([DISTINCT] expr [,expr ...]
  2. [ORDER BY {unsigned_integer | col_name | expr}
  3. [ASC | DESC][,col_name ...]]
  4. [SEPARATOR str_val])

舉個例子:

  1. SELECT t.id as exam_id ,t.`name`
  2. , GROUP_CONCAT(t2.label_id order by t2.label_id SEPARATOR ',')as label_ids
  3. from center_exam.examination t ,center_exam.exam_label t2
  4. where t.id = t2.biz_id
  5. and t2.type ='examination'
  6. AND t.tenant_id =1
  7. GROUP BY t.id

參考資料

MySQL GROUP_CONCAT 函式 官方手冊

使用 WHERE ID IN (…) or 臨時表 or 批處理

要查詢的記錄數非常大時,使用 IN 還是 臨時表?

我們知道使用 WHERE ID IN (…) 語句時,如果 ID 有加索引,SQL 執行時是會使用索引的。

有一種說法是,當 IN 中查詢的數量非常大時,MySQL 無法使用索引,需要使用一張臨時表儲存 id,再 join 臨時表查詢。

但是在實踐中,對有上萬條 id 的 in 語句進行分析,臨時表的寫法只快了一點點,並沒有發現臨時表的寫法有明顯的效能優勢。且使用 explain 分析 SQL 的執行計劃,大 idlist 的 in 條件語句,執行計劃與小 idlist 的語句並無不同。 
關於使用 in 條件與臨時表效能差異的討論

  1. // todo

另外,使用 id in (……)條件需要擔心的是,MySQL 對單條語句的長度是有限制的,由 sysvar_max_allowed_packet 引數控制,預設為 1M 。 
sysvar_max_allowed_packet說明 
所以,一般情況下使用 id in (……) 是沒有問題的,對於效能要求嚴苛的情況,再考慮臨時表也可以。

** mysql 中 in 記錄數超過一定比例不會使用索引,建議 保留臨時表的使用**

批處理

關於批處理的一些相關討論

目前專案中使用的 jdbcTemplate.batchUpdate(),並沒有做到真正的批處理,通過抓包分析,發現仍然是分多次 SQL 請求到資料庫,要做到真正的批處理請求,需要在 jdbc 驅動的連線串中加上rewriteBatchedStatements=true引數,這樣batchUpdate()的多條語句才能合併成一個請求傳送到資料庫。