SQLAlchemy查询进阶:过滤、排序与聚合 - 手把手带你从基础查询到实战分析
文章目录
- SQLAlchemy查询进阶:过滤、排序与聚合 - 手把手带你从基础查询到实战分析
- 学习开场:为什么你的查询代码又长又难维护?
- 环境准备:搭建你的实战演练场
- 1. 安装必要的包
- 2. 创建测试数据库
- 基础概念:SQLAlchemy查询的核心三要素
- 1. 查询对象(Query Object)
- 2. 延迟执行(Lazy Loading)
- 3. 链式调用(Method Chaining)
- 实战演练一:过滤查询 - 精准找到你要的数据
- 场景:找出所有VIP用户
- 过滤条件的多种写法对比
- 复杂过滤实战:多条件组合查询
- 实战演练二:排序与分页 - 让数据有序呈现
- 场景:用户消费排行榜
- 实战演练三:聚合查询 - 数据统计与分析
- 基础聚合函数
- 关联查询的聚合统计
- 应用场景:电商数据分析报表
- 性能优化与避坑指南
- 常见性能问题及解决方案
- 优化示例:解决N+1查询问题
- 调试技巧:查看生成的SQL
- 学习总结:你的查询技能升级路线图
- ✅ 已掌握的技能
- 🚀 下一步学习建议
- 学习交流与进阶
刚开始用SQLAlchemy时,你是不是也只会用
session.query(User).all()?当产品经理说“按注册时间倒序,只显示VIP用户,还要统计每个月的注册人数”时,是不是瞬间头大?别慌,今天我就带你搞定SQLAlchemy的查询三剑客:过滤、排序与聚合。
学习开场:为什么你的查询代码又长又难维护?
我刚开始用SQLAlchemy时,也踩过不少坑。记得有一次,产品要一个用户分析报表,我写了200多行的查询代码,各种if-else判断,最后自己都看不懂了。后来才发现,SQLAlchemy的查询API设计得非常优雅,只是我没掌握正确的方法。
这篇文章能帮你解决什么?
- 告别手写复杂SQL字符串拼接,用Pythonic的方式构建查询
- 掌握过滤条件的多种写法,从简单到复杂一网打尽
- 学会排序、分页、聚合统计,轻松应对各种报表需求
- 理解查询背后的执行原理,写出高性能的数据库操作代码
环境准备:搭建你的实战演练场
1. 安装必要的包
# 基础包pipinstallsqlalchemy==1.4.46 pipinstallpymysql==1.0.2# 可选:用于数据展示pipinstallpandas pipinstalltabulate2. 创建测试数据库
-- 在MySQL中执行CREATEDATABASEIFNOTEXISTSecommerce_db;USEecommerce_db;-- 用户表CREATETABLEusers(idINTPRIMARYKEYAUTO_INCREMENT,usernameVARCHAR(50)NOTNULLUNIQUE,emailVARCHAR(100)NOTNULLUNIQUE,ageINT,is_vipBOOLEANDEFAULTFALSE,registration_dateDATENOTNULL,total_spentDECIMAL(10,2)DEFAULT0.00,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);-- 订单表CREATETABLEorders(idINTPRIMARYKEYAUTO_INCREMENT,user_idINTNOTNULL,order_noVARCHAR(50)NOTNULLUNIQUE,amountDECIMAL(10,2)NOTNULL,statusENUM('pending','paid','shipped','delivered','cancelled')DEFAULT'pending',created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,FOREIGNKEY(user_id)REFERENCESusers(id)ONDELETECASCADE);-- 插入测试数据INSERTINTOusers(username,email,age,is_vip,registration_date,total_spent)VALUES('张三','zhangsan@example.com',25,TRUE,'2023-01-15',1500.00),('李四','lisi@example.com',30,FALSE,'2023-02-20',800.00),('王五','wangwu@example.com',22,TRUE,'2023-03-10',2500.00),('赵六','zhaoliu@example.com',35,FALSE,'2023-01-25',300.00),('钱七','qianqi@example.com',28,TRUE,'2023-04-05',4200.00);INSERTINTOorders(user_id,order_no,amount,status)VALUES(1,'ORD20230115001',500.00,'delivered'),(1,'ORD20230220001',1000.00,'shipped'),(2,'ORD20230221001',800.00,'paid'),(3,'ORD20230310001',1500.00,'delivered'),(3,'ORD20230315001',1000.00,'delivered'),(4,'ORD20230130001',300.00,'cancelled'),(5,'ORD20230405001',2000.00,'shipped'),(5,'ORD20230410001',2200.00,'pending');基础概念:SQLAlchemy查询的核心三要素
在深入实战前,我们先理解三个核心概念:
1. 查询对象(Query Object)
SQLAlchemy的查询不是立即执行的,而是构建一个查询对象。这就像点菜时先写菜单,最后才交给厨房。
2. 延迟执行(Lazy Loading)
查询只有在真正需要数据时才会执行,这避免了不必要的数据库访问。
3. 链式调用(Method Chaining)
SQLAlchemy的查询方法可以像链条一样连接起来,让代码更清晰。
实战演练一:过滤查询 - 精准找到你要的数据
场景:找出所有VIP用户
fromsqlalchemyimportcreate_engine,Column,Integer,String,Boolean,Date,DECIMAL,TIMESTAMP,Enum,ForeignKeyfromsqlalchemy.ext.declarativeimportdeclarative_basefromsqlalchemy.ormimportsessionmaker,relationshipfromdatetimeimportdate# 创建引擎和会话engine=create_engine('mysql+pymysql://root:password@localhost:3306/ecommerce_db')Session=sessionmaker(bind=engine)session=Session()Base=declarative_base()# 定义模型classUser(Base):__tablename__='users'id=Column(Integer,primary_key=True)username=Column(String(50),nullable=False,unique=True)email=Column(String(100),nullable=False,unique=True)age=Column(Integer)is_vip=Column(Boolean,default=False)registration_date=Column(Date,nullable=False)total_spent=Column(DECIMAL(10,2),default=0.00)created_at=Column(TIMESTAMP,default='CURRENT_TIMESTAMP')# 定义关系orders=relationship('Order',back_populates='user')def__repr__(self):returnf"<User(username='{self.username}', email='{self.email}')>"classOrder(Base):__tablename__='orders'id=Column(Integer,primary_key=True)user_id=Column(Integer,ForeignKey('users.id'),nullable=False)order_no=Column(String(50),nullable=False,unique=True)amount=Column(DECIMAL(10,2),nullable=False)status=Column(Enum('pending','paid','shipped','delivered','cancelled'),default='pending')created_at=Column(TIMESTAMP,default='CURRENT_TIMESTAMP')# 定义关系user=relationship('User',back_populates='orders')def__repr__(self):returnf"<Order(order_no='{self.order_no}', amount={self.amount})>"# 方法1:使用filter_by(简单相等条件)defget_vip_users_simple():"""获取所有VIP用户 - 简单版"""vip_users=session.query(User).filter_by(is_vip=True).all()print("VIP用户(简单版):")foruserinvip_users:print(f" -{user.username}({user.email})")returnvip_users# 方法2:使用filter(更灵活的条件)defget_vip_users_filter():"""获取所有VIP用户 - 使用filter"""vip_users=session.query(User).filter(User.is_vip==True).all()print("\nVIP用户(使用filter):")foruserinvip_users:print(f" -{user.username}")returnvip_users# 测试运行if__name__=="__main__":get_vip_users_simple()get_vip_users_filter()过滤条件的多种写法对比
| 场景 | filter_by写法 | filter写法 | 说明 |
|---|---|---|---|
| 相等条件 | filter_by(is_vip=True) | filter(User.is_vip == True) | filter_by更简洁 |
| 不等条件 | 不支持 | filter(User.age != 25) | 必须用filter |
| 大于小于 | 不支持 | filter(User.age > 25) | 必须用filter |
| 多条件与 | filter_by(is_vip=True, age=25) | filter(User.is_vip==True, User.age==25) | 都支持 |
| 或条件 | 不支持 | filter(or_(User.age<25, User.age>30)) | 需要导入or_ |
| 模糊查询 | 不支持 | filter(User.username.like('张%')) | 必须用filter |
复杂过滤实战:多条件组合查询
fromsqlalchemyimportand_,or_,not_defcomplex_filter_example():"""复杂过滤条件示例"""fromdatetimeimportdate# 场景:找出年龄在25-30岁之间,或者是VIP的用户query=session.query(User).filter(or_(and_(User.age>=25,User.age<=30),User.is_vip==True))print("复杂查询结果:")foruserinquery.all():print(f" -{user.username}, 年龄:{user.age}, VIP:{user.is_vip}")# 场景:找出2023年第一季度注册的非VIP用户q1_start=date(2023,1,1)q1_end=date(2023,3,31)q1_users=session.query(User).filter(and_(User.registration_date.between(q1_start,q1_end),User.is_vip==False)).all()print("\n2023年Q1注册的非VIP用户:")foruserinq1_users:print(f" -{user.username}, 注册日期:{user.registration_date}")returnquery.all()# 运行复杂查询complex_filter_example()实战演练二:排序与分页 - 让数据有序呈现
场景:用户消费排行榜
defuser_spending_ranking():"""用户消费金额排序"""# 按消费金额降序排列users_by_spending=session.query(User).order_by(User.total_spent.desc()).all()print("用户消费排行榜:")print("-"*50)print(f"{'排名':<5}{'用户名':<10}{'消费金额':<10}{'VIP':<5}")print("-"*50)fori,userinenumerate(users_by_spending,1):vip_status="是"ifuser.is_vipelse"否"print(f"{i:<5}{user.username:<10}¥{user.total_spent:<8}{vip_status:<5}")returnusers_by_spendingdefmulti_column_sort():"""多列排序:先按VIP状态,再按消费金额"""# VIP用户在前,然后按消费金额降序users_sorted=session.query(User).order_by(User.is_vip.desc(),# VIP在前(True > False)User.total_spent.desc()).all()print("\n多列排序(VIP优先 + 消费降序):")foruserinusers_sorted:vip="VIP"ifuser.is_vipelse"普通"print(f" -{vip}用户{user.username}: ¥{user.total_spent}")returnusers_sorteddefpagination_example(page=1,page_size=2):"""分页查询示例"""# 计算偏移量offset=(page-1)*page_size# 分页查询paginated_users=session.query(User)\.order_by(User.id)\.offset(offset)\.limit(page_size)\.all()# 获取总数(用于计算总页数)total_count=session.query(User).count()total_pages=(total_count+page_size-1)//page_sizeprint(f"\n分页查询(第{page}页,每页{page_size}条):")print(f"总记录数:{total_count}, 总页数:{total_pages}")foruserinpaginated_users:print(f" -{user.username}(ID:{user.id})")returnpaginated_users,total_count,total_pages# 运行排序和分页示例user_spending_ranking()multi_column_sort()pagination_example(1,2)pagination_example(2,2)实战演练三:聚合查询 - 数据统计与分析
基础聚合函数
fromsqlalchemyimportfunc,descdefbasic_aggregation():"""基础聚合统计"""# 1. 统计用户总数total_users=session.query(func.count(User.id)).scalar()print(f"用户总数:{total_users}")# 2. 统计VIP用户数vip_count=session.query(func.count(User.id)).filter(User.is_vip==True).scalar()print(f"VIP用户数:{vip_count}")# 3. 平均年龄avg_age=session.query(func.avg(User.age)).scalar()print(f"平均年龄:{avg_age:.1f}岁")# 4. 总消费金额total_spent_all=session.query(func.sum(User.total_spent)).scalar()print(f"总消费金额: ¥{total_spent_all:.2f}")# 5. 最大/最小年龄max_age=session.query(func.max(User.age)).scalar()min_age=session.query(func.min(User.age)).scalar()print(f"年龄范围:{min_age}-{max_age}岁")return{'total_users':total_users,'vip_count':vip_count,'avg_age':avg_age,'total_spent':total_spent_all}defgroup_by_example():"""分组统计示例"""# 按VIP状态分组统计vip_stats=session.query(User.is_vip,func.count(User.id).label('user_count'),func.avg(User.age).label('avg_age'),func.sum(User.total_spent).label('total_spent')).group_by(User.is_vip).all()print("\n按VIP状态分组统计:")print("-"*60)print(f"{'VIP状态':<10}{'用户数':<8}{'平均年龄':<10}{'总消费':<12}")print("-"*60)foris_vip,count,avg_age,total_spentinvip_stats:vip_status="VIP用户"ifis_vipelse"普通用户"print(f"{vip_status:<10}{count:<8}{avg_age:<10.1f}¥{total_spent:<10.2f}")# 按月统计注册用户数monthly_reg=session.query(func.date_format(User.registration_date,'%Y-%m').label('month'),func.count(User.id).label('new_users')).group_by('month').order_by('month').all()print("\n月度注册用户统计:")formonth,countinmonthly_reg:print(f"{month}:{count}人")returnvip_stats,monthly_regdefhaving_example():"""HAVING子句示例:筛选分组结果"""# 找出平均消费金额超过1000的用户年龄段age_group_stats=session.query(User.age,func.count(User.id).label('user_count'),func.avg(User.total_spent).label('avg_spent')).group_by(User.age).having(func.avg(User.total_spent)>1000).all()print("\n平均消费超过1000元的年龄段:")forage,count,avg_spentinage_group_stats:print(f" 年龄{age}岁:{count}人,平均消费¥{avg_spent:.2f}")returnage_group_stats# 运行聚合查询basic_aggregation()group_by_example()having_example()关联查询的聚合统计
defjoin_aggregation():"""关联表的聚合统计"""# 统计每个用户的订单数量和总金额user_order_stats=session.query(User.username,func.count(Order.id).label('order_count'),func.sum(Order.amount).label('total_order_amount'),func.avg(Order.amount).label('avg_order_amount')).join(Order,User.id==Order.user_id)\.group_by(User.id)\.order_by(desc('total_order_amount')).all()print("\n用户订单统计:")print("-"*70)print(f"{'用户名':<10}{'订单数':<8}{'订单总额':<12}{'平均订单额':<12}")print("-"*70)forusername,count,total,avginuser_order_stats:print(f"{username:<10}{count:<8}¥{total:<10.2f}¥{avg:<10.2f}")# 统计订单状态分布order_status_stats=session.query(Order.status,func.count(Order.id).label('count'),func.sum(Order.amount).label('total_amount')).group_by(Order.status).all()print("\n订单状态分布:")forstatus,count,amountinorder_status_stats:print(f"{status}:{count}单,总金额¥{amount:.2f}")returnuser_order_stats,order_status_stats# 运行关联聚合查询join_aggregation()应用场景:电商数据分析报表
让我们把这些技术组合起来,实现一个真实的电商数据分析报表:
defecommerce_analysis_report():"""电商数据分析报表"""print("="*60)print("电商数据分析报表")print("="*60)# 1. 核心指标概览print("\n1. 核心指标概览")print("-"*40)total_users=session.query(func.count(User.id)).scalar()total_orders=session.query(func.count(Order.id)).scalar()total_gmv=session.query(func.sum(Order.amount)).scalar()or0avg_order_value=total_gmv/total_ordersiftotal_orders>0else0print(f"总用户数:{total_users}人")print(f"总订单数:{total_orders}单")print(f"总交易额(GMV): ¥{total_gmv:.2f}")print(f"客单价: ¥{avg_order_value:.2f}")# 2. 用户分层分析print("\n2. 用户分层分析")print("-"*40)user_tiers=session.query(func.floor(User.total_spent/1000).label('tier'),func.count(User.id).label('user_count'),func.sum(User.total_spent).label('total_spent')).group_by('tier').order_by('tier').all()fortier,count,spentinuser_tiers:tier_range=f"¥{tier*1000}-{(tier+1)*1000-1}"iftier>=4:tier_range=f"¥{tier*1000}+"print(f"{tier_range}:{count}人,消费¥{spent:.2f}")# 3. 月度趋势分析print("\n3. 月度趋势分析")print("-"*40)monthly_trend=session.query(func.date_format(Order.created_at,'%Y-%m').label('month'),func.count(Order.id).label('order_count'),func.sum(Order.amount).label('gmv'),func.count(func.distinct(Order.user_id)).label('active_users')).group_by('month').order_by('month').all()formonth,orders,gmv,usersinmonthly_trend:print(f"{month}:{orders}单,GMV¥{gmv:.2f},活跃用户{users}人")# 4. VIP用户价值分析print("\n4. VIP用户价值分析")print("-"*40)vip_value=session.query(User.is_vip,func.count(User.id).label('user_count'),func.avg(User.total_spent).label('avg_spent'),func.avg(session.query(func.count(Order.id)).filter(Order.user_id==User.id).scalar_subquery()).label('avg_orders')).group_by(User.is_vip).all()foris_vip,count,avg_spent,avg_ordersinvip_value:user_type="VIP用户"ifis_vipelse"普通用户"print(f"{user_type}:{count}人,人均消费¥{avg_spent:.2f},人均{avg_orders:.1f}单")return{'total_users':total_users,'total_orders':total_orders,'total_gmv':total_gmv,'monthly_trend':monthly_trend}# 生成完整报表report_data=ecommerce_analysis_report()性能优化与避坑指南
常见性能问题及解决方案
| 问题现象 | 可能原因 | 解决方案 | 性能提升 |
|---|---|---|---|
| N+1查询问题 | 循环中查询关联数据 | 使用joinedload()或subqueryload() | 10-100倍 |
| 查询返回大量数据 | 没有分页,一次性加载所有数据 | 使用limit()和offset()分页 | 视数据量而定 |
| 频繁的count查询 | 每次分页都执行count | 缓存count结果或使用近似值 | 2-5倍 |
| 复杂的聚合查询慢 | 没有合适的索引 | 为分组和过滤字段添加索引 | 10-100倍 |
| 内存占用过高 | 加载了不需要的列 | 使用with_entities()只选择需要的列 | 30-70% |
优化示例:解决N+1查询问题
fromsqlalchemy.ormimportjoinedloaddefoptimized_user_orders():"""优化版:一次性加载用户及其订单"""# 错误做法:N+1查询print("错误做法(N+1查询):")users=session.query(User).limit(3).all()foruserinusers:orders=session.query(Order).filter(Order.user_id==user.id).all()# 每次循环都查询!print(f"{user.username}有{len(orders)}个订单")# 正确做法:使用joinedloadprint("\n正确做法(使用joinedload):")users_with_orders=session.query(User)\.options(joinedload(User.orders))\.limit(3).all()foruserinusers_with_orders:print(f"{user.username}有{len(user.orders)}个订单")returnusers_with_orders# 运行优化示例optimized_user_orders()调试技巧:查看生成的SQL
defdebug_sql_generation():"""调试:查看SQLAlchemy生成的SQL"""fromsqlalchemy.dialectsimportmysql# 构建一个复杂查询query=session.query(User.username,func.count(Order.id).label('order_count')).join(Order)\.group_by(User.id)\.having(func.count(Order.id)>1)# 查看生成的SQL(不执行)sql_statement=query.statementcompiled_sql=str(sql_statement.compile(dialect=mysql.dialect(),compile_kwargs={"literal_binds":True}))print("生成的SQL语句:")print("-"*80)print(compiled_sql)print("-"*80)returncompiled_sql# 查看SQLdebug_sql_generation()学习总结:你的查询技能升级路线图
通过今天的学习,你已经掌握了SQLAlchemy查询的三大核心技能:
✅ 已掌握的技能
- 精准过滤:使用
filter()和filter_by()精确筛选数据 - 灵活排序:单列、多列排序,支持升序降序
- 智能分页:
limit()和offset()实现数据分页 - 强大聚合:
count()、sum()、avg()等聚合函数 - 分组统计:
group_by()和having()实现数据分组分析
🚀 下一步学习建议
- 高级关联查询:学习
subquery()、cte()(公共表表达式) - 查询性能优化:深入理解执行计划,学习索引优化
- 异步查询:掌握SQLAlchemy 2.0的异步API
- 复杂报表:学习窗口函数、递归查询等高级特性
学习交流与进阶
恭喜你完成了SQLAlchemy查询进阶的学习!现在你已经能够处理大多数业务场景的数据查询需求了。
欢迎在评论区分享:
- 你在实际项目中遇到过哪些复杂的查询需求?
- 文中的哪个示例对你帮助最大?
- 在优化查询性能时,你有哪些独门秘籍?
我会认真阅读每一条留言,并为初学者提供针对性的解答。记住,数据库查询就像学游泳,光看教程不行,一定要多写多练!
推荐学习资源:
- SQLAlchemy官方文档 - 最权威的参考资料,特别是ORM查询部分
- 《Python数据库编程实战》 - 系统学习Python操作各种数据库
- SQLZoo - 在线练习SQL语句,打好SQL基础
- GitHub上的SQLAlchemy示例项目 - 查看真实项目中的最佳实践
下篇预告:
下一篇将分享《SQLAlchemy高级关联查询:从一对一关系到多对多复杂查询》,带你掌握:
- 一对一、一对多、多对多关系的定义与查询
- 关联查询的性能优化技巧
- 自关联查询的实现方法
- 使用关联查询构建复杂业务报表
最后的小建议:
学习数据库查询就像学习一门新语言,开始可能会觉得语法复杂,但一旦掌握,就能优雅地表达各种数据需求。今天学的内容,建议你明天就在自己的项目中用起来,遇到问题再回来查阅,这样学习效果最好。
记住我的经验之谈:我刚开始时总想一次性掌握所有高级特性,结果基础不牢。后来发现,先把过滤、排序、聚合这三大件练熟,80%的查询需求都能搞定。剩下的20%,等真正需要时再学也不迟。
祝你查询愉快,代码无bug! 🚀