暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

换个角度理解PostgreSQL的search_path

5003 2024-12-06
146

一、简单思考

快问快答,在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),实际运行如下图所示

IMG_256
明明创建了三个func函数,为什么这样查询只出来两条数据呢,难道丢数据了吗?这个时候可以查询一下PostgreSQL的pg_proc系统表

SELECT proname,pronamespace::regnamespace,proargtypes,prorettype::regtype FROM pg_proc WHERE proname = 'func';
复制

如下图,可以看到在系统表中确确实实有三条数据,和之前的创建语句一一对应

IMG_257
调用并运行IMG_258

可以看到数据实际上并没有丢失,并且能正常按照逻辑运行。而出现使用\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
复制

结果如下

IMG_259

阅读到此处应该对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是如何确认的呢?我们接着往下看。IMG_260

接着查看recomputeNamespacePath ,开头就看见熟悉的身影,这个rawname拷贝了namespace_search_path,然后按","分割存储在namelist中,而他的数据又是那么亲切,那么有没有一种可能它就是search_path在内部的表现,所以先查看下namespace_search_path

IMG_261

在src/backend/utils/misc/guc_tables.c 我们可以看见这麽一段,所以在外部是search_path,在内部为namespace_search_path

IMG_262

接着我们回到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数据

IMG_263

接着就是处理oidlist  而我们不需要注意太多的东西  最开始说到,我们实际上只要关注activeCreationNamespace是如何被赋值的,因为activeCreationNamespace就是最终的namespace,而如图所示,它取到了oidlist中的第一个也就是public的oid,所以最终protest3会被创建在public这个公共模式下。


此处还可以留意一下activeSearchPath,后续其实在对上下文可见性那块其实有处理。IMG_264

由于此次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
复制

运行结果如下

IMG_265

通过开启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;
复制

运行结果如下

IMG_266

果不其然,schema_b.func()被隐藏了,为当前上下文不可见状态。都看到这了,干脆再翻一翻代码看看,这里面到底是怎么处理的。找到pg_function_is_visible对应的内部函数pg_function_is_visible

IMG_267

显而易见的,我们需要关注的是FunctionIsVisible函数,依旧是namespace.c文件

IMG_268

其实此处注释也说得很清楚了,就算是schema在search_path中,当函数名和参数完全一致时,会存在隐藏的情况,更加详细需要看FuncnameGetCandidates函数。而FuncnameGetCandidates关于此处的逻辑如注释所言(不想再将细节展开了已经太长了,第一次写有点缺经验),当前search_path中内容为schema_a,schema_b,而这两个schema中存在了一个一模一样(函数名和参数完全匹配)的func()对象,它内部会选择将schema_b.func()隐藏,再加上schema_b.func(aa text),最终呈现出两条记录。

IMG_269
其实PostgreSQL并不仅仅是对于函数对象,在使用带有“\”的命令,会选择隐藏数据。可以使用SQL查询一下is_visible等函数就知道了。

SELECT proname FROM pg_proc WHERE proname LIKE  '%is_visible';
复制

运行结果IMG_270

可以看到像表、类型等等这些个对象都会存在这种情况,由于在PostgreSQL中函数、存储过程这些存在多态的性质,所以实际上如果你去查看表对应的pg_table_is_visible这种函数其实理解起来更为简单,如下图:

IMG_271


六、总结

在PostgreSQL中,用户通过设置search_path ,来指定当前应该使用哪些schema,以及这些个schema的先后顺序。在创建的其他对象的时候,如果没有手动附加schema信息,那么将会按照search_path的规则(一般是第一个,$user这种存在额外的处理,如果不存在与当前用户同名的schema,则会选择下一个在路径中的schema),作为当前创建对象的模式。如果使用带有"\"的psql命令(如\df、\do、\dt等等,可以根据上述的is_visible相关函数推敲一下)去查找对象时,如果在search_path中的各个schema下有着能够完全匹配的对象,则有可能会隐藏部分数据,其实也就是数据库认为在当前search_path下,如果不附加schema信息,最优先的选择。


七、碎碎念

由于篇幅的原因诸多细节未完全展开,若文中存在错误或不当之处,敬请指出,以便我进行修正和完善。希望这篇文章能够帮助到你。

 

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
1人已赞赏
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论