查询执行过慢优化求助:6k+用户数据加载耗时超1分钟
哇,6k+用户查询耗时近一分钟确实够闹心的,咱们从查询逻辑、索引、代码细节三个层面来拆解优化,一步步把速度提上来!
一、先抓核心瓶颈:优化查询逻辑与聚合方式
你的查询里有多个LEFT JOIN和聚合函数,很容易产生笛卡尔积(比如一个用户对应多条视频记录,Join后数据量会爆炸式增长),这是性能慢的核心原因之一。
1. 把聚合查询从JOIN改成子查询,避免数据膨胀
原来的写法是Join整个btq_user_track_blog_video和event_data表再聚合,会生成大量中间数据。换成子查询单独计算每个用户的统计值,能大幅减少数据传输量:
// 替换原来的total_video_viewed列和对应的JOIN $videoSubQuery = BtqUserTrackBlogVideoQuery::create() ->select(array('UserId', 'COUNT(Counter) as total_video_viewed')) ->groupBy('UserId') ->getSql(); $criteria->addSelectColumn("(SELECT total_video_viewed FROM ($videoSubQuery) t WHERE t.user_id = btq_user.id) as total_video_viewed"); // 替换event_data的两个统计列和对应的JOIN $eventVideoSubQuery = EventDataQuery::create() ->select(array('BtqUserId', 'COUNT(Id) as kms_total_video_viewed')) ->filterByEventParentId(2) ->groupBy('BtqUserId') ->getSql(); $criteria->addSelectColumn("(SELECT kms_total_video_viewed FROM ($eventVideoSubQuery) t WHERE t.btq_user_id = btq_user.id) as kms_total_video_viewed"); $eventBlogSubQuery = EventDataQuery::create() ->select(array('BtqUserId', 'COUNT(Id) as kms_total_blog_viewed')) ->filterByEventParentId(1) ->groupBy('BtqUserId') ->getSql(); $criteria->addSelectColumn("(SELECT kms_total_blog_viewed FROM ($eventBlogSubQuery) t WHERE t.btq_user_id = btq_user.id) as kms_total_blog_viewed"); // 记得删掉原来对应的LEFT JOIN语句: // $criteria->addJoin(self::ID, BtqUserTrackBlogVideoPeer::USER_ID, Criteria::LEFT_JOIN); // $criteria->addJoin(self::ID, EventDataPeer::BTQ_USER_ID, Criteria::LEFT_JOIN);
2. 修正GROUP BY的逻辑,用唯一键代替EMAIL
你现在GROUP BY btq_user.email,但EMAIL可能重复(比如同一邮箱注册多个账号),而且GROUP BY非唯一键的聚合效率远低于主键/唯一键。改成GROUP BY用户ID:
// 替换原来的GROUP BY EMAIL $criteria->addGroupByColumn(self::ID); // 删掉$criteria->addGroupByColumn(self::EMAIL);
3. 按需加载JOIN表,避免不必要的关联
如果模板里不是所有场景都需要BtqUserSalesChoice、LeadSchedule这些表的数据,可以根据搜索参数判断是否添加JOIN:
// 比如只有当需要显示销售选择数据时才JOIN if (isset($search_params['choice_type']) || /* 其他需要该表的条件 */) { $criteria->addJoin(self::BTQ_USER_SALES_CHOICE_ID, BtqUserSalesChoicePeer::ID, Criteria::LEFT_JOIN); // 同时添加对应的SELECT列 $criteria->addSelectColumn("btq_user_sales_choice.type as choice_type"); $criteria->addSelectColumn("btq_user_sales_choice.opt_value as choice_value"); $criteria->addSelectColumn("btq_user_sales_choice.opt_text as choice_text"); }
二、给数据库加索引,让查询飞起来
没有合适的索引,数据库会做全表扫描,6k+数据量全表扫描自然慢。针对你的查询,必须加这些索引:
| 表名 | 索引字段组合 | 说明 |
|---|---|---|
btq_user | datain, is_dummy_detail | 用于日期过滤、排序和排除dummy数据 |
btq_user | id(主键,应该已经有了) | 子查询关联、GROUP BY的核心索引 |
btq_user | email | 如果必须保留GROUP BY EMAIL的话需要加 |
btq_user_track_blog_video | user_id | 子查询统计视频观看数的关联索引 |
event_data | btq_user_id, event_parent_id | 子查询统计视频/博客观看数的联合索引 |
btq_user | state_id, drid, btq_user_sales_choice_id | 各个LEFT JOIN的外键索引 |
比如给event_data加联合索引的SQL:
CREATE INDEX idx_event_data_user_parent ON event_data(btq_user_id, event_parent_id);
三、代码细节优化,减少不必要的开销
1. 去掉手动的addslashes,用Propel的参数绑定
Propel的Criteria会自动处理参数转义和绑定,手动加addslashes不仅多余,还可能导致SQL错误或注入风险:
// 删掉这行:$param = addslashes($param);
2. 分页处理,避免一次性加载所有数据
如果模板里是一次性渲染6k+用户,不仅查询慢,前端渲染也会卡。改成分页查询,比如每次查100条:
// 在函数末尾添加分页逻辑 $page = isset($search_params['page']) ? (int)$search_params['page'] : 1; $perPage = 100; $criteria->setOffset(($page - 1) * $perPage); $criteria->setLimit($perPage);
3. 只SELECT需要的字段
检查模板里是否真的用到了你SELECT的所有列,比如pfu_customer_id、schedule_id这些如果没用到,就删掉对应的addSelectColumn,减少数据传输量。
四、最后一步:用EXPLAIN分析执行计划
不管做了多少优化,都要通过EXPLAIN看数据库实际怎么执行查询:
EXPLAIN [你的查询SQL];
重点看这几个指标:
type列:最好是range或ref,如果是ALL说明全表扫描,要调整索引key列:有没有用到你加的索引rows列:预估扫描的行数,如果远大于实际返回行数,说明索引有问题
内容的提问来源于stack exchange,提问作者Dev Troubleshooter

