1. 内容整体设计与思路拆解
1.1 为什么SMP需要一套脚本语言
做GBISP(通用业务信息平台)做到第四期,我越来越觉得SMP(软件制作平台)这套内置脚本语言才是整个平台的灵魂。GBISP负责把订单、客户、库存、审批这些业务数据统一管起来,但数据放在那里不动,它只是记录。真正让业务跑起来的,是SMP里那些用脚本写出来的逻辑:订单超时自动提醒、库存不足自动锁单、对账单按客户分组汇总、报表按部门权限自动裁剪数据……这些动作如果用传统开发方式做,每来一个新需求就要改代码、发版、重启,业务等不起。SMP的价值,就是让开发者(甚至懂一点逻辑的业务人员)在界面上直接写脚本、绑事件、跑流程,改完立即生效,不用动平台本身。
到了第七十六篇“语言基础知识”,我们已经从变量、类型、分支循环、函数定义一路讲到了模块化组织和异常兜底。今天这篇聊的是数据集操作——也就是SMP脚本里对二维表数据(行和列)做筛选、排序、分组、统计、关联和分页的那套方法集。为什么把它单独拎出来当一篇“基础知识”?因为GBISP里百分之八十的业务场景,本质上都是“从一堆数据里算出想要的那一小撮”。搞懂数据集操作,等于拿到了整个平台最值钱的那把钥匙。
1.2 这篇适合谁读
如果你已经在用SMP搭过几个简单的信息管理页面,对表单、列表、详情页这些基础构件有概念,但遇到复杂一点的统计需求就只能在脚本里写一堆for循环硬算,那这篇就是写给你的。它不需要你有计算机科班背景,但最好已经读过这个系列前面几篇关于变量、条件判断、循环和函数的内容——因为数据集操作底层还是那些东西,只是换成了一批更顺手的接口。
我接手过一个典型的场景:某公司内部的服务工单系统,GBISP里数据表每天新增几千条工单记录,运营要看“各小组本周平均响应时长、超时率、按优先级分布”。最开始实现方式是写脚本全表遍历,一边循环一边if判断分组,再逐个累加和计数。数据量小时没问题,可等工单一多,页面打开要等十来秒,运营直接投诉。后来我把那段逻辑全部换成SMP的数据集操作方法集,脚本行数缩了一半,执行时间缩到原来的十分之一。这种体验,才是SMP该有的样子。
2. 数据集的基本概念与核心操作定位
2.1 数据集在SMP里到底是什么
在SMP脚本里,数据集(DataFrame)就像一张Excel表格的内存副本:有列名,有多行数据,每一行是一个记录对象。它可以从GBISP的实体表查询出来,也可以把表单提交的参数组装成数据集,还能由另一个数据集经过操作生成新数据集——这正好对应SQL的“查询结果也是表”的思路。
我把数据集操作分成三类:
- 整表变换:筛选、排序、去重、截取前N条、分页
- 行列计算:新增计算列、修改列值、删除列、重命名字段
- 多表组合:关联(类似LEFT JOIN)、纵向合并(类似UNION)、分组聚合
这些操作不带任何界面元素,纯粹面向数据计算。好处是页面只负责展示最终结果,中间有多少次筛选和计算,全部在脚本里完成,页面不需要记任何中间状态,刷新即重算,逻辑始终干净。
2.2 为什么强调“链式调用”
SMP脚本里的数据集操作基本都是“每步都返回新数据集”的写法,这是刻意的设计。比如:
var ds = DB.query("select * from order_info where status != 'CANCELLED'"); var result = ds .filter(function(row) { return row.amount >= 1000; }) .sortBy("order_time", false) .limit(50) .calcFields({ "totalWithTax": function(row) { return row.amount * 1.13; } }) .page(1, 20);每一步都不改动原数据,而是生成一个新数据集给下一步。写起来像一个流水线,读起来也像——从查询结果开始,先过滤掉小额订单,再按时排序,截前50条,然后加一列含税金额,最后取第一页20条。想在哪一步加一个调试输出,就在那个位置插入一行打印,不会破坏前后逻辑。
这种链式风格还顺带解决了一个大坑:原数据复用。如果不小心在某个环节把原数据集改了,后面想再取原始数据就得重新查库。SMP强制每一步生成新数据集,则可以放心地让同一个原始数据集被多条计算链路同时使用——比如一份订单数据,一条链路统计月度趋势,另一条链路做客户排行榜,互不干扰。
2.3 和SQL的关系:SMP脚本该不该写SQL
经常有人问我:“GBISP底层就是关系型数据库,我直接用SQL不是更熟吗?为什么还要学封装好的数据集方法?”
我的回答是:SQL当然可以写,而且有些复杂查询用SQL一步到位效率最高。SMP脚本的DB.query()方法也支持直接传完整SQL语句,这在需要复杂多表关联和数据库端聚合时非常省事。
但问题在于:GBISP里很多数据不是直接从数据库来的。比如页面表单收集到的多条明细、流程引擎传过来的审批意见列表、外部接口返回的JSON数组……这些数据在内存里是结构化的行集,但根本不在数据库里。你没法对它们写SQL,只能靠SMP数据集方法在内存中做变换。
另一个场景是条件动态变化。写死SQL很容易,难的是SQL片段要跟着前端选择的条件动态拼。早期我见过有人用字符串拼接SQL,一旦用户输入带引号或百分号就会出各种奇怪问题——要么查询结果不对,要么整个页面报错。SMP数据集方法则完全在内存里操作,没有SQL注入这层风险,条件分支也很自然。
所以我的经验是:跨表复杂聚合、大表按索引快速过滤,用SQL;内存里的多步加工、条件动态组合、步骤可调试、逻辑需要复用,用SMP数据集方法。两者配合,SMP脚本写起来才顺手。
3. 数据集核心方法详解与实操要点
3.1 筛选:filter的用法与常见误解
筛选是数据集操作里最常用的动作,作用是从一个数据集中挑出满足条件的行,返回新数据集。
SMP脚本里filter的基本写法有两种。第一种是传入条件对象:
var urgentOrders = allOrders.filter({ "priority": "HIGH", "status": "OPEN" });上面这个写法是“同时满足”语义,相当于SQL里的AND。第二种是传入函数,函数接收每一行记录,返回true表示保留:
var bigOrders = allOrders.filter(function(row) { return row.amount > 5000 && row.customerType != "INTERNAL"; });函数写法灵活,可以做范围判断、取反、字符串匹配,甚至调用其他函数做复杂计算。我在实际项目中经常遇到这样的需求:筛选订单金额大于某个变量值、且客户名称匹配某个关键字。用条件对象写不出来,用函数一行搞定。
写filter要注意三个坑:
第一,筛选条件里的字段名必须是数据集里真实存在的列名,大小写也要一致。SMP对列名是大小写敏感的,比如列名定义成了order_time,你过滤器里写orderTime直接取不到值,返回undefined,条件判定自然出错。
第二,null值处理要主动。业务数据经常有空值,尤其是金额字段。如果你写成:
return row.amount > 1000;那么amount为null的行,null > 1000的结果是false(在SMP里比较运算对null返回false),会被正常过滤掉,一般符合预期。但如果你的需求是“金额为空也要保留”,就要显式写:
return row.amount == null || row.amount > 1000;否则很容易把本该保留的行丢掉,而且排查起来还不能一眼看到——因为页面上没数据时你根本不知道是查询条件本身排除了它,还是根本没查出来。
第三,filter不会改变原始数据集,这一点我前面强调过,但还是要再说一遍。有人习惯在filter之后继续用原数据集的变量名:
var ds = DB.query("select * from orders"); ds = ds.filter(function(row) { return row.amount > 0; });这样写没问题,因为重新赋给了同一个变量。但如果你在多个地方引用了最开始的ds,就要小心赋值覆盖后其他地方的数据变了。我更倾向于用不同变量名接收每次操作的结果,比如sourceDs、afterFilter、afterSort,清晰得多。
3.2 排序与截断:sortBy、limit、page的使用和性能思考
排序列是用户最常感知的操作。SMP里sortBy接收两个参数:列名和是否降序排列。比如:
var sorted = ds.sortBy("createdAt", false); // 按创建时间倒序多个列排序需要连续调用,或者查一下SMP版本是否支持多列参数写法。我用的版本支持传入数组:
var sorted = ds.sortBy(["status", "createdAt"], [true, false]); // 先按status升序,再按createdAt降序排序性能在这里要特别提醒一下:sortBy是在内存里全量排序的。数据量大(比如超过十万行)时,全量排序会有可见的耗时。如果你最终只需要前20条,先排序再截断是必要的——因为不排序没法保证“取最新20条”。但如果你不需要排序,就别加,省一次全量排序的时间。如果数据量稳定在几十万行以上,建议在数据库查询阶段就用ORDER BY把排序做好,不要拉全量到内存再排。
limit和page都是截断操作。limit(N)取前N行,page(n, size)跳过前(n-1)乘size行后取size行,用来做分页。我一般习惯先filter再sort最后再limit或page,顺序不要乱。如果把limit写在filter前面,那过滤后的数据已经不是完整的全集,后面的排序和分页都可能得到错误结果——尤其是分页,每页数据都是从截断后的子集里再切,会造成不同页数据重复或遗漏。
有个使用技巧:如果数据集要同时输出到“全部数据下载”和“当前页表格”,建议从同一个排序好的数据集出发,一个分支用page生成表格数据,另一个分支直接用全量做导出。不要为导出重新排序,因为两次排序的稳定性可能不同,用户下载后发现顺序和页面上不一样,又得来问。
3.3 字段加工:calcFields、map与数据清洗
calcFields是我用得最多的方法。它给数据集的每一行追加一个或多个新列,列的值由函数计算得出。语法:
var dsWithTax = orders.calcFields({ "taxAmount": function(row) { return round2(row.amount * 0.13); }, "totalAmount": function(row) { return round2(row.amount * 1.13); } });calcFields不修改原始行对象,只是在返回的新数据集里增加列,需要原始数据时仍然可用原变量。这是SMP很贴心的设计——数据加工流水线上每一步都能追溯。
map方法比calcFields更底层,它可以对每一行做任意变换,包括改原有字段、删掉某些字段甚至改变行的结构。比如把一行订单记录里的客户名和客户编号拼接成一个新的“客户展示名”字段,同时把敏感字段剔除掉,用map是正解。
实际项目里,我经常在返回前端展示前用map做数据脱敏和字段裁剪:订单详情里有客户手机号,但列表页只需要显示后四位;内部备注字段不能让普通用户看到。如果用SQL直接查出来原样返回,前端就得兜底处理,很容易漏。SMP脚本里把这些逻辑统一做掉,接口返回什么前端就显示什么,权限边界清清楚楚。
字段加工碰到的坑主要是类型。SMP的列值有Number、String、Date、Boolean等类型,在做金额计算时一定要确认列类型是Number而不是String。很多数据表导入时把金额列存成了文本(比如带着货币符号或千分位逗号),这时候直接做加法就会出现类型报错,或者更隐蔽的字符串拼接:1000 + 200 = "1000200"。
处理办法是在算子前做一次类型清洗:
function toNum(v) { if (v == null) return 0; var s = String(v).replace(/[,¥\s]/g, ""); var n = parseFloat(s); return isNaN(n) ? 0 : n; }清洗完再参与计算,永远不要在业务逻辑里散落一堆parseFloat。同一份清洗函数放公共模块,所有脚本共用它,就不会再有人因为数据格式不同而算错金额了。
3.4 分组聚合:groupBy与aggregate的正确打开方式
做统计报表时groupBy和aggregate是核心。groupBy按一个或多个字段把数据集拆成若干组,aggregate针对每组做聚合计算或输出汇总行。
SMP里的常见用法:
var monthlyStat = orders .groupBy("month") .aggregate({ "orderCount": { "sum": 1 }, "totalAmount": { "sum": "amount" }, "avgAmount": { "avg": "amount" }, "maxAmount": { "max": "amount" } });groupBy参数还能传数组,实现多字段分组:
var cityCateStat = orders .groupBy(["city", "category"]) .aggregate({ "orderCount": { "sum": 1 } });这个逻辑和SQL里的GROUP BY完全对应,但好处是不用拼SQL,分组字段可以完全动态。前端下拉选了“按城市分组”,脚本里就传groupBy("city");选了“按城市+品类”,就传数组。SMP脚本不用像SQL那样重建语句,只改一个参数即可。
aggregate的聚合器常用sum、avg、max、min、count,个别版本还有stddev、median这类统计函数。需要注意:对空值列做avg时,SMP默认忽略null行但不忽略0行。如果业务上0和null含义不同,预期“不算0的均值”,就要在分组前先把0值替换成null,或者反过来把null替换成0——按业务语义选一个,别让统计结果悄悄跑偏。
分组聚合后如果想按聚合结果排序,我给一个通用模式:
var sortedStat = monthlyStat.sortBy("totalAmount", false);因为aggregate返回的也是数据集,可以接着用前面那些方法。把多个操作串联起来,一份脚本就能完成“筛选-分组-聚合-排序-取前N”的完整报表链路。
3.5 多表关联与纵向合并:join和union的使用场景
GBISP里实体表之间经常有关系,比如订单表关联客户表、工单表关联处理人表。SMP脚本可以在内存里做join,用法:
var ordersWithCustomer = orders.join( customers, "customerId", // 左表关联键 "id", // 右表关联键 "LEFT", // 关联类型 LEFT / INNER / RIGHT { customerName: "name", customerLevel: "level" } );第四个参数指定从右表取哪些字段到结果集里,并可以重命名,避免两个表都有“name”字段时冲突。
join适合小数据量关联(几千到几万行),如果两个表都是大表,还是优先在数据库里用SQL join做完再拉取。内存join的特长场景是:数据来自不同渠道——一部分来自数据库,一部分来自接口返回的JSON数组,还有一部分是流程引擎传过来的临时数据,它们不在同一个数据库里,只能用脚本来拼。
union就是纵向合并两个列结构相同(或兼容)的数据集。经常用在“本月和上月数据合并对比”、“线上线下渠道合并统计”这些场景。如果两边的列名不完全一致,union之前先用map或calcFields把列名对齐,否则合并后会出现大量null列。
3.6 分页与大数据量场景的取舍策略
前面对分页做了基本介绍,但大数据量场景还是要专门说一说,因为这直接决定页面会不会卡死。
SMP的page(n, size)是在内存数据集上切片的。如果底层SQL直接把全量数据拉出来了,那数据量再大也是先拉全量、内存占用高、网络传输慢。正确做法是能用数据库分页就在查询阶段分页:
var pageData = DB.query( "select * from order_info order by created_at desc limit ? offset ?", [pageSize, (pageNum - 1) * pageSize] );但如果后续还需要做过滤、统计,或者原始数据来源比较复杂(比如多个数据集join之后的时候),就不得不先拉全量再在内存里处理。那时候要控制数据规模在十万行以内,超过这个量级,建议考虑在GBISP里建中间表:定时任务把明细汇总成每日统计表,报表画面直接查统计表,而不是实时扫明细。
我个人踩过最大的坑是“为了一个统计数字把几百MB的明细拉进内存”。后来在两张大表上join再聚合,直接跑了十五秒超时。改成在数据库SQL里join和分组后,返回的数据集只有几十行,速度提升到毫秒级。数据集方法再方便,也替代不了数据库在海量数据聚合上的优势。该下沉到SQL的计算,别犹豫,果断下沉。
4. 实操过程与核心环节实现:订单月度分析页面
4.1 业务目标与准备数据
纸上谈兵到这里,我用一个完整的实操案例串一遍今天讲的内容。假设GBISP里有两张实体表:
- order_info:字段有order_id、customer_id、amount、order_time、status、city
- customer_info:字段有id、name、level(VIP/NORMAL)、industry
业务需求是在GBISP里做一个“月度订单分析页”,展示:
- 本月总订单数、总金额
- 按城市分组的订单数、金额
- 按客户等级分组的金额占比
- 本月VIP客户订单TOP10列表
- 所有数据能按时间范围筛选
4.2 脚本实现与逐步说明
我习惯把查询参数放在最前面,方便后续调整:
var startDate = "2025-11-01"; var endDate = "2025-11-30"; // 第一步:从库里查订单,连客户表的客户名和等级 var ds = DB.query( "select o.order_id, o.customer_id, o.amount, o.order_time, o.city, o.status, " + "c.name as customer_name, c.level as customer_level " + "from order_info o left join customer_info c on o.customer_id = c.id " + "where o.order_time >= ? and o.order_time < ? and o.status != 'CANCELLED'", [startDate, endDate + " 23:59:59"] ); // 第二步:过滤掉异常数据(金额为负或为空) var clean = ds.filter(function(row) { return row.amount != null && row.amount > 0; }); // 第三步:基础统计 var totalCount = clean.count(); var totalAmount = clean.aggregate({ "totalAmt": { "sum": "amount" } }).get(0).totalAmt; // 第四步:按城市分组 var byCity = clean .groupBy("city") .aggregate({ "orderCount": { "sum": 1 }, "cityAmount": { "sum": "amount" } }) .sortBy("cityAmount", false); // 第五步:按客户等级分组 var byLevel = clean .groupBy("customer_level") .aggregate({ "levelAmount": { "sum": "amount" } }) .sortBy("levelAmount", false); // 第六步:VIP客户订单TOP10(先按金额倒序,然后取前10) var vipTop10 = clean .filter(function(row) { return row.customer_level == "VIP"; }) .sortBy("amount", false) .limit(10) .calcFields({ "orderDate": function(row) { return fmtDate(row.order_time); } }); // 输出结果对象,供界面绑定 return { "totalCount": totalCount, "totalAmount": round2(totalAmount), "byCity": byCity.toList(), "byLevel": byLevel.toList(), "vipTop10": vipTop10.toList() };这段脚本集中展示了筛选、聚合、分组、排序、截断和字段加工的组合用法。逻辑从上往下读,每一步的输入输出很清晰。
4.3 页面绑定与运行效果
在SMP里新建一个分析页面,放几个统计卡片和两个表格组件,数据源分别绑定上面返回的byCity、byLevel和vipTop10。因为脚本返回的是纯数组,绑定非常简单,界面刷新时脚本自动重算,不需要额外写请求逻辑。
我本地用模拟数据测过:生成两万条订单、五百个客户,脚本从查询到输出结果,全程在一到两秒内完成,页面渲染也流畅。如果数据量再上一个数量级,就要考虑让SQL直接做分组聚合,脚本只做展示数据的组装。
4.4 监控与调试:每步都留一手
SMP脚本编辑器支持单步打印,我强烈建议在正式环境调试时,每操作一步就把当前数据集的count和toList打印出来看一眼:
console.log("after clean:", clean.count()); console.log("byCity:", byCity.toList());这样一旦结果跟预期不一致,立刻能定位到是哪一步出了问题。最怕的是几十行脚本写完,直接跑,结果不对,然后从头一行行猜。打印是穷人的调试器,也是最可靠的调试器。
5. 常见问题与排查技巧实录
5.1 字段名大小写与列名不存在问题
症状:filter或sortBy不生效,甚至直接报“column not found”。
排查步骤:先打印数据集的第一行,查看实际列名是什么:
var first = ds.get(0); console.log(Object.keys(first));然后对比你代码里写的字段名。SMP对大小写敏感,写order_time就不要写成OrderTime。列名不存在时,filter里row.xxx拿到的是undefined,比较运算结果全是false,安静地丢掉全部数据,最迷惑。加上一行打印能立刻看清。
另外要注意SQL查询里select子句写的别名就是最终列名。如果你写了“select o.amount as amt”,数据集这一列就叫amt,不叫amount。在脚本里继续写amount,下面所有计算都会出错。养成习惯:写完查询先打印一行列名清单。
5.2 聚合结果不对:是不是把null算进去了
场景:想统计每月的客户平均下单金额,但结果明显偏低。排查发现,有大量订单的金额是null,被当作0参与了平均计算。业务上“没填金额”和“金额为0”应该区别对待。
处理办法:分组聚合前先统一空值语义。如果业务认定null应该跳过,就先把null行过滤掉;如果认定应该当0参与统计,就不用管。关键是明确选择并写下来,别让维护的人猜。
另外,aggregate的sum聚合器有没有忽略null,不同SMP版本行为不完全一致。稳妥做法是自己先做一次清洗,把null转成0或者过滤掉,再交给聚合器。自己动手,永远比依赖隐式行为稳妥。
5.3 join关联出来的数据重复或变多
症状:join之后数据集行数比左表还多。这是因为右表存在多个匹配行(比如一个客户有多个联系方式记录),一对多join天然产生重复。
排查思路:先检查右表的关联键是否有唯一性约束。如果右表确实会重复,按业务决定取哪一条,通常是用右表过滤只保留每个关联键的最新一条:
var dedupedCustomers = customers .sortBy("updatedAt", false) .groupBy("id") .aggregate({ "keepRow": { "first": "self" } });或者更简单,在SQL查询里用ROW_NUMBER按关联键排序去重后再join。千万别以为join做出来行数不对就是平台有bug,绝大多数时候是数据本身有多对多关系。
还有一种情况是关联键本身类型不一致。订单表里customer_id是Number,客户表里id是String,即使值看起来一样("1001"和1001),join也匹配不上。处理方式是统一类型:
orders.calcFields({ "customerIdStr": function(r) { return String(r.customer_id); } }) .join(customers, "customerIdStr", "id", "LEFT", {...});这种类型不一致的问题在数据导入类系统里非常常见,join前检查两边字段类型的习惯要培养起来。
5.4 分页数据错乱:没有先排序就分页
这是新手高频问题。数据集本身的存储顺序是查询结果顺序或数据插入顺序,分页只是按这个顺序切片。如果不做排序,两次查询之间底层数据如果有新增或修改,同一页可能看到不同的数据,用户会以为系统“串行”了。
我的习惯是任何分页查询之前先做一次稳定排序,哪怕只是按主键升序。这样分页的边界是可预期的,也方便排查。主键是自增数字时,按它排序通常不会错。
顺便提一下:两个不同时间执行的分页请求到底会不会串数据,取决于底层查询是否每次都重新执行。SMP脚本默认每次打开页面都重新跑一遍,所以数据会波动是正常的。要固定的话,可以把排序键做成可配置的,让用户自己决定按什么排序,而不是用默认顺序糊弄过去。
5.5 内存溢出或脚本执行超时
症状:数据集操作在数据量大时一直转圈,甚至报超时。
处理步骤:
- 先确认数据量到底多大,打印ds.count()。
- 再确认哪些操作耗内存,最常见的是全量sortBy和join。排序尽量下沉到SQL的ORDER BY;join尽量两边都小。
- 如果确实需要内存处理大数据,考虑分批:比如按月分批处理,再把结果合并成新数据集。
我在某次做历史数据迁移脚本时,把三年两百万条明细一次性拉进内存,页面直接崩溃。改成在SQL里按月分组查询,只把十二个月的汇总结果拉回来,整个批量任务只用了不到一分钟。
还有一个容易忽略的点:calcFields和map里如果调用了外部接口或耗时函数,每一行都执行一遍,行数多时时间呈线性放大。这时候就应该在进入数据集操作前先把外部数据准备好,而不是在每一行里去查。把“对每行的计算”尽量限制在纯内存运算,是保证脚本性能的基本原则。
5.6 快速自查表
我把这次涉及的问题整理成一张速查表,贴在脚本工程注释里,团队协作时能省很多沟通:
| 现象 | 大概率原因 | 处理建议 |
|---|---|---|
| filter结果为空/数据变少 | 列名大小写不对或字段类型不匹配 | 打印首行列名,核对字段名和类型 |
| sortBy不生效 | 排序列名拼写错误,或字段值是字符串而非数字 | 先打印列值,确认类型后再排序 |
| 聚合值偏低 | null被当作0参与运算 | 聚合前统一空值语义,过滤或转0 |
| join后行数增多 | 右表关联键不唯一 | 先对右表去重,再执行join |
| 分页数据重复/遗漏 | 分页前未排序,或limit写在filter之前 | 固定排序键,先filter后limit/page |
| 脚本超时/内存溢出 | 全量拉取+内存排序join | 尽量下沉SQL,大表走汇总中间表 |
| 类型报错 | 金额存成字符串参与运算 | 公共清洗函数统一转Number |
6. 对SMP语言学习的整体体会
6.1 从“学语法”到“用语法”的转折点
这一篇“数据集操作”之所以在整个基础系列里位置特殊,是因为它第一次让脚本从“写给自己看的逻辑”变成“解决业务问题的工具”。前面学变量是学字,学循环是学词,学函数是学句,到数据集操作才真正连成段落,能完整表达一段业务语义。
我自己带过几个刚接触SMP的同事,他们前几篇学得都挺好,一到写真实需求就卡住,原因几乎都是:不会把“这个页面要展示什么数据”“需要哪些中间结果”翻译成数据集操作链。所以我建议在学习这个系列时,尽量把每个方法都对应到一个具体业务场景去记:filter对应“只要未取消的订单”,sortBy对应“按时间从新到旧”,groupBy对应“按城市汇总”,limit对应“榜单取前十个”。
6.2 SMP方法是手段,数据思维才是根本
我在前面的章节里反复强调“先想清楚再动手”,做数据集操作尤其如此。接到一个统计需求,先花两分钟在脑子里把数据流画出来:原始数据在哪、第一步怎么过滤、第二步怎么分组、第三步要不要排序截断、最终输出什么结构。这个流程想明白,写代码就是按图索骥。
这里说的“画出来”是指在草稿纸上写清单,不是在系统里画流程图。太多人一上来就写脚本,写了一半发现分组维度不对,又回头改,来回折腾。先想清楚数据流,比快捷键技巧、语法糖重要一百倍。
6.3 最后一个个人小建议
我在SMP脚本里所有数据集操作,都会默认加一行开头注释,写明这段脚本处理的是哪张业务表、最终输出给哪个页面。因为这种脚本的生命周期往往比写它的人在公司的时间还长。一段时间后原开发走了,接手的人翻开脚本,看到注释和清晰的操作链,几十秒就能明白逻辑。如果当时图快没写注释,后来人就得靠打印和数据比对反推,代价远大于当初多花的一分钟。
数据集操作这部分内容,本身不复杂,但它是业务逻辑从“看得见”到“算得出”的必经之路。下一次做报表、做分析页、做数据看板时,不妨先把SQL放下,试着用这些方法把数据流搭出来,你会发现逻辑清晰了很多,脚本也好维护得多。