Cloudreve轻量网盘部署实战:PHP工程师的私有云落地指南
2026/10/2 3:07:17
在数据库操作中,我们经常需要处理"存在则更新,不存在则插入"的场景。MySQL 提供了INSERT ... ON DUPLICATE KEY UPDATE语句来高效实现这一需求,特别是在批量操作时,其性能优势更为明显。
INSERTINTOtable_name(column1,column2,...)VALUES(value1,value2,...),(value1,value2,...),...ONDUPLICATEKEYUPDATEcolumn1=VALUES(column1),column2=VALUES(column2),...;| 操作方式 | 网络往返次数 | 执行效率 | 自增ID影响 | 触发器 |
|---|---|---|---|---|
| 单独INSERT+UPDATE | 高 | 低 | 可能改变 | DELETE+INSERT触发器 |
| ON DUPLICATE KEY UPDATE | 低 | 高 | 保持不变 | UPDATE触发器 |
| REPLACE INTO | 低 | 中 | 会改变 | DELETE+INSERT触发器 |
-- 批量插入/更新5条记录INSERTINTOproducts(id,name,price,stock,update_time)VALUES(1,'Product A',19.99,100,NOW()),(2,'Product B',29.99,50,NOW()),(3,'Product C',39.99,75,NOW()),(4,'Product D',49.99,200,NOW()),(5,'Product E',59.99,30,NOW())ONDUPLICATEKEYUPDATEprice=VALUES(price),stock=VALUES(stock),update_time=NOW();-- 只有当新价格比旧价格低时才更新INSERTINTOproducts(id,name,price,stock)VALUES(1,'Product A',18.99,100)ONDUPLICATEKEYUPDATEprice=IF(VALUES(price)<price,VALUES(price),price),stock=VALUES(stock);-- 库存增量更新INSERTINTOproducts(id,stock_change)VALUES(1,10),(2,-5),(3,20)ONDUPLICATEKEYUPDATEstock=stock+VALUES(stock_change);-- 先创建临时表或使用多值INSERTINSERTINTOproduct_updates(product_id,price_change,stock_change)VALUES(1,0,10),(2,2.5,0),(3,-1.0,5);-- 然后执行批量更新INSERTINTOproducts(id,price,stock)SELECTpu.product_id,p.price+IFNULL(pu.price_change,0),p.stock+IFNULL(pu.stock_change,0)FROMproduct_updates puLEFTJOINproducts pONpu.product_id=p.idONDUPLICATEKEYUPDATEprice=VALUES(price),stock=VALUES(stock);-- 从数据仓库同步到OLTP系统INSERTINTOdw_products(product_id,product_name,category,price)SELECTid,name,category,priceFROMstaging_productsONDUPLICATEKEYUPDATEproduct_name=VALUES(product_name),category=VALUES(category),price=VALUES(price),sync_time=NOW();-- 批量更新用户行为计数器INSERTINTOuser_metrics(user_id,metric_date,logins,purchases)VALUES(1001,'2023-05-20',1,0),(1002,'2023-05-20',1,1),(1003,'2023-05-20',0,1)ONDUPLICATEKEYUPDATElogins=logins+VALUES(logins),purchases=purchases+VALUES(purchases);-- 批量更新缓存表INSERTINTOcache_user_profiles(user_id,username,last_active,data_version)SELECTid,username,last_login_time,2FROMusersWHEREstatus='active'ONDUPLICATEKEYUPDATEusername=VALUES(username),last_active=VALUES(last_active),data_version=VALUES(data_version);# Python示例:分批处理大数据量defbatch_upsert(connection,table,data,batch_size=1000):foriinrange(0,len(data),batch_size):batch=data[i:i+batch_size]placeholders=", ".join(["(%s, %s, %s, %s)"]*len(batch))values=[itemforsublistinbatchforiteminsublist]sql=f""" INSERT INTO{table}(id, col1, col2, col3) VALUES{placeholders}ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2), col3 = VALUES(col3) """withconnection.cursor()ascursor:cursor.execute(sql,values)connection.commit()确保用于检测重复的键(主键或唯一键)有适当的索引:
-- 为频繁用于冲突检测的列添加索引ALTERTABLEordersADDUNIQUEINDEXidx_order_no(order_no);-- 使用事务确保批量操作的原子性STARTTRANSACTION;INSERTINTOlarge_table(id,col1,col2)VALUES(1,'A','B'),(2,'C','D'),...-- 大量数据ONDUPLICATEKEYUPDATEcol1=VALUES(col1),col2=VALUES(col2);-- 只有在所有行都处理成功后才提交COMMIT;# Python示例:获取实际插入/更新的行数cursor=connection.cursor()cursor.execute(upsert_sql,params)affected_rows=cursor.rowcount# 注意:在批量操作中,rowcount返回的是总影响行数# 实际插入的行数 = affected_rows - (更新的行数*2)-- MySQL 8.0+ 可以使用ROW_COUNT()和LAST_INSERT_ID()INSERTINTO...ONDUPLICATEKEYUPDATE...;SELECTROW_COUNT();-- 返回-1表示所有行都是更新,正数表示插入的行数| 指标 | INSERT ON DUPLICATE KEY UPDATE | REPLACE INTO |
|---|---|---|
| 操作类型 | 直接更新 | 删除后插入 |
| 自增ID | 保持不变 | 可能改变 |
| 触发器 | UPDATE触发器 | DELETE+INSERT触发器 |
| 批量性能 | 优秀 | 良好 |
| 原子性 | 是 | 是 |
明确业务需求:
批量大小选择:
错误处理:
try:batch_upsert(connection,"products",data_list)exceptExceptionase:# 记录错误并考虑重试机制logger.error(f"Batch upsert failed:{str(e)}")# 可能需要拆分批次重试监控性能:
-- 检查慢查询日志SETGLOBALslow_query_log='ON';SETGLOBALlong_query_time=1;-- 秒INSERT ... ON DUPLICATE KEY UPDATE是MySQL中处理"存在则更新,不存在则插入"场景的高效解决方案,特别是在批量操作时表现出色。通过合理使用这一语句,可以:
在实际应用中,应根据业务需求选择合适的批量大小,添加适当的错误处理和监控,以充分发挥这一特性的优势。