返回博客

会员数据分析里的幽灵门店:默认渠道桶污染全复盘

某后台管理系统做了一次数据兜底效果评估:「有多少会员绑了真实门店,有多少可以通过消费记录兜底迁移?」

跑出来的数字让业务侧很开心——兜底可救回的会员将近 30 万。

然后我们发现,其中 29.9 万条的「绑定门店」叫做「微信渠道」。

现象

SQL 如下,看起来没任何问题:

SELECT COUNT(*) AS rescuable
FROM member_info u
JOIN store s ON u.belong_counter_code = s.counter_code AND s.del_flag = 0
WHERE u.open_card_counter_code IS NULL OR u.open_card_counter_code = '';

结果 300k+。把这个数字汇报给业务,准备大张旗鼓做迁移。

但等我细查之后发现,真正「绑了真实门店、开卡门店却为空」的会员只有 95 人。

根因

几乎所有 POS / 会员中台系统都会留一个或多个「虚拟兜底门店」,专门承接那些通过线上渠道注册、还没有线下门店归属的会员:

| counter_code | counter_name | 本质 | |---|---|---| | 200611 | 微信渠道 | 线上兜底桶 | | 00000 | online | 线上兜底桶 | | 999999 | 总部 | HQ 兜底桶 |

这些「门店」在 store 表里 del_flag=0,有合法的省市信息,字段形状和真实门店完全一致。直接 JOIN 的 SQL 会把它们当成正常门店处理。

坑:虚拟门店在数据库里跟真实门店长得一模一样

store 表没有 is_virtual 这样的标志位。del_flag=0、有省市编码、有 counter_name——看上去一切正常,但它就是个垃圾桶。

当你按 GROUP BY counter_code 做门店级指标,或者按 JOIN store 做「有真实门店归属」的过滤时,这个桶会安静地混进来,把数字推高几个数量级。

最危险的场景:「绑定了门店的会员有 N 万」——很可能其中有几万人绑的是「微信渠道」,压根没有物理门店归属。

正解

在任何门店级分析 SQL 里,第一步就是把虚拟门店枚举出来并排除:

-- 先探测:哪些 counter_code 持有异常多的会员?
SELECT counter_code, counter_name, COUNT(*) AS cnt
FROM member_info
GROUP BY counter_code
ORDER BY cnt DESC
LIMIT 10;
-- 某个 code 持有 10 万+,其余门店各自几百 → 这个就是兜底桶
-- 正确的「真实门店」过滤条件
WHERE s.counter_code NOT IN ('200611', '00000', '999999')
  AND s.counter_name NOT LIKE '%渠道%'
  AND s.counter_name NOT LIKE '%online%'
  AND s.del_flag = 0

把这份排除列表固化成一个常量或视图,全项目复用,不要每个 SQL 各自维护一份:

// PHP 示例
class SyntheticStores {
    const CODES = ['200611', '00000', '999999'];
    const NAME_KEYWORDS = ['渠道', 'online', '总部', '临时', '测试'];
}
正解:先探测再查询,把虚拟桶枚举进常量

接手任何新的会员/门店系统时,在跑任何指标之前先做两件事:

  1. store 表的全量 counter_name,关键词:渠道 / online / wechat / 总部 / 临时 / 测试
  2. counter_code GROUP BY 看会员分布,持有量比其他门店高出 1-2 个数量级的那个就是兜底桶

发现后立刻建常量,整个分析层统一引用,不要在每条 SQL 里重复判断。

写路径 vs. 读路径的不同处理

会员通过线上渠道注册时,绑定到虚拟门店是合理的业务行为——新会员还没有线下消费记录,没有物理门店可归属。

因此:

  • 写路径(注册、首次绑定):不要拒绝 200611,这是正确的中间状态
  • 读路径(分析、报表、KPI):必须排除虚拟门店,数字才有业务意义

一句话外卖

POS / 会员中台里的「默认渠道门店」是个合法的数据库行,但它是垃圾桶而不是门店——所有分析 SQL 必须在第一行就把它过滤掉,否则你的「绑定率 98%」只是个幻觉。