应为distinct字段添加单列索引,避免在text/blob字段上使用distinct,优先用count(distinct)替代全量去重,关联查询中需防范隐式去重失效。

在Yii框架中用Model::find()->select(['field'])->distinct()做去重查询时,若未预估数据量和索引情况,可能触发全表扫描并拖慢接口响应,尤其在百万级用户表中查DISTINCT city会导致查询耗时从20ms飙升至3.2秒。
确认DISTINCT字段是否有有效索引
执行 SHOW INDEX FROM your_table_name 查看目标字段是否已建索引。没有索引时,MySQL必须对全部行排序或哈希去重,CPU和内存开销陡增。
若字段无索引,立即添加: ALTER TABLE your_table_name ADD INDEX idx_city (city);
【注意】复合索引不能替代单列索引用于DISTINCT优化——比如已有INDEX(user_id, city),对SELECT DISTINCT city仍无效。
避免在TEXT/BLOB字段上使用distinct
Yii模型中若对content、description等TEXT类型字段调用->distinct(),MySQL会强制将其转为临时表并全量加载到内存,极易触发tmp_table_size超限而降级为磁盘临时表。
这一步操作起来很简单,直接改写查询逻辑:把TEXT字段从select列表中移除,只保留用于去重的INT/VARCHAR/DATE类字段。
如果业务真需要基于长文本内容去重,必须先用MD5(content)生成摘要列,再对该摘要列建索引并distinct。
用count(DISTINCT)替代全量去重结果集
第一步:判断前端是否真的需要全部去重后的值列表。如果是做下拉筛选项,通常只需知道“有多少个选项”而非“每个选项是什么”。
第二步:将原查询 User::find()->select('city')->distinct()->asArray()->all() 替换为 (new Query())->select('COUNT(DISTINCT city)')->from('user')->scalar()。
第三步:若后续仍需展示具体城市名,再单独查一次带LIMIT的去重结果,避免一次查出上万条唯一城市名压垮PHP内存。
这一步能将QPS提升4倍以上——因为COUNT(DISTINCT)可利用索引快速统计,不生成中间结果集。
警惕关联查询中的隐式去重失效
方法一:用join + distinct时,必须确保join条件精确且无笛卡尔积风险。例如:User::find()->select('u.city')->distinct()->innerJoin(['o' => 'order'], 'o.user_id = u.id')->where(['o.status' => 1]) —— 若order表存在一个用户多条订单,distinct仍生效;但若join写成left join且order表有NULL匹配行,则u.city可能重复出现。
方法二:更稳妥的做法是先子查询去重再join:User::find()->select('*')->from(['u' => (new Query())->select('DISTINCT city')->from('user')])。
【关键前提】子查询必须加别名(如'u'),否则Yii会报错“Syntax error near '('”。











