postgresql内核开发之Oracle date类型兼容

                ORACLE的date类型,不专注于date,还带有time,不像postgres,date就date,不带时间,两者之间的差异就造成了一些应用做迁移时的麻烦,如果原来逻辑就是需要date有时分秒,不能接受postgres的date没有时分秒,这就需要增加一个新的date类型。

        古人云:谋而后动,知止而有得。先看看postgres date类型是一个什么样的存在,如果能做些改动,即能让它有时分秒,也不影响原来用法,那是最完美的。那么怎么看date类型?或者说怎么研究一个想要了解的类型呢?在postgresql内部,对于类型xxxx,有一类xxxx_in或xxxxin函数,作为为各个类型的入口函数,当然也有一类xxxx_out或xxxxout函数,作为各个类型的出口函数。这里的“入”指的是外部类型转到数据库内部类型的过程,“出”指的是内部类型转到外部类型的过程,以 date类型为例,外部类型是我们能看懂的,如“2016-12-03”,但在数据库内部它就是被转成了一串看起来无意义的数字6181,出口函数又将这些无意义的值转能我们能看懂的东西打印出来,如这个6181又变回了2016-12-03”。像其它的类型如int、varchar、text、timestamp等之类,有int4in,int8in,varcharin,textin,timestamp_in之类的in函数,自然也有out函数。至于这么做的好处还请大家自己思考了。

    言归正传,继续讨论date类型,打开datein函数,代码如下:

Datum
date_in(PG_FUNCTION_ARGS)
{
	char	   *str = PG_GETARG_CSTRING(0);
	DateADT		date;
	fsec_t		fsec;
	struct pg_tm tt,
			   *tm = &tt;
	int			tzp;
	int			dtype;
	int			nf;
	int			dterr;
	char	   *field[MAXDATEFIELDS];
	int			ftype[MAXDATEFIELDS];
	char		workbuf[MAXDATELEN + 1];

	dterr = ParseDateTime(str, workbuf, sizeof(workbuf),
						  field, ftype, MAXDATEFIELDS, &nf);
	if (dterr == 0)
		dterr = DecodeDateTime(field, ftype, nf, &dtype, tm, &fsec, &tzp);
	if (dterr != 0)
		DateTimeParseError(dterr, str, "date");

	switch (dtype)
	{
		case DTK_DATE:
			break;

		case DTK_CURRENT:
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
			  errmsg("date/time value \"current\" is no longer supported")));

			GetCurrentDateTime(tm);
			break;

		case DTK_EPOCH:
			GetEpochTime(tm);
			break;

		case DTK_LATE:
			DATE_NOEND(date);
			PG_RETURN_DATEADT(date);

		case DTK_EARLY:
			DATE_NOBEGIN(date);
			PG_RETURN_DATEADT(date);

		default:
			DateTimeParseError(DTERR_BAD_FORMAT, str, "date");
			break;
	}

	/* Prevent overflow in Julian-day routines */
	if (!IS_VALID_JULIAN(tm->tm_year, tm->tm_mon, tm->tm_mday))
		ereport(ERROR,
				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
				 errmsg("date out of range: \"%s\"", str)));

	date = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE;

	/* Now check for just-out-of-range dates */
	if (!IS_VALID_DATE(date))
		ereport(ERROR,
				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
				 errmsg("date out of range: \"%s\"", str)));

	PG_RETURN_DATEADT(date);
}

        这里 char *str = PG_GETARG_CSTRING(0); 能拿到我们发的date字符串,比方说 select '2016-12-03'::date,那么这个’2016-12-03‘就能被str取到。接下来 ParseDateTime函数和DecodeDateTime函数会把时间字符串解析转换到一个时间结构 tm中,然后在  

date = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE;

中把tm转成了一个date,最后返回的就是这个date,date的类型是DateADT,这就是时间内部的数据结构,那具体是什么呢?请往下看:

typedef signed int int32;

typedef int32 DateADT

DateADT date;

也就是说时间类型实际就是一个无符号的int,那么这个int怎么表示一个日期呢,这就得看上面的date2j函数了。date2j函数是求公元前4713年11月24作为开始到传入的tm中的日期这一天经过的所有天数,为什么这选这天作为开始可能有历史典故吧。这个天数的上限就是int的上限2147483647,所以它能表示的时间是公元前4713年11月24 到公元 5874898年6月3号,这个范围之间天数就是2147483647。至于个POSTGRES_EPOCH_JDATE是公元前4713年11月24到2000年1月1号的经过的天数,结合起来,date里的数值就是从2000年1月1号开始以来经过的天数,这是postgres内部的date表现形式,是真实在内存中参与运算的值,也是存在硬盘上的值,可以做个实现验证下,如下图:


建表只有一列date, 插入一行日期值,求出这个日期和2000年1月1日之间的天数72318361。再取到这个表的oid, 在数据库data目录打开这个表oid对应的数据文件,因为只插了一行记录,那么肯定在页面最后面,不用再通过页面头数据通过页内偏移求位置。直接翻到文件最后,因为是64位,按8字节对齐,只有一列且无空值,那么元组头占23字节,对齐补一个字节元组头共占24字节。date类型实际存储用的是int,占4个字节,元组头后的4个字节为元组数据字节,加上头一共28字节,为了对齐再补4个字节,这行元组共计占32字节,其中第25到28个字节为我们插入的date值,再看打开后的页面尾部截图:


每行为16字节,因是数据页所以不用预留特殊空间,根据上面计算最后的两行32字节是我们插入的元组,该元组的第25到28字节,最后一行的第9到12个字节应该为date '200000-12-12'实际存储值,这四个字节为:99 7D 4F 04。再把我们上面求的天数72318361,转成16进制如下:



72318361转成16进制为4 4F 7D 99,我用的电脑是小端机,内存中值要调换下顺序,换一下就是99 7D 4F 04,正是我们存在页面上的date值。

           postgres的页面及元组具体结构的以后会详细介绍,本文不做介绍。这个例子充分说明了数据库类型外部表现形式和内部表现形式之间的差异,那对我们实现一个带时分秒的date有什么帮助吗?很遗憾,只充分说明了原生date没办法再放时分秒了,想直接扩展行不通,没地方存了。难道我们要重新定义一个date?比方说8字节整型来存放,容量肯定没问题,但工作量过大,改动也多,稳定性不容易保证。

          那如何快速实现一个呢?可以参考下EDB(Enterprise DB),EDB是基于postgresql对oracle兼容做的最好的数据库,而且做的很成功,可以学习下他们的思路,请看EDB中date相关测试语句:


很明显,EDB把date类型换成了timestamp, timestamp肯定能承担带时分秒的date的功能,那我们如何实现呢?

          找到语法解析逻辑中解析date的地方,换成timestamp的逻辑即可。在gram.y中 GenericType 是通用类型解析,里面就包括了date,我们对date单独处理,转换成timestamp。如下图:


修改后重新编译安装,再跑效果如下图:


         效果同EDB一样,但存在的问题也和EDB一样,带了精度,如下EDB效果图:


秒后面还有精度,但oracle的date只精确到秒,没有后面的尾巴,当然我们也是可以去掉的,指定成timestamp(0)就可以,再次修改内容如下图:


再运行新的修改版本如下效果:


这样用起来和oracle完全一样了。当然与时间相关的函数还要处理下,原date类型作为参数的可以不要动,但返回为date类型的要视情况做些修改,比方说to_date函数原来返回的是date,修改后成了timestamp,功能要做些调整,此处不再发散介绍了。另外,修改后date功能变成了timestamp,为了减少影响,最好通过参数控制起来, 如果不需要时date还是原来date,需要时把参数打开,date就变成timestamp,参数如何增加,后续会继续介绍。


  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值