数据库内核月报

数据库内核月报 - 2019 / 02

PgSQL · 源码解析 · Json — 从使用到源码

Author: zhuyan

PostgreSQL从9.2开始支持Json类型,把它当成标准类型一种,渐渐地提供了12个SQL函数。这篇文章先简单介绍一下Json,然后对于12个函数每一个给出一个执行的例子,最后根据一条SQL语句,从源码角度分析如何执行的。源码那部分跟着代码看效果可能会好很多。

Json 简介

JSON用于描述资料结构,有以下形式存在。

一个Json数据类型的例子:

{
  "jobname":"linux_os_vmstat",
    "schedule":{
      "type":{"interval":
        "5m"
      },
      "start":"now",
      "end":"None"
    },
    "values":{
      "event":["cpu_r","cpu_w"],
      "data":["cpu_r"],
      "threshold":[1,1]
    },
    "objects":{
      "wintest1":"cpu"
    }
}

二  PostgreSQL 中的Json

在PostgreSQL 9.2中,增加了Json数据类型和与Json类型相关的两个函数(row_to_json 和array_to_json)。我们可以在PG中像其它类型一样存取Json类型的数据,也可以在数据库中把数据转化为Json数据格式输出。PG中提供几种操作符操纵Json数据,并且在之后的几个版本中,增加了Json相关的函数。

2.1   操作符

操作符 右操作数类型 描述 例子
-> int 得到Json数组的元素 '[1,2,3]'::json->2
-> text 得到Json对象的域值 '{"a":1,"b":2}'::json->'b'
->> int 得到Json数组的元素(text格式输出) '[1,2,3]'::json->>2
->> text 得到Json对象的域值(text格式输出) '{"a":1,"b":2}'::json->>'b'
#> array of text 得到指定位置的Json对象 '{"a":[1,2,3],"b":[4,5,6]}'::json#>'{a,2}'
#>> array of text 得到指定位置的Json对象(text格式输出) '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}'

这是官方文档中的表格,表格中以text格式输出意思是只输出需要的值,而不关心类型。由第一节知道,Json除了Object和Array之外,合法的值有string,number,bool和null。拿string来说,合法的是加双引号,text类型就只有里面的值。在实际使用中输出结果与数据库编码有关,通常使用的是UTF-8类型和ASCII码。混合编码或者其他类型可能导致错误。具体使用在下节例子中会感受的到。

2.2   函数

postgreSQL 9.3.6目前支持12个与Json相关的函数操作,可以将这些函数分为两类,一类不操纵Json类型数据,只是提供一个其他类型数据向Json转化的接口(如row_to_json)。另一类就是对Json操作的函数,快速获得其中某些特性(如 json_object_key)。下面就对每一个函数给出一个使用的例子。所有的操作都是基于两张表。一张表job表头(id : int  jobdesc : json),其中一行数据如Json简介一节中的示例,用到其它行数据会指明。另一张表films和数据如下:

code  |   title   | did | date_prod  |  kind  |   len
-------+-----------+-----+------------+--------+----------
UA502 | Bananas   | 105 | 1971-07-13 | Comedy | 01:22:00
UA123 | Apples    | 110 | 1999-09-09 | Comedy | 01:44:00
CN111 | Onec More | 111 | 1909-08-11 | Active | 01:54:00
  1. array_to_json(anyarray [, pretty_bool]) 把数组转化成Json类型数据。第一个参数是一个数组,第二个bool类型的表示数组中的元素会不会分行显示。

    先用array_agg函数生成数组:

    bank=# select array_to_json(array_agg(t)) from (select code,title from films) t;
    
    {"(UA502,Bananas)","(UA123,Apples)","(CN111,\"Onec More\")"}
    

    然后再将数组转化为Json:

    bank=# select array_to_json(array_agg(t)) from (select code,title from films) t;
      
    [{"code":"UA502","title":"Bananas"},{"code":"UA123","title":"Apples"},{"code":"CN111","title":"Onec More"}]
    

    第二个参数默认为false,如果为true:

    bank=# select array_to_json(array_agg(t),true) from (select code,title from films) t;
    
    [{"code":"UA502","title":"Bananas"},
    {"code":"UA123","title":"Apples"},
    {"code":"CN111","title":"Onec More"}]
    
  2. row_to_json(record [, pretty_bool]) 关于row_to_json的妙用可参看这里

    select row_to_json(t) from (select code,title from films) t;
       
    {"code":"UA502","title":"Bananas"}
    {"code":"UA123","title":"Apples"}
    {"code":"CN111","title":"Onec More"}
    
  3. to_json(anyelement) 其它格式转化为Json

    bank=# select to_json(t) from (select code,title from films) t;
       
    {"code":"UA502","title":"Bananas"}
    {"code":"UA123","title":"Apples"}
    {"code":"CN111","title":"Onec More"}
    
  4. json_array_length(json)

    bank=# select json_array_length(array_to_json(array_agg(t),true)) from (select code,title from films) t;
       
    3
    
  5. json_each(json) 把一个Json 最外层的Object拆成key-value的形式

    bank=# select json_each(to_json(t)) from (select code,title from films where code = 'UA502') t;
       
    (code,"""UA502""")
    (title,"""Bananas""")
    

    以这种方式使用,value会多出两个双引号,但是像下面这种方式使用就不会,原因还不太明白。

    bank=# select * from json_each( (select jobdesc from job where jobdesc->>'jobname' = 'linux_os_vmstat') );
    
      key    |              value
      ----------+----------------------------------
      jobname  | "linux_os_vmstat"
      schedule | {
            |       "type":{"interval":
            |           "5m"
            |       },
            |       "start":"now",
            |       "end":"None"
            |   }
      values   | {
            |       "event":["cpu_r","cpu_w"],
            |       "data":["cpu_r"],
            |       "threshold":[1,1]
            |   }
      objects  | {
            |       "wintest1":"cpu"
            |   }
    
  6. json_each_text(from_json json) 只是输出格式为text

    bank=# select json_each_text(to_json(t)) from (select code,title from films where code = 'UA502') t;
       
      (code,UA502)
    (title,Bananas)
    
  7. json_extract_path(from_json json, VARIADIC path_elems text[]) 根据第二个参数提供的路径确定Json中返回的对象。

    bank=# select json_extract_path(jobdesc,'objects','wintest1') from job where jobdesc->>'jobname' = 'linux_os_vmstat';
    
    "cpu"
    
  8. json_extract_path_text(from_json json, VARIADIC path_elems text[])

    bank=# select json_extract_path_text(jobdesc,'objects','wintest1') from job where jobdesc->>'jobname' = 'linux_os_vmstat';
    
    cpu
    
  9. json_object_keys(json) 获得最外层 object的key

    bank=# select json_object_keys(jobdesc) from job where jobdesc->>'jobname' = 'linux_os_vmstat';
    
    jobname
    schedule
    values
    objects
    
  10. json_populate_record(base anyelement, from_json json, [, use_json_as_text bool=false] 这个函数较复杂,作用是按照第一个参数定义的数据类型,把第二个参数的Json数据按照这种类型转换输出,第三个参数表示输出为Json类型的话是不是text类型输出。而且这个函数不能处理嵌套的object数据。也就是说key下面value就必须是待转化的值了。一次只能处理一行数据,觉得这个函数在以后版本还有待完善。

首先要定义下类型:

   bank=# create type JJ as (jobname text,school text);

本次操作的数据为 {“jobname”:”cs”,”school”:”csu”}

   bank=# select* from json_populate_record(null::JJ,(select jobdesc from job where jobdesc->>'jobname' = 'cs'));


   jobname | school
   ---------+--------
   cs      | csu

11. json_populate_recordset(base anyelement, from_json json, [, use_json_as_text bool=false] 和上一个函数不同之处就在于一次可以处理多行数据。

要处理的数据为:{“jobname”:”cs”,”school”:”csu”} 和 {“jobname”:100,”school”:”csu”}

   bank=# select* from json_populate_recordset(null::JJ,(select json_agg(jobdesc) from job where jobdesc->>'school' = 'csu'));

   jobname | school
   ---------+--------
   cs      | csu
   100     | csu

12. json_array_elements(json) 把一个Json数组的每一个元素取出来。

   bank=# select * from json_array_elements( (select jobdesc->'values'->'event' from job where jobdesc->>'jobname' = 'linux_os_vmstat') );
   "cpu_r"
   "cpu_w"

三 一条查询语句

1. 先谈插入

当Json作为一种标准的变长数据类型,进入到内存之后实际上是转化为变长的text类型。存到磁盘上和其他变长数据类型一样,当数据大于2k的时候就会出发toast机制,先对数据试着进行压缩,压缩之后还是大于2kb,就线外存储,放到另一张表里去。

struct varlena
{
  char    vl_len_[4];   /* Do not touch this field directly! */
  char    vl_dat[1];
};

这就是变长的存储结构,第一个变量是数据的长度(其实也不是长度,而是长度经过运算处理的结果),第二个变量是数据,只用一个大小为1的char数组表示数据的起始位置。对于变长数据有一系列的宏操作。因此新加入的Json类型操作在内存中都是与text类型进行交互。源码的重点也在于如何解析字符串。

2.执行一次查询

用一个查询语句的执行过程,来分析在源码中是如何处理Json类型数据的。使用job表完成:

select jobdesc->'jobname' from job where jobdesc->>'school' = 'csu';

首先要在缓存中找到job表的元组数据,如果缓存没有就到文件块中取。然后扫描每一个元组数据,得到一个元组数据后就需要调用Json提供的接口对数据进行分析,判断是否有一个名为‘school’的object的域值为‘csu’。如果是则返回名为‘jobname’的object域值给调用端,不是的话就返回空的结果。Json接口只负责返回名为’school ’的object域值,判断域值是否满足条件也由调用端决定。

取出的一个元组数据text类型,需要转化为Json词法分析上下文这种数据结构:

typedef struct JsonLexContext
{
  char  * input;             // 输入的待解析的json字符串,
  int input_length;          //字符串的长度
  char  * token_start;           //每次分析起始位置      (蓝色)
  char  * token_terminator;    //每次分析的结束位置    (蓝色)
  char  * prev_token_terminator; //上次分析的结束为止    (蓝色)
  JsonTokenType token_type;          //分析的字串类型
  int lex_level;     //分析的“深度”        (蓝色)
  int line_number;         //当前分析到的行数      (红色)
  char  * line_start;    //当前行的起始位置      (红色)
  StringInfo  strval;            //分析得到的结果
} JsonLexContext;

这个结构就像游标一样,遍历一个Json类型数据,对其中每一个object进行解析,得到值再与where子句后面的条件比较,确定是否满足条件。具体是通过makeJsonLexContext这个函数完成由text到JsonLexContext的转化,input指向text转化而来数据的起始位置,表中蓝色为控制变量,在分析过程中不断变化,表示分析过程的状态。红色是为了出错时可以定位到具体的位置。strval为每一次分析得到的临时结果。

除了初始化词法分析上下文,还要初始化最终的保存结果的结构,不同的操作可能对应不同的结构结构(如-> 和 其它SQL函数调用),本例中使用GetState:

typedef struct GetState
{
  JsonLexContext *lex;    //词法解析上下文
  JsonSearch  search_type;  //搜索类型:object array path
  int search_index;         //搜索索引
  int array_index;    //数组索引
  char  *search_term;           //SQL语句传入的搜索条件的值
  char  *result_start;          //结果起始位置指针(和lexcontext
  //token_terminator一起得到tresult
  text  *tresult;   //最终结果
  bool  result_is_null;         //结果是否为空
  bool  normalize_results;  //是否是text类型 (由解引操作符得到   ->   ->>
  bool  next_scalar; //函数get_scalar是否可以得到tresult结果(字符串 数值 布尔值)
  char  path;           //路径
  int npath;            //路径数量
  char  current_path;         //当前分析到的路径指针
  bool  *pathok;    //bool类型的数组判断走过的路径是否每一步都正确
  int *array_level_index; //数组分析深度的指针
  int *path_level_index;  //路径分析深度的指针
}GetState;

初始化后search_term的值为‘school’,用来进行最后的判断。需要解释下的就是path,请参考上一节函数示例7和8 。path记录的就是函数中的路径。

解析需要判断是否符合条件,并将符合条件的值存近tresult中,这些工作由JsonSemAction完成:

typedef struct JsonSemAction
{
  void     *semstate;
  json_struct_action object_start;
  json_struct_action object_end;
  json_struct_action array_start;
  json_struct_action array_end;
  json_ofield_action object_field_start;
  json_ofield_action object_field_end;
  json_aelem_action array_element_start;
  json_aelem_action array_element_end;
  json_scalar_action scalar;
} JsonSemAction;

都是一些函数指针,不同类型的函数类型也不一样。semstate是GetState的指针,当然如果是其它类型的state就是其它类型state的指针。

{
  "jobname":"linux_os_vmstat",   
}

例: 假如拿到的第一行元组数据为简介中所示数据,调用json_lex函数吃掉第一个”{  ”符号,表明这是一个object,token_type初始化为JSON_TOKEN_OBJECT_START , 再调用parse_object函数,继续推进吃掉“ “ ”号,知道接下来是一个字符串,调用json_lex_string,把值jobname读到strval里面,同时token_type变为JSON_TOKEN_STRING。此时该分析一个object的值域,调用函数parse_object_field,在这个函数中,首先把存在strval里面的值拿出来,再吃掉“:”,之后根据JsonSemAction找到对应类型的处理函数,此处对应的是get_object_field_start,这是函数主要是判断本次解析是否符合条件(也就是strval中的值是否是’school ’), 根据下一个符号的类型判断是否需要递归。因为一个object的值域可以是一个object,一个array或者一个简单的值,每一种有对应的函数。本例中下一个字符是““ ”,还是string,调用函数parse_scalar,推进JsonLexContext到字符串末尾。最后调用JsonSemAchtion中的get_object_field_end,在这里判断是否符合条件,如果符合就把值域写到tresult中。这也就是一个符合条件的返回结果。如果不符合就继续解析。

当根据where子句的条件找到一个元组变量的时候,就使用select中的条件得到元组变量中对应的值域,解析方式都是相同的。当然不会找到一个就停止,要返回所有满足条件的值,就需要遍历所有的元组。

3. 其它

总的来讲解析用JsonLexContext当作游标,不同类型的state当作结果集变量,JsonSemAction判断是否正确。但并不是所有的操作都是这样,当调用Json 支持的SQL函数的时候,不同的函数都有不同的处理方式,比如row_to_json,得到一行元组变量和它的类型给数据加上{}或者[]等,变成有效的Json格式数据就ok。

参考:

http://www.ietf.org/rfc/rfc4627.txt

http://www.postgresql.org/docs/9.3/static/functions-json.html#FUNCTIONS-JSON-OP-TABLE

http://hashrocket.com/blog/posts/faster-json-generation-with-postgresql

http://www.linuxidc.com/Linux/2013-12/94354.htm