很多人踩过这个坑。写Python的时候,从数据库拉数据慢得要死。第一个反应就是Python不行。其实不是。你翻翻数据库,看看自己写的SQL,大概率写得稀烂。
一个常见的场景。从MySQL里查几百万条订单数据。用Python的pymysql或者SQLAlchemy,用ORM去查。查完了再用pandas处理。一跑就是半小时。然后你说Python慢。其实SQL查出来的数据本身就有问题。
举个例子。你写SELECT FROM orders WHERE status = 'paid'。拿回来几十个字段。然后你只用到三个字段。多余的字段占内存,占网络传输时间。Python读这些数据,内存暴涨,处理变慢。解决方案很简单:在SQL里只查你需要的字段。SELECT order_id, user_id, amount FROM orders WHERE status = 'paid'。光这一条,很多人的脚本就能快一半。
还有一个更隐蔽的问题。你从数据库查了100万行数据,然后pandas里做groupby。这个groupby其实是能推到SQL里做的。比如你想算每个用户的总金额。你在pandas里做df.groupby('user_id')['amount'].sum()。数据全拉到Python内存里再聚合。SQL本身就能直接聚合:SELECT user_id, SUM(amount) FROM orders GROUP BY user_id。返回的只有几千行。内存占用从几百兆降到几兆。
把聚合、过滤、排序这些操作尽量推给数据库。数据库就是干这个的。它的索引、执行计划、内存管理都专门优化过。pandas再强,也堆不过几千万行数据在内存里算。
还有一种情况。你从数据库拉数据,每次都全量拉。比如每天跑一个报表,把整个表全部拉下来。然后pandas里过滤当天的数据。第一天还好,三个月后数据量翻了几倍。查询越来越慢。正确的做法是:SQL里加上时间条件,只查你需要的那天或者那周的数据。数据库如果有时间字段的索引,查起来快得飞起。
再比如关联查询。你查订单和用户信息。写法是SELECT FROM orders JOIN users ON orders.user_id = users.user_id。这个没问题。但如果你先查订单,然后在pandas里用merge做关联。那就麻烦了。数据库的JOIN有索引和优化器支撑。pandas的merge要在内存里做哈希匹配。数据量大一点就卡死了。尽量在SQL里完成所有关联,把最终结果交给pandas做轻量分析。
有人觉得pandas厉害,什么都能干。但pandas的强项是数据处理和分析,不是数据搬运。你把数据搬运的活交给数据库,分析加工交给pandas。这才是正确分工。
还有一个小细节。Python连数据库的时候,用cursor.fetchone一条一条拿。这个操作太慢了。应该用pandas的pd.read_sql()。它底层会一次拉一批数据,效率高很多。同时你可以传chunksize参数,分批处理,防止内存爆炸。
举个具体的例子。有人写了个脚本统计用户流失。SQL里把过去一年的订单全部拉出来,然后在pandas里算每个用户最近一次下单时间。数据量上千万行。脚本跑一个多小时。改成SQL里直接算最近一次下单时间:SELECT user_id, MAX(order_date) as last_order FROM orders GROUP BY user_id。返回的只有几十万行。pandas拿过来再处理,几分钟就完事了。
很多人遇到性能问题就怪技术。怪Python慢,怪pandas内存大。其实问题出在数据流设计上。把数据处理行为放对位置,SQL能干的让SQL干,pandas处理剩下的轻量计算。这样配合,你的代码效率能提升一个量级。
最后一条建议。写SQL之前想清楚:我要从数据库拿什么结果?这个结果是不是已经能直接用了?我能不能少拿点数据?想清楚再动手。你的脚本就不会再让人说“Python太慢了”。