数据科学家必备的10条高阶SQL实战指南
1. 这10条SQL不是“应知”而是你每天都在用的呼吸节奏在数据科学团队里我见过太多人把SQL当成“取数工具”——写个SELECT * FROM table导出Excel扔给Python处理。结果呢一个2000万行的用户行为表本地pandas读取卡死三次Jupyter Kernel重启五次最后发现只要加一句WHERE event_time 2024-01-01和GROUP BY user_id原始SQL就能直接返回3872个活跃用户ID列表连Python都不用启动。这10条SQL根本不是“应该知道”的知识清单而是数据科学家在真实战场上的肌肉记忆它们是你连接数据库时手指自动敲出的节奏是你看报表异常时第一反应要验证的逻辑是你和工程师对齐口径时最硬的底牌。关键词SQL、数据科学家、聚合查询、窗口函数、关联分析、业务指标。如果你还在用Excel做漏斗分析用Python遍历百万行做用户分群或者每次改个指标都要等ETL跑完第二天——那你不是缺算法是缺这10条SQL的实操直觉。它们覆盖了95%以上的日常分析场景从单表快速探查比如看某类商品销量分布是否符合幂律到多表精准归因比如判断是渠道投放质量下降还是落地页转化率崩了再到动态分层计算比如实时计算每个用户的LTV分位数。这不是语法考试是生存技能——今天我就带你一条一条拆解每条都配真实业务场景、参数选择逻辑、性能陷阱和我踩过的血泪坑。2. 整体设计逻辑为什么是这10条而不是20条或5条2.1 选型原则从“数据库能做什么”转向“业务问题要什么”很多教程列SQL是按语法难度排的先SELECT再JOIN最后窗口函数。但真实世界不是这样。我带过7个数据分析新人他们第一个卡点永远不是“怎么写LEFT JOIN”而是“老板问‘上个月复购率为什么跌了5%’我该查哪几张表、怎么关联、怎么定义复购”。所以这10条的排序完全基于问题驱动优先级前3条基础过滤聚合分组解决80%的“快看一眼”需求日报核对、异常初筛、临时取数中间4条多表关联子查询日期处理去重逻辑覆盖90%的“归因分析”场景为什么A指标涨了B指标却跌了哪个环节漏了用户后3条窗口函数条件聚合递归/层级专攻“动态分层”和“路径分析”用户生命周期阶段判定、漏斗各环节转化率、组织架构下汇报关系穿透。提示不选“CREATE TABLE”“INSERT INTO”这类DDL/DML语句因为99%的数据科学家没有建表权限也不选“EXPLAIN PLAN”这种DBA级命令虽然它重要但属于进阶优化范畴不在“必须日日用”的核心清单里。2.2 领域适配为什么金融、电商、SaaS的SQL长得不一样同一句COUNT(DISTINCT user_id)在不同行业代表完全不同的业务含义和实现难点电商场景COUNT(DISTINCT buyer_id) 要排除刷单小号得加设备指纹去重WHERE device_fingerprint NOT IN (SELECT fraud_device FROM fraud_rules)SaaS场景COUNT(DISTINCT account_id) 要区分试用期客户和付费客户得嵌套CASE WHENCASE WHEN plan_type trial THEN 0 ELSE 1 END金融场景COUNT(DISTINCT user_id) 必须满足GDPR得先脱敏再聚合SELECT COUNT(DISTINCT SHA2(email, 256)) FROM users。所以这10条不是静态语法而是可插拔的业务逻辑模块。比如第7条“窗口函数排名”在电商是“按GMV给商家分TOP100”在教育平台是“按完课率给课程分S/A/B/C档”底层ROW_NUMBER()没变但PARTITION BY和ORDER BY的字段组合直接决定分析结论是否可信。2.3 性能红线为什么有些写法看着对却让DBA半夜打电话我曾用一条看似优雅的SQL查用户留存导致公司Redshift集群CPU飙到98%运维同事冲进会议室拍桌子“你那条LATERAL VIEW explode()是不是想炸掉整个数仓”——问题出在第9条“自连接模拟递归”上。原写法是SELECT a.user_id, b.order_date FROM orders a JOIN orders b ON a.user_id b.user_id AND b.order_date a.order_date LIMIT 100;表面看只是查每个用户的第二笔订单但实际执行时数据库要为a表每行扫描b表全量2000万行×2000万行4×10¹⁴次比较。正确解法是用ROW_NUMBER()窗口函数第7条替代SELECT user_id, order_date FROM ( SELECT user_id, order_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS rn FROM orders ) t WHERE rn 2;计算量从O(n²)降到O(n log n)响应时间从47分钟缩至3.2秒。这10条的每一条我都标注了典型数据量阈值如“单表超500万行慎用子查询”、索引依赖提示如“WHERE date_col需有B-tree索引”、替代方案对比如“相关子查询 vs JOIN vs 窗口函数”因为真正的SQL能力不在于会不会写而在于预判哪条路会堵车。3. 核心细节解析与实操要点每条SQL背后的业务心跳3.1 基础过滤WHERE 多条件组合——不是语法是业务规则翻译器新手常犯的错把业务语言“近30天高价值用户”直接翻译成WHERE order_amount 1000 AND order_date 2024-03-01。但真实业务中“近30天”可能是自然月WHERE order_date DATE_TRUNC(month, CURRENT_DATE) - INTERVAL 1 month也可能是滚动30天WHERE order_date CURRENT_DATE - INTERVAL 30 days还可能是财年周期WHERE order_date BETWEEN 2024-04-01 AND 2024-04-30。更致命的是“高价值”的定义是单笔订单1000还是近30天累计5000或是RFM模型里的R7 F3 M2000我实测过某电商平台的订单表用自然月过滤WHERE order_date 2024-03-01查3月GMV结果比BI系统报表少12.7%——因为大量订单的order_date是支付成功时间而财务记账用的是发货时间ship_date。最终方案是双时间维度校验WHERE (order_date 2024-03-01 AND order_date 2024-04-01) OR (ship_date 2024-03-01 AND ship_date 2024-04-01 AND order_status shipped)注意日期字段务必确认时区我吃过亏数据库存的是UTC时间但业务方要的是北京时间UTC8直接WHERE order_date 2024-03-01会漏掉3月1日0:00-7:59的订单。正确写法是WHERE order_date 2024-03-01::TIMESTAMP AT TIME ZONE UTC AT TIME ZONE Asia/Shanghai或更稳妥的WHERE order_date 2024-02-29 16:00:00UTC时间。3.2 聚合统计SUM/COUNT/AVG CASE WHEN——指标口径的终极战场COUNT()和COUNT(column)的区别90%的人答不对。COUNT()统计行数包括NULLCOUNT(column)只统计非NULL值。这在计算“支付成功率”时就是生死线如果payment_status字段有NULL表示未支付用COUNT(*)会把未支付订单也算进分母导致成功率虚高。正确写法是SELECT COUNT(*) AS total_orders, COUNT(CASE WHEN payment_status paid THEN 1 END) AS paid_orders, COUNT(CASE WHEN payment_status IS NULL THEN 1 END) AS pending_orders, ROUND(100.0 * COUNT(CASE WHEN payment_status paid THEN 1 END) / NULLIF(COUNT(*), 0), 2) AS success_rate_pct FROM orders;这里用了三个关键技巧NULLIF避免除零错误COUNT()可能为0如新上线渠道还没订单NULLIF(COUNT(), 0)把0转成NULL除以NULL返回NULL而非报错CASE WHEN内联条件比WHERE过滤更灵活可同时算多个指标如付费订单数、退款订单数、待支付订单数ROUND100.0强转浮点防止整数除法截断如5/100100.0*保证结果是小数。我在某SaaS公司做续费率分析时发现市场部报表续费率是82%而我们SQL算出来是76%。追查发现他们用COUNT(*)当分母把已注销客户statuscancelled也计入了“到期客户总数”而我们用COUNT(CASE WHEN status IN (active,trial) THEN 1 END)严格定义分母。最后共识续费率分母必须是“合同到期且状态为active/trial的客户”这才是业务本质。3.3 分组洞察GROUP BY HAVING——从“是什么”走向“为什么”GROUP BY不是简单分类而是业务维度切片。新手常堆砌字段GROUP BY user_id, product_id, category, region... 结果分组数爆炸内存溢出。真正高手用GROUP BY回答具体问题问“各城市客单价分布”GROUP BY city问“高价值用户集中在哪些品类”GROUP BY category HAVING AVG(order_amount) 500问“哪些渠道的用户7日留存率低于均值”GROUP BY channel HAVING AVG(retention_7d) (SELECT AVG(retention_7d) FROM user_stats)。重点在HAVING子句——它过滤的是分组后的聚合结果不是原始行。比如查“订单量超1000的省份”不能写WHERE order_count 1000order_count还没算出来必须SELECT province, COUNT(*) AS order_count FROM orders o JOIN users u ON o.user_id u.user_id GROUP BY province HAVING COUNT(*) 1000 ORDER BY order_count DESC;我踩过的坑某次分析用户流失原因想查“过去30天登录次数为0的用户所在城市”写了-- 错误WHERE在GROUP BY前执行u.last_login_date是单个值不是聚合结果 SELECT city, COUNT(*) FROM users u WHERE u.last_login_date CURRENT_DATE - INTERVAL 30 days GROUP BY city;正确解法是用聚合函数定义“30天未登录”-- 正确用MAX()确保取到每个用户的最新登录时间 SELECT city, COUNT(*) FROM users GROUP BY city HAVING MAX(last_login_date) CURRENT_DATE - INTERVAL 30 days;3.4 多表关联INNER/LEFT JOIN ON条件——数据血缘的显微镜JOIN不是技术操作是业务关系建模。INNER JOIN要求两边都存在LEFT JOIN保留左表全部。选错类型结论全错。案例分析“商品曝光到购买的转化漏斗”。曝光日志表impressions有1000万行订单表orders有50万行用INNER JOIN impressions i ON i.product_id o.product_id得到50万行——但这50万是“被曝光且成交的商品”不是“所有成交商品”因为有些商品是通过搜索直接购买没走曝光链路。正确漏斗应该是曝光UV去重user_id曝光后7天内购买UV转化率 步骤2 / 步骤1。这需要LEFT JOIN 时间窗口SELECT COUNT(DISTINCT i.user_id) AS exposure_uv, COUNT(DISTINCT CASE WHEN o.order_id IS NOT NULL THEN i.user_id END) AS convert_uv, ROUND(100.0 * COUNT(DISTINCT CASE WHEN o.order_id IS NOT NULL THEN i.user_id END) / NULLIF(COUNT(DISTINCT i.user_id), 0), 2) AS cvr_pct FROM impressions i LEFT JOIN orders o ON i.user_id o.user_id AND o.order_date BETWEEN i.impression_date AND i.impression_date INTERVAL 7 days AND i.product_id o.product_id;实操心得JOIN条件务必包含业务强约束。比如用户表和订单表关联不能只写ON u.user_id o.user_id必须加AND u.status active排除已注销用户否则会把测试账号、员工账号的订单也算进来。我在某社交App就因此误判DAU增长——20%的“新增用户”其实是运营同学用测试号刷的。3.5 子查询嵌套WHERE/SELECT/FROM子查询——复杂逻辑的乐高积木子查询分三类用错场景会拖垮性能WHERE子查询标量子查询用于单值比较如WHERE revenue (SELECT AVG(revenue) FROM competitors)SELECT子查询生成计算列如SELECT user_id, (SELECT COUNT(*) FROM orders WHERE user_id u.user_id) AS order_cntFROM子查询派生表最安全可建索引如FROM (SELECT user_id, MAX(order_date) AS last_order FROM orders GROUP BY user_id) t。性能陷阱SELECT子查询是相关子查询对主表每行执行一次。100万用户表就要执行100万次子查询。换成FROM子查询只需执行1次聚合-- 慢100万次扫描orders表 SELECT u.user_id, u.name, (SELECT COUNT(*) FROM orders o WHERE o.user_id u.user_id) AS order_cnt FROM users u; -- 快1次聚合1次JOIN SELECT u.user_id, u.name, COALESCE(t.order_cnt, 0) AS order_cnt FROM users u LEFT JOIN ( SELECT user_id, COUNT(*) AS order_cnt FROM orders GROUP BY user_id ) t ON u.user_id t.user_id;业务应用计算“用户价值分层”。某游戏公司要分S/A/B/C档规则是S档近30天充值5000且登录天数15A档充值2000或登录天数10其余B/C。用子查询实现SELECT user_id, CASE WHEN recharge_30d 5000 AND login_days_30d 15 THEN S WHEN recharge_30d 2000 OR login_days_30d 10 THEN A ELSE B END AS tier FROM ( SELECT u.user_id, COALESCE(r.recharge_sum, 0) AS recharge_30d, COALESCE(l.login_days, 0) AS login_days_30d FROM users u LEFT JOIN ( SELECT user_id, SUM(amount) AS recharge_sum FROM payments WHERE pay_date CURRENT_DATE - INTERVAL 30 days GROUP BY user_id ) r ON u.user_id r.user_id LEFT JOIN ( SELECT user_id, COUNT(DISTINCT login_date) AS login_days FROM logins WHERE login_date CURRENT_DATE - INTERVAL 30 days GROUP BY user_id ) l ON u.user_id l.user_id ) t;3.6 日期智能处理DATE_TRUNC/INTERVAL/EXTRACT——时间维度的手术刀业务方说“上个月”数据库不知道是哪个月。DATE_TRUNC是救命稻草DATE_TRUNC(month, CURRENT_DATE)→ 当前月第一天2024-04-01DATE_TRUNC(week, CURRENT_DATE)→ 当前周周一ISO标准周一为每周第一天DATE_TRUNC(quarter, CURRENT_DATE)→ 当前季度第一天2024-04-01。但陷阱在时区。某跨境电商用DATE_TRUNC(day, event_time)统计日活发现周末DAU暴跌——因为event_time存的是UTC而业务要的是美国西海岸时间UTC-7。正确写法-- UTC时间转PST再截断 DATE_TRUNC(day, event_time AT TIME ZONE UTC AT TIME ZONE America/Los_Angeles)INTERVAL用法更易错。CURRENT_DATE - INTERVAL 1 month不是减30天而是减1个日历月。2024-03-31减1个月是2024-02-29不是2024-02-31不存在而2024-01-31减1个月是2023-12-31。所以“近30天”必须用CURRENT_DATE - INTERVAL 30 days不能用- INTERVAL 1 month。EXTRACT提取时间成分用于分桶分析EXTRACT(HOUR FROM event_time)→ 小时维度0-23看流量高峰EXTRACT(DOW FROM event_time)→ 星期几0Sunday看周末效应EXTRACT(YEAR FROM event_time)→ 年份做同比。我在某新闻App做点击率分析发现工作日CTR比周末高23%但用EXTRACT(DOW FROM event_time)细看周一到周四CTR稳定在4.2%周五飙升到5.8%下班摸鱼高峰周六回落到3.1%周日又升到4.5%晨间阅读。结论不能简单说“周末CTR低”而要运营“周五晚8点推送热点”这才是SQL给的颗粒度。3.7 窗口函数ROW_NUMBER/RANK/DENSE_RANK PARTITION BY——动态排名的引擎窗口函数是SQL的“高光时刻”它让一行数据看到全局。三大排名函数区别ROW_NUMBER()严格序号1,2,3,4并列也强行分先后RANK()跳过并列名次1,1,3,4两个第1下一个就是第3DENSE_RANK()不跳过1,1,2,3两个第1下一个就是第2。业务选择排“销售额TOP10商家”用ROW_NUMBER()确保只有10个排“用户LTV分位”用PERCENT_RANK()内置函数排“各城市人均消费”用RANK()因为并列第1的城市第2名理应是第3名跳过。实战案例计算“用户最近3次订单的平均间隔天数”。不用窗口函数得写三层嵌套子查询用窗口函数两行搞定SELECT user_id, AVG(days_between_orders) AS avg_interval_days FROM ( SELECT user_id, order_date, -- 计算与上一笔订单的间隔 DATEDIFF(day, LAG(order_date) OVER (PARTITION BY user_id ORDER BY order_date), order_date) AS days_between_orders, -- 只取最近3次按时间倒序 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn FROM orders ) t WHERE rn 3 GROUP BY user_id;注意LAG()默认取上一行但ORDER BY必须明确。如果ORDER BY order_date ASCLAG取的是更早的订单ORDER BY order_date DESCLAG取的是更晚的订单即“下一笔”。我第一次写反了算出的间隔全是负数debug半小时才发现ORDER BY方向错了。3.8 条件聚合COUNT/SUM FILTER/WHERE——一个GROUP BY干十件事PostgreSQL用FILTERMySQL/Redshift用CASE WHEN本质相同在聚合时加条件。这是替代多个子查询的利器。比如分析“各渠道获客成本CAC与用户质量7日留存率”传统写法要两个子查询-- 传统两个子查询效率低 SELECT c.channel, c.total_cost / u.user_count AS cac, u.retained_users / u.user_count AS retention_7d FROM ( SELECT channel, SUM(cost) AS total_cost FROM ad_spend GROUP BY channel ) c JOIN ( SELECT channel, COUNT(*) AS user_count, COUNT(CASE WHEN day7_active 1 THEN 1 END) AS retained_users FROM users GROUP BY channel ) u ON c.channel u.channel;用条件聚合一个GROUP BY搞定-- 高效单次扫描条件聚合 SELECT channel, SUM(cost) / NULLIF(COUNT(*), 0) AS cac, ROUND(100.0 * COUNT(CASE WHEN day7_active 1 THEN 1 END) / NULLIF(COUNT(*), 0), 2) AS retention_7d_pct FROM ( SELECT u.channel, COALESCE(s.cost, 0) AS cost, u.day7_active FROM users u LEFT JOIN ad_spend s ON u.channel s.channel AND u.acquisition_date s.date ) t GROUP BY channel;业务价值某在线教育公司要监控“试听课转化率”指标定义是分母所有试听用户user_type trial分子试听后7天内购买正价课的用户。用条件聚合SELECT course_category, COUNT(*) AS trial_users, COUNT(CASE WHEN purchase_after_trial 1 THEN 1 END) AS converted_users, ROUND(100.0 * COUNT(CASE WHEN purchase_after_trial 1 THEN 1 END) / NULLIF(COUNT(*), 0), 2) AS conversion_rate FROM users GROUP BY course_category;3.9 自连接模拟递归查找用户推荐关系链——没有CONNECT BY的破局之道MySQL 8.0、PostgreSQL支持递归CTE但多数企业数仓如Redshift、BigQuery旧版不支持。这时用自连接模拟“找推荐人”的3级关系-- 查找用户A的推荐人1级、推荐人的推荐人2级、再上一级3级 SELECT u1.user_id AS user_a, u1.referrer_id AS referrer_1, u2.referrer_id AS referrer_2, u3.referrer_id AS referrer_3 FROM users u1 LEFT JOIN users u2 ON u1.referrer_id u2.user_id LEFT JOIN users u3 ON u2.referrer_id u3.user_id WHERE u1.user_id A;但注意自连接层级越多性能越差。3级连接数据量是O(n³)。优化思路提前剪枝WHERE加条件限制范围如WHERE u1.signup_date 2024-01-01用窗口函数替代如果只要“推荐层级深度”用COUNT(*) OVER (PARTITION BY root_user ORDER BY level)物化中间表对高频查询的推荐链预计算并存入宽表。我在某社交App做裂变分析发现自连接查5级推荐链要12分钟。改用“迭代式物化”先算1级存表level1再用level1 JOIN users算2级存level2依此类推。首次耗时长但后续查询秒级响应且支持实时更新。3.10 去重与唯一性DISTINCT vs GROUP BY vs ROW_NUMBER()——数据质量的守门员DISTINCT是表级去重GROUP BY是分组后聚合ROW_NUMBER()是行级标记。选错数据就废了。场景统计“各城市独立用户数”用户表有重复记录因数据同步问题。SELECT COUNT(DISTINCT user_id) FROM users WHERE city Beijing→ 正确去重后再计数SELECT COUNT(*) FROM (SELECT DISTINCT user_id, city FROM users) t WHERE city Beijing→ 冗余DISTINCT已在子查询完成SELECT city, COUNT(*) FROM users GROUP BY city→ 错没去重重复user_id被多次计数。更隐蔽的坑SELECT DISTINCT user_id, MAX(order_amount)。DISTINCT作用于整行所以(user_id1, order_amount100)和(user_id1, order_amount200)被视为两行MAX()失效。正确写法SELECT user_id, MAX(order_amount) AS max_order FROM orders GROUP BY user_id;业务实战某电商做“新客首单分析”要求“首次下单的用户”。新手写SELECT DISTINCT user_id FROM orders ORDER BY order_date LIMIT 100结果取到的是任意100个用户不是最早下单的100个。正确解法SELECT user_id, MIN(order_date) AS first_order_date FROM orders GROUP BY user_id ORDER BY first_order_date LIMIT 100;4. 实操过程与核心环节实现从零搭建一个用户健康度仪表盘4.1 场景设定SaaS公司CEO要的3个核心指标假设你刚加入一家CRM SaaS公司CEO在站会上说“我要知道1昨天有多少客户在用我们的产品2这些客户里有多少是‘健康’的不会在30天内流失3健康客户中谁贡献了最多的收入”——这就是你要用这10条SQL构建的仪表盘。数据表结构usersuser_id, signup_date, plan_type (free/trial/premium), status (active/cancelled)eventsevent_id, user_id, event_type (login, export_data, create_contact), event_timesubscriptionssub_id, user_id, start_date, end_date, amountpaymentspay_id, sub_id, amount, pay_date。4.2 第一步定义“活跃用户”DAU——用日期截断去重DAU不是简单COUNT(*)要排除机器人、测试账号、已注销用户SELECT DATE_TRUNC(day, e.event_time) AS activity_date, COUNT(DISTINCT e.user_id) AS dau FROM events e JOIN users u ON e.user_id u.user_id WHERE e.event_type IN (login, export_data, create_contact) -- 真实业务行为 AND u.status active -- 排除已注销 AND u.plan_type ! free -- 免费用户不算商业目标 AND e.event_time CURRENT_DATE - INTERVAL 7 days -- 近7天 GROUP BY DATE_TRUNC(day, e.event_time) ORDER BY activity_date DESC;关键参数event_type列表必须由产研确认不能自己猜plan_type ! free是业务规则免费用户DAU不计入营收健康度。4.3 第二步定义“健康用户”——用窗口函数条件聚合健康用户 近7天有登录 近30天有导出数据 订阅未到期。用窗口函数标记行为再聚合SELECT user_id, -- 行为标记1有登录1有导出 MAX(CASE WHEN event_type login THEN 1 ELSE 0 END) AS has_login_7d, MAX(CASE WHEN event_type export_data THEN 1 ELSE 0 END) AS has_export_30d, -- 订阅状态1订阅有效 MAX(CASE WHEN s.end_date CURRENT_DATE THEN 1 ELSE 0 END) AS is_sub_active FROM events e JOIN users u ON e.user_id u.user_id LEFT JOIN subscriptions s ON u.user_id s.user_id WHERE e.event_time CURRENT_DATE - INTERVAL 30 days AND u.status active GROUP BY user_id HAVING MAX(CASE WHEN event_type login THEN 1 ELSE 0 END) 1 AND MAX(CASE WHEN event_type export_data THEN 1 ELSE 0 END) 1 AND MAX(CASE WHEN s.end_date CURRENT_DATE THEN 1 ELSE 0 END) 1;4.4 第三步计算健康用户收入贡献——用关联条件聚合把健康用户ID列表上一步结果JOIN到支付表算收入WITH healthy_users AS ( -- 上一步的健康用户ID列表 SELECT user_id FROM (...) -- 省略即4.3的查询 ), revenue_by_user AS ( SELECT h.user_id, SUM(p.amount) AS revenue_30d FROM healthy_users h JOIN subscriptions s ON h.user_id s.user_id JOIN payments p ON s.sub_id p.sub_id WHERE p.pay_date CURRENT_DATE - INTERVAL 30 days GROUP BY h.user_id ) SELECT user_id, revenue_30d, -- 收入分位用窗口函数 PERCENT_RANK() OVER (ORDER BY revenue_30d) AS revenue_percentile FROM revenue_by_user ORDER BY revenue_30d DESC LIMIT 10;4.5 最终仪表盘SQL整合三步一键输出WITH dau AS ( SELECT DATE_TRUNC(day, e.event_time) AS activity_date, COUNT(DISTINCT e.user_id) AS dau_count FROM events e JOIN users u ON e.user_id u.user_id WHERE e.event_type IN (login, export_data, create_contact) AND u.status active AND u.plan_type ! free AND e.event_time CURRENT_DATE - INTERVAL 1 day GROUP BY DATE_TRUNC(day, e.event_time) ), healthy_users AS ( SELECT e.user_id, MAX(CASE WHEN e.event_type login THEN 1 ELSE 0 END) AS has_login, MAX(CASE WHEN e.event_type export_data THEN 1 ELSE 0 END) AS has_export, MAX(CASE WHEN s.end_date CURRENT_DATE THEN 1 ELSE 0 END) AS is_sub_active FROM events e JOIN users u ON e.user_id u.user_id LEFT JOIN subscriptions s ON u.user_id s.user_id WHERE e.event_time CURRENT_DATE - INTERVAL 30 days AND u.status active GROUP BY e.user_id HAVING MAX(CASE WHEN e.event_type login THEN 1 ELSE 0 END) 1 AND MAX(CASE WHEN e.event_type export_data THEN 1 ELSE 0 END) 1 AND MAX(CASE WHEN s.end_date CURRENT_DATE THEN 1 ELSE 0 END) 1 ), revenue_top10 AS ( SELECT h.user_id, SUM(p.amount) AS revenue_30d, PERCENT_RANK() OVER (ORDER BY SUM(p.amount)) AS percentile FROM healthy_users h JOIN subscriptions s ON h.user_id s.user_id JOIN payments p ON s.sub_id p.sub_id WHERE p.pay_date CURRENT_DATE - INTERVAL 30 days GROUP BY h.user_id ORDER BY revenue_30d DESC LIMIT 10 ) SELECT (SELECT dau_count FROM dau) AS yesterday_dau, (SELECT COUNT(*) FROM healthy_users) AS healthy_user_count, (SELECT COUNT(*) FROM healthy_users) * 100.0 / NULLIF((SELECT dau_count FROM dau), 0) AS health_rate_pct, (SELECT STRING_AGG(CONCAT(user_id, :$, revenue_30d), ; ) FROM revenue_top10) AS top10_revenue_contributors;5. 常见问题与排查技巧实录那些让DBA想删库的瞬间5.1 性能雪崩为什么加了个ORDER BY查询从1秒变10分钟现象某次查用户订单加ORDER BY order_date DESC LIMIT 100响应时间从1.2秒暴涨到11分钟。排查步骤看执行计划EXPLAIN ANALYZE SELECT ...发现Sort节点占总耗时98%且Sort Method: external merge Disk: 24576kB——内存不够写磁盘了查索引SELECT * FROM pg_indexes WHERE tablename orders发现只有(user_id)索引没有(order_date)建复合索引CREATE INDEX idx_orders_user_date ON orders(user_id, order_date DESC)验证执行时间降至0.8秒。根

相关新闻

最新新闻

日新闻

周新闻

月新闻