多维聚合结果操纵:安全降维、折叠与粒度升降的工程实践
1. 项目概述当数据聚合从“加总”走向“空间折叠”你有没有遇到过这样的场景销售报表里区域经理要按“省份→城市→门店”三级下钻看毛利财务总监却需要把同一份数据按“产品线→季度→销售渠道”重新切片而风控团队又得交叉分析“高风险客户逾期账期放款月份”的组合分布这时候Excel 的透视表开始卡顿SQL 的 GROUP BY 嵌套三层就写得头皮发麻更别说还要动态切换维度、保留明细层级关系、或者计算同比环比时自动对齐时间轴——这已经不是简单的“求和”或“计数”而是数据在多维空间里的真实折叠与展开。Multi-Dimensional Aggregation多维聚合说白了就是把数据当成一个可旋转、可切片、可缩放的立方体Cube而Data Manipulation数据操作就是那把精准的手术刀它不只负责切出横截面比如“华东区2024年Q1销售额”更要能拉伸某条棱比如把“城市”升维到“省份”、旋转坐标系比如把“时间”从行切换到列、甚至局部放大某个角比如只聚焦“VIP客户在促销周的复购率”。这不是炫技而是现代BI、实时风控、智能推荐背后最底层的硬功夫。本文聚焦的 Part 20正是这个硬功夫中最容易被低估、也最容易踩坑的一环如何在保持多维语义完整性的前提下安全、高效、可逆地操纵聚合结果本身。它不教你怎么写第一个 SUM而是告诉你当你的聚合结果已经是一个 5 维数组、包含 23 个度量、关联着 7 个层次结构时删掉一个维度、合并两个层级、或者把“日粒度”平滑降采样为“周粒度”背后到底发生了什么哪些操作会悄悄破坏数据一致性哪些看似无害的“重命名”会让下游所有仪表盘集体报错如果你正用 Pandas 做复杂分组、用 DAX 写 Power BI 度量、用 Druid 或 ClickHouse 构建实时 OLAP 引擎或者只是每天和 Tableau/Superset 打交道——这篇就是为你写的实战手记没有理论空谈只有我踩过坑、调过参、压测过千万级数据后总结出的 4 类核心操作模式、12 个关键陷阱以及一套可直接抄作业的验证 checklist。2. 多维聚合的本质解构为什么“操纵聚合结果”比“写聚合查询”难十倍2.1 聚合不是终点而是中间态从“数据流”视角重看 Aggregation很多人的认知还停留在“GROUP BY → 聚合函数 → 出结果”这个线性链条上。但真实的数据工程流水线里聚合结果从来不是终点而是一个带状态的中间产物。想象一下一个电商实时大屏每秒接收 5000 条订单事件系统需要在 200ms 内更新“近 1 小时各品类 UV、GMV、转化率”三个指标。这里的“近 1 小时”不是固定时间窗而是滚动窗口“各品类”也不是扁平列表而是“一级类目→二级类目→品牌→SKU”的树状结构而“UV”需要去重“GMV”需要累加“转化率”则要跨两个聚合结果做除法。如果每次刷新都重跑全量 SQL延迟必然超标。所以工业级方案一定是先用 Flink 或 Kafka Streams 对原始事件流做预聚合Pre-Aggregation生成一个内存中的、带版本号的、支持增量更新的“聚合状态快照”。这个快照就是 Part 20 所谓的“Multi-Dimensional Aggregation Result”——它不是一个静态表格而是一个活的数据结构体内部封装了维度层级定义、度量计算逻辑、时间戳、版本号、甚至血缘元数据。Data Manipulation 的对象正是这个结构体本身而非其导出的 CSV 文件。这就像你不能对着一张打印出来的建筑蓝图去修改承重墙而必须回到 CAD 模型里调整参数。我见过太多团队把聚合结果导出成 Parquet 存 HDFS然后用 Spark SQL 对 Parquet 文件再做一层 GROUP BY美其名曰“二次聚合”。结果呢时间维度错乱原始是事件时间导出后变成处理时间、去重基数崩坏HyperLogLog 状态丢失、层级关系断裂父级汇总值无法与子级对齐。根源就在于混淆了“聚合结果”和“聚合结果的快照”。2.2 四维坐标系定义一个聚合结果的最小完备要素要安全操纵一个聚合结果你必须先精确锚定它的“四维坐标”。缺一不可否则操作必出错维度空间Dimension Space不是维度列表而是维度间的拓扑关系。例如“地区”维度必须明确定义其层次结构Region → Province → City → Store并声明每个层级的唯一键如 CityID和名称如 CityName。更重要的是要定义层级间的关系约束一个 Province 下可以有多个 City但一个 City 只能属于一个 Province1:N 关系而“时间”维度则需声明其粒度Day、Week、Month及周期性闰年、月末最后一天。我曾调试过一个故障BI 报表中“华东区 Q1 销售额”总是比财务系统少 3%查了三天才发现ETL 脚本里把“时间”维度的 Week 层级定义为“ISO Week”而财务系统用的是“自然周周一至周日”导致 12 月最后一周被错误分配到次年 Q1。度量空间Measure Space不只是字段名和类型。每个度量必须绑定其聚合语义Aggregation Semantics和计算上下文Computation Context。例如“销售额”是可加性度量AdditiveSUM 即可“平均客单价”是半可加性Semi-Additive只能按时间维度 SUM按客户维度则必须重新计算而“库存余额”是不可加性Non-Additive任何跨时间或跨仓库的 SUM 都无意义必须取期末快照值。更关键的是“计算上下文”一个“复购率”度量其分子复购用户数和分母总用户数必须来自同一时间窗口、同一用户群定义、同一去重逻辑。如果操纵时只改了分母的过滤条件而忘了同步更新分子结果就是灾难。时间上下文Temporal Context这是最容易被忽视的维度。聚合结果必须携带业务时间Business Time和处理时间Processing Time的双重戳记。业务时间指数据所代表的真实世界时间如订单创建时间处理时间指该聚合结果被计算出来的时间。两者偏差Latency直接决定数据新鲜度。一次“降采样”操作如把小时级聚合转为天级如果只修改了时间维度粒度却没更新业务时间戳的范围比如原为2024-01-01 00:00:00到2024-01-01 23:00:00降采样后应为2024-01-01整天下游所有基于时间的对比分析都会偏移。元数据契约Metadata Contract这是保障上下游协作的生命线。包括数据源 Schema 版本、聚合引擎版本、使用的近似算法如 HyperLogLog 的 p 参数、精度声明如“UV 误差率 0.8%”、以及最重要的——变更影响范围Impact Scope。例如当你决定将“客户等级”维度从 4 级普通/银卡/金卡/黑卡合并为 3 级普通/会员/黑卡时元数据契约必须明确声明“此变更将影响所有依赖‘客户等级’作为切片维度的报表且历史数据将按新规则回刷回刷耗时约 4.2 小时”。没有这份契约任何操作都是裸奔。2.3 操纵即重构四种核心操作模式及其底层代价在明确了聚合结果的四维本质后“Data Manipulation”就不再是随意增删字段而是四种有严格数学定义的结构重构Structural Refactoring操作。每种操作都对应特定的计算代价和一致性风险操作类型核心动作数学本质典型代价以 10 亿行原始数据为例最高风险点维度裁剪Dimension Pruning移除一个或多个维度将高维立方体投影到低维子空间如移除“门店”维度只保留“省份×时间”空间投影Projection低通常为 O(1) 内存拷贝无需重算破坏层级完整性如移除“城市”后“省份”汇总值可能因缺失城市校验而失真维度折叠Dimension Folding合并两个维度为一个复合维度如将“产品类别”和“品牌”合并为“品类_品牌”笛卡尔积压缩Cartesian Compression中需遍历所有组合生成新键值对键冲突如“手机_苹果”和“电脑_苹果”合并后键相同需强制加前缀粒度升降Granularity Scaling改变某一维度的粒度如“日”→“周”“城市”→“省份”上卷Roll-up或下钻Drill-down高需重计算所有受影响度量尤其对非可加性度量时间对齐错误周粒度未按 ISO 标准对齐、基数坍塌下钻时未保留父级键度量派生Measure Derivation基于现有度量创建新度量如用“GMV”和“订单数”计算“客单价”空间映射Mapping极低纯内存计算O(n) 时间复杂度上下文漂移如“客单价”分母用了错误的时间窗口导致分子分母不匹配提示永远优先选择“度量派生”而非“维度折叠”。我曾接手一个遗留系统为优化查询性能将 12 个维度强行折叠成 3 个超长字符串键如keyp1_c2_t3_s4_r5...。结果是无法单独过滤任一维度必须用 LIKE 模糊匹配、无法做任何层级下钻、新增维度需全量重建索引。后来我们花了 3 个月用标准星型模型 物化视图替代查询性能反而提升 40%维护成本下降 90%。记住维度是分析的入口不是存储的包袱。3. 实操核心四大操作的落地细节、参数选择与避坑指南3.1 维度裁剪Dimension Pruning如何安全地“砍掉”一个维度而不伤筋动骨维度裁剪看似最简单实则暗藏杀机。核心原则是裁剪不是删除而是降维保真。目标是让裁剪后的结果在其剩余维度空间内依然保持数学上的“正确性”和业务上的“可用性”。第一步识别裁剪的合法性边界并非所有维度都能随意裁剪。必须通过三重校验可加性校验被裁剪维度上的所有度量必须是完全可加性Fully Additive。例如“销售额”可加“订单数”可加但“平均折扣率”不可加不能对平均值再求平均。如果强行裁剪结果毫无意义。我用一个 Python 脚本自动化校验# 假设 agg_result 是一个 Pandas DataFrameindex 为多级索引Province, City, Store # measures [gmv, order_cnt, avg_discount] def check_additivity(agg_result, measures, prune_dimCity): 检查指定维度裁剪后各度量是否仍满足可加性 # 获取裁剪前的 Province 级别汇总 before_province agg_result.groupby(levelProvince)[measures].sum() # 手动裁剪按 Province 分组求和模拟裁剪效果 after_province agg_result.groupby(levelProvince)[measures].sum() # 对于非可加性度量需特殊处理如 avg_discount 需用加权平均 if avg_discount in measures: # 计算 Province 级别的加权平均sum(gmv * avg_discount) / sum(gmv) weighted_num (agg_result[gmv] * agg_result[avg_discount]).groupby(levelProvince).sum() weighted_den agg_result[gmv].groupby(levelProvince).sum() after_province[avg_discount] weighted_num / weighted_den return before_province.equals(after_province)运行此脚本若返回False则说明该度量不支持此维度裁剪必须先做转换。第二步执行裁剪的两种安全路径路径 A推荐在聚合引擎层直接定义视图。例如在 Druid 中不修改底层数据源而是创建一个Virtual Column视图其维度字段只包含Province和Time所有查询走此视图。好处是零数据迁移、可灰度、可回滚。路径 B谨慎物理裁剪并重写元数据。适用于必须降低存储成本的场景。关键步骤使用GROUP BY Province, Time重算所有度量生成新数据集强制更新元数据契约在数据目录如 Atlas中将新数据集的dimension_hierarchy字段更新为[Province, Time]并标记pruned_from为原数据集 ID注入校验断言在数据管道中加入断言确保新数据集的Province级别gmv总和等于原数据集所有City的gmv总和允许微小浮点误差。这是防止 ETL 逻辑错误的最后一道防线。第三步终极避坑——“伪裁剪”的幻觉陷阱最大的坑是你以为裁剪了维度其实只是隐藏了它。例如在 Tableau 中把“城市”字段从行拖到“详细信息”卡里看起来报表只显示“省份”但后台 SQL 依然GROUP BY Province, City只是没展示 City 列。这会导致查询性能毫无改善依然扫描所有 City 数据如果 City 维度有大量 NULL 值COUNT(DISTINCT City)会严重失真当你试图在此基础上再加一个“品牌”维度时会触发笛卡尔爆炸。实操心得在执行任何裁剪前务必用EXPLAIN查看最终生成的 SQL 或执行计划。真正的裁剪Plan 里GROUP BY子句中绝对不能出现被裁剪的维度名。我在一家公司审计时发现他们所谓的“已裁剪门店维度”Explain 显示GROUP BY province, city, store只是前端做了MAX(store_name)聚合本质上是用MAX掩盖了维度存在数据一致性早已崩坏。3.2 维度折叠Dimension Folding当“品类品牌”必须变成一个键维度折叠是为了解决“维度爆炸”问题但极易引发语义混乱。核心挑战在于如何保证折叠后的键既能唯一标识原组合又不失业务可读性与查询灵活性。关键参数选择折叠策略与分隔符策略选择绝不用CONCAT(category, brand)这种简单拼接。必须使用防冲突编码。最佳实践是SHA256(CONCAT(category_id, |, brand_id))其中category_id和brand_id是数字主键非名称|是强分隔符。为什么category_id12, brand_id3和category_id1, brand_id23简单拼接123会冲突名称拼接如手机苹果在多语言环境下不稳定“iPhone” vs “苹果手机”SHA256 保证全球唯一、长度固定64字符、且不可逆保护敏感信息。分隔符选择必须是原维度值中绝对不可能出现的字符。我曾用:作为分隔符结果发现某品牌名就叫A:B Corp导致键解析失败。现在一律用\x00ASCII 0或|并在文档中强制规定所有上游系统输入的维度值必须过滤掉该分隔符。实操步骤从定义到上线的七步法定义折叠规则文档明确写出folded_key SHA256(CONCAT(category_id, \x00, brand_id))并附上示例在维度表中添加计算列在dim_product表中增加category_brand_key VARCHAR(64)列并用上述公式填充修改事实表外键将fact_sales表中的category_id和brand_id两列替换为category_brand_key并建立索引重写聚合逻辑所有GROUP BY category_id, brand_id替换为GROUP BY category_brand_key构建反向映射视图创建v_category_brand_mapping视图提供category_brand_key → category_name, brand_name的查询能力供 BI 工具下钻使用更新元数据契约在数据目录中标记category_brand_key为“复合维度”并关联其构成的原子维度上线灰度与监控先对 5% 的流量启用新键监控查询 P95 延迟、结果一致性与旧逻辑比对、以及v_category_brand_mapping的查询成功率。避坑指南折叠不是终点而是起点折叠后你失去了对原子维度的独立控制力。因此必须配套建设动态解构能力当用户在 BI 工具中点击“手机_苹果”想看“所有苹果手机”系统必须能实时从v_category_brand_mapping中查出category_id12并下发WHERE category_id 12的查询而不是WHERE category_brand_key LIKE xxx%性能灾难变更熔断机制如果某天需要将“品牌”维度拆分为“自有品牌”和“授权品牌”那么category_brand_key必须整体废弃启动全量回刷。此时元数据契约中的impact_scope就至关重要它会提前通知所有下游负责人“本次变更将导致所有依赖该键的报表停服 8 小时”。3.3 粒度升降Granularity Scaling从“日”到“周”一次不能错的精密手术粒度升降是风险最高的操作因为它是对时间或空间坐标的重标定。一次错误的升降可能导致整个时间序列分析失效。“日→周”升降的核心难点时间对齐问题不在计算而在定义。“周”是什么不同系统有不同答案ISO Week周一为每周第一天第 1 周是包含该年第一个周四的周自然周周一至周日财年周公司自定义如每年 7 月 1 日为财年第一天第一周为 7 月 1 日所在周。我的解决方案三阶段对齐法源头对齐在数据接入层Kafka Consumer 或 Flink Source就将原始事件的event_time转换为标准化的week_id。我用 Flink 的Tumble窗口函数并显式指定offset-- Flink SQL按 ISO Week 对齐起始时间为 1970-01-01Unix epoch SELECT TUMBLE_START(event_time, INTERVAL 1 WEEK, INTERVAL 1970-01-01 DAY) AS week_start, TUMBLE_END(event_time, INTERVAL 1 WEEK, INTERVAL 1970-01-01 DAY) AS week_end, SUM(gmv) AS weekly_gmv FROM events GROUP BY TUMBLE(event_time, INTERVAL 1 WEEK, INTERVAL 1970-01-01 DAY);这里INTERVAL 1970-01-01 DAY是关键它强制所有窗口对齐到 Unix epoch确保全球一致。存储对齐在 OLAP 引擎如 ClickHouse中week_id字段必须是Int32类型值为toRelativeWeekNum(event_time)的结果而非字符串2024-W01。原因Int32支持高效范围查询WHERE week_id BETWEEN 202401 AND 202404而字符串需要LIKE或正则性能差百倍。展示对齐在 BI 层week_id的格式化必须由前端或 BI 工具完成且必须与源头定义严格一致。我要求所有前端工程师必须调用统一的formatWeekId(week_id)函数该函数内部硬编码 ISO Week 规则禁止自行解析。“城市→省份”的空间升降避免层级坍塌空间升降的陷阱是子级信息丢失导致父级汇总失真。例如某省有 100 个市其中 5 个市的数据因网络问题延迟 2 小时到达。如果按“城市”粒度聚合这 5 个市的当天数据会缺失但如果直接“升降”到“省份”系统会用已有 95 个市的数据求和得出一个“不完整”的省级汇总而下游用户根本不知道。我的防御体系层级完整性断言Hierarchy Integrity Assertion在聚合任务中加入检查# 检查每个 Province 下City 的覆盖率 city_coverage df.groupby(Province)[City].nunique() / total_cities_per_province if (city_coverage 0.95).any(): raise RuntimeError(fProvince coverage below 95%: {city_coverage[city_coverage 0.95]})降级策略Degradation Policy当覆盖率不足时不输出“不完整”结果而是返回NULL并打上statusINCOMPLETE标签或者用历史均值进行插补需在元数据中明确标注“此值为插补”血缘追踪在结果数据中增加city_coverage_ratio字段记录本次聚合的子级覆盖比例供下游决策。3.4 度量派生Measure Derivation用“GMV”和“订单数”安全计算“客单价”度量派生是风险最低的操作但也是最容易因“上下文漂移”而失效的操作。核心是确保派生度量的计算上下文与所有参与度量的上下文完全一致。上下文一致性检查清单必须逐项核对检查项说明不一致的后果时间窗口对齐gmv和order_cnt的time_range必须完全相同同一起止时间、同一粒度客单价 SUM(GMV_24H) / COUNT(ORDER_7D)结果毫无意义用户群定义一致gmv的用户群如active_user_flag1与order_cnt的用户群过滤条件必须完全相同分母漏掉部分用户客单价虚高去重逻辑统一如果order_cnt是COUNT(DISTINCT order_id)则gmv的计算也必须基于同一order_id集合gmv包含了取消订单order_cnt已去重分子分母不匹配货币单位归一gmv的单位必须是基础货币如 CNY且所有汇率转换已在上游完成混合 USD 和 CNY 计算结果错误空值处理策略gmv或order_cnt为NULL时客单价应为NULL而非0或INF需在 SQL 中显式NULLIF出现大量INF值污染分析实操代码模板ClickHouse-- 安全的客单价计算内置所有上下文校验 SELECT province, toMonday(event_date) AS week_start, -- 强制 ISO Week 对齐 -- 关键用同一个子查询获取所有上下文一致的度量 (SELECT SUM(gmv) FROM fact_sales s2 WHERE s2.province s1.province AND s2.event_date toMonday(s1.event_date) AND s2.event_date toMonday(s1.event_date) INTERVAL 7 DAY AND s2.user_status active) AS weekly_gmv, (SELECT COUNT(DISTINCT order_id) FROM fact_sales s2 WHERE s2.province s1.province AND s2.event_date toMonday(s1.event_date) AND s2.event_date toMonday(s1.event_date) INTERVAL 7 DAY AND s2.user_status active) AS weekly_order_cnt, -- 安全除法NULLIF 防止除零assumeNotNull 保证类型 assumeNotNull(NULLIF(weekly_gmv, 0) / NULLIF(weekly_order_cnt, 0)) AS avg_order_value FROM (SELECT DISTINCT province, event_date FROM fact_sales) s1 GROUP BY province, week_start;注意事项永远不要在 BI 工具中做跨表度量派生。我见过最惨的案例Tableau 中一个工作表连接sales_fact含 gmv另一个工作表连接order_fact含 order_cnt然后用SUM([gmv]) / SUM([order_cnt])计算客单价。结果是两个事实表的WHERE过滤条件不同sales 表加了statuspaidorder 表没加导致分子分母完全错配。正确做法是所有派生度量必须在数据仓库层用一个原子 SQL 完成计算并物化为新表或视图。4. 常见问题与排查技巧实录那些让我凌晨三点还在改 SQL 的 Bug4.1 问题速查表12 个高频故障与 5 分钟定位法故障现象可能原因5 分钟定位法我的修复经验聚合结果数值突变如某天 GMV 突然翻倍1. 新增维度导致笛卡尔积2. 时间窗口重叠如BETWEEN 2024-01-01 AND 2024-01-31与 2024-01-01 AND 2024-02-01混用1. 检查GROUP BY子句维度数是否增加2. 用EXPLAIN看时间过滤条件是否生成了OR条件在 Flink 作业中强制所有时间过滤使用 AND 半开区间并在 CI 流程中加入 SQL Linter 检查下钻时数据“消失”如从省份点进城市总和变小1. 层级关系定义错误某城市未关联到省份2. 数据质量城市维度表有脏数据province_idNULL1. 查询dim_city表WHERE province_id IS NULL2. 用COUNT(*)对比dim_province和dim_city的关联行数建立维度表的“健康度看板”实时监控NULL率、FOREIGN KEY违反率阈值超限自动告警同比环比计算结果为 NULL1. 基准期数据缺失如计算 2024-01-01 同比但 2023-01-01 无数据2. 时间对齐失败2024-01-01 是周一2023-01-01 是周日1. 直接查基准期原始数据是否存在2. 检查LAG()或JOIN的 ON 条件是否用了date_sub(event_date, INTERVAL 1 YEAR)而非date_trunc(year, event_date)所有同比环比必须用date_trunc先对齐到相同粒度如WEEK再计算偏移杜绝日期算术BI 报表加载极慢但数据库监控 CPU 正常1. 前端做了错误的GROUP BY如在 Tableau 中把度量拖到“行”卡2. 聚合结果被当作明细表使用未启用物化视图1. 查 BI 工具生成的 SQL看GROUP BY是否包含不该有的字段2. 查数据库pg_stat_statements看慢查询是否SELECT * FROM agg_table在 BI 工具侧强制所有数据源配置为“只读聚合视图”禁用对事实表的直接查询权限“降采样”后周粒度数据与日粒度 SUM 不等1. 日粒度数据有重复或缺失2. 降采样逻辑未处理NULLSUM(NULL)NULL而非01. 对日粒度表SELECT date, COUNT(*) FROM daily GROUP BY date ORDER BY date找异常计数2. 用COALESCE(SUM(gmv), 0)替换SUM(gmv)在日粒度聚合任务中加入ROW_NUMBER() OVER (PARTITION BY date ORDER BY ts DESC)去重并用CHECKSUM校验每日数据完整性4.2 独家避坑技巧从血泪史中提炼的 3 条铁律铁律一永远不要信任“默认值”所有粒度、对齐方式、空值策略必须显式声明我曾在一个项目中Flink 作业的Tumble窗口未指定offset默认使用INTERVAL 1970-01-01 DAY。上线后一切正常直到半年后客户要求按“财年周”分析我们修改了offset结果发现历史所有周粒度数据都错位了因为offset改变后窗口划分逻辑彻底改变旧数据无法重算。教训所有时间相关参数必须作为作业的强制配置项写入application.conf并在作业启动时打印到日志。现在我的标准日志开头必有INFO [JobManager] Starting aggregation job with config: time_granularityWEEK, time_offset1970-01-01, null_policyCOALESCE_TO_ZERO铁律二聚合结果的 Schema 变更是最高危操作必须走“双写比对灰度”流程有一次产品经理要求给“客户等级”维度增加一个“钻石会员”级别。开发同学直接ALTER TABLE dim_customer ADD COLUMN level_new STRING并更新了 ETL 脚本。结果是新老数据混杂level_new为NULL的老数据在GROUP BY level_new时被聚合成一个神秘的NULL分组所有报表都多出一条“未知等级”数据。正确流程是双写阶段新作业写入dim_customer_v2表同时保留dim_customer_v1比对阶段用SELECT * FROM dim_customer_v1 EXCEPT SELECT * FROM dim_customer_v2确认差异仅在新增字段灰度阶段先让 10% 的报表服务读v2监控 24 小时确认无异常后全量切换。铁律三为每一个聚合结果配备一个“健康度探针Health Probe”这不是可选项而是必需品。探针是一个轻量级 SQL每 5 分钟运行一次检查核心指标-- 聚合结果健康度探针示例sales_daily_agg SELECT daily_agg_health AS probe_name, COUNT(*) AS row_count, MIN(event_date) AS min_date, MAX(event_date) AS max_date, -- 关键检查最新一天数据是否完整应有 24 小时 CASE WHEN MAX(event_date) CURRENT_DATE - INTERVAL 1 DAY AND COUNT(CASE WHEN