Skip to content

MySQL - 处理重复项

本教程提供了在 MySQL 中管理重复记录的全面指南。您将了解重复数据为何会引发问题,并探讨使用 SQL 命令和通过客户端应用程序逻辑来预防、识别和删除重复数据的现代技术。

重复数据,或数据冗余,可能会在数据库中引起严重问题:

  • 数据完整性: 变得不清楚哪条记录是正确或最新的,导致报告不一致和不可靠。
  • 存储效率低下: 重复数据浪费磁盘空间,增加存储成本和备份时间。
  • 性能下降: 包含冗余数据的大型表会降低查询性能。
  • 应用程序错误: 当软件逻辑遇到意外的重复条目时,可能会中断或产生不正确的结果。

处理重复数据的最佳方法是首先防止它们进入数据库。这通过数据库约束(database constraints)实现。

一个 PRIMARY KEY(主键)约束确保表中的每一行都是唯一可识别的。它不允许 NULL 值,并且索引列必须是唯一的。一个表只能有一个主键。

CREATE TABLE users (
id INT AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
username VARCHAR(50) NOT NULL,
PRIMARY KEY(id),
UNIQUE KEY (email) -- 强制电子邮件唯一性也是个好主意
);

一个 UNIQUE(唯一)约束也确保列或一组列中的所有值都是唯一的。与 PRIMARY KEY 不同,一个表可以有多个 UNIQUE 约束,并且它们可以允许一个 NULL 值。

CREATE TABLE products (
product_code VARCHAR(20) NOT NULL,
product_name VARCHAR(100) NOT NULL,
UNIQUE (product_code) -- 防止产品代码重复
);

当存在约束时,如果标准 INSERT 语句违反了唯一性,它将失败。MySQL 提供了优雅处理这种情况的方法:

  • INSERT IGNORE: 如果新行是重复的,MySQL 会静默丢弃它而不会产生错误。原始行保持不变。
  • REPLACE INTO: 此命令是 MySQL 的扩展。如果行是新的,它作用类似于 INSERT。如果它是重复的,它会首先 DELETE(删除)现有行,然后 INSERT(插入)新行。(注意:这会改变行的内部 ID 并可能导致级联删除)。
  • INSERT … ON DUPLICATE KEY UPDATE(推荐): 这是最灵活和广泛使用的方法。它尝试执行 INSERT,如果发现重复键,则改为对现有行执行 UPDATE。这避免了删除并保留了原始行的 ID。
-- 使用现代、推荐的方法
INSERT INTO products (product_code, product_name)
VALUES ('A-123', 'Super Widget')
ON DUPLICATE KEY UPDATE product_name = 'Super Widget v2';

第二部分:查找和识别重复数据

Section titled “第二部分:查找和识别重复数据”

如果表中已存在重复数据,您首先需要找到它们。

这是查找哪些值重复以及它们出现多少次的经典方法。

-- 假设有一个可能包含重复数据的 contacts 表
SELECT
email, -- 可能重复的列
COUNT(*) as duplicate_count
FROM contacts
GROUP BY email
HAVING COUNT(*) > 1;

此查询将返回 contacts 表中出现多次的所有电子邮件地址及其计数。

一旦您识别出重复数据,您就需要一个删除它们的策略,通常是保留一个版本(例如,最旧的或最新的)。

方法 1:使用临时表(安全但需要停机时间)

Section titled “方法 1:使用临时表(安全但需要停机时间)”

此方法安全且易于理解。您创建一个只包含唯一行的新表,然后替换原始表。

-- 步骤 1:创建一个包含您想保留的唯一行的临时表。
CREATE TABLE contacts_temp AS
SELECT * FROM contacts
GROUP BY email; -- 或者使用 MIN(id) 来保留第一个条目
-- 步骤 2:删除原始表。
DROP TABLE contacts;
-- 步骤 3:将临时表重命名为原始名称。
RENAME TABLE contacts_temp TO contacts;

方法 2:使用自连接(Self-Join)或子查询(Subquery)(高级)

Section titled “方法 2:使用自连接(Self-Join)或子查询(Subquery)(高级)”

此方法原地删除重复数据。它更复杂但避免了表替换。它会删除所有 id 小于具有相同电子邮件的另一行的行。

DELETE t1 FROM contacts t1
INNER JOIN contacts t2
WHERE
t1.id < t2.id AND
t1.email = t2.email;

方法 3:使用窗口函数(现代且强大)

Section titled “方法 3:使用窗口函数(现代且强大)”

在 MySQL 8.0+ 版本中,结合 ROW_NUMBER() 窗口函数使用公共表表达式(CTE,Common Table Expression)是一种简洁且强大的处理方式。

WITH NumberedContacts AS (
SELECT
id,
ROW_NUMBER() OVER(PARTITION BY email ORDER BY id ASC) as row_num
FROM contacts
)
DELETE FROM contacts
WHERE id IN (SELECT id FROM NumberedContacts WHERE row_num > 1);

此查询按 email 对数据进行分区,对每个分区内的行进行编号,然后删除编号大于 1 的任何行,从而有效地只保留每个电子邮件的第一个条目。

当数据库约束被违反时,您的应用程序代码应准备好处理潜在的重复条目错误。

import mysql.connector
import os
def add_user(email, username):
connection = None
try:
connection = mysql.connector.connect(
host=os.getenv('DB_HOST', 'localhost'),
user=os.getenv('DB_USER', 'root'),
password=os.getenv('DB_PASSWORD', 'password'),
database='your_database'
)
cursor = connection.cursor()
insert_query = "INSERT INTO users (email, username) VALUES (%s, %s)"
cursor.execute(insert_query, (email, username))
connection.commit()
print(f"User '{username}' with email '{email}' added successfully.")
except mysql.connector.Error as err:
# MySQL 中重复条目的错误代码是 1062
if err.errno == 1062:
print(f"Error: A user with the email '{email}' already exists.")
else:
print(f"Database Error: {err}")
if connection:
connection.rollback()
finally:
if connection and connection.is_connected():
cursor.close()
connection.close()
# --- 测试用例 ---
add_user('test@example.com', 'testuser1') # 应该成功
add_user('test@example.com', 'testuser2') # 应该优雅地失败