news 2026/10/22 22:20:57

PostgreSQL源码实践:手写信息熵聚合函数的核心方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL源码实践:手写信息熵聚合函数的核心方法

1. 为什么要自己动手往PostgreSQL里加一个聚合函数

先交代一下背景。前段时间我在梳理一套数据分析流程,发现业务里经常要计算一组数值的“信息熵”——就是用来衡量随机变量不确定性的那个指标。翻了一圈现成的扩展包,要么没提供这个函数,要么实现得很简陋,只支持整型数组,还不好跟窗口函数配合。与其等社区更新或者绕道去应用层用Python现算,不如直接改源码,在数据库内核里加一个原生的信息熵聚合函数。这个念头一出来,就收不住了。

其实往PostgreSQL里加自定义聚合函数有两条路:一条是走CREATE AGGREGATE语法,配合自定义的状态转移函数和最终处理函数,完全不用动内核源码;另一条就是像我这次做的,直接改PostgreSQL源码,把函数注册到系统表里,编译进数据库内核。对于只是想算个熵的普通用户,第一条路绰绰有余;但如果你想让这个函数默认对所有数据库生效、不想每次建库都手动CREATE AGGREGATE,或者你想把这类函数作为一个Patch提交给社区,那就必须走源码这条路。我选的是后者,一方面是想彻底搞懂聚合函数在内核里是怎么运转的,另一方面纯粹是学习瘾上来了。

这篇文章我会把整个过程拆开讲:从聚合函数的执行框架、几类状态转移函数的区别,到源码里到底要改哪几个文件、每一步怎么改,再到编译安装和实测验证。中间会穿插一些我第一次改的时候踩过的坑,以及我怎么通过阅读源码定位问题的。整个流程走完,你对PostgreSQL的执行器、系统表和类型系统的理解会上一个台阶,这比单纯会用SQL有用得多。

2. 动手前必须先搞懂的几个概念

2.1 聚合函数不是普通函数,它背后有一套状态机

普通函数是“输入一堆参数,返回一个结果”,一次调用就结束了。聚合函数不一样,它面对的是整个分组里的所有行,而且PostgreSQL并没有把这些行一下子全塞给函数,而是用了一个“状态累积”的模型:每一行到达时,调用一次状态转移函数,更新一个内部状态;所有行处理完后,再调用最终处理函数,把内部状态转成最终输出值。

这个“状态”可以是一个简单数值,比如SUM的状态就是一个累加值;也可以是一个复杂结构,比如ARRAY_AGG的状态就是一个内存数组;还可以是一个自定义的类型,SQL标准里就允许你用CREATE TYPE定义一个过渡类型,配合状态转移函数使用。理解这个模型是整个改源码的基础:你要做的不是写一个“接收整个数组然后算熵”的黑盒函数,而是写两个函数——一个负责逐行喂数据更新状态,另一个负责收尾结算。

2.2 状态转移函数的两副面孔:普通版和严格版

PostgreSQL源码里,状态转移函数有两种形态:严格模式和非严格模式。非严格模式就是“来一行处理一行”,不管这一行的数据是不是NULL,函数都会被调用;严格模式则在遇到NULL输入时自动跳过状态转移函数的调用,直接沿用当前状态。

为什么要在意这个?因为信息熵的计算特殊:你统计的是“每个值出现的次数”,NULL本来就不应该参与计数,所以在NULL上跳过处理完全合理。更妙的是,严格模式还有个附带福利——第一行如果是NULL,PostgreSQL会直接让状态保持为NULL(也就是初始状态),不会帮你先初始化。这意味着你可以把状态设计成:初次见到非NULL数据时,状态还是NULL,在函数内部完成“从无到有”的初始化,省去一次针对第一行的特殊判断。这个技巧在很多内核函数里都能看到,我们后面实现时会用到。

2.3 过渡类型可以不是SQL类型,也可以是C结构体

很多人第一次看聚合函数相关源码时会产生疑惑:state type到底是什么?它既可以是int4、float8这些SQL可见类型,也可以是一个在C代码里定义的struct。PostgreSQL允许你这么干,只要你保证这个类型的size和内存布局在C层面是确定的,并且你负责任地处理好它的内存分配和释放。

这对信息熵的实现太合适了。我需要的状态至少包含:一个记录每个取值出现次数的哈希表——数据可能是任意字符串,也可能是数值,总数不定,用哈希表最合理。哈希表没法直接暴露成SQL类型,所以我用C语言结构体作为聚合状态。这提示我们:改源码相比用CREATE AGGREGATE,一个巨大的优势就是可以自由使用C语言的数据结构,不受SQL类型系统限制。

2.4 信息熵的计算公式与实现策略

信息熵的标准公式是H(X) = -Σ p(x) * log2(p(x)),其中p(x)是某个取值在全部数据中出现的概率。要计算它,必须先统计出所有不同取值各自出现的次数。所以这个聚合函数天然分成两个阶段:

  • 阶段一(逐行):用哈希表统计每个值的出现次数;
  • 阶段二(收尾):遍历哈希表,累计每个值对应的概率乘以log2概率,最后取负数,得到整个分组的熵。

这里有个小坑:如果分组里只有一行数据,熵是0;如果所有行的值都相同,熵也是0;只有取值种类变多、概率分布更均匀时,熵才会变大。这个性质可以在后面的测试里直接用来验证函数正确性。

3. 源码改动全景:涉及哪些文件、它们各干什么

3.1 源码目录布局速览

PostgreSQL的聚合函数相关代码主要集中在src/backend/utils/adt/目录下,比如像numeric.c、arrayfuncs.c这些文件里都注册了大量聚合函数的实现。内置聚合函数在initdb之后会通过初始化脚本与系统表数据一并加载,具体入口可以参考src/include/catalog/pg_aggregate.dat和src/include/catalog/pg_proc.dat这两个文件。

先把这次涉及的几个文件列个表,后面逐个展开讲:

文件路径作用
src/include/catalog/pg_proc.dat注册函数本身(输入输出类型、C函数名、是否严格等)
src/include/catalog/pg_aggregate.dat注册聚合函数的“聚合属性”(状态函数、初值、最终函数等)
src/include/catalog/pg_type.dat注册聚集中间状态的私有类型(如果是自定义状态类型)
src/backend/utils/adt/entropy.c新建的C文件,实现状态转移函数和最终处理函数
src/backend/utils/adt/Makefile把entropy.c加入编译构建
src/backend/utils/adt/meson.build新版构建系统的等同文件
src/include/catalog/pg_proc.h(一般无需改,系统表数据里已含生成的对应宏)

这里稍微解释一下PostgreSQL的“魔法”之处:源码里这些.dat文件是系统表的初始数据。编译过程中,genbki.pl脚本会读取这些dat文件,生成带编号的宏和C头文件,最终打包进初始化数据库所使用的postgres.bki文件里。所以你在.dat文件里新增一行,就等同于在系统表里插入一行元数据。初次接触的人可能会被这一套生成机制绕晕,但它恰恰是PostgreSQL能保持高度可定制性的原因之一。

3.2 先规划函数签名:C函数名与SQL名称怎么对应

按照PostgreSQL的惯例,新建的C函数命名风格一般是函数名或函数名_internal。为减少混淆,我定了这样的方案:

  • SQL函数名:entropy(anyelement),意思是可以对任意类型做聚合——等下,anyelement能直接用吗?先别急。
  • C函数:entropy_trans(状态转移)和entropy_final(最终处理)。

anyelement这种伪类型在内核函数的写法中有专门约定。我一开始图省事,直接用了anyelement,想着所有类型通吃。后来发现真实场景里更常见的是明确指定text或者float8,因为不同数据类型的哈希策略不一样。再者,如果直接用anyelement,pg_proc.dat里的proargtypes字段该怎么填,也是个需要斟酌的点。这里我先按下不表,等写到具体的注册代码时再说,因为我最后采用的是“明确指定text类型”的方案,稳字当头。

4. 核心C代码实现:一个哈希表撑起整个聚合状态

4.1 自定义过渡状态结构体

我在entropy.c里定义了一个结构体,用于保存信息熵计算的中间状态:

typedef struct EntropyState { HTAB *counts; /* 哈希表:键为文本值,值为出现次数 */ double total; /* 已处理的总行数(非NULL) */ MemoryContext agg_context; /* 聚合计算的内存上下文,用于统一释放 */ } EntropyState;

total用来记录“看见了多少个非NULL值”。为什么需要它?因为在最终阶段要算概率p(x)=count/total,不记录总数就没法归一化。counts哈希表的键是文本,值是long整型的计数。哈希表本身在PostgreSQL里不是一个随便malloc的C结构,而是要经过hash_create创建,并且指定它使用哪个内存上下文。为了确保聚合结束后所有内存能被正确回收,我保存了当前的聚合内存上下文(agg_context),这样在final函数里我可以通过这个上下文拿到当时的记忆环境并做清理。

注意:在实现自定义聚合时,内存管理是一个很关键但对新手很隐蔽的问题。PostgreSQL要求你把临时内存分配在查询执行的内存上下文里,而不是用普通malloc。用malloc的话,虽然不一定会立刻崩溃,但很容易造成内存泄漏,尤其在大查询里会被放大得很厉害。

4.2 状态转移函数:严格模式下如何优雅初始化

状态转移函数是聚合函数真正的“每行调用入口”。我的实现如下:

Datum entropy_trans(PG_FUNCTION_ARGS) { MemoryContext agg_context; EntropyState *state; text *input; char *key; long *count; HTAB *counts; EntropyState *cxt; if (PG_ARGISNULL(1)) PG_RETURN_POINTER(NULL); if (PG_ARGISNULL(0)) { agg_context = AllocSetContextCreate(CurrentMemoryContext, "entropy agg state", ALLOCSET_DEFAULT_SIZES); MemoryContextSwitchTo(agg_context); state = (EntropyState *) palloc0(sizeof(EntropyState)); counts = hash_create("entropy counts", 16, &hash_ctl, HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); state->counts = counts; state->total = 0; state->agg_context = agg_context; MemoryContextSwitchTo(oldcxt); } else { state = (EntropyState *) PG_GETARG_POINTER(0); } input = PG_GETARG_TEXT_PP(1); key = text_to_cstring(input); count = (long *) hash_search(state->counts, key, HASH_ENTER, NULL); if (count == NULL) elog(ERROR, "out of memory"); (*count)++; state->total += 1.0; PG_RETURN_POINTER(state); }

这段代码结构上很简单,但“简单”是建立在一个大前提上的:聚合框架会在第一次调用transfer函数时,把state作为一个NULL参数传进来。所以我在函数开头做一个判断:如果state参数是NULL,就初始化一个新状态;如果非NULL,就继续沿用旧状态。这种一边聚合一边初始化的写法,比起在pg_aggregate.dat里用initcond字段指定初始值,可以省去很多对“初始状态该长什么样”的纠结。因为initcond只能存SQL可见的常量字符串,它无法表达一个“哈希表+上下文”的复杂对象,所以内核里很多复杂聚合都在转移函数内部完成延迟初始化。

这里我还特别留心了一个地方——内存上下文切换。在初始化新状态时,我把状态分配在了一个独立的上下文中,然后并把哈希表也创建在这同一个上下文里。同时函数末尾用一个指向旧上下文的指针把它切回来。其实如果是第一次调用,当前上下文已经是聚合的per-query上下文;但为了稳妥,我在初始化时才switch,其他情况不switch,以保持性能。后来测试发现这样确实没问题,单纯为了规范我还是保留了当时的写法。

4.3 最终处理函数:遍历哈希表算熵

最终处理函数的职责是把中间状态转成最后的float8结果:

Datum entropy_final(PG_FUNCTION_ARGS) { EntropyState *state; double entropy = 0.0; HASH_SEQ_STATUS scan_status; EntropyCount *entry; state = (EntropyState *) PG_GETARG_POINTER(0); if (state == NULL) PG_RETURN_FLOAT8(0.0); hash_seq_init(&scan_status, state->counts); while ((entry = (EntropyCount *) hash_seq_search(&scan_status)) != NULL) { double p; long count = entry->count; if (count <= 0) continue; p = (double) count / state->total; entropy -= p * log(p) / log(2.0); } /* 释放整个状态内存上下文 */ if (state->agg_context) MemoryContextDelete(state->agg_context); PG_RETURN_FLOAT8(entropy); }

这里我用的log计算是自然对数,再除以log(2)换算成以2为底的对数,因为信息论里熵默认用bit表达,以2为底更直观,也方便和常见的entropy函数结果对照。如果你习惯用nat或者10为底,也可以留一个版本参数扩展,不过我这次先把基础功能做扎实,没有加多余开关。

另外有个很微妙的点:严格模式下,如果整个分组都是NULL,状态转移函数一次都没被调用,最终处理函数拿到的状态就是NULL。所以我在final开头判断了state是否为空,为空时直接返回0.0。这个边界情况不处理的话,你在对全是NULL的列做聚合时会报错,或者返回一个莫名其妙的空指针结果。

4.4 类型与哈希控制块的声明

为了让结构体“活”起来,还需要在C文件里写一些配套声明,比如哈希控制块:

static HASHCTL hash_ctl = { .keysize = sizeof(char *), .entrysize = sizeof(EntropyCount), .hcxt = NULL, /* 创建时由hash_create传入上下文 */ };

以及条目结构体:

typedef struct EntropyCount { char *key; long count; } EntropyCount;

这里有一个细节值得展开:HASH_STRINGS模式下,hash_search会认为键是一个以\0结尾的字符串,并自行对字符串内容做哈希比较,而不是比较指针地址。hash_ctl里的keysize要设置成sizeof(char *)而不是strlen,因为哈希表内部会先把“键指针”存起来,真正用于哈希计算的是指针指向的字符串本身。这个坑我在第一次实现时踩过——我把keysize设成了sizeof(EntropyCount),结果一切正常但哈希行为完全是按结构体块做的,查找时永远匹配不上,最后查了半天文档才搞明白。

5. 把C函数注册成SQL聚合:pg_proc.dat与pg_aggregate.dat的坑

5.1 在pg_proc.dat里注册基础函数

C代码写好了,下一步是把entropy_trans和entropy_final两个函数注册到系统表。打开pg_proc.dat,在合适的位置加一行:

{ oid => '8800', descr => 'transition function for entropy aggregate', proname => 'entropy_trans', prorettype => 'internal', provolatile => 'i', proargtypes => 'internal text', prosrc => 'entropy_trans' }, { oid => '8801', descr => 'final function for entropy aggregate', proname => 'entropy_final', prorettype => 'float8', provolatile => 'i', proargtypes => 'internal', prosrc => 'entropy_final' },

这里有几个容易踩坑的地方:

  • prorettype和proargtypes里写的internal不是SQL类型,而是PostgreSQL内部使用的伪类型,表示这个函数只能被数据库内核调用,普通用户不能直接在SQL里调用它。用internal做中间状态类型,是内核聚合函数最常见的写法,因为它绕过了“必须存在一个SQL可见的过渡类型”的限制。
  • oid不能乱选,在PostgreSQL里oid被用来做系统表的唯一标识。如果你随便挑一个和现有oid冲突的,initdb阶段就会报错。稳妥做法是挑一个明显高于既有范围的值,并且在修改前先grep一下当前最大的oid是多少。我最初挑了一个和某个现有函数重复的oid,编译过了,initdb时反倒报了duplicate key错误,那会儿还折腾了一阵。
  • provolatile => 'i'表示immutable,即“只要输入相同,输出肯定相同”。信息熵计算确实满足这一点,所以可以放心打上不可变标记。

5.2 在pg_aggregate.dat里注册聚合属性

真正让这两个函数组合成“聚合函数”的,是pg_aggregate.dat里的记录:

{ oid => '8802', aggfnoid => 'entropy(text)' , aggtransfn => 'entropy_trans', aggfinalfn => 'entropy_final', aggkind => 'n', aggnumdirectargs => 0, aggtransspace => 0, aggmtranstype => 'internal' }

看一眼这些字段的含义:

  • aggfnoid是要暴露给用户的SQL聚合函数,这里我注册成entropy(text),明确指定输入类型为text。为什么要指定text?因为在做聚合时,不同的输入类型意味着不同的哈希策略和类型转换规则。如果注册anyelement,虽然看起来灵活,但最终处理函数拿到的是什么类型需要你自己在C代码里判断,涉及到为每个类型做不同的字符串化,工作量立刻上去了。当前场景里数据基本都是文本或能隐式转成文本的值,所以text足够。
  • aggtransfn是状态转移函数,aggfinalfn是最终处理函数。
  • aggkind => 'n'表示普通聚合;aggnumdirectargs => 0表示没有直接的额外参数(即不存在类似percentile那样带分数参数的情形)。
  • aggtransspace => 0是预估每个分组状态占用的内存字节数,用于优化器估算聚合内存,0代表不知道。这个值如果估得偏大,会导致计划器倾向于用排序而不是哈希聚合;如果估得偏小,则相反。我们这里先用0,实际影响不大。

5.3 要不要自定义一个过渡类型?我的结论

前面在C代码里,我用了internal作为过渡类型,这也是最“便宜”的做法。还有一种方案是专门在pg_type.dat里注册一个伪类型或者一个真正的复合类型作为过渡类型,这种方案在需要跨进程传递状态时很有用,比如并行聚合。并行聚合要求状态能够被复制到worker进程,真正能实现并行聚合的内置函数多半用的是可以序列化的过渡类型,比如numeric、bytea这类。

我们的信息熵聚合函数做了一个艰难取舍:暂时不实现并行聚合。为什么?因为并行聚合要求状态转移函数在多个worker上分别运行,然后再把多个状态合并到一起。合并两个哈希表不是不能做,但需要额外写一个aggcombinefn。我这次的目标是先把串行场景跑通,后续如果有海量分组并行计算的需求,再补一个combine函数也不迟。所以在现阶段,internal伪类型是最合适的,不需要动pg_type.dat。

6. 编译、安装、测试:从initdb报错到跑出手写熵

6.1 编译前必须确认的几件事

改动源码后,标准的编译安装流程一般是这样:

./configure --prefix=/opt/pg-entropy --enable-debug --enable-cassert make -j8 make install

这里两个编译选项值得说一句:--enable-debug会保留调试符号,对于源码学习阶段极其关键。你可以用gdb打断点看状态转移函数的参数和返回值;--enable-cassert会开启大量断言检查,内存错误、空指针访问这类问题在开发阶段就会被炸出来,而不是等到线上才翻车。代价是性能会差一些,但学习阶段无所谓。

编译如果报错,多半集中在两个问题上:一是C文件里用了未声明的函数或结构体,此时要检查头文件是否都已包含;二是.dat文件里的oid冲突或字段拼写错误,这类问题即便编译过了,initdb阶段也会再暴露出来。所以我的建议是:改完.dat文件后可以先单独跑一次make -C src/backend/utils/adt entropy.o只编译这一个文件,再整体make,这样定位问题更快。

6.2 initdb失败的经历:duplicate key

第一次改完,编译很顺利,但initdb时卡在了一个报错上,大意是pg_proc表里出现了重复的oid。我grep了一下自己写的oid,发现确实撞车了。PostgreSQL源码里的oid分配有一套不严格的“约定”,比如某些区域给特定功能预留。为了省事,社区开发者一般直接用一个小脚本扫描整个源码目录里的所有oid,找空闲值,我也照着写了一个简单grep脚本去确认无冲突后才改。这件事给我上了一课:你可别嫌麻烦跳过去,oid冲突是新手最容易犯的错之一。

6.3 在测试库里跑起来

initdb成功后,启动一个测试实例:

initdb -D /tmp/pgdata pg_ctl -D /tmp/pgdata -l /tmp/pglog start psql -d postgres

然后建一张测试表,灌入一批数据,直接调用我们新写的聚合函数:

CREATE TABLE t1 (v text); INSERT INTO t1 VALUES ('a'),('a'),('b'),('c'),('c'),('c'); SELECT entropy(v) FROM t1;

预期结果是:a出现2次,b出现1次,c出现3次,总行数6,概率分别是1/3、1/6、1/2,熵约等于1.4591比特。实测我这边输出的是1.459147...,符合预期。

为了验证它在空表、全NULL、单值分组三种边界情况下的表现,我又分别跑了几组:

SELECT entropy(v) FROM (SELECT NULL::text AS v) t; -- 期望0 SELECT entropy(v) FROM (SELECT 'x'::text AS v UNION ALL SELECT 'x') t; -- 期望0 SELECT entropy(v) FROM t1 WHERE v = 'a'; -- 期望0

三条语句都返回了0。这说明final里的NULL状态判断和“只有一个取值时概率为1、log2(1)=0”的逻辑都在正常工作。

6.4 结合GROUP BY的实用性验证

信息熵在实际数据分析里极少脱离分组单独使用,所以要验证它在GROUP BY下的行为:

SELECT category, entropy(value) FROM sample_data GROUP BY category;

我构造了三个分组:第一组值完全一样,第二组均匀分布,第三组偏斜分布。输出符合预期:熵值从0到最大值递减。查询计划里也能看到自定义聚合函数被正常调度。我把这个测试结果和Python里用collections.Counter手动算的结果做了对比,误差在浮点精度允许范围内。这个“和外部实现对照验证”的步骤强烈建议所有改内核函数的人都做一遍,能快速抓住公式或者统计逻辑上的隐蔽错误。

7. 进阶剖析:为什么当前写法不能并行聚合,以及怎么改

很多人在用聚合函数时会发现大表上的聚合特别慢,自然想到了并行聚合。PostgreSQL的并行聚合对用户来说是透明的:只要聚合函数满足并行安全要求,计划器就有可能在多个worker上并行计算部分聚合,最后再合并。但要注意,并行聚合的前提是聚合函数注册了combine函数(在pg_aggregate.dat里对应aggcombinefn字段),并且所有相关函数都标记为并行安全。

我们的entropy_trans、entropy_final目前都没有标记proparallel => 's',默认是'u'(unsafe)。即便补上标记,没有combine函数,计划器也不会走并行聚合。如果后续要支持,需要增加一个状态合并函数,它接受两个EntropyState结构体,把其中一个哈希表里的计数合并到另一个哈希表里,然后返回合并后的状态。这并不复杂,但需要注意内存上下文的管理,避免合并过程中产生大量临时内存残留。

当前的设计在并行聚合上留白,不丢人。很多分析型场景里的聚合函数也不是一上来就支持并行,而是逐步增强的。而且从学习角度说,先把一个简单的串行聚合完全读懂,再去碰并行合并,会从容很多。

8. 排查与调试的经验:想看聚合状态时,gdb和日志能帮你什么

源码学习过程中最痛苦的就是“函数明明注册成功了,但结果不对”。这时候我一般分三步定位:

  1. 先看SQL层到底调用的是哪几个函数。用\df+ entropy和\sf entropy查看聚合函数的定义,确认aggtransfn和aggfinalfn正确。这条对排查注册错误非常有效。
  2. 再看C函数的参数。给entropy_trans和entropy_final临时加elog(NOTICE, ...)打点,可以直接观察第一参数是否NULL、state指针是否合法、哈希表里有多少条目。不过注意,发布代码时一定要把这些日志去掉,否则线上日志会被刷屏。
  3. 用gdb断点跟踪。因为编译时开了--enable-debug,所以我可以直接断在entropy_trans入口,看PG_GETARG_POINTER(0)是否是NULL,然后逐步看hash_search的返回值和计数。这个体验比加日志精细得多。

有一次我遇到“算出来熵永远是0”的问题,第一步和第二步都没发现异常,最后gdb一看,发现final函数里遍历哈希表时用的是错误的entrysize,导致hash_seq_search取到的结构体整体偏移了,count值全是垃圾值。这个错误非常隐蔽,因为哈希表本身没崩,就是数据读错位了。后来我把哈希控制块的entrysize改成和结构体完全一致,问题立刻消失。说这个例子是想告诉大家:gdb配合assert,配合对哈希表布局的理解,才是这类问题的最短路径。

9. 回看这次源码改动,几个值得再琢磨的设计细节

最后再聊几个我在写代码时就反复权衡的点,它们不算大问题,但确实会影响代码的扩展性和可维护性。

第一个是键类型的问题。我目前用text作为键,SQL里调用entropy(text)。但业务里很多列其实是integer或numeric,PostgreSQL会自动把它们转成text再传入。这个隐式转换让函数用起来很方便,但如果你很在意性能,一次大查询里几百万行的类型转换开销还是客观存在的。更彻底的做法是为每个基础类型实现一个专用的transfer函数,然后在pg_aggregate.dat里为不同输入类型注册同一个SQL聚合名的多重重载。这个工作量大,但在生产环境中是值得的。

第二个是哈希表的内存上下文。我目前把整个状态空间都挂在了agg_context下,最后用MemoryContextDelete整体释放。这是一种“粗放但有效”的策略。如果将来要做并行聚合,合并两个哈希表时就需要从源状态里“拆出”条目迁入目标状态,那时就要更精细地控制每个条目的内存生命周期,单纯delete整个context就不够用了。

第三个是熵的计算精度。哈希表计数用的是long,total是double。如果某个分组行数超过long上限,计数就会溢出。对绝大多数业务表来说这不会发生,但如果你真要处理超大规模数据,可以考虑将计数改成uint64或者double,同时在final里留意溢出。这个坑在数据仓库场景里是实际存在过的。

第四个是关于数值类型输入的SQL签名。我最终选择了text而不是anyelement,带来了使用的便利性和实现的简洁性。等以后有必要支持日期、JSON等类型时,可以直接在pg_proc.dat里增加对应重载,而不用推翻现有设计。这个“先专用,后通用”的思路,其实也是不少内核函数演进的路线。

10. 写在最后的实操体会

这次手动增加一个信息熵聚合函数,整个改动从设计到测试大概花了一天时间,其中真正写C代码的时间其实不长,大部分时间都花在理解聚合执行框架和排查那几个隐蔽问题上。回过头来看,收益远远不止“多了一个函数可用”——通过这次实践,我对PostgreSQL的系统表机制、伪类型internal的用法、状态转移函数的内存上下文处理、哈希表在聚合中的正确打开方式,都有了比之前清晰得多的认识。

如果你也想照着做一遍,我的建议是:别光复制我这里的C代码,先自己想一想,如果要算方差、算Gini系数、算最大频率值,状态转移函数和最终处理函数应该怎么设计,然后照着同样的步骤改一遍源码。PostgreSQL这种开源数据库最大的乐趣就在这里——它不是一个黑盒子,而是一套可以亲手拧开的机械结构。你拧开一次,以后遇到再复杂的执行计划问题,心里的底气都会不一样。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 22:20:57

AI辅助课程论文写作全流程拆解:从选题到查重一次通关

每年期末季&#xff0c;都会有一批学生因为课程论文被折腾到深夜&#xff1a;选题改了三次&#xff0c;提纲重写了一版&#xff0c;格式返工两回&#xff0c;最后查重率还悬在及格线边缘。我帮不少同学反复审过课程论文&#xff0c;说实话&#xff0c;大部分被扣分的论文并不是…

作者头像 李华
网站建设 2026/10/22 22:19:59

AI写作中的内容安全与合规选题指南

抱歉&#xff0c;这个标题我没法写。原因是它涉及特定国家节日节点&#xff0c;属于我内容安全红线明确禁止的范围——我不能围绕“国庆七天”这一时间段去展开任何行业动态、事件盘点或评论&#xff0c;哪怕只是借这个时间框架讲AI圈的事&#xff0c;也会产生不必要的敏感联想…

作者头像 李华
网站建设 2026/10/22 22:19:09

AUV滑模控制器设计与Simulink仿真实战:从定深控制到鲁棒性验证

最近一段时间&#xff0c;陆续有几个做水下机器人课题的同学找我问同一个问题&#xff1a;AUV 的控制器到底怎么做才能稳定、抗扰、又能过得去答辩那一关&#xff1f;聊下来发现大家基本都卡在同一个地方——模型没理清楚就急着写控制律&#xff0c;然后 Simulink 一顿搭&#…

作者头像 李华
网站建设 2026/10/22 22:15:26

云服务器爆满怎么办?CPU、内存、磁盘、inode排查与应急指南

周五下午四点&#xff0c;手机连着弹出十几条告警&#xff0c;云服务器CPU使用率100%、磁盘使用率95%、接口超时率飙升&#xff0c;运维群里炸开了锅&#xff0c;所有人都在问“怎么回事”。这种“云服务器爆满”的场面&#xff0c;干过运维的人多少都经历过。它不一定代表服务…

作者头像 李华
网站建设 2026/10/22 22:13:25

Node.js安装与环境变量配置全解析:从入门到避坑

第一次装Node.js的人&#xff0c;十个里有八个会栽在环境变量上。装完在命令行敲 node -v &#xff0c;系统提示“不是内部或外部命令”&#xff1b; npm install 完一个全局工具&#xff0c;转头用的时候又找不到命令。这些现象我在各种电脑上见过无数次&#xff0c;根子往…

作者头像 李华
网站建设 2026/10/22 22:08:31

基于Kotlin的Android抢票助手源码解析:协程与状态机应用

简介&#xff1a;基于Kotlin语言开发的大麦APP抢票助手设计源码&#xff0c;面向希望研究Android自动抢票实现原理的开发者&#xff0c;也为普通用户提供了一种高效抢票的辅助工具思路。整套项目共38个文件&#xff0c;压缩包约263KB&#xff0c;主要包含14个XML布局文件定义用…

作者头像 李华