每次搭建数据处理流程,我都会先看两样东西:数据量多大,脏数据有多少。因为这两点直接决定流程跑得快不快、结果准不准。在Dataiku DSS的Visual Recipe里,有三个选项几乎每个配方都会出现——预过滤(Pre-filter)、后过滤(Post-filter)和计算列(Computed Column)。很多人用了很久,还是分不清它们到底在什么时机执行、彼此之间是什么关系,遇到"过滤条件写哪里都一样"的误解。这篇文章就把这三个通用步骤彻底拆开,讲清楚各自的定位、配置方法、适用场景,以及串起来用的时候会踩到哪些坑。
如果你刚开始学Dataiku DSS,或者已经用了一段时间但对这几个步骤"只知其然、不知其所以然",这篇文章都适合你。我会尽量用实际项目里会遇到的例子说话,不堆术语,但该讲的原理一点也不会少。
1. 为什么Visual Recipe里总能看到这三样东西:三个通用步骤的定位与分工
1.1 DSS把流程拆成"处理块",而不是一条全局脚本
用过Dataiku DSS的人都知道,它的核心操作单元叫Recipe(配方),常见的有Prepare、Group、Join、Window、Pivot、Stack等。每个Recipe的界面都是同一个框架:左侧是输入数据集,右侧是输出数据集,中间是脚本区。脚本区里列着一排处理步骤,每个步骤是一个小方块,可以拖动、折叠、单独配置。
很多人第一次用的时候会感到奇怪:既然有Filter步骤可以用来过滤行,有Formula步骤可以用来计算列,为什么每个Recipe顶部还要单独留出一块"Pre-filter"、一块"Post-filter"?其实答案很简单——DSS的设计者希望把一个数据处理流程拆成三个逻辑层面:进入处理流程之前的入口控制、处理过程中的业务计算、输出之前的最终筛选。这三个层面在绝大多数数据处理任务里都存在,所以DSS干脆把它们做成了所有Visual Recipe的通用步骤。
这组通用步骤的完整关系可以这样理解:原始数据进来之后,先经过预过滤(Pre-filter),把明显不该参与计算的数据挡在门外;然后进入配方主体,也就是真正的处理逻辑,可能在中间任意位置插入计算列(Computed Column),对数据进行加工;等所有处理都完成之后,再通过后过滤(Post-filter)做最终收口,把不符合业务要求的结果剔除;最后才写出到输出数据集。
1.2 三个通用步骤在Recipe内的实际位置与执行顺序
这里有一个非常容易忽略的关键点:执行顺序。预过滤在配方主体开始之前执行,后过滤在配方主体全部结束之后执行,而计算列可以出现在配方主体内的任意位置。这个顺序不是随便定的,它直接影响你能在哪个阶段拿到什么粒度的数据。
举一个具体例子:在Group Recipe里面,如果你想先过滤掉某些明细行再做分组,那条件必须写在Pre-filter,因为分组操作在配方主体阶段才发生;如果你想在分组完成之后只保留满足某些条件的组,那条件必须写在Post-filter,因为组级别的新指标是在分组阶段才计算出来的。这和SQL里where和having的区别非常像,实际上DSS的预过滤/后过滤设计,很大程度上就是为了对应这种"处理前筛选"和"处理后筛选"的语义差别。
1.3 第一眼怎么区分"预过滤"和"后过滤"
看一眼界面位置就能区分:在Recipe顶部、输入数据集下方的区域是Pre-filter;在底部、输出数据集上方的区域是Post-filter。DSS的界面还会用分隔线把它们跟配方主体隔开,视觉上很清楚。
但在实际使用中,判断该用哪一个,靠的不是位置,而是"这个条件依赖哪些字段"。如果条件只依赖原始输入字段,比如订单日期、门店编码、是否为空,那预过滤和后过滤的最终结果看似一样,但执行效率完全不同。如果条件依赖的是计算之后才有的字段,比如聚合后的订单总数、净额字段、订单区间标签,那几乎只能用后过滤。
2. 预过滤(Pre-filter):在计算开始前把脏数据挡在门外
2.1 预过滤和"滤行"步骤的本质区别:时机、性能、可下推
预过滤最常见的误解是"它不就是个Filter嘛"。确实,功能上有重叠,但两者的执行时机和性能表现差别很大。
Filter步骤是配方主体里的一个普通步骤,它运行的时候,数据已经完整进入Recipe的计算流程了。也就是说,即使你最终只保留1万行,前面几百万行的读取、加载、解析工作都已经发生了。而Pre-filter是DSS在进入配方主体之前,先在输入数据集层面做的一层拦截,条件会尽量下推到底层执行引擎。如果输入数据接的是数据库,预过滤条件会被翻译成SQL的where条件,直接在数据库里把数据过滤掉;如果用的是Spark引擎,同样会尽量做下推,减少进入计算框架的数据量。
这就好比你进工厂之前先在安检口把不合格的原料挑出来,而不是把所有原料都搬到车间里再挑。对大表来说,预过滤的性能优势是数量级的差异。我曾经处理过一张几千万行的订单明细表,如果不在预过滤阶段把日期范围钉死,后面任何一步操作都要带着历史年份的数据一起跑,一次Build可能就从几十秒膨胀到十几分钟。
2.2 典型条件表达式怎么写
Pre-filter的默认模式是条件表达式,写法上有点像SQL和Excel公式的混合体。官方把它叫做Filter Expression,支持比较运算符、逻辑运算符、缺失值判断、正则匹配等。
几个常用的写法:
order_date >= '2024-01-01' AND order_date < '2025-01-01'store_id <> 'TEST' AND order_id IS NOT MISSINGupper(city_name) IN ('上海', '北京', '广州')regexp_match(phone_no, '^1[3-9][0-9]{9}$')第一段是日期范围,第二段是剔除测试门店和空订单,第三段是城市白名单,第四段是手机号格式校验。这些条件可以组合使用,用AND、OR、NOT连接。
需要注意一个细节:DSS的表达式引擎对字符串和日期比较的处理跟数据库不完全一样。日期字段最好不要跟字符串硬比,尽量用DSS提供的日期函数先把格式统一,比如parse_date(order_date, 'yyyy-MM-dd') >= parse_date('2024-01-01', 'yyyy-MM-dd')。否则很容易出现"条件看着正确、跑出来行数不对"的诡异问题。
2.3 数据量大的输入表,预过滤为什么是首选
如果你的流程要跑在每天几千万行甚至上亿行数据上,预过滤几乎是必须养成的习惯。原因不只是性能,还包括下游链路的稳定性。
假设你有一个每日全量快照表,里面包含过去三年的数据,但实际业务只需要分析最近30天。如果你不做预过滤,那么每个下游Recipe每次运行都会扫描三年数据,除非底层有分区裁剪,否则这些数据会一路跟着你的流程走到最后。等它经历了多次Join、多次聚合,占用的计算资源和存储空间都被放大了很多倍。
如果把过滤条件放在预过滤里,尤其是在连接数据库或Hive数仓的场景下,条件能直接下推到源端,扫描量瞬间降下来。哪怕只是省掉一年历史数据,执行时间、资源占用、对上游的查询压力都能明显改善。我接数据分析任务的时候,拿到需求第一件事就是确认"你要的范围是什么",然后立刻在预过滤里把范围写死,再考虑后面的加工逻辑。
2.4 一个小补充:行采样、去重,这些都是预过滤的隐藏功能
预过滤区域除了条件表达式,还提供一些其他选项,核心是行采样(Sampling)和行数限制。比如你可以设置只取前1万行、按比例随机采样、或者基于某个条件做等距抽样。这些功能在做算法探索、模型初筛时特别有用,因为不需要把全量数据先算一遍。
不过要提醒一句:行采样只适合探索阶段,不适合生产链路。一旦流程上线,所有下游结果都会依赖这个采样结果,如果采样方式不当,得到的分析结论可能有偏。我平时只在做特征工程试跑时用采样,正式流程里从来不用。
另外一个容易忽略的点:预过滤的"去重"并不是它的默认能力。DSS本身有去重步骤(比如Unique Rows),但如果你想在预过滤阶段直接去掉重复记录,通常需要通过表达式自己实现。如果你的输入表存在业务上的重复键,最稳妥的做法是在配方主体的开头放一个去重步骤,而不是依赖预过滤做复杂逻辑。
3. 后过滤(Post-filter):真正收口的过滤器
3.1 后过滤出现在哪个环节,为什么叫"收口"
后过滤的位置在配方主体的末尾、输出数据集之前。它和预过滤最容易混淆的地方,就是"过滤"两个字。但实际上,后过滤处理的是配方主体完成之后的完整结果。所有计算列在这一刻已经生成,所有聚合指标已经算完,所有Join已经完成,后过滤在这时候做的是最终的业务规则筛选。
打个比方,预过滤是入口安检,计算列是车间加工,后过滤就是出厂质检。质检不合格的产品不会出厂。逻辑上,一个数据处理流程可以没有后过滤,但一旦有了,它代表的就是"最终交付标准"。
3.2 典型场景一:聚合结果筛选(having思维)
后过滤最常见的场景是在聚合之后筛选组。比如你按客户ID做分组,统计每个客户的订单数、累计金额、最近一次下单日期,然后只保留订单数大于等于5、累计金额超过5000的客户。这个条件只能在分组之后才能判断,所以必须放在后过滤。
用Group Recipe做这个需求的时候,Pre-filter可以放"订单日期在2024年""订单状态不是已取消"等明细级条件;配方主体做分组聚合;后过滤放"订单数>=5 AND 累计金额>=5000"。这套写法逻辑上是完整的,也跟SQL的where和having语义完全对应。如果你把"订单数>=5"写进预过滤,系统会直接报错或者永远返回0行,因为这个字段在预过滤阶段根本不存在。
3.3 典型场景二:计算完衍生指标后再做业务规则过滤
另一个常见场景是:计算列已经算出了净额、税率、价值等级等衍生指标,但业务上只允许某些等级、某些金额范围的数据流向下游。这时候后过滤就是天然的位置。
举个例子,成交金额字段是单价乘以数量,再乘以折扣系数。如果折扣系数异常高导致成交金额变成负数,那这行数据大概率是录入错误。你可以用计算列算出net_amount,然后后过滤写net_amount > 0。这个条件既简单又直观,放在后过滤里一点也不费劲。
还有一种做法是把后过滤条件跟计算列联动,比如order_level = "高价值",这样下游看到的就只有高价值订单。注意,这种过滤会让低价值订单的数据在下游彻底消失。如果你还需要保留低价值订单做备用分析,就不要在这里过滤,而是用多个输出分支去解决。
3.4 后过滤容易被误解的三件事
第一件,后过滤不能把前面已经计算过的内容"修正"回来。如果某个计算列在算的过程中因为空值产生了错误结果,后过滤只能把它挡住,不能把值改对。要改值,还是得回到计算列本身去处理空值逻辑。
第二件,后过滤的过滤条件在输出前统一生效,不管你的Recipe有多少个输出数据集,它们都会受到同一后过滤的影响。有人以为"每个输出各配一个后过滤",其实不是。如果你需要对不同输出用不同规则,建议在配方主体里分裂(Split)或用多个输出前各自加独立筛选步骤。
第三件,在Join Recipe里,后过滤作用于Join完成后的整行结果。一个很典型的错误是:用户想模拟inner join的效果,于是用left join之后在后过滤里写right_table_key IS NOT MISSING。结果虽然把右表为空的行剔除了,但join保留方式依然是left,语义上容易误导后续维护者,而且一旦别人把后过滤条件删掉,整个结果就悄悄变回left join。更清晰的做法是直接修改Join步骤的保留策略为inner。
4. 计算列(Computed Column):把业务公式变成可复用的数据列
4.1 计算列的本质:列式表达式引擎,不单是Excel公式
计算列是Visual Recipe里最常用的通用步骤之一,它做的事情很纯粹:根据一个表达式,为数据表新增一列或更新某一列。但它的能力边界远大于Excel公式。
在DSS中,计算列背后是一套表达式引擎,支持数值运算、字符串处理、正则提取、日期时间计算、条件分支、JSON解析、数组处理等。更关键的是,在部分Recipe里,计算列还能访问到聚合上下文,比如在Group Recipe的分组后阶段,你可以用sum(amount)、avg(rate)这类聚合函数。这让计算列不只是"算字段",还能参与业务口径的完整落地。
不过要区分一个概念:计算列不等于"将所有业务逻辑都塞进一个表达式"。我见过很多新手把几十个字段的复杂判断全部堆到一个计算列里,结果表达式长达几百行,出错了根本不知道是哪个分支的问题。好的做法是把大逻辑拆成多个计算列,每一列只负责一件事,比如先算毛额、再算折扣、再算净额、再算等级,这样每一步都能单独验证。
4.2 三种表达式方言:DSS Expression、Python、SQL
DSS的计算列允许你选择不同的表达式写法。默认最常用的是DSS Expression,这是一套跨引擎的表达式语言,语法上靠近Excel和SQL,并且在很多引擎上可以下推执行。其次是Python表达式,灵活性强,但代价是性能通常较差,因为Python逻辑往往无法下推到底层数据库或Spark集群,只能逐行执行。第三是SQL表达式,它跟你底层引擎的SQL方言一致,在连接数据库时性能最好,但可移植性差。
我的建议很简单:默认用DSS Expression。它既能在Explore页面对行验证,又能最大程度保留下推优化的可能性。Python表达式留在那些确实需要复杂逻辑、比如解析不规则JSON或自定义递归算法的时候再用。SQL表达式在数据源就是数据库、且你需要调用数据库特有函数的场景下用。
4.3 最常用的若干表达式与函数
这里把我项目里最常用的一组表达式整理出来,它们覆盖了80%的日常需求。
数值计算:
quantity * unit_pricegross_amount * (1 - discount_rate)round(net_amount / 1.13, 2)字符串处理:
upper(trim(store_name))contains(channel, 'online')regexp_extract(order_no, '(\\d{8})', 1)日期处理:
strftime(order_date, "%Y-%m")age_days = date_diff(now(), order_date, 'day')条件分支:
if(net_amount >= 1000, "高价值", if(net_amount >= 200, "中价值", "低价值"))空值处理:
ifnull(customer_score, 0)这些表达式基本可以直接套用。唯一要留心的是日期函数在不同引擎里的格式差异。比如strftime在DSS Expression里是格式化日期,而在SQL方言里可能写成DATE_FORMAT。你在配置计算列的时候,顶部有引擎选择,别选错。
4.4 何时选"新增列"何时选"替换列"
计算列创建的时候,DSS会让你选择是创建新列还是更新已有列。这个选择看起来很简单,但业务影响很大。
我的经验是:所有新建业务字段都优先"创建新列",给一个清晰的中文或英文命名。比如net_amount、order_month、customer_segment。如果后续业务口径变了,就重新创建一个新列,把旧的保留下来作为对比参考;而不是直接覆盖原字段。这样做的最大好处是数据链路可追溯——下游同事看到某一列的时候,能通过命名判断它的业务含义和历史来源。
当然也有该用"更新已有列"的时候。比如原始表的status字段含义模糊,你想把空值统一改成UNKNOWN,同时把大小写整理干净,这时候直接在原列上更新更合理。但注意,更新列会改变上游Schema,如果下游已经有很多依赖这个字段的流程,改完可能引发连锁变化。
4.5 计算列放前还是放后的优先级问题
计算列可以在配方主体的不同位置插入。很多人觉得"反正都是多算一列,放哪都一样",但粒度不同的时候完全不一样。
以Group Recipe为例,如果你在分组之前插入计算列,那这个计算列只能基于明细行的字段,不能使用任何聚合函数;如果你在分组之后插入计算列,那它可以基于分组结果里的聚合字段。比如你想算客单价值区间,就必须在分组完成后,用sum(amount)或avg(amount)的结果再判断,此时计算列的位置只能放在分组步骤之后。
Prepare Recipe里的位置选择更自由,但同样有顺序问题。比如你先过滤掉了某些行,再算总金额,和先算总金额再过滤,结果是一样的;但如果计算列本身用到了某个会变化的字段,顺序就要格外小心。DSS允许你随时拖拽计算列步骤到配方主体的其他位置,我建议每次拖完都Build一次确认结果变化。
5. 一个完整案例:用三个通用步骤搭出一条订单清洗链路
5.1 案例背景:一张混乱的订单明细表
我手头正好有一个典型场景。某零售公司给了一张订单明细表,叫orders_raw_2024,里面字段包括:order_id、order_date、store_id、channel、product_id、quantity、unit_price、discount_rate、customer_id。
这张表的问题很常见:包含测试门店TEST的记录;存在空的order_id;日期范围里混着2024年之前的历史数据;quantity出现0或负数;discount_rate偶发异常,导致最终成交金额为负。我们最终要产出一张"有效订单分析表",只保留2024年正常订单,并且计算好净额、税额、订单月份和客单价值等级。
5.2 第一步:预过滤把范围钉死
我在Prepare Recipe的Pre-filter区域写:
order_date >= '2024-01-01' AND order_date < '2025-01-01' AND store_id <> 'TEST' AND order_id IS NOT MISSING这一步做完之后,数据量大概从680万行降到410万行。最关键的是,这些过滤发生在配方主体运行之前,后续每一步都基于这个更干净、更小的子集,整个流水线跑起来明显轻快。
5.3 第二步:计算列处理业务口径
然后在配方主体里连续插入几个计算列:
gross_amount = quantity * unit_pricediscount_amount = gross_amount * discount_ratenet_amount = gross_amount - discount_amountvat_amount = net_amount * 0.13order_month = strftime(order_date, "%Y-%m")order_level = if(net_amount >= 1000, "高价值", if(net_amount >= 200, "中价值", "低价值"))这六列看起来简单,但每一步我都能在界面里预览结果,确认没有空值异常。如果某一步出现net_amount为负,我就能立刻定位到是discount_rate的问题,而不是后面业务规则的问题。
5.4 第三步:后过滤做最终质检
最后在Post-filter区域写入最终保留条件:
quantity > 0 AND net_amount > 0这个条件把"数量异常"和"金额为负"的记录彻底挡在输出门外。有人会问:这些条件能不能放到预过滤?答案是不行,因为net_amount是计算列算出来的,预过滤阶段还没有这个字段。这也正好展示了预过滤、计算列、后过滤三者配合时的逻辑分层:入口筛原始字段,中间算业务字段,出口筛业务结果。
5.5 执行结果与可复用模式
跑完之后,输出表从410万行降到390万行,再去掉空customer_id后最终300多万行。这个结果直接接给下游做客户分层和相关分析,数据基础是干净的。
这套链路完全可以抽成模式:预过滤只做低成本的入口筛选,计算列负责把所有业务口径落到字段上,后过滤做最终交付检查。三个步骤各司其职,别把过滤条件乱放,既省性能又容易维护。
6. 实操中踩过的坑和性能建议
6.1 预过滤与后过滤对查询下推的影响
预过滤的另一个隐藏优势是下推。如果输入数据集连接的是数据库,DSS会把预过滤条件翻译成SQL的where,数据在数据库端就被过滤了;但后过滤因为发生在Recipe内部,一般不参与这种源端下推。所以一个直接建议就是:能用预过滤表达的条件,尽量在预过滤里做,别留到后过滤。
比如你有1000万行数据,其中只有100行是2024年的,你在预过滤里写日期条件,数据库几乎瞬间返回100行;你要是忘了写,让1000万行全量进入Recipe,最后到后过滤再筛,那前面跑过的所有步骤都在空转。这种事我在接手的旧流程里见过太多次,改动往往只需要一分钟,效果却是天壤之别。
6.2 计算列"看似没问题但很慢"的两个原因
第一个原因是用了Python表达式。很多人图省事直接写Python函数,但Python在DSS里通常逐行执行,无法利用SQL引擎的批处理能力。碰到几百万行数据,一个简单的Python lambda也能拖慢整个流程。能换成DSS Expression一律换掉。
第二个原因是计算列放在Join或Group之前,导致分组/连接时多携带了一堆中间列。虽然只多几列,但在大数据规模下,列数膨胀会拉大Shuffle的数据量,整个作业都会变慢。我一般会先做完连接和聚合,再在最后加计算列,把衍生字段放在靠近输出的位置。
6.3 调试表达式的三板斧
第一板斧:在Dataset预览页面直接加一个临时Filter,把你准备写进Pre-filter或Post-filter的表达式单独验证一遍。这样你能立刻看到哪些行被保留、哪些行被剔除,不会出现"配方跑完才发现条件写反"的尴尬。
第二板斧:先Build再全量Schema。每次改完计算列,Build一次,然后点开输出的Schema和Sample,确认列的类型、空值比例、最大最小值是否符合预期。这一步能拦截掉90%的隐性错误。
第三板斧:给计算列命名加统一前缀。比如calc_net_amount、calc_order_month,这样做的好处是数据血缘图里一眼就能看清哪些字段是加工出来的,哪些是原始字段,排错时少花很多时间。
6.4 我的推荐工作流:先粗筛、再细算、后收口
根据我自己的经验,一条稳健的Visual Recipe链路长这样:输入原始表之后,第一时间在预过滤里做"粗筛"——把日期范围、业务范围、明显脏数据全部挡掉;然后进入配方主体,完成必要的Join、Group、去重等操作;接近输出的时候再集中插入计算列,把业务口径一一算出来;最后在后过滤做"收口",按最终交付标准剔除不符合要求的行。
这个顺序也是DSS默认推荐的使用姿势。顺着这个思路做,每个Recipe读起来就像一份清晰的说明书:入口条件是什么,中间做了什么加工,出口交付标准是什么。别人接你的流程,甚至不需要过多口述就能看懂。
我操作下来的体会是,这三个通用步骤看似简单,但它们承载的其实是数据流程设计里最常见的三个问题:哪些数据不该进来、哪些字段需要被造出来、哪些结果不该出去。把这三个问题想清楚,Dataiku DSS里的很多Recipe配置就不再是"照着选项填",而是自然而然的事情。