PL/Python 是 PostgreSQL 提供的一种不可信过程语言,它将 CPython 解释器直接嵌入数据库后端进程。这种设计让存储函数可以 import pandas、numpy、scipy 等 Python 科学计算库,而不必把数据发送到外部脚本。pandas 的 DataFrame 与数据库结果集在概念上很接近,都表示二维表格,但 pandas 提供了更灵活的布尔筛选、分组聚合、缺失值填充和时间序列处理能力。当业务规则复杂到 SQL 难以维护时,使用 PL/Python 配合 pandas 可以在数据库内部完成数据处理,减少网络传输。

环境准备与函数基本结构
要使用 PL/Python,首先需要在 PostgreSQL 中安装扩展。通常需要超级用户执行 CREATE EXTENSION plpython3u;。不同操作系统对应的 PostgreSQL 软件包名称略有差异,有些发行版需要单独安装 postgresql-plpython3 或 postgresql-contrib 包。扩展创建完成后,还要确认数据库所在主机的 Python 环境中已经安装 pandas,因为函数运行在数据库服务进程内,而不是客户端环境中。
下面是一个最简单的函数示例,用来验证 pandas 是否可以在 PL/Python 中正常加载:
CREATE EXTENSION IF NOT EXISTS plpython3u; CREATE OR REPLACE FUNCTION py_pandas_version() RETURNS text LANGUAGE plpython3u AS $$ import pandas as pd return pd.__version__ $$;
调用 SELECT py_pandas_version(); 如果返回 pandas 的版本号,说明运行环境已经就绪。创建函数时 LANGUAGE plpython3u 表示使用 Python 3 解释器。函数体用 $$ 包围,内部可以像普通 Python 脚本一样导入模块、定义变量和处理逻辑。PL/Python 中还有 GD 和 SD 两个全局字典,分别用于函数级缓存和静态数据缓存,合理使用可以避免重复加载开销较大的数据。
需要特别注意的是,PL/Python 属于不可信语言,函数可以访问文件系统和网络。因此数据库默认只允许超级用户或具备 USAGE 权限的角色创建这类函数。在多租户环境下,应该遵循最小权限原则,避免普通用户随意创建能够读取服务器文件的存储过程。
将查询结果转换为 DataFrame
PL/Python 通过 plpy.execute 执行 SQL 语句,并返回一个结果对象。该结果对象可以迭代为字典列表,每一行是一个字典,键是列名,值是对应的 Python 对象。这种结构与 pandas 的 DataFrame 构造方式很契合。通常情况下,可以先用 list() 将结果转换为字典列表,再交给 pd.DataFrame 创建二维表。
例如,要读取订单表中金额大于某个阈值的记录,可以这样写:
plan = plpy.prepare(
"SELECT id, name, amount, created_at FROM orders WHERE amount > $1",
["numeric"]
)
rows = plpy.execute(plan, [100])
df = pd.DataFrame(list(rows))
转换完成后,就可以使用 pandas 提供的各种方法进行数据处理。例如按客户名称分组统计总金额和订单数量:
summary = df.groupby('name').agg(
total_amount=('amount', 'sum'),
order_count=('amount', 'count')
).reset_index()
类型映射是这一环节容易踩坑的地方。plpy.execute 返回的 numeric 字段通常被转换为 Python 的 Decimal 对象,pandas 可能将其保留为 object 类型,导致后续数值运算异常。遇到这种情况,可以使用 pd.to_numeric 进行显式转换。日期字段则可能以 datetime.date 或 datetime.datetime 对象返回,必要时通过 pd.to_datetime 统一格式。如果结果集很大,先将全部行放入 Python 列表再构造 DataFrame 会占用较多内存,建议在 SQL 层尽量过滤和聚合,只把必要的行拉到 Python 端。
在函数内处理并返回数据
pandas 处理完的数据通常需要返回给调用方。PL/Python 函数可以返回标量、复合类型记录或者 jsonb 等类型。如果希望以表格形式返回多行结果,可以在创建函数时使用 RETURNS TABLE 语法,并在函数体中返回字典列表。PL/Python 会自动将字典列表映射为对应的复合类型结果集。
下面的示例定义了一个函数,根据订单表中的客户名称分组汇总,并返回客户名、总金额和订单数量三列:
CREATE OR REPLACE FUNCTION order_summary()
RETURNS TABLE(customer_name text, total_amount numeric, order_count bigint)
LANGUAGE plpython3u
AS $$
import pandas as pd
rows = list(plpy.execute("SELECT name, amount FROM orders"))
df = pd.DataFrame(rows)
summary = df.groupby('name').agg(
total_amount=('amount', 'sum'),
order_count=('amount', 'count')
).reset_index()
return summary.to_dict('records')
$$;
如果调用方更习惯接收 JSON,可以直接返回 jsonb 类型。pandas 的 to_json 方法可以生成 JSON 字符串,但为了让 PostgreSQL 正确识别为 jsonb 对象,建议先用 json.loads 解析成 Python 的字典或列表再返回。这样 PL/Python 会将其转换为 jsonb,而不是简单的字符串。
import pandas as pd
import json
df = pd.DataFrame(list(plpy.execute("SELECT name, amount FROM orders")))
out = df.groupby('name').agg(total=('amount', 'sum')).reset_index()
return json.loads(out.to_json(orient='records'))
错误处理同样值得关注。PL/Python 中的异常会触发事务回滚,如果希望给调用方返回明确的错误信息,可以使用 plpy.error('错误描述')。对于 pandas 处理过程中可能出现的异常,应该用 try/except 捕获后再决定是抛出数据库错误还是忽略继续。不要直接让 pandas 的原始异常穿透到数据库层,这样不利于排查问题。
性能优化与使用边界
PL/Python 配合 pandas 适合处理中小规模数据,通常数万行以内比较合适。因为数据从数据库结果集到 Python 对象,再到 DataFrame,会经历多次复制和类型转换。对于大数据集,应当尽量在 SQL 中完成过滤、连接、聚合等重活,只把最终需要 Python 处理的少量行带入函数。如果确实需要处理数百万行,更推荐在数据库外部使用专门的 Python 脚本分批读取和计算,而不是把重负载放在数据库进程内。
如果函数需要频繁访问某张参考表,并且该表更新不频繁,可以使用 GD 全局字典进行缓存。例如:
if 'ref_df' not in GD:
GD['ref_df'] = pd.DataFrame(list(plpy.execute("SELECT * FROM ref_table")))
ref_df = GD['ref_df']
这样可以避免每次调用函数都重新查询并构造 DataFrame。但 GD 仅作用于当前数据库会话,数据修改后不会自动失效。如果参考表发生了变化,需要有机制清理缓存,或者在缓存键中加入版本号来标识数据更新时间。
最后要认识到 PL/Python 的资源消耗。pandas 的计算可能占用较多 CPU 和内存,在高并发写入场景下可能拖慢整个数据库实例。最好为这类函数设置合适的超时时间,并结合连接池、资源监控等手段进行控制。总体来说,当转换逻辑足够复杂、数据量可控时,PL/Python 加 pandas 是一个高效的组合;当逻辑简单或数据量巨大时,单一 SQL 或外部处理框架仍然是更安全的选择。