跳至主要内容

SQL 去重:全面方法与最佳实践

探索使用 SQL 过滤并永久删除重复行的多种方法。了解在 SQL Server、MySQL 与 PostgreSQL 中去重的实用做法。
· 8分钟 阅读

使用 AI 探索

重复记录是常见问题,会损害数据完整性并拖慢数据库性能。去除这些重复行对于保持数据准确、优化存储,以及提升查询性能至关重要。本文将针对不同使用场景与数据库管理系统,介绍在 SQL 中删除重复行的多种技术。

在开始之前,强烈建议您学习 DataCamp 的Introduction to SQL 和 Learn SQL 课程,夯实用 SQL 提取与分析数据的基础知识。此外,我认为可下载的 SQL Basics Cheat Sheet 十分实用,涵盖最常用的 SQL 函数,便于随时查阅。

要点速览

  • 使用 SELECT DISTINCT 或 GROUP BY 获取唯一行,而不修改数据表

  • 结合 CTE 使用 ROW_NUMBER() 与 DELETE,可精确控制永久删除哪些重复行

  • 在任意 DBMS 中,可用带子查询的 DELETE(NOT IN / MIN())实现直接的去重

  • 针对大数据集,使用临时表分批安全地处理去重

  • 通过主键、唯一约束与合理的数据库规范化,主动预防重复

理解 SQL 中的重复行

SQL 中的重复行指表内在全部或部分选定列上取值相同的记录。造成重复行的常见原因包括:

  • 缺少主键: 当表未定义主键或唯一约束时,系统无法阻止插入重复数据。这可能发生在表未进行规范化以及/或者存在传递依赖问题时。
  • 数据集成问题: 当合并来自不同来源的数据集时,不当的连接或数据格式不一致,可能会意外引入重复。
  • 人工录入错误: 人为失误(如多次录入同一记录)也是重复行的常见原因。

在接下来的内容中,我们将介绍如何在 SQL 中去重,分为两部分:第一部分讲解如何在为报告或仪表盘检索数据时去重;第二部分讲解如何在数据库中执行去重。

如何识别重复行

在删除重复之前,先识别哪些行是重复的。使用 GROUP BY 搭配 HAVING COUNT(*) > 1 可找出出现超过一次的行:

SELECT Name, COUNT(*) AS duplicate_count
FROM customers
GROUP BY Name
HAVING COUNT(*) > 1;

该查询返回每个出现超过一次的 Name 及其出现次数。您也可以通过在 SELECT 与 GROUP BY 子句中同时添加多列,将其扩展到多列组合。

若想为每个重复分组中的行标注其位置,可使用 ROW_NUMBER():

SELECT ID, Name,
       ROW_NUMBER() OVER (PARTITION BY Name ORDER BY ID) AS row_num
FROM customers;

row_num > 1 的行即为重复。识别后,可从下文选择合适的删除方法。

在检索数据时去重的方法

在 SQL 检索记录时,有多种去重方法。每种方法与所用 DBMS(如 SQL Server、MySQL、PostgreSQL)相关。本节将介绍这些方法,并指出各数据库的注意事项。请注意,这些方法仅过滤数据以返回唯一记录,不会修改底层数据表。

使用 DISTINCT 关键字

DISTINCT 关键字用于 SELECT 语句,检索唯一行。针对 MySQL、PostgreSQL 与 SQL Server,其去重语法基本一致。下面的查询将从 customers 表中检索唯一的客户姓名。

SELECT DISTINCT Name 
FROM customers;

结合聚合函数使用 GROUP BY

GROUP BY 子句结合 MAX()、MIN() 或 COUNT() 等聚合函数,可帮助从表中去除重复记录。通过 GROUP BY,您可以选择需要保留的记录并删除其他重复项。

假设您要删除重复的客户记录,但保留 ID 最大的一条。可结合 GROUP BY 与 MAX(),如下所示。

-- Delete duplicate rows from the 'customers' table (aliased as c1)
DELETE c1
FROM customers c1
-- Find the maximum ID for each unique Name
JOIN (
    SELECT Name, MAX(ID) AS MaxID
    FROM customers
    GROUP BY Name
) c2
-- Match rows based on 'Name' and keep the row with the maximum ID
ON c1.Name = c2.Name 
AND c1.ID < c2.MaxID;

MySQL 与 SQL Server 支持上述结合聚合函数与 JOIN 的 GROUP BY 语法。

将 ROW_NUMBER() 与公用表表达式(CTE)结合

结合 ROW_NUMBER() 与 CTE,可以按自定义规则筛除重复。ROW_NUMBER 与 PARTITION BY、ORDER BY 搭配时,会为每行分配唯一的序号。此方法允许过滤不符合条件的行。

以下查询识别重复并保留首次出现,删除其余。

-- Common Table Expression (CTE) to rank rows based on 'Name'
WITH CTE AS (
    SELECT ID, Name, ROW_NUMBER() OVER (PARTITION BY Name ORDER BY ID ASC) AS RowNum
    FROM customers
)
-- Select only the unique records where RowNum = 1
SELECT ID, Name
FROM CTE
WHERE RowNum = 1;

此方法适用于现代版本的 SQL Server、MySQL 和 PostgreSQL。对于大型数据集或更复杂的条件尤为有用,因为它允许精确指定要保留的重复项。

使用自连接(self-JOIN)去重

自连接可以将表与自身进行比较,便于基于特定条件识别并删除重复行。下面的示例通过自连接删除较大的 ID,仅保留每个姓名的首次出现。

-- Delete duplicate rows using self-join
DELETE c1
FROM customers c1
JOIN customers c2
ON c1.Name = c2.Name AND c1.ID > c2.ID;

上述方法适用于主流数据库,包括 SQL Server、MySQL 和 PostgreSQL。想进一步学习如何用聚合函数与连接筛选数据,请查看我们的 Intermediate SQL 课程。

在数据库中去重的方法

除了在查询中去重,您也可以从数据库中永久删除重复记录。这对于维持数据质量非常重要。以下方法可用于在数据库中去重。

使用 ROW_NUMBER() 与 DELETE

ROW_NUMBER() 会在定义的分区内为行分配序号。结合 DELETE 使用时,可基于特定列对行进行排序并打分,从而识别重复并移除不需要的记录。此方法适用于现代版本的 MySQL(8.0 起)、PostgreSQL 和 SQL Server。

假设您希望基于 Name 去重,仅保留首次出现(ID 最小)的记录:

-- Common Table Expression (CTE) to rank rows based on 'Name'
WITH CTE AS (
    SELECT ID, Name, ROW_NUMBER() OVER (PARTITION BY Name ORDER BY ID ASC) AS RowNum
    FROM customers
)
-- Delete rows from the 'customers' table where the row number is greater than 1
DELETE FROM customers
WHERE ID IN (SELECT ID FROM CTE WHERE RowNum > 1);

将 DELETE 与子查询结合

有时,使用带子查询的简单 DELETE 即可删除数据库中的重复。这种方法适用于较旧版本的 MySQL 或 PostgreSQL(缺少 ROW_NUMBER() 时)。

下面的查询会删除 customers 表中每个 Name 非最小 ID 的行,仅保留每个唯一 Name 对应的最小 ID 的记录。

-- Delete rows from the 'customers' table
DELETE FROM customers
WHERE ID NOT IN (
    -- Subquery to find the minimum ID for each unique Name
    SELECT MIN(ID)
    FROM customers
    GROUP BY Name
);

结合 HAVING 子句使用 GROUP BY

当需要检查特定列上的重复值时,可结合 GROUP BY 与 HAVING 子句识别重复。此方法允许基于给定条件删除特定行,兼容 SQL Server、MySQL 与 PostgreSQL。

以下查询先识别出现超过一次的姓名,再删除重复,同时为每个 Name 保留 ID 最小的行。

-- Step 1: Identify which Names have duplicates
SELECT Name, COUNT(*) AS duplicate_count
FROM customers
GROUP BY Name
HAVING COUNT(*) > 1;

-- Step 2: Delete duplicate rows, keeping the smallest ID for each Name
DELETE FROM customers
WHERE ID NOT IN (
    SELECT MIN(ID)
    FROM customers
    GROUP BY Name
);

使用临时表进行批处理

对于大型数据集,临时表有助于高效批处理去重。当单条查询可能引发性能问题时,此方法尤为有效。下面的查询创建一个临时表,存储每个 Name 的最小 ID,并删除 customers 表中不在 temp_customers 中的行。

-- Create a temporary table with unique records
CREATE TEMPORARY TABLE temp_customers AS
SELECT MIN(ID) AS KeepID, Name
FROM customers
GROUP BY Name;

-- Delete duplicates not in the temporary table
DELETE FROM customers
WHERE ID NOT IN (SELECT KeepID FROM temp_customers);

-- Clean up
DROP TABLE temp_customers;

上述使用 CREATE TEMPORARY TABLE 的语法仅支持 MySQL 与 PostgreSQL。

在 SQL Server 中去重

SQL Server 提供多种删除数据库重复记录的方法,包括结合 DISTINCT 与 INTO、使用 ROW_NUMBER(),以及使用临时表。

结合 DISTINCT 与 INTO

您可以在 SELECT 语句中使用 DISTINCT 创建仅含唯一记录的新表。在确认新表记录无误后,可删除旧表。下面示例从 customers 表生成去重后的 unique_customers 表。

-- Select distinct rows from 'customers' and create a new table 'unique_customers'
SELECT DISTINCT *
INTO unique_customers
FROM customers;
-- Drop the original 'customers' table to remove it from the database
DROP TABLE customers;
-- Rename the 'unique_customers' table to 'customers' to replace the original table
EXEC sp_rename 'unique_customers', 'customers';

使用 ROW_NUMBER()

您也可以使用 ROW_NUMBER() 删除 SQL Server 中的重复记录。假设 Customers 表按 CustomerName 存在重复,且您希望对每个重复组仅保留首次出现。

-- Common Table Expression (CTE) to assign a row number to each customer 
WITH CTE AS (
    SELECT CustomerID, CustomerName, ROW_NUMBER() OVER (PARTITION BY CustomerName ORDER BY CustomerID ASC) AS RowNum
    FROM Customers
)
-- Delete rows from the CTE
DELETE FROM CTE
WHERE RowNum > 1;

使用临时表

由于 SQL Server 不支持 CREATE TEMPORARY TABLE,可使用 SELECT INTO。SQL Server 的临时表以 # 作为表名前缀。

-- Create a temporary table
SELECT MIN(CustomerID) AS ID, CustomerName
INTO #temp_customers
FROM customers
GROUP BY CustomerName;
-- Delete rows from the 'customers' table where the ID is not in the temporary table
DELETE FROM customers
WHERE CustomerID NOT IN (SELECT ID FROM #temp_customers);
-- Optionally drop the temporary table after use
DROP TABLE #temp_customers;

建议您学习我们的 SQL Server Fundamentals 技能路径,以提升表连接与数据分析能力。SQL Server Developer 职业路径将帮助您掌握在 SQL Server 中编写、排错与优化查询的技能。

速查表:SQL 去重方法

下表汇总了本文涉及的所有去重方法,便于您快速选择适合自身场景的方案。

方法适用场景是否修改数据?数据库支持
SELECT DISTINCT从查询结果中获取唯一行否所有 DBMS
GROUP BY + 聚合获取带聚合值的唯一行否所有 DBMS
ROW_NUMBER() + CTE(SELECT)在查询中灵活过滤重复否SQL Server、MySQL 8.0+、PostgreSQL
ROW_NUMBER() + CTE(DELETE)以精细控制永久删除重复是SQL Server、MySQL 8.0+、PostgreSQL
DELETE 搭配子查询使用 NOT IN / MIN() 去重是所有 DBMS
自JOIN + DELETE成对比较行以删除重复是所有 DBMS
临时表方案大数据集的批处理是MySQL、PostgreSQL(SQL Server 用 #temp)
SELECT DISTINCT INTO创建表的干净副本是(替换表)SQL Server

最佳实践

重复行是影响数据质量与数据库性能的常见问题。请参考以下最佳实践,防止重复记录被插入数据库。

  • 使用主键: 主键列确保每条记录唯一,阻止重复值进入表中。
  • 实施唯一约束: 对任意列应用唯一约束,可确保非主键列(如邮箱或电话号码)上也不存在重复。
  • 合理的数据库设计与规范化: 有效的模式设计与数据库规范化有助于减少冗余与重复,确保各类记录存放在恰当的表中。
  • 使用唯一索引: 利用唯一索引确保某些列组合的唯一性,而无需对整张表施加全局约束。
  • 定期进行数据审计: 按业务规则定期运行查询,识别潜在的重复记录。

结语

识别并删除重复行对于保持数据库高效与数据准确至关重要。进行修改前请务必备份数据,以避免意外的数据丢失。

如果您希望成为一名熟练的数据分析师,欢迎查看我们的 Associate Data Analyst in SQL 职业路径,学习必备技能。若您想学习如何使用 SQL 构建专业仪表盘,也可选修 Reporting in SQL 课程。最后,建议获取 SQL Associate Certification 认证,以展示您在 SQL 数据分析方面的掌握程度,并在数据从业者中脱颖而出。

Allan Ouko

作者

Allan Ouko

数据科学技术写作者,具备数据分析、商业智能和数据科学的一线实践经验。我撰写以行业为导向的实用内容,涵盖 SQL、Python、Power BI、Databricks 和数据工程,并以真实的分析工作为基础。我的写作兼顾技术深度与业务影响,帮助专业人士将数据转化为自信的决策。

常见 SQL 问答

SQL 数据库中出现重复行的原因是什么?

重复行可能由多种因素导致,包括数据库设计不当、缺少主键、来自多源的数据集成、人工录入错误,或在数据迁移中未正确执行校验等。

能否基于多列来防止重复?

可以。您可通过复合键或唯一约束在多列上强制唯一性,确保这些列值的组合保持唯一。

DISTINCT 关键字如何去除重复行?

使用 DISTINCT 只会在查询结果中去重,不会改变底层数据。

哪些方法可以永久删除数据库中的重复记录?

您可以使用 ROW_NUMBER() 搭配 DELETE、带子查询的 DELETE、带 HAVING 的 GROUP BY,以及通过临时表进行批处理等方法,永久删除数据库中的重复行。

重复数据会影响我的数据库性能吗?

会。重复数据会增加存储成本、拖慢查询速度,并使数据分析更复杂,从而对性能产生负面影响。

如何在 SQL 中查找重复行?

使用 GROUP BY 搭配 HAVING COUNT(*) > 1 查找重复。例如:SELECT Name, COUNT(*) FROM customers GROUP BY Name HAVING COUNT(*) > 1; 会返回所有出现超过一次的姓名。您也可以使用 ROW_NUMBER() OVER (PARTITION BY Name ORDER BY ID) 为每个重复组内的行赋予排名——排名大于 1 的行即为重复。

从大型 SQL 表中去重的最快方法是什么?

对于大表,使用临时表方案更快:先用 SELECT DISTINCT 或 GROUP BY 将唯一行插入临时表,截断原表后再将干净数据写回。这避免了对数百万行逐行删除的低效。或者,如需精细控制保留哪条重复,使用带 CTE 的 ROW_NUMBER() 也很高效。无论哪种方式,都应先备份数据,并在预生产环境充分测试。

如何在 SQL 中查找重复行?

使用 GROUP BY 搭配 HAVING COUNT(*) > 1 查找重复。例如:SELECT Name, COUNT(*) FROM customers GROUP BY Name HAVING COUNT(*) > 1; 会返回所有出现超过一次的姓名。您也可以使用 ROW_NUMBER() OVER (PARTITION BY Name ORDER BY ID) 为每个重复组内的行赋予排名——排名大于 1 的行即为重复。

主题