拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

KADB匿名代码块兼容性实测:DO语句在MPP分布式架构下的行为解析

KADB匿名代码块兼容性实测:DO语句在MPP分布式架构下的行为解析

最近在给一套基于PostgreSQL内核的MPP分析型数据库做功能验证,其中一个重点就是测试KADB对匿名代码块的支持情况。说到"匿名代码块",可能很多从业务SQL起步的同学会觉得陌生,但做过数据库迁移或者写过复杂批量任务的人都清楚,这玩意儿在实际运维和二次开发里相当常用。简单说,匿名代码块就是不需要CREATE PROCEDURE、不需要持久化对象,随手写一段PL/pgSQL程序体交给数据库执行的一次性代码,PostgreSQL里用DO语句来做。

这篇文章我就把整个测试过程、踩过的坑、以及最终如何判定"支持"还是"部分支持"的完整思路记录下来。内容不针对某个特定版本,手头这套环境是典型的master-segment分布式架构,和单机PostgreSQL的行为差异很大,值得单独拎出来说。适合正在做国产化数据库选型、KADB兼容性验证、或者想在MPP架构上跑复杂PL/pgSQL逻辑的工程师参考。

1. 先把概念理清楚:KADB是什么,匿名代码块又是什么

1.1 KADB数据库的定位

KADB是一款面向分析型场景的分布式数据库,底层基于PostgreSQL生态,整体架构采用主节点加多个数据节点的MPP模式。所谓MPP,你可以理解成一个大仓库被拆分成多个小隔间,每个节点只管自己那一亩三分地,查询时所有节点一起干活,最后把结果汇总回来。这种架构决定了它单表扫描能力非常强,适合大数据量聚合分析,但在一些细节行为上,和单机PostgreSQL会有明显差异。

之所以要单独验证KADB的匿名代码块支持,是因为很多兼容性测试只盯着标准SQL和存储过程,很少专门去测DO语句。但实际上,DO语句在很多自动化脚本、数据修复、临时计算场景中是逃不掉的一环。如果数据库不支持,或者支持得"残缺",运维脚本就得全部改成持久化函数,工作量完全不是一个量级。

再说一句,KADB因为继承了PostgreSQL的生态,SQL语法、psql客户端、扩展插件这些对使用习惯来说是很友好的。但"继承"不等于"完全一致",内核里的分布式调度器、事务管理器都是重写的,行为差异往往就藏在这种不起眼的地方。

1.2 匿名代码块(DO语句)的原理

PostgreSQL里执行一段匿名代码块用的是DO关键字,语法长这样:

DO $$ BEGIN RAISE NOTICE 'hello, anonymous block'; END $$;

这个$$不是写错了,而是美元引用符,用来把一整段PL/pgSQL代码包起来。美元引用的好处是可以避免单引号转义的麻烦,而且可以自定义标签,比如$body$ ... $body$,这样嵌套多层字符串也不会乱套。

匿名代码块的生命周期很短暂,大致分成四步:解析、编译、执行、销毁。第一次输入DO语句时,服务端解析器会把代码体交给PL/pgSQL引擎编译成内部执行计划,然后立即执行,执行完毕就释放掉,没有留下任何数据库对象。这也是它和存储过程最大的区别:存储过程有名字、有权限体系、可以长期保存并反复调用,匿名块则是"一次性筷子"。

也正是因为这种"用完即弃"的特性,匿名块特别适合做那些只跑一次、没必要入库的临时逻辑,比如批量更新前的预检查、测试数据清理、调试一段复杂的循环逻辑等。

1.3 为什么要在KADB上单独验证这个能力

很多人在做数据库选型的时候有个惯性思维:既然你说兼容PostgreSQL,那PostgreSQL能跑的SQL你肯定也能跑。这个想法在标准SQL层面基本成立,但在PL/pgSQL层面就得打个问号了。尤其是MPP数据库,DO语句在分布式架构下存在几个天然的风险点:

第一,执行位置问题。单机数据库的DO语句就在本地执行,所有变量、游标、临时表的生命周期都很简单。但在MPP架构里,DO语句通常在主节点上解析执行,内部涉及的SQL语句需要分发到各个数据节点去执行,这个"分发"过程能不能正确处理变量绑定、临时表、事务状态,就是个大问题。

第二,事务语义问题。DO语句本身是包在一个事务里的,如果内部抛异常,整个事务回滚。但MPP下事务要协调多个节点,两阶段提交、全局事务ID这些机制都参与进来,出错回滚的代价比单机大得多。

第三,功能裁剪问题。有些数据库厂商为了性能或稳定性,会刻意裁剪掉PL/pgSQL里"过于灵活"的功能,比如动态SQL、游标、某些系统函数调用。这些裁剪往往不会写在产品介绍里,只能靠自己实测。

所以,KADB支不支持DO语句、支持到什么程度,直接影响后续运维脚本和二次开发的编码方式,这个验证工作必须做,还得做得细致。

2. 测试前的环境准备与边界条件

2.1 测试环境与版本信息

先交代一下我这边的大致环境,方便大家对照复现。测试用的是KADB的分布式集群形态,配置了一个主节点、两个数据节点,底层操作系统就是常规的Linux服务器。客户端工具用的psql,这也是PostgreSQL生态里最标准的交互工具。

组件数量用途
KADB主节点1SQL入口,DO语句在此解析
KADB数据节点2存储数据,执行下发的DML
psql1执行测试SQL和DO块
测试账号1模拟普通业务用户权限

需要提醒的是,版本号不同,行为可能有差异。我手头这套环境应该是内核版本比较新的分支,但我不建议你完全照搬结论,最好拿着下面这些测试用例在自己的环境里跑一遍,以实测结果为准。

2.2 创建测试库与账号,准备必要权限

测试之前要把账号权限准备好,否则一上来就会被各种权限错误卡住。我习惯单独建一个测试账号,不直接用超级用户,这样才能暴露出普通用户在真实业务中会遇到的问题。

CREATE ROLE test_user LOGIN PASSWORD 'Test@123'; CREATE DATABASE testdb OWNER test_user; GRANT ALL ON DATABASE testdb TO test_user;

这里有一个关键点要特别说明:DO语句能否执行,和PL/pgSQL语言的权限直接相关。在PostgreSQL较新的版本里,plpgsql被标记为trusted语言,普通用户默认有USAGE权限。但KADB在MPP架构下做了很多权限裁剪,有时候普通用户执行DO块会报permission denied for language plpgsql。遇到这个报错,解决办法是:

GRANT USAGE ON LANGUAGE plpgsql TO test_user;

别小看这条命令,很多测试卡在第一步就是栽在这儿。

2.3 明确测试矩阵:要覆盖哪些场景

测试不能想到哪测到哪,我习惯先列一个测试矩阵,把各种维度都覆盖一遍。针对匿名代码块,我把场景分成六类:

测试项验证目标
基础DO块语法解析和基本执行是否正常
变量与控制结构DECLARE、FOR、IF等是否兼容
异常处理EXCEPTION块能否捕获错误
事务回滚块内出错后DDL/DML是否整体回滚
动态SQLEXECUTE拼接语句是否可用
分布式行为临时表、分布式表DML、长事务表现

每个测试项跑完后,我不仅看输出结果,还会去查数据库日志、执行计划,确认语句是不是真的在每个数据节点上正确执行了。很多人只盯着"有没有报错",忽略了"执行路径是否正确",这两个维度得出的结论可能完全相反。

3. 核心测试过程:一步步验证匿名代码块

3.1 最基础的DO匿名块:看能不能跑起来

第一个测试永远是最简单的,先验证"能跑"。我执行的语句是:

DO $$ BEGIN RAISE NOTICE 'KADB support anonymous code block'; END $$;

正常执行后,psql窗口会输出:

NOTICE: KADB support anonymous code block DO

看到这个结果,说明基础语法没问题。但先别高兴太早,这只是第一步。我在测试时还额外做了两个动作:一是打开详细错误输出,用\set VERBOSITY verbose命令,这样一旦后面出现错误,能看到完整的错误位置和内部调用栈;二是接着查了数据库日志,确认这条DO语句确实是在主节点解析、并生成了执行任务,而不是在某些兼容模式下被特殊处理掉了。

这个"最基础"的用例过了,才敢往下测复杂功能。如果连这么简单的匿名块都跑不通,那后面全部不用测了,直接给结论"不支持"就完事。

3.2 带变量和控制结构的匿名块测试

基础语法能跑,接下来就测PL/pgSQL的核心能力:变量声明、循环、条件判断。我用的测试用例是算1到10的累加和:

DO $$ DECLARE v_total INTEGER := 0; v_i INTEGER; BEGIN FOR v_i IN 1..10 LOOP v_total := v_total + v_i; END LOOP; RAISE NOTICE 'sum = %', v_total; END $$;

输出结果:

NOTICE: sum = 55 DO

这说明DECLARE变量声明、FOR循环、字符串格式化输出这几个特性都正常。我继续加码,测了IF条件嵌套、WHILE循环、CASE语句,结果也都符合预期。PL/pgSQL的流程控制这一块,KADB基本是把PostgreSQL的底子完整继承下来了。

到这里可以初步判定:KADB对匿名代码块的支持不是"完全不可用",而是有真实实现基础的。但距离"完全支持"还得继续往下测,尤其是异常处理和事务回滚,这两种行为在MPP架构下最容易隐身出问题。

3.3 异常捕获与事务回滚行为

匿名块里可以写EXCEPTION捕获异常,被捕获的异常不会导致整个事务失败,这个是PL/pgSQL的常见用法。我测试了除零错误的捕获:

DO $$ BEGIN BEGIN PERFORM 1 / 0; EXCEPTION WHEN division_by_zero THEN RAISE NOTICE 'caught division by zero'; END; END $$;

输出:

NOTICE: caught division by zero DO

异常捕获正常。但这还不够,我还要测一个更关键的行为:整个DO块的事务性。也就是说,如果块内先执行了一段DDL,然后故意抛出异常,前面的DDL应该被整体回滚掉,表不应该存在。测试用例:

DO $$ BEGIN CREATE TABLE t_rollback_test(id INT); RAISE EXCEPTION 'force rollback'; END $$;

这条语句执行后肯定会报错,但表会不会留下来呢?我接着查询:

SELECT to_regclass('t_rollback_test');

结果返回空,说明表确实被回滚了,这个事务语义是正确的。这个测试很有价值,因为很多MPP数据库在多个节点协同做DDL时,回滚机制很容易出岔子,能在KADB上看到整体回滚,说明事务协调这块做得比较到位。

顺带说一句,PL/pgSQL块内直接写COMMIT是不允许的,这在PostgreSQL里面就是明确限制,KADB同样继承了这个限制。如果确实需要分步提交,得用外部事务来控制,而不是在匿名块里硬写。

3.4 MPP分布式下的特殊场景测试

单机行为测完,接下来才是本次测试的重头戏:分布式环境下的特殊行为。这一节的测试结果,才是决定KADB匿名代码块"够不够用"的关键。

第一个场景是临时表。在PostgreSQL单机下,DO块里创建临时表是很常见的操作。我在KADB里执行:

DO $$ BEGIN CREATE TEMP TABLE tmp_test(id INT); INSERT INTO tmp_test VALUES (1), (2); RAISE NOTICE 'temp table count = %', (SELECT COUNT(*) FROM tmp_test); END $$;

执行结果正常,计数值输出2。但要注意,MPP下临时表默认挂在master节点上,如果后续的分布式查询想关联这个临时表,可能会因为数据分布问题产生广播或者报错,这个属于分布式使用的边界认知,不是匿名块本身的问题。

第二个场景是分布式表上的DML。我建了一张按id分布的表,然后在匿名块里做更新:

CREATE TABLE dist_test(id INT, val TEXT) DISTRIBUTED BY (id); INSERT INTO dist_test SELECT generate_series(1, 100), 'old'; DO $$ BEGIN UPDATE dist_test SET val = 'new' WHERE id <= 50; END $$;

执行成功。但光看不报错不够,我还用EXPLAIN查看了执行计划,确认UPDATE是分发到两个数据节点并行执行的。这才是分布式下真正有效的验证方式。

第三个场景是长事务和锁的观察。匿名块里如果做大量数据修改,整个块就是单个长事务,这一点在MPP下会被显著放大,因为所有数据节点都要持有锁直到全局事务结束。我故意在块里模拟了较长时间的循环更新,果然在并发测试时,其他会话出现了锁等待。这个问题不算"不支持",但属于使用时必须设计的边界条件,后面我还会细说。

4. 测试中遇到的问题与排查实录

4.1 权限不足导致无法执行DO

第一个问题出现在普通用户登录后执行最基础DO语句时。当时我用test_user连接数据库,跑最简单的匿名块直接报错:

ERROR: permission denied for language plpgsql

当时的反应是愣了一下的。因为碰过不少PostgreSQL分支,大部分版本里plpgsql默认对Public开放使用,没想到KADB这里限制得比较严。排查思路其实很清晰:先查语言权限,再查用户角色。

用psql查看语言的权限信息:

\dL+

确认plpgsql语言没有授予Public或者test_user,然后执行授权语句:

GRANT USAGE ON LANGUAGE plpgsql TO test_user;

授权后再执行DO语句就正常了。这里给各位提个醒:在KADB或者类似MPP数据库上做权限收敛的时候,别光管表、库、模式的权限,语言的USAGE权限也是个隐蔽的坑,尤其在跑自动化脚本的时候,一不小心就会被这个卡一下。

4.2 匿名块中DDL的可见性问题

另一个印象深刻的问题是,有一次我在匿名块里创建了一张普通表,块内查询正常,但块结束后在外部用\d却看不到这张表。一开始以为是KADB的回滚机制出了问题,后来仔细一查才发现,是我自己在上一个测试里故意加了RAISE EXCEPTION但没注意块是否整体回滚,导致那张表被回滚删除了。

这是测试中很容易犯的"操作盲区":DO块本身的边界就是事务边界,块内做的所有修改,要么全成功,要么全回滚。如果你希望DDL能保留下来,就必须保证整个块没有任何异常抛出。排查这种问题的时候,不能光盯着当前会话,要回到事务语义上去理解,同时配合to_regclass('表名')去确认对象到底存不存在。

4.3 连接超时与锁等待

第三个问题是性能维度的。我在测一个跑大批量更新的匿名块时,块里面循环了十万行数据的更新操作,结果另一个并发会话执行查询时卡住了,等了几分钟直接报:

ERROR: canceling statement due to lock timeout

这是因为KADB的MPP架构下,一个更新事务要在所有涉及的数据节点上同时持锁,锁的范围比单机数据库大得多。匿名块又是一个整体事务,从块开始到块结束,所有中间步骤的锁一直不释放,一旦并发上来,就导致其他查询长时间等锁。

解决思路有两个方向。一是给会话设置锁超时时间,让等待尽快暴露:

SET lock_timeout = '5s';

二是把大事务拆小,不要在单个匿名块里做超大循环,改成分批处理,每批事务独立提交。结合分布式锁的特性,后者才是真正治本的办法。匿名块擅长的是"短小精悍"的逻辑,不适合做成"大而全"的批量作业。

4.4 判定"支持"而不只是"能用"的完整标准

测试到最后,我给自己定了一个结论标准。要判定KADB支持匿名代码块,不能只看"它不报错",至少得满足以下四点,我整理成一张速查表:

判定维度验证方式结果
语法兼容标准DO语句能否编译执行通过
语义正确变量、控制流、异常结果是否符合预期通过
事务一致块内出错后所有修改是否整体回滚通过
分布路径内部DML是否实际下发到数据节点执行通过

四条全部满足,才敢下定论"支持"。如果你只测前两条,很容易得出"完全兼容PostgreSQL"的乐观结论,进而在生产环境踩到分布式特有的坑。我的结论是:KADB对匿名代码块的核心功能是支持的,但使用上要接受MPP架构带来的锁放大和长事务约束。

5. 实测总结与后续使用建议

5.1 结论判定:核心支持,边界受限

经过完整测试,最终结论可以归纳成一句话:KADB对匿名代码块的功能支持基本到位,核心语法和事务语义都能用,属于"核心支持、边界受限"的状态。我用一个三色清单来归纳,方便后续查阅:

能力分类具体说明
完全支持DO基础语法、变量、流程控制、异常捕获、事务回滚、分布式表DML
有条件支持临时表使用受限于master节点;长事务在并发下可能放大锁等待
受限不支持块内不能显式COMMIT;DO不返回结果集;普通用户需额外授权语言权限

如果你手头的KADB版本和我的有差异,别照抄结论,把第二章的测试矩阵拉出来跑一遍,半小时就能得出自己的结论。

5.2 适合用匿名代码块的场景

实测下来,下面这类场景用KADB匿名块是挺顺手的。

第一类是一次性数据修复。比如线上某张分区表里出现了重复的脏数据,需要按业务规则逐条判断并删除。写成DO块,循环加判断全在一个事务里,要么全成功要么全回滚,还不用在库里留一个永久函数,非常干净。

第二类是复杂数据校验。在做数据迁移或者同步任务之后,想快速核对两边数据是否一致,可以写一个DO块把各种校验规则串起来,发现不一致直接RAISE EXCEPTION,让整个校验任务以错误状态暴露出来。

第三类是自动化运维脚本里的临时逻辑。比如初始化一批测试账号、给一批历史数据打标签,这种不常用但确实需要的逻辑,写成匿名块嵌在Shell脚本里,比创建存储过程更灵活,也不会污染数据库对象。

5.3 不适合用匿名代码块的场景

有适合的就一定有不适合的。如果你发现自己正在下面几种场景里反复用DO块,那就该考虑换方案了。

第一,公共逻辑的重复调用。同一个处理逻辑如果多个业务都在用,必须创建正式的存储过程或者函数,否则每次都要复制粘贴大段DO脚本,维护成本极高,而且别人根本不知道这段逻辑藏在哪个脚本里。

第二,需要参数化输入的场景。DO语句本身不接收外部参数,想传参只能靠拼接字符串或者设置自定义变量,非常别扭。这种需求直接用CREATE FUNCTION带参数才是正路。

第三,需要返回结果集给应用层的场景。DO块不返回结果集,所有输出只能靠RAISE NOTICE打到日志里。如果你期望像调用函数一样拿到一张结果表,别用DO,写成RETURNS TABLE的函数。

第四,高频短事务场景。DO块每次执行都要走编译流程,虽然单次开销不大,但如果一秒执行几百次,就明显不如预编译好的SQL语句或函数高效了。


测试这个功能的时候,我最大的体会就是:数据库的"兼容性"从来不是二进制的黑或白,而是分层次、分场景的灰度。KADB在标准PostgreSQL语法上的兼容做得不错,但MPP架构带来的分布式语义差异,必须靠实测去感知。就拿匿名代码块来说,如果没有最后那轮分布式表的DML测试,我可能就只停留在"语法能跑"的表面结论上,根本意识不到长事务在并发环境下的锁放大问题。最后再分享一个小建议:在KADB上使用匿名代码块之前,先给PL/pgSQL语言做好授权,再给你的脚本里加上lock_timeout,这两个小动作能让你的匿名块跑得稳很多。

返回列表