某国产数据库研发被洗脑,坚称“PG比MySQL快不符合预期”
某国产数据库内核研发被洗脑了? PG比MySQL快这么多是不符合预期的!
最近被某国产数据库内核研发同学问到一个PG性能“诡异”的问题, 问题倒是顺利解释清楚了.
但是! 这位国产数据库的内核同学表示不服!
PG凭什么这么快, 不符合预期啊~~~
原话是这样的:
mysql单条sql,在db端统计的耗时是100us左右,即使有plan cache
不用考虑sql的执行,我们觉得(PostgreSQL) pl执行不符合预期,单行数据处理只有4us
我在想, 同学是不是可能“被洗脑”了? 为什么PG比MySQL快就不符合预期?
抱歉啊, 通篇没有提是哪个国产数据库厂商, 也没有提这位同学的名字.
大家也别私信我了, 我不会说的, 为了保护这位兄弟~~~
下面我们一起回顾一下这个过程啊, 然后后面解释一下原因.
首先是问题:
(我简单总结了一下, 因为聊天嘛, 文字一来一回很多, 不方便大家抓重点.)
测试1, 使用PG plpgsql 循环执行100万次insert, 写入100万行.
测试2, 使用sysbench 单线程发送100w条insert. (每个事务100条, 跑1万次).
两者的性能差距非常大,pl里面5-6s搞定,走sql需要几十秒,有5-10倍左右的差距。
测试1只需要4秒, 相当于一次insert只需要4 us.
同学觉得不合理, 预期应该是50us-100us, 判断依据是 “mysql单条sql,在db端统计的耗时是100us左右,即使有plan cache” , PG这么快肯定哪里出问题了.
我简单回复了一下, 同学不太相信:
可能: 交互次数不一样, 提交次数不一样, SQL硬解析次数不一样
存储过程是一个事务, 只需要提交一次, 循环N次insert虽然命令多, 但是会使用绑定变量, 没有多次硬解.
如果是客户端发起一百万次请求(之前我不知道只发送1万次, 一次100条), 那数据包来回就有1百万次, 肯定也是慢的.
如果你身边也有中MySQL毒太深的同学需要解药,可以建议他读一下这两篇文章:
复现
下面我在PolarDB for PostgreSQL 15版本, 本地Mac book 容器中复现一下这个速度怎么就这么快, 使用pl时到底有没有执行了这么多次insert ?
环境参考: 《2025-PolarDB 进阶课程, 穷鬼玩PolarDB RAC一写多读集群 系列文章》
结果
\timing
create unlogged table test (id int, info text);
do language plpgsql $$
declare
begin
for i in 1..1000000 loop
insert into test values (i, 'test');
end loop;
end;
$$;
Time: 5200.467 ms (00:05.200)
查询command ID, 用来确认是不是执行了这么多次insert
postgres=# select xmin,xmax,cmin,cmax,ctid,* from test limit 5;
xmin | xmax | cmin | cmax | ctid | id | info
------+------+------+------+-------+----+------
2022 | 0 | 0 | 0 | (0,1) | 1 | test
2022 | 0 | 1 | 1 | (0,2) | 2 | test
2022 | 0 | 2 | 2 | (0,3) | 3 | test
2022 | 0 | 3 | 3 | (0,4) | 4 | test
2022 | 0 | 4 | 4 | (0,5) | 5 | test
(5 rows)
注意这里的cmin,cmax, 代表在某个事务中的Command ID. 一个inline code对应1个事务, xmin也都相同, 也就是说这个plpgsql中确实执行了1000000条insert 语句.
postgres=# select 5200.467/1000000;
?column?
------------------------
0.00520046700000000000
(1 row)
相当于每条insert耗时 5.2 us
下面深入解释一下为什么这么快, 实际上存储过程中会自动使用SPI接口, 加速语句的调用. 我在很早的文章中介绍过, 一时间没有想起来, 就没有告诉这位同学:
《执行计划选择算法 与 绑定变量 - PostgreSQL prepared statement: SPI_prepare, prepare|execute COMMAND, PL/pgsql STYLE: custom & generic plan cache》 https://github.com/digoal/blog/tree/master/201212/20121224_01.md
为什么这么快? 现在有AI了, 就用DeepSeek V3 AI来回答吧.
postgresql plpgsql是调用SPI_prepare_params来执行SQL的吗?
在PostgreSQL的PL/pgSQL函数中,SQL语句的执行确实会通过 SPI(Server Programming Interface) 进行处理,但具体调用链比直接使用SPI_prepare_params更复杂。以下是详细解析:
1. PL/pgSQL的执行流程
当PL/pgSQL函数执行SQL语句时,会经历以下关键步骤:
1 语法解析
PL/pgSQL解析器将函数体内的SQL语句识别为可执行单元。
2 SPI调用
通过SPI接口将SQL语句传递给PostgreSQL执行引擎,具体调用链如下:
exec_stmt_execsql() // PL/pgSQL执行入口
→ SPI_prepare() // 准备SQL语句(生成未解析的语法树)
→ SPI_prepare_params() // 带参数化查询的版本(PG11+)
→ SPI_execute_plan() // 执行已准备的计划
2. SPI_prepare_params 的角色
PostgreSQL 11+ 引入:
该函数是SPI_prepare()的增强版,支持参数类型推导(无需显式指定参数类型):
SPIPlanPtr SPI_prepare_params(const char *src,
ParserSetupHook parserSetup,
void *parserArg,
int cursorOptions)
PL/pgSQL中的调用:
当执行动态SQL(如EXECUTE ... USING)时,PL/pgSQL可能会调用此函数优化参数化查询准备。
3. 静态SQL vs 动态SQL
SPI_prepare() + SPI_execute_plan() | $1, $2占位符) | |
SPI_prepare_params()(PG11+ | USING子句) |
示例对比:
-- 静态SQL(PL/pgSQL自动处理参数)
CREATE FUNCTION static_query(id int) RETURNS void AS $$
BEGIN
UPDATE users SET status = 'active' WHERE user_id = id; -- 自动转为 $1
END;
$$ LANGUAGE plpgsql;
-- 动态SQL(显式调用SPI_prepare_params)
CREATE FUNCTION dynamic_query(id int) RETURNS void AS $$
BEGIN
EXECUTE 'UPDATE users SET status = $1 WHERE user_id = $2'
USING 'active', id; -- PG11+ 可能使用SPI_prepare_params
END;
$$ LANGUAGE plpgsql;
4. 性能优化机制
1 计划缓存
PL/pgSQL会缓存已准备的查询计划(SPIPlan),避免重复解析:
// 在plpgsql.c中
if (expr->plan == NULL) {
expr->plan = SPI_prepare_params(...);
}
2 参数类型推导
SPI_prepare_params通过parserSetupHook自动推断参数类型,减少手动类型声明的开销。
5. 验证方法
若要确认具体调用,可通过以下方式调试:
1 GDB跟踪:
gdb postgres
break SPI_prepare_params
2 日志分析:
在postgresql.conf中设置:
log_statement = 'all'
debug_print_parse = on
总结
PL/pgSQL静态SQL:通常调用 SPI_prepare() + SPI_execute_plan()动态SQL(PG11+):可能优先使用 SPI_prepare_params()优化参数化查询核心机制:所有SQL最终通过SPI接口与执行引擎交互,PL/pgSQL负责查询计划的生命周期管理
如果需要深入SPI调用细节,建议参考PostgreSQL源码中的src/backend/executor/spi.c和src/pl/plpgsql/src/pl_exec.c。
为什么比普通sql速度快几十倍?
PostgreSQL 中 PL/pgSQL 比普通 SQL 快几十倍的核心原因在于其 执行计划缓存、减少网络开销、批量处理优化等机制 。以下是具体技术解析:
和我之前的解释逻辑一致!
1. 执行计划缓存(Plan Caching)
普通SQL的瓶颈
每次执行原始SQL时都需要: 语法解析 → 查询重写 → 执行计划生成 → 优化
这些步骤消耗大量CPU资源(尤其是复杂查询)。
PL/pgSQL的优化
首次执行后缓存执行计划:
// 在PL/pgSQL引擎中(pl_exec.c)
if (expr->plan == NULL) {
expr->plan = SPI_prepare(...); // 首次生成计划
}
plan = SPI_execute_plan(expr->plan, ...); // 后续直接复用
效果:
对于高频调用的查询(如循环内的SQL),跳过90%以上的解析/优化开销。
2. 网络与协议开销消除
实测对比(TPS测试):
普通SQL:约 1,000 QPS(需反复解析) PL/pgSQL:可达 50,000 QPS(计划缓存+内存执行)
3. 批量操作优化
普通SQL的逐行操作
-- 应用程序循环发送(高延迟)
INSERT INTO users(name) VALUES ('Alice');
INSERT INTO users(name) VALUES ('Bob');
...
PL/pgSQL的批量处理
CREATE FUNCTION insert_users(names text[]) RETURNS void AS $$
BEGIN
FOR i IN 1..array_length(names, 1) LOOP
INSERT INTO users(name) VALUES (names[i]); -- 单次事务完成
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- 调用时一次性传递所有参数
SELECT insert_users(ARRAY['Alice', 'Bob', ...]);
性能提升点:
事务开销从N次降为1次 WAL日志组提交优化
4. 向量化参数绑定
PL/pgSQL通过SPI接口直接传递内存中的参数数组,避免普通SQL的文本解析:
// SPI_execute_plan 的参数传递(spi.c)
PortalStart(portal, params, 0, snapshot); // 直接绑定二进制参数
效果:
参数绑定速度比文本SQL快10倍以上(实测数据)
5. 上下文切换减少
BEGIN/COMMIT |
延迟对比(本地测试):
简单INSERT: 普通SQL:~0.5ms(含网络) PL/pgSQL:~0.02ms(纯内存)
6. 典型加速场景示例
案例:批量更新用户状态
-- 慢速方案(应用程序循环)
UPDATE users SET status = 'active' WHERE id = 1;
UPDATE users SET status = 'active' WHERE id = 2;
-- 每次需解析+优化+事务提交
-- 快速方案(PL/pgSQL)
CREATE FUNCTION update_users(ids int[]) RETURNS void AS $$
BEGIN
UPDATE users SET status = 'active' WHERE id = ANY(ids);
END;
$$ LANGUAGE plpgsql;
-- 单次调用完成所有更新
SELECT update_users(ARRAY[1, 2, 3, ...]);
性能差异:
更新1万行: 普通SQL:~50秒 PL/pgSQL:~0.5秒(快100倍)
何时选择PL/pgSQL?
高频小查询(如每秒数千次的简单操作) 复杂事务逻辑(需多语句原子执行) 数据密集型计算(在数据库内减少数据传输)
何时仍用普通SQL?
简单的一次性查询 ORM生成的动态查询 需要跨数据库兼容的场景
通过合理使用PL/pgSQL,可轻松实现数十倍的性能提升,尤其在OLTP高并发场景下优势显著。
我觉得到这里, 同学都不一定服气. 因为他又拿AI的回复来理论:
AI 里面回复的 这里的qps是5w/s,现在要处理100w,理论上应该是20s搞定的,现在5s左右搞定了!
我只能说 你别纠结具体的数字, 逻辑上正确, 不同硬件表现不一样而已 AI 对数字哪有那么敏感!
还不服? 那就只能祭出海波老师了, 他会从CPU指令级消耗来细致入微的对比两者的性能差异原因. 到时候不服都不行!