一、简单思考
快问快答,在PostgreSQL中运行以下SQL语句,最后使用\df func的运行结果是什么样的?
-- 准备两个SCHEMACREATE SCHEMA schema_a;
CREATE SCHEMA schema_b;
-- 创建三个func函数
CREATE OR REPLACE FUNCTION schema_a.func()
RETURNS TEXT
AS $$
DECLARE
va text DEFAULT 'schema_a';
BEGIN
RAISE NOTICE 'schema_a.func()';
RETURN va;
END $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION schema_b.func()
RETURNS TEXT
AS $$
DECLARE
va text DEFAULT 'schema_b';
BEGIN
RAISE NOTICE 'schema_b.func()';
RETURN va;
END $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION schema_b.func(aa TEXT)
RETURNS TEXT
AS $$
DECLARE
BEGIN
RAISE NOTICE 'schema_a.func(TEXT)';
RETURN aa;
END $$ LANGUAGE plpgsql;
set search_path = 'schema_a','schema_b';
-- \df func 出来几条数据、出来的数据内容大概是什么复制
简单思考一会...
二、运行结果
好了,不卖关子了,谜底揭晓。如果此时使用\df func查询,最终出来将会是两条数据,分别是schema_a.func()和schema_b.func(aa text),实际运行如下图所示
明明创建了三个func函数,为什么这样查询只出来两条数据呢,难道丢数据了吗?这个时候可以查询一下PostgreSQL的pg_proc系统表
SELECT proname,pronamespace::regnamespace,proargtypes,prorettype::regtype FROM pg_proc WHERE proname = 'func';复制
如下图,可以看到在系统表中确确实实有三条数据,和之前的创建语句一一对应
调用并运行
可以看到数据实际上并没有丢失,并且能正常按照逻辑运行。而出现使用\df func只出来两条数据这种情况是由psql工具中隐含的search_path和某些可见性规则决定的。考虑到可能有部分同学对于search_path不熟悉或者正在学习阶段,所以接下来我会介绍和描述如何使用search_path,对于此处的显示问题的实际分析在第五节,熟悉search_path的同学可以直接快进到第五节。
三、SEARCH_PATH——模式搜索路径
search_path也称模式搜索路径,而模式在PostgreSQL中其实指的是schema(可以使用CREATE SCHEMA语句进行创建),对应的系统表名为pg_namespace,虽然说ORACLE好像没有schema的概念,但其实ORACLE中它的user其实等价于schema,比如说存在一个scott用户,那么它的schema就等价于scott,比如说存在ta表,使用SELECT
* FROM ta和使用SELECT * FROM scott.ta没有区别。回到PostgreSQL,在PostgreSQL的一个数据库(数据库实例和数据库在此处不做过多的赘述)中可以存在多个schema,schema下可以包含表、函数、存储过程、视图、操作符等一系列的对象。不同的schema可以存在同名对象互不影响、也互不冲突、如上述的schema_a.func()和schema_b.func()。
使用schema的好处在于:1、允许多个用户使用一个数据库并且不会互相干扰 。2、将数据库对象组织成逻辑组,易于管理。
3、第三方应用的对象可以放在独立的模式中,这样它们就不会与其他对象的名称发生冲突。
到这个时候你会发现既然schema有这麽些好处,但是如果使用起来的话,感觉还是很麻烦,比如说调用一个对象,还得手动附加上它所属的schema名,那SQL语句不得写老长了?要是能更加易于使用就好了,所以search_path体现出它的作用了。search_path “模式搜索路径”,其实就是指定当前数据库的schema的先后查找顺序,PostgresSQL将会按照这个顺序,去确认当前的操作的这个对象应该属于哪个schema。一般默认的search_path是"$user", public,我们可以使用SHOW search_path查看,这个的第一个$user含义为如果存在与当前用户同名的schema,会默认先搜索当前用户同名的schema,如果不存在的话,则会采用公共模式public。接下来简单演示一下。
-- 此时为schema_a, schema_b
SHOW search_path;
-- 修改回默认值,也可使用SET search_path = '$user',public;
SET search_path TO '$user',public;
-- 查看设置结果
SHOW search_path;
-- 查看当前用户和当前所属数据库 此处以我的postgres、postgres为例
\c -
-- 查看当前数据库有哪些schema
SELECT * FROM pg_namespace;
-- 此时创建存储过程protest1 不主动附加schema
CREATE OR REPLACE PROCEDURE protest1()
AS $$
DECLARE
BEGIN
NULL;
END $$ LANGUAGE plpgsql;
-- 查看protest1 详细情况 可以看到由于$user不存在 所以它的schema为public
\sf protest1
-- 创建与当前用户同名的schema
CREATE SCHEMA postgres;
-- 此时创建存储过程protest2 不主动附加schema
CREATE OR REPLACE PROCEDURE protest2()
AS $$
DECLARE
BEGIN
NULL;
END $$ LANGUAGE plpgsql;
-- 查看protest2 详细情况 由于此时$user存在 所以它的schema与当前用户同名
\sf protest2复制
结果如下
阅读到此处应该对search_path的使用有了一个大概的了解。那么在PostgreSQL内部,以此处的函数或存储过程(在内部其实一样的处理逻辑)的创建为例,研究下search_path是如何辅助pg数据库确认该使用哪个schema的呢?
四、PostgreSQL如何根据search_path确认schema的?
代码来自PostgreSQL主分支,现在版本为17 ,接下来调试创建存储过程protest3。
-- 删除与用户同名的schema
DROP SCHEMA postgres;
-- 此时创建存储过程protest3 不主动附加schema
CREATE OR REPLACE PROCEDURE protest3()
AS $$
DECLARE
BEGIN
NULL;
END $$ LANGUAGE plpgsql;复制
我们直接快进到确认schema的代码位置,位于namespace.c的QualifiedNameGetCreationNamespace函数,如下图由于我们并没有附加schema,所以此处schemaname为空,走到recomputeNamespacePath(),而下方获取到【activeCreationNamespace】之后,就实际确认了当前的protest3应该归属于哪个schema。那么【activeCreationNamespace】是如何确认的呢?我们接着往下看。
接着查看recomputeNamespacePath ,开头就看见熟悉的身影,这个rawname拷贝了namespace_search_path,然后按","分割存储在namelist中,而他的数据又是那么亲切,那么有没有一种可能它就是search_path在内部的表现,所以先查看下namespace_search_path
在src/backend/utils/misc/guc_tables.c 我们可以看见这麽一段,所以在外部是search_path,在内部为namespace_search_path
接着我们回到recomputeNamespacePath 函数接着查看,紧接着它对namelist进行一个foreach操作,从代码的逻辑不难看出
主要针对三类进行处理
1、$user,根据当前的roleid获取到用户名,依据用户名查询pg_namespace是否存在同名schema,如果存在获取oid数据 判断是否有效 进行acl检查
将其存储在oidlist当中
2、pg_temp,此处不涉及 感兴趣后续可自行阅读
3、其他的常规schema,直接按照设定的schema 如:public 查询pg_namespace中的数据,如果存在获取oid数据 判断是否有效 进行acl检查 将其存储在oidlist当中
由于同用户名的schema并不存在
此处oidlist有且仅有public这个公共模式的oid数据
接着就是处理oidlist 而我们不需要注意太多的东西 最开始说到,我们实际上只要关注activeCreationNamespace是如何被赋值的,因为activeCreationNamespace就是最终的namespace,而如图所示,它取到了oidlist中的第一个也就是public的oid,所以最终protest3会被创建在public这个公共模式下。
此处还可以留意一下activeSearchPath,后续其实在对上下文可见性那块其实有处理。
由于此次baseSearchPathValid置为了true,当search_path没有发生变化的时候,将不再执行这些操作,直接获取这个activeCreationNamespace即可。如果你也想这样子调试的话,需要注意一下。
五、最终结果分析
好了,search_path的使用也讲了,PostgreSQL如何根据search_path确认使用哪个schema的逻辑也讲了,而我们开篇的问题还没进行分析,接下来回到正题。
-- 重新设置回去进行分析
set search_path = 'schema_a','schema_b';
-- 可以使以"\"开头的命令对应的实际执行SQL语句打印出来
\set ECHO_HIDDEN on
-- 再次查看
\df func
\set ECHO_HIDDEN off复制
运行结果如下
通过开启ECHO_HIDDEN,我们可以看到\df func实际执行的SQL语句,而在这段SQL语句中令人最为在意的一个点就在于pg_catalog.pg_function_is_visible这个函数,难道有个func函数被隐藏了,简单改写下这个实际运行SQL语句,将pg_function_is_visible从where条件去除,添加至查询列中
SELECT n.nspname as "Schema",
p.proname as "Name",
pg_catalog.pg_get_function_result(p.oid) as "Result data type",
pg_catalog.pg_get_function_arguments(p.oid) as "Argument data types",
CASE p.prokind
WHEN 'a' THEN 'agg'
WHEN 'w' THEN 'window'
WHEN 'p' THEN 'proc'
ELSE 'func'
END as "Type",
pg_catalog.pg_function_is_visible(p.oid) as "is_visible"
FROM pg_catalog.pg_proc p
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
WHERE p.proname OPERATOR(pg_catalog.~) '^(func)$' COLLATE pg_catalog.default
ORDER BY 1, 2, 4;复制
运行结果如下
果不其然,schema_b.func()被隐藏了,为当前上下文不可见状态。都看到这了,干脆再翻一翻代码看看,这里面到底是怎么处理的。找到pg_function_is_visible对应的内部函数pg_function_is_visible
显而易见的,我们需要关注的是FunctionIsVisible函数,依旧是namespace.c文件
其实此处注释也说得很清楚了,就算是schema在search_path中,当函数名和参数完全一致时,会存在隐藏的情况,更加详细需要看FuncnameGetCandidates函数。而FuncnameGetCandidates关于此处的逻辑如注释所言(不想再将细节展开了已经太长了,第一次写有点缺经验),当前search_path中内容为schema_a,schema_b,而这两个schema中存在了一个一模一样(函数名和参数完全匹配)的func()对象,它内部会选择将schema_b.func()隐藏,再加上schema_b.func(aa text),最终呈现出两条记录。
其实PostgreSQL并不仅仅是对于函数对象,在使用带有“\”的命令,会选择隐藏数据。可以使用SQL查询一下is_visible等函数就知道了。
SELECT proname FROM pg_proc WHERE proname LIKE '%is_visible';复制
运行结果
可以看到像表、类型等等这些个对象都会存在这种情况,由于在PostgreSQL中函数、存储过程这些存在多态的性质,所以实际上如果你去查看表对应的pg_table_is_visible这种函数其实理解起来更为简单,如下图:
六、总结
在PostgreSQL中,用户通过设置search_path ,来指定当前应该使用哪些schema,以及这些个schema的先后顺序。在创建的其他对象的时候,如果没有手动附加schema信息,那么将会按照search_path的规则(一般是第一个,$user这种存在额外的处理,如果不存在与当前用户同名的schema,则会选择下一个在路径中的schema),作为当前创建对象的模式。如果使用带有"\"的psql命令(如\df、\do、\dt等等,可以根据上述的is_visible相关函数推敲一下)去查找对象时,如果在search_path中的各个schema下有着能够完全匹配的对象,则有可能会隐藏部分数据,其实也就是数据库认为在当前search_path下,如果不附加schema信息,最优先的选择。
七、碎碎念
由于篇幅的原因诸多细节未完全展开,若文中存在错误或不当之处,敬请指出,以便我进行修正和完善。希望这篇文章能够帮助到你。