1. 从“查重”这个日常需求说起不知道你有没有遇到过这种情况从数据库里拉出一堆用户数据准备做个分析结果发现同一个人的记录出现了好几次。或者你想看看网站都有哪些不同的访问来源结果查询结果里同一个域名重复了N遍。这种时候你心里肯定在想“能不能把这些重复的玩意儿给去掉让我看得清楚点”没错这就是“数据去重”的典型场景。在数据库操作里它是个高频又基础的需求。今天我们不聊那些复杂的ETL流程就聚焦在SQLAlchemy这个Python里最流行的ORM对象关系映射工具上看看怎么用它又快又好地搞定数据去重。我刚开始用SQLAlchemy那会儿也在这个问题上迷糊过。明明在SQL语句里写个DISTINCT关键字那么简单怎么到了ORM里就感觉有点绕呢特别是当你想对多列组合进行去重时那个语法乍一看还真有点让人摸不着头脑。后来踩过几次坑把官方文档翻来覆去看了几遍又在项目里实际用了无数次之后总算摸清了门道。今天我就把这些实战经验用最直白的话分享给你保证你看完就能上手再也不用为去重发愁。简单来说SQLAlchemy的去重核心就是distinct()这个方法和func.distinct()这个函数。它们俩分工合作能帮你处理从单列到多列的各种去重需求。下面我们就从最简单的单列去重开始一步步深入到更复杂的场景。2. 单列去重一招搞定重复值单列去重是最常见的情况。比如你有一张users表里面有个city字段你想知道你的用户都分布在哪些不同的城市而不关心每个城市有多少人。2.1 基础玩法使用distinct()函数在SQLAlchemy的ORM查询中最直接的单列去重方法是使用sqlalchemy.func模块里的distinct()函数。你可以把它理解为一个“包裹器”把你要去重的列包起来告诉数据库“嘿处理这一列的时候重复的只算一个。”我们来个实际的例子。假设我们有一个Article模型代表文章其中有一个category字段表示文章分类。from sqlalchemy import create_engine, Column, String, Integer, func, distinct from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker # 1. 定义模型 Base declarative_base() class Article(Base): __tablename__ articles id Column(Integer, primary_keyTrue) title Column(String) category Column(String) # 文章分类 author Column(String) # 2. 创建引擎和会话 engine create_engine(sqlite:///example.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine) session Session() # 3. 查询所有不重复的文章分类 distinct_categories session.query(func.distinct(Article.category)).all() print(distinct_categories)运行上面的代码distinct_categories会是一个元组组成的列表像[(科技,), (生活,), (体育,)]这样每个元组里只有一个元素就是那个唯一的分类名。这里有个小细节要注意query(func.distinct(Article.category))查询返回的是Article.category这一列的去重结果。.all()方法返回的是一个列表里面的每个元素都是一个元组哪怕你只查询了一列。所以你需要通过result[0]这样的方式来获取具体的值。2.2 另一种写法使用.distinct()方法除了func.distinct()Query对象本身还有一个.distinct()方法。这个方法用在查询语句的末尾作用是对整个查询结果集进行去重。对于单列去重两种方法效果一样但语法略有不同。# 使用 .distinct() 方法实现单列去重 distinct_categories_method session.query(Article.category).distinct().all() print(distinct_categories_method)这段代码和上一段的效果是完全一样的。.distinct()方法更接近我们写原生SQL时的思维顺序先选择要查询的列Article.category然后声明要对结果去重.distinct()。那么func.distinct()和.distinct()方法该怎么选呢我的经验是对于简单的单列去重用.distinct()方法更直观代码也更简洁。它就像是在查询语句上加了个“去重开关”。而func.distinct()函数的能力更强尤其是在一些复杂场景下比如它还可以用在func.count(distinct(Article.category))这样的聚合函数里面用来计算不重复的数量。这个我们后面会讲到。2.3 你可能遇到的“坑”与解决方案在实际使用中我遇到过两个比较典型的问题。第一个坑去重结果顺序不稳定。你可能会发现两次同样的去重查询返回的分类列表顺序不一样。这其实不是SQLAlchemy的问题而是数据库特别是像MySQL这样的数据库在没有ORDER BY子句时并不保证返回结果的顺序。对于去重后还想排序的情况你需要显式地加上order_by。# 去重后按分类名称字母顺序排序 distinct_categories_ordered session.query(Article.category).distinct().order_by(Article.category).all()第二个坑对包含NULL值的列去重。数据库中的NULL代表缺失或未知的值。在去重时多个NULL值通常会被视为相同的但具体处理方式可能因数据库而异。你需要明确业务逻辑是否要把NULL也当作一个有效的、可去重的类别在大多数情况下如果你关心NULL它会被保留为一个独立的结果项。3. 多列去重组合条件的精准过滤单列去重够简单吧但现实项目往往更复杂。比如你想从订单记录里找出所有“不同的用户-商品组合”。也就是说同一个用户买同一个商品只算一次不管他买了多少回。这时候就需要多列去重了。多列去重的核心思想是将多列值的组合视为一个整体只有这个整体完全一致才被认为是重复的。3.1 使用.distinct()方法进行多列去重这是最常用、最推荐的方式。语法非常直观在query()中放入所有需要参与去重的列然后在后面调用.distinct()方法。我们新建一个Sales销售记录模型来演示。class Sales(Base): __tablename__ sales id Column(Integer, primary_keyTrue) user_id Column(Integer) # 用户ID product_id Column(Integer) # 商品ID quantity Column(Integer) # 购买数量 sale_date Column(String) # 查询所有不重复的 (用户ID, 商品ID) 组合 unique_combinations session.query(Sales.user_id, Sales.product_id).distinct().all() print(unique_combinations)执行这段代码你会得到一个列表里面的元素是像(1, 101), (1, 102), (2, 101)这样的元组。即使user_id1和product_id101这个组合在表里出现了10次它在结果中也只会出现一次。关键点session.query(Sales.user_id, Sales.product_id)定义了我们想要查询和去重的列。.distinct()方法作用于这个查询结果集它会比较每一行数据的user_id和product_id这两个字段的组合剔除完全相同的行。3.2 理解.distinct()与func.distinct()在多列去重中的区别这里非常重要网上有些旧的教程或示例可能会让你用func.distinct()来实现多列去重比如写成session.query(func.distinct(Sales.user_id, Sales.product_id)).all()。我实测下来在绝大多数现代版本的SQLAlchemy和数据库驱动下func.distinct()函数通常只接受一个参数。也就是说func.distinct(Sales.user_id, Sales.product_id)这种写法很可能会报错或者无法达到预期的多列去重效果。所以请牢记这个实战原则多列去重无脑用.distinct()方法就对了。这是SQLAlchemy ORM层为你封装好的、最符合直觉的用法它会生成正确的SELECT DISTINCT user_id, product_id FROM sales这样的SQL语句。3.3 进阶场景去重后还想获取其他列的信息有时候我们的需求会更刁钻一点。比如我想知道有哪些不同的用户-商品组合并且还想顺便看看每个组合最近一次的购买时间。这是一个经典的“分组取最新一条记录”的变体。单纯的.distinct()是做不到的因为它只负责去重不会帮你从重复的多条记录中挑选一条出来。这时就需要结合窗口函数或者子查询。这里介绍一个利用group_by和聚合函数func.max的常用技巧from sqlalchemy import desc # 查询每个不同的用户-商品组合以及该组合下最近一次的销售日期 result session.query( Sales.user_id, Sales.product_id, func.max(Sales.sale_date).label(latest_date) # 使用max获取最新的日期 ).group_by(Sales.user_id, Sales.product_id).order_by(desc(latest_date)).all() for row in result: print(f用户 {row.user_id} 购买商品 {row.product_id} 的最后一次日期是 {row.latest_date})在这个查询里group_by(Sales.user_id, Sales.product_id)实现了按组合“分组”func.max(Sales.sale_date)计算出每组内最大的日期即最近日期。这虽然不是严格意义上的“去重”但在业务上常常能达到类似“保留最新一条重复数据”的效果非常实用。4. 性能优化让你的去重查询快如闪电当数据量小的时候怎么去重都行。但一旦表里有几百万、上千万行数据一个没写好的去重查询可能分分钟让你的数据库卡死。下面分享几个我踩过坑才总结出来的性能优化技巧。4.1 索引是去重查询的“加速器”数据库索引对于DISTINCT操作至关重要。如果没有合适的索引数据库就需要对整个表进行全表扫描和排序成本极高。什么样的索引对去重有帮助单列去重在该列上建立索引。多列去重建立一个包含所有去重列的复合索引。顺序很重要应该与你在group_by或distinct中列出的顺序保持一致。例如对于我们之前的Sales表如果频繁按(user_id, product_id)进行去重查询就应该建立这样一个索引# 在模型定义中使用DDL语句或迁移工具 # CREATE INDEX idx_user_product ON sales (user_id, product_id);在SQLAlchemy中虽然ORM定义不直接创建索引但你可以通过__table_args__来声明或者使用Alembic这样的迁移工具来管理索引。有了这个索引数据库可以快速定位和排序不同的组合查询速度会有数量级的提升。4.2 只查询你需要的列这是一个基本原则但在去重时尤其重要。绝对不要写成session.query(Sales).distinct().all()去查询整个模型对象# 性能差的做法查询所有列 bad_query session.query(Sales).distinct().all() # 数据库需要处理所有列的数据 # 性能好的做法只查询需要去重的列 good_query session.query(Sales.user_id, Sales.product_id).distinct().all()当你使用query(Sales)时SQLAlchemy会生成SELECT * FROM sales数据库需要读取每一行的所有字段然后在内存中进行庞大的去重比较这会产生大量的磁盘I/O和内存消耗。而query(Sales.user_id, Sales.product_id)只读取必要的两列数据量小得多效率自然就高了。4.3 考虑使用EXISTS或GROUP BY替代DISTINCT在某些特定场景下DISTINCT可能不是最优解。比如你想检查是否存在重复值或者需要对去重后的结果进行复杂的过滤。检查是否存在重复使用GROUP BY ... HAVING COUNT(*) 1往往比先DISTINCT再计数更高效。去重后过滤有时先通过子查询GROUP BY得到唯一键再关联回原表获取其他信息比直接在大表上做复杂DISTINCT更优。例如找出那些被超过一个用户购买过的商品from sqlalchemy import having # 使用 GROUP BY HAVING 来查找重复的商品购买组合 duplicate_products session.query( Sales.product_id ).group_by(Sales.product_id).having(func.count(Sales.user_id) 1).all()数据库优化器对GROUP BY的处理有时比对DISTINCT更友好尤其是在结合聚合条件的时候。多一种思路就多一种性能优化的可能。5. 实战中的复杂案例与避坑指南掌握了基础和优化我们来看看一些更贴近真实项目的复杂案例。这些场景你可能现在用不到但遇到了就知道该怎么解决了。5.1 案例一关联模型下的去重这是ORM查询中最让人头疼的情况之一。假设我们有User和Order两个模型一个用户有多个订单。现在我想找出所有下过单的不重复用户。class User(Base): __tablename__ users id Column(Integer, primary_keyTrue) name Column(String) orders relationship(Order, back_populatesuser) class Order(Base): __tablename__ orders id Column(Integer, primary_keyTrue) user_id Column(Integer, ForeignKey(users.id)) amount Column(Integer) user relationship(User, back_populatesorders) # 错误尝试这样会导致去重的是Order对象而不是User # wrong session.query(User).join(Order).distinct().all() # 正确做法对用户ID进行去重 correct_users session.query(User).join(Order).distinct(User.id).all()注意这里的.distinct(User.id)。当查询涉及连接join时直接使用.distinct()可能会因为连接产生的重复行而导致去重逻辑混乱。明确指定基于主模型User的主键id进行去重是更安全、更清晰的做法。SQLAlchemy会生成类似SELECT DISTINCT ON (users.id) users.* ...的SQL在支持DISTINCT ON的数据库如PostgreSQL中或者相应的子查询来确保每个用户只出现一次。5.2 案例二去重计数Count Distinct业务上经常需要统计不重复的数量比如“总共有多少位不同的活跃用户”。这需要把distinct和聚合函数count结合起来。from sqlalchemy import func # 统计有多少个不同的用户下过订单 unique_user_count session.query(func.count(func.distinct(Order.user_id))).scalar() print(f下过单的不同用户数{unique_user_count})这里的关键是func.count(func.distinct(Order.user_id))。func.distinct()在这里扮演了关键角色它作为func.count()的参数确保了只对不同的user_id进行计数。最后的.scalar()方法用于获取这个单一的计算结果值。5.3 常见错误与调试技巧distinct()位置错误记住.distinct()是Query对象的方法应该放在query()之后filter()、order_by()等其他子句之前或之中但必须在all()、first()等执行方法之前。去重结果不符合预期首先把你构建的Query对象用print(query.statement.compile(compile_kwargs{literal_binds: True}))打印出来看看生成的原始SQL是什么。很多时候问题就出在SQL逻辑上而不是SQLAlchemy的用法上。内存溢出对于海量数据去重即使优化了查询结果集也可能非常大。考虑使用.yield_per(n)来流式加载结果或者分页查询.limit().offset()避免一次性加载所有数据到内存。去重看起来是个小功能但在数据清洗、报表生成、数据分析等环节无处不在。用对了SQLAlchemy的distinct不仅能写出更干净的代码还能有效提升应用性能。下次再遇到重复数据希望你能自信地选出最适合的那一招。