Query树PostgreSQL中非常重要的一个数据结构。其一个特点是可被序列化/反序列化。这个特性很重要的一个应用是规则系统(Rule System)。例如,PostgreSQL中的视图(View)是构建在规则系统之上的,当我们定义一个视图:
CREATE VIEW v_test AS SELECT * FROM test WHERE grade = 9;
CREATE VIEW
复制
本质上是将AS后的语句的Query树进行序列化并存储到系统里:
SELECT ev_action FROM pg_rewrite WHERE ev_class = (SELECT oid FROM pg_class WHERE relname='v_test');
ev_action
--------------------------------------------------------------------------------------------------------------------------------
({QUERY :commandType 1 :querySource 0 :canSetTag true :utilityStmt <> :resultRelation 0 :hasAggs false :hasWindowFuncs false :hasTargetSRFs false :hasSubLinks false :hasDistinctOn false :hasRecurs
ive false :hasModifyingCTE false :hasForUpdate false :hasRowSecurity false :cteList <> :rtable ({RTE :alias {ALIAS :aliasname old :colnames <>} :eref {ALIAS :aliasname old :colnames ("empno" "grade
" "depno" "name" "sal")} :rtekind 0 :relid 50695 :relkind v :rellockmode 1 :tablesample <> :lateral false :inh false :inFromCl false :requiredPerms 0 :checkAsUser 0 :selectedCols (b) :insertedCols
...})
复制
通过这个序列化的Query树,我们可以知道这个Query的类型(commandType)、目标表、目标列等查询所需要的详细信息。而经过反序列化后,就可以还原为一棵完整的Query树,这棵Query树便可以直接进行Plan,生成执行计划,然后执行。实际上,我们查询视图的定义时,也会发现系统里的定义和我们原始的定义是有显著的差别的:
SELECT definition FROM pg_views WHERE viewname='v_test';
definition
---------------------------
SELECT test.empno, +
test.grade, +
test.depno, +
test.name, +
test.sal +
FROM test +
WHERE (test.grade = 9);
(1 row)
复制
这是因为系统里的定义实际上就是根据序列化的Query树(如上),经过反序列化再反编译而来的,并不是原始的定义语句。
应该说,PostgreSQL这种机制设计的还是非常优秀的。但是,在实际的代码实现中还存在一些不足。最致命的一个缺陷是,如果一些原因(比如缺陷修复等)导致Query树的结构定义有变动(比如增加了一个节点),这时候系统更新就是个非常麻烦的事。你将不得不重新初始化整个数据库群集!如果只是将程序进行了升级,将会导致很严重的问题,甚至可能导致系统无法连接!
我们来看一下pg_views中的definition是怎么来的。这个definition实际上就是通过backend/nodes/readfuncs.c中提供的_readQuery方法,再经过一些转化而来的。这个__readQuery其实就是反序列化Query树的过程。
static Query *
_readQuery(void)
{
READ_LOCALS(Query);
READ_ENUM_FIELD(commandType, CmdType);
READ_ENUM_FIELD(querySource, QuerySource);
local_node->queryId = UINT64CONST(0); /* not saved in output format */
READ_BOOL_FIELD(canSetTag);
...
READ_DONE();
}
复制
这些READ_XXX方法就是通过不断往前推进字符串(序列化的Query树)指针直到末尾,从而重新构建一棵Query树。所以,如果Query树的定义变动了,比如增加了节点,而原来系统里存在的这些Query树并没有新节点的信息,而READ_XXX方法的指针是不断往前推进的,因而最终会导致空指针的错误,从而引发系统的崩溃。
针对这个问题,我们在Halo产品中研发了Query树节点可重入读的技术。简单来说,就是READ_XXX方法的指针会根据需要重新调整位置,从而避免空指针的错误。其中一个比较关键的技术实现是token可重入读,部分代码如下:
/* reenterable pg_strtok */
const char *
pg_strtok_reentrant(char *token_name, int *length)
{
...
/* Reenterable token read support */
token_len = strlen(token_name) + 1;
token1 = malloc(sizeof(char) * (token_len + 1));
token1[0] = ':';
token1[1] = '\0';
strcat(token1, token_name);
token1[token_len] = '\0';
token2 = malloc(sizeof(char) * (token_len + 1));
token2[0] = '\0';
strncpy(token2, ret_str, token_len);
token2[token_len] = '\0';
if (strcasecmp(token1, token2) != 0)
{
*length = -1;
pg_strtok_ptr = prev_strtok_ptr;
free(token1);
free(token2);
return ret_str;
}
...
}
复制
我们通过比较读到的token和传入的token是否相等,如果不相等,工作指针便会回退到工作前的位置,从而实现了可重入读。
评论
