PostgreSQL码农集散地

某国产数据库研发被洗脑,坚称“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毒太深的同学需要解药,可以建议他读一下这两篇文章:

再见MySQL,你好PG

PGer真讨厌


复现

下面我在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

SQL类型
底层调用
参数处理
静态SQL
SPI_prepare() + SPI_execute_plan()
自动参数替换($1, $2占位符)
动态SQL
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. 网络与协议开销消除

场景
开销来源
PL/pgSQL优势
应用程序发送原始SQL
网络往返 + 协议解析(libpq协议)
完全在服务端内存中执行
参数化查询
每次重新绑定参数
参数在内存中直接传递(无序列化)

实测对比(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. 上下文切换减少

步骤
普通SQL(应用程序)
PL/pgSQL
查询发起
应用进程 → 网络 → 服务端
服务端内部直接调用
结果返回
服务端 → 网络 → 应用进程
内存变量直接传递
事务控制
显式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指令级消耗来细致入微的对比两者的性能差异原因. 到时候不服都不行!