Oracle PL/SQL Programming 第10章:Dates and Timestamps 读书笔记

总的目录和进度,请参见开始读 Oracle PL/SQL Programming 第6版

日期非常复杂:它们不仅是高度格式化的数据,而且有无数的规则来确定有效值和有效计算(闰日和年份、夏令时变化、国家和公司假期、日期范围等)。

日期类型可存放年月日,时分秒和时区的信息。

Datetime Datatypes

最开始只有DATE,9i后有了TIMESTAMP和两个INTERVAL类型。

目前有4个datetime数据类型:

  1. DATE
    存储日期和时间,精确到秒。 不包括时区。
  2. TIMESTAMP
    解析到十亿分之一秒(精确到小数点后九位),其他同DATE。例如TIMESTAMP '1997-01-31 09:26:50.124'
  3. TIMESTAMP WITH TIME ZONE
    带时区,其他同TIMESTAMP。例如TIMESTAMP '1999-04-15 8:00:00 -8:00'
  4. TIMESTAMP WITH LOCAL TIME ZONE
    与上一个类型的区别是,数据库中始终以数据库时区存储,此处存在本地(会话)时区到数据库时区的转换。从数据库检索值时,该值将从数据库时区转换为本地(会话)时区。SYSTIMESTAMP和SYSDATE都属于此类型。

理解TIMESTAMP WITH LOCAL TIME ZONE类型,可以以视屏会议为例。我定了一个北京时间下午2点的会,我把时间存入库中(会转换为数据库时区),那国外同事可以知道对应的本地时间是多少。

如果不希望数据库做时区转换,就用TIMESTAMP 或 TIMESTAMP WITH TIME ZONE 。

问题:如何知道本地时区和数据库时区?

sql 复制代码
-- DB timezone
select dbtimezone from dual;

-- session timezone
select sessiontimezone from dual;

-- DB timezone + session timezone
show nls

-- timezone名称
select * from V$TIMEZONE_NAMES;

小知识:COORDINATED UNIVERSAL TIME

协调世界时,缩写为 UTC,是使用高度准确和精确的原子钟测量的,它构成了我们全球民用时间系统的基础。 例如,时区都是根据与 UTC 的偏差程度来定义的。 UTC 是原子时,通过闰秒机制定期调整,使其与地球自转确定的时间保持同步。

虽然 GMT 和 UTC 显示的时间相同,但还是有区别的:GMT 现在被认为只是一些欧洲和非洲国家正式使用的时区。 但 UTC 不是一个时区,而是新的时间标准,它是全球时钟时间和时区的基础。

为什么使用缩写 UTC 而不是 CUT? 因为标准机构无法就使用英语缩写 CUT 还是法语缩写 TUC 达成一致,因此妥协为在 UTC 上做出了妥协,因为 UTC 与这两种语言都不匹配。

有关 UTC 的更多信息,请参阅美国国家标准技术研究院关于 UTC 的文档常见问题解答页面。

Declaring Datetime Variables

TIMESTAMP可以定义一个秒的精度,默认为6,最大为9。

sql 复制代码
TIMESTAMP [(fractional_seconds_precision)]

因此TIMESTAMP(0) 相当于DATE。

SYSTIMESTAMP 的精度为6。可以看出,其返回类型为TIMESTAMP WITH TIME ZONE:

sql 复制代码
SQL> select SYSTIMESTAMP from dual;

SYSTIMESTAMP                          
--------------------------------------
14-MAR-24 10.26.06.401821000 PM +08:00

Choosing a Datetime Datatype

选择的数据类型取决于您想要存储时间的精度。

  • 如果精度需要到几分之一秒,请使用一种 TIMESTAMP 类型。
  • 如果希望数据库自动在数据库时区和会话时区之间转换,请使用 TIMESTAMP WITH LOCAL TIME ZONE。
  • 如果需要跟踪输入数据的会话时区,请使用 TIMESTAMP WITH TIME ZONE。
  • 可以使用 TIMESTAMP 代替 DATE。 不包含亚秒精度的 TIMESTAMP 占用 7 个字节的存储空间,此时等同于 DATE 数据类型。 当 TIMESTAMP 确实包含亚秒数据时,占用 11 个字节。

其他一些考虑:

  • 除非为了兼容之前的代码,或数据库低于9i,否则不要用DATE。
  • PL/SQL和数据库中的数据类型要一致,避免精度损失。

Getting the Current Date and Time

函数 时区 返回的数据类型
CURRENT_DATE session DATE
CURRENT_TIMESTAMP session TIMESTAMP WITH TIME ZONE
LOCALTIMESTAMP session TIMESTAMP
SYSDATE database DATE
SYSTIMESTAMP database TIMESTAMP WITH TIME ZONE

带CURRENT的是session时区,带SYS的是数据库时区。

选择哪个函数,取决于你需要时区吗?需要数据库还是会话的时区?

time_zone初始化参数可以设置session时区:

sql 复制代码
show parameter time_zone;
select SESSIONTIMEZONE from dual;

alter session set time_zone = 'Europe/London';
select SESSIONTIMEZONE from dual;

alter session set time_zone = local;
select SESSIONTIMEZONE from dual;

输出是这样的:

sql 复制代码
SESSIONTIMEZONE                                                            
---------------------------------------------------------------------------
Asia/Shanghai

SESSIONTIMEZONE                                                            
---------------------------------------------------------------------------
Europe/London

SESSIONTIMEZONE                                                            
---------------------------------------------------------------------------
Asia/Shanghai

如果需要的类型在以上表中没有,那就自己显式转换。

例如如果我需要数据库服务器的TIMESTAMP(无时区):

sql 复制代码
select SYSTIMESTAMP from dual;
SYSTIMESTAMP                          
--------------------------------------
15-MAR-24 03.26.14.219021000 PM +08:00

select CAST(SYSTIMESTAMP AS TIMESTAMP) from dual;
CAST(SYSTIMESTAMPASTIMESTAMP)  
-------------------------------
15-MAR-24 03.26.29.158148000 PM

又例如从SYSDATE转换为TIMESTAMP:

sql 复制代码
select TO_CHAR(sysdate,'DD-MON-YYYY HH:MI:SS AM') from dual;
TO_CHAR(SYSDATE,'DD-MON-YYYYHH:M
--------------------------------
15-MAR-2024 03:30:32 PM

实际时间的精度与底层硬件相关,因为都是调底层函数,例如Linux就是用GetTimeOfDay。

Interval Datatypes

时间间隔类型(以下简称间隔类型)是时间的增量。如几分钟后,几年前。

支持2种间隔类型,均为ISO SQL标准(详见A Guide to the SQL Standard):

  • INTERVAL YEAR TO MONTH,精度到月
  • INTERVAL DAY TO SECOND,精度到小数秒

为何要2种,1种不行吗?这是由于年月日时分秒中,月是唯一长度不确定的时间组件。2月有28或29天,其他月有30或31天。因此以月为分隔线,形成了2种间隔类型。这种月历是有罗马皇帝尤利乌斯·凯撒设计的。

Declaring INTERVAL Variables

间隔类型的3个组件可以指定精度(或最大值):

  • 年,从0到4,默认为2
  • 日,从0到9,默认为2
  • 秒,从0到9,默认为6

其他则无需指定,月的范围为0-11,时为0-23,分为0-59。

When to Use INTERVALs

以下为需要使用间隔类型的场景。

查找两个日期时间值之间的差异

如计算工龄。

例子:

sql 复制代码
set verify off

SELECT
    TO_TIMESTAMP('&&fromdate', 'dd-mon-yyyy') - TO_TIMESTAMP('&&todate', 'dd-mon-yyyy')
    as diff_datetime
FROM
    dual;

SELECT
    (TO_TIMESTAMP('&&fromdate', 'dd-mon-yyyy') - TO_TIMESTAMP('&&todate', 'dd-mon-yyyy')) year to month
	as interval_year_to_month
FROM
    dual;
    
SELECT
    EXTRACT(YEAR FROM (TO_TIMESTAMP('&&fromdate', 'dd-mon-yyyy') - TO_TIMESTAMP('&&todate', 'dd-mon-yyyy')) year TO MONTH) 
	as interval_years
FROM
    dual;
    
SELECT
    EXTRACT(MONTH FROM (TO_TIMESTAMP('&&fromdate', 'dd-mon-yyyy') - TO_TIMESTAMP('&&todate', 'dd-mon-yyyy'))year TO MONTH) 
	as interval_months
FROM
    dual;

提示输入时,以此输入29-DEC-198826-DEC-1995

输出:

sql 复制代码
DIFF_DATETIME        
---------------------
-2553 00:00:00.000000


INTERV
------
-07-00


INTERVAL_YEARS
--------------
            -7


INTERVAL_MONTHS
---------------
              0

显然,如果没有间隔类型,代码会复杂很多。

注意,以上INTERVAL YEAR TO MONTH 运算时进行了舍入。

指定时间段

例如跟踪流水线某一环节所用的平均时长,最大时长,最小时长(假设记录了开始和结束时间)。

Datetime Conversions

主要指字符串和日期时间之间的转换。

作者的随书示例代码showdaterange.sql,给出了最大最小的日期范围:

sql 复制代码
Latest date: 12-31-9999
。。。
Earliest date: 01-01-4712

此代码用试错的方法得到以上结果,使用当前日期,每次加1或减1,直到出错。

另外,如果日期没有指定时间部分,则时间部分默认为12:00:00 a.m,即凌晨。而12:00:00 p.m为正午。

转换的格式是有日期格式模型指定的。

From Strings to Datetimes

不要做隐式转换,其依赖于NLS_DATE_FORMAT设置。

PL/SQL支持以下的转换函数:

  • TO_DATE:从字符串或数字(表示儒略日期,即自公元前 4712 年 1 月 1 日以来经过的天数。)到DATE。
  • TO_TIMESTAMP:从字符串到timestamp
  • TO_TIMESTAMP_TZ:从字符串到带时区(数据库或会话)的timestamp

示例:

sql 复制代码
SELECT
    TO_TIMESTAMP('06/2/2002 09:00:00.50 PM', 'mm/dd/yyyy hh:mi:ssxff AM')
FROM
    dual;

-- 按照日期格式模型,x为Local radix(小数点) character, ff为小数秒
SELECT
    TO_TIMESTAMP_TZ('06/2/2002 09:00:00.50 PM', 'mm/dd/yyyy hh:mi:ssxff AM TZD')
FROM
    dual;

输出为:

sql 复制代码
TO_TIMESTAMP('06/2/200209:00:00
-------------------------------
02-JUN-02 09.00.00.500000000 PM

-- 应该是把session时区代入了,而非数据库时区
TO_TIMESTAMP_TZ('06/2/200209:00:00.50PM','MM/
---------------------------------------------
02-JUN-02 09.00.00.500000000 PM ASIA/SHANGHAI

日期错误在ORA-01800 和 ORA-01899 之间。

From Datetimes to Strings

使用TO_CHAR (datetime)。此函数可转换datetime或interval。

sql 复制代码
to_char(datetime|interval [,fmt [,nlsparam]])

若未指定fmt,则使用NLS设置的值。

示例:

sql 复制代码
-- fm表示返回一个没有前导或尾随空格的值。
select TO_CHAR (SYSDATE, 'FMMonth DD, YYYY') from dual;
select TO_CHAR (SYSDATE, 'fmMon DDth, YYYY') from dual;
-- 显示日期的年中日、月中日和周中日
select TO_CHAR (SYSDATE, 'fmDDD fmDD D ') from dual;
select TO_CHAR (SYSDATE, '"In month "RM" of year "YEAR') from dual;
select TO_CHAR(systimestamp,'YYYY-MM-DD HH:MI:SS.FF4 TZH:TZM') from dual;

输出:

sql 复制代码
TO_CHAR(SYSDATE,'FMMOND
-----------------------
Mar 15TH, 2024


TO_CHAR(SYSDATE,'FMMONTHDD,YYYY')            
---------------------------------------------
March 15, 2024

TO_CHAR(S
---------
75 15 6 

TO_CHAR(SYSDATE,'"INMONTH"RM"OFYEAR"YEAR')                      
----------------------------------------------------------------
In month III  of year TWENTY TWENTY-FOUR

TO_CHAR(SYSTIMESTAMP,'YYYY-MM-D
-------------------------------
2024-03-15 10:06:34.8542 +08:00

注意:对 TIMESTAMP 类型有效的格式元素对 DATE 类型无效。

因此,最后一个例子中,如果将函数systimestamp换为sysdate,则会报错。

sql 复制代码
SQL Error: ORA-01821: date format not recognized

实际上格式并没有错,错在应用到了错误的数据类型上。

Working with Time Zones

指定时区可以用以下几种方法:

  • 使用距 UTC 时间一定小时数和分钟数的正位移或负位移, 位移必须在−12:59 到+13:59 范围内。 例如,-5:00 相当于美国东部标准时间。
  • 使用时区区域名称,如US/Eastern, US/Pacific。
  • 使用时区区域名称和缩写的组合,如 US/Eastern EDT 表示美国东部夏令时。

下例中没有指定时区,因此使用当前的会话时区:

sql 复制代码
TO_TIMESTAMP_TZ ('12312005 083015.50', 'MMDDYYYY HHMISS.FF')

再次提醒,隐式操作是不建议的,因此建议利用SESSIONTIMEZONE获取会话时区,然后代入TO_TIMESTAMP_TZ的参数。

datetime包括日期和时间两部分,因此,以下比较是错误的:

sql 复制代码
IF SYSDATE = TO_DATE('1-Jan-2015','dd-Mon-yyyy')

应改为:

sql 复制代码
IF TRUNC(SYSDATE) = TO_DATE('1-Jan-2015','dd-Mon-yyyy');

下例中,TZH和TZM分别表示时区小时和时区分钟。详见Datetime Format Elements

sql 复制代码
TO_TIMESTAMP_TZ ('123120 083015.50 −5:00', 'MMDDYY HHMISS.FF TZH:TZM')

下例中,TZR表示时区区域。

sql 复制代码
TO_TIMESTAMP_TZ ('02-Nov-2014 01:30:00 EST', 'dd-Mon-yyyy hh:mi:ss TZR')

但重点在于,取决于是否为夏令时。此表示可能会产生歧义,即表示两个不同的时间。如果将会话参数 ERROR_ON_OVERLAP_TIME 设置为 TRUE(默认值为 FALSE),则每当您由于夏令时更改而指定不明确的时间时,数据库都会给出错误消息。

仅凭时区区域名称并不能区分标准时间和夏令时。 为了消除歧义,您还必须指定时区缩写,我在接下来的两个示例中已完成此操作。 使用缩写 EDT 指定东部夏令时间。以下为正确(不产生歧义的)写法:

sql 复制代码
-- TZD指定了夏令时
TO_TIMESTAMP_TZ ('02-Nov-2014 01:30:00.00 US/Eastern EDT',
                         'dd-Mon-yyyy hh:mi:ssxff TZR TZD')

为了避免歧义,建议使用小时和分钟指定时区偏移量(如 -5:00),或使用完整区域名称和时区缩写的组合(如 US/Eastern EDT)。 如果您单独使用地区名称并且夏令时存在歧义,数据库将通过假设适用标准时间来解决歧义。

最佳做法是使用完整的地区名称,例如 US/Eastern 或 America/Detroit,而不是三个字母的缩写 EST。 有关更多信息,请参阅 Oracle Metalink 注释 340512.1 时间戳和时区 - 常见问题。

您可以通过查询 V$TIMEZONE_NAMES 视图来获取 Oracle 支持的时区区域名称和时区缩写的完整列表。当您查询时,请注意时区缩写不是唯一的。

时区标准

时区的名称没有标准,而且可能重复。所以,TO_TIMESTAMP 函数不允许您单独使用缩写来指定时区。

sql 复制代码
-- 比等于1的多多了
select tzabbrev, count(*) from V$TIMEZONE_NAMES group by tzabbrev having count(*) > 1;

select * from V$TIMEZONE_NAMES where tzabbrev = 'UTC';

Requiring a Format Mask to Match Exactly

将字符串转换为日期时间时,TO_* 转换函数通常会进行一些调整:

  • 字符串中多余的空格将被忽略。
  • 数值(例如日期或年份)不必包含前导零。
  • 待转换字符串中的标点符号可以简单地匹配格式中标点符号的长度和位置。

FX格式元素要求字符数据和格式模型之间精确匹配。因此以下示例都会报错:

sql 复制代码
TO_DATE ('1-1-4', 'fxDD-MM-YYYY')
TO_DATE ('7/16/94', 'FXMM/DD/YY')
TO_DATE ('JANUARY^1^ the year of 94', 'FXMonth-dd-"WhatIsaynotdo"yy')

FX是一个切换开关,因此以下示例是正确的,对于月份前的FX,实际上是关闭了精确匹配要求:

sql 复制代码
TO_DATE ('07-1-1994', 'FXDD-FXMM-FXYYYY')

Easing Up on Exact Matches

FM(fill mode)格式元素返回一个没有前导或尾随空格的值。FM起抵消FX的作用。

sql 复制代码
-- 错误
select TO_DATE ('07-1-94', 'FXDD-FXMM-FXYYYY') from dual;

-- 正确
select TO_DATE ('07-1-94', 'FXfmDD-FXMM-FXYYYY') from dual;
select TO_DATE ('07-1-94', 'FXfmDD-MM-FXYYYY') from dual;
select TO_DATE ('07-1-94', 'FXfmDD-MM-YYYY') from dual;

与FX一样,FM也是一个切换开关。

Interpreting Two-Digit Years in a Sliding Window

RR格式元素让您仅使用两位数即可将 20 世纪的日期存储在 21 世纪中。

注意,通常20世纪是指1901年1月1日到2000年12月31日。而RR的20世纪由1900-1999年组成,21世纪由2000-2099年组成。

如果当前年份是本世纪上半叶(0 到 49 年),则:

  • 如果输入为 0 到 49,RR 将返回当前世纪。
  • 如果输入为 50 到 99,RR 将返回上一个世纪。

如果当前年份是本世纪后半叶(50 到 99 年),则:

  • 如果输入为 0 到 49,RR 将返回下个世纪。
  • 如果输入为 50 到 99,RR 将返回当前世纪。

例如:

sql 复制代码
SELECT TO_CHAR(TO_DATE('27-OCT-98', 'DD-MON-RR'), 'YYYY') "Year" FROM DUAL;
Year
----
1998


SELECT TO_CHAR(TO_DATE('27-OCT-18', 'DD-MON-RR'), 'YYYY') "Year" FROM DUAL;
Year
----
2018

RR已经置入了默认的日期格式中:

sql 复制代码
select value from nls_session_parameters
where parameter = 'NLS_DATE_FORMAT';

VALUE                                                           
----------------------------------------------------------------
DD-MON-RR

Converting Time Zones to Character Strings

时区信息由以下元素组成:

  • 与 UTC 的时差和分钟差
  • 时区区域名称
  • 时区缩写

所有这些元素都单独存储在 TIMESTAMP WITH TIME ZONE 变量中。 与 UTC 的偏移始终存在,其他2个元素是否显示取决于最初是否指定了该信息。 因为UTC 偏移量和时区区域之间存在一对多关系。

例如:

sql 复制代码
set serveroutput on
DECLARE
   ts1 TIMESTAMP WITH TIME ZONE;
   ts2 TIMESTAMP WITH TIME ZONE;
   ts3 TIMESTAMP WITH TIME ZONE;
BEGIN
     ts1 := TO_TIMESTAMP_TZ('2002-06-18 13:52:00.123456789 −5:00',
                          'YYYY-MM-DD HH24:MI:SS.FF TZH:TZM');
     ts2 := TO_TIMESTAMP_TZ('2002-06-18 13:52:00.123456789 US/Eastern',
                          'YYYY-MM-DD HH24:MI:SS.FF TZR');
     ts3 := TO_TIMESTAMP_TZ('2002-06-18 13:52:00.123456789 US/Eastern EDT',
                          'YYYY-MM-DD HH24:MI:SS.FF TZR TZD');

     DBMS_OUTPUT.PUT_LINE(TO_CHAR(ts1,
        'YYYY-MM-DD HH:MI:SS.FF AM TZH:TZM TZR TZD'));
     DBMS_OUTPUT.PUT_LINE(TO_CHAR(ts2,
        'YYYY-MM-DD HH:MI:SS.FF AM TZH:TZM TZR TZD'));
     DBMS_OUTPUT.PUT_LINE(TO_CHAR(ts3,
        'YYYY-MM-DD HH:MI:SS.FF AM TZH:TZM TZR TZD'));
  END;

输出:

sql 复制代码
2002-06-18 01:52:00.123457000 PM +05:00 +05:00 
2002-06-18 01:52:00.123457000 PM -04:00 US/EASTERN EDT
2002-06-18 01:52:00.123457000 PM -04:00 US/EASTERN EDT

Padding Output with Fill Mode

FM格式元素可以去除默认的前导零和填充空格。例如:

sql 复制代码
SQL> select TO_CHAR (SYSDATE, 'Month DD, YYYY') from dual;

TO_CHAR(SYSDATE,'MONTHDD,YYYY')              
---------------------------------------------
March     17, 2024

SQL> select TO_CHAR (SYSDATE, 'FMMonth DD, YYYY') from dual;

TO_CHAR(SYSDATE,'FMMONTHDD,YYYY')            
---------------------------------------------
March 17, 2024

Date and Timestamp Literals

ISO SQL 标准支持日期和时间戳literal。

sql 复制代码
-- 日期literal
DATE '2024-03-17' 
-- 时间戳literal
-- +8:00表示时区,可选。如果省略,则时区将默认为会话时区。
TIMESTAMP '2024-03-17 23:04:00.122334 +08:00' 

select sysdate - DATE '2011-10-10' as duration from dual;
  DURATION
----------
4543.38997

日期和时间戳literal的格式由 ANSI/ISO 标准规定。注意,其与NLS_DATE_FORMAT会话设置无关。

Interval Conversions

interval包含的元素:

元素名 范围
YEAR 1 到 999,999,999
MONTH 0 到 11
DAY 0 到 999,999,999
HOUR 0 到 23
MINUTE 0 到 59
SECOND 0 到 59.999999999

Converting from Numbers to Intervals

NUMTOYMINTERVALNUMTODSINTERVAL 函数允许您将单个数值转换为间隔数据类型之一。

示例,单位是必选的:

sql 复制代码
SQL> select NUMTOYMINTERVAL (10.75,'Year') from dual;

NUMTOY
------
+10-09

SQL> select NUMTOYMINTERVAL (10.80,'Year') from dual;

NUMTOY
------
+10-10

SQL> select NUMTOYMINTERVAL (25,'month') from dual;

NUMTOY
------
+02-01

SQL> select NUMTODSINTERVAL(9000, 'minute') from dual;

NUMTODSINTERVAL(900
-------------------
+06 06:00:00.000000

Converting Strings to Intervals

NUMTO 系列从数字转换为interval,从字符串转换为interval使用TO_YMINTERVALTO_DSINTERVAL

例如:

sql 复制代码
TO_YMINTERVAL('01-02')
TO_DSINTERVAL('100 00:00:00')

所有的元素都必须指定,以上为常用的格式,实际的格式由SQL标准定义。

Formatting Intervals for Display

您可以将interval传递给 TO_CHAR,但 TO_CHAR 将忽略任何格式掩码。

如果需要指定格式,可以用EXTRACT抽取元素,然后拼接字符串。

sql 复制代码
SQL> select EXTRACT ( month from sysdate) from dual;

EXTRACT(MONTHFROMSYSDATE)
-------------------------
                        3

Interval Literals

格式如下,详见文档

sql 复制代码
INTERVAL 'character_representation' start_element TO end_element

文档中说的是leading 和trailing element,都是起止的意思,不过可以是最大范围的子集,这个子集必须是连续的。

例如:

sql 复制代码
-- 3为精度,必须指定,因为年的默认精度为2
select INTERVAL '123-2' YEAR(3) TO MONTH from dual;

select INTERVAL '40 5' DAY TO HOUR from dual;

-- DAY to SECOND的子范围,这里精度也是必须指定,理由同上
select INTERVAL '400 5' DAY(3) TO HOUR from dual;

-- 会规范化为HOUR TO SECOND
select INTERVAL '72:15' HOUR TO MINUTE from dual;
INTERVAL'72:15'HOUR
-------------------
+03 00:15:00.000000

CAST and EXTRACT

CAST是Oracle 8出现的,用于数据类型转换;EXTRACT是9i出现的,用于抽取时间元素。

The CAST Function

CAST可以在datetime和字符串之间转换,或在两个datetime类型之间转换(如timestamp和date)。

在日期时间与字符串之间转换时,CAST 遵循 NLS 参数设置。 设置见 V$NLS_PARAMETERS。

相关的3个设置为,这也是CAST支持的三种datetime类型:

  • NLS_DATE_FORMAT:涉及DATE时,默认为DD-MON-RR
  • NLS_TIMESTAMP_FORMAT:涉及 TIMESTAMP和TIMESTAMP WITH LOCAL TIME ZONE,默认为DD-MON-RR HH.MI.SSXFF AM
  • NLS_TIMESTAMP_TZ_FORMAT:涉及TIMESTAMP FOR TIME ZONE 时,默认为DD-MON-RR HH.MI.SSXFF AM TZR

CAST和TO_系列函数(如TO_DATE, TO_TIMESTAMP和TO_TIMESTAMP_TZ)有功能重叠。区别在于:TO_系列函数只接受字符串输入,而CAST还可以接受datetime输入。

首选TO_系列函数,如果其不能满足,再用CAST。

注意:在 SQL 语句中,您可以在 CAST 中指定数据类型的大小,如 CAST (x AS VARCHAR2(40))。 但是,PL/SQL 不允许您指定目标数据类型的大小。

The EXTRACT Function

帮助见这里

可以抽取的元素为:

  • 年 到 秒
  • 时区相关信息(小时,分钟,区域, 缩写)

EXTRACT在需要使用日期时间元素来控制程序流,或需要日期时间元素作为数值时,非常有用 。

Datetime Arithmetic

datetime算术主要包括:

  • datetime加减interval
  • 两个datetime之间的interval
  • interval之间的加减
  • interval乘除数值

Date Arithmetic with Intervals and Datetimes

DAY TO SECOND较简单:

sql 复制代码
select systimestamp - INTERVAL '100 5:30:30' DAY(3) TO SECOND from dual;
SYSTIMESTAMP-INTERVAL'1005:30:30'DA
-----------------------------------
11-DEC-23 03.06.57.144734000 AM GMT

YEAR TO MONTH就比较复杂,因为每月的天数可能是28,29,30,或31天。

例如5月的最后一天加一个月应该是6月的最后一天还是无效的6月31日?这两种情形Oracle都支持:

sql 复制代码
select ADD_MONTHS(DATE '2024-5-31', 1) from dual;
ADD_MONTH
---------
30-JUN-24

select DATE '2024-5-31' +  INTERVAL '1' MONTH from dual;
ORA-01839: date not valid for month specified
01839. 00000 -  "date not valid for month specified"
*Cause:    
*Action:

ADD_MONTHS的帮助见这里

If date is the last day of the month or if the resulting month has fewer days than the day component of date, then the result is the last day of the resulting month. Otherwise, the result has the same day component as date.

YEAR TO MONTH不适合处理月末值。对于2月29号,加一年同样会有问题。但ADD_MONTHS可以很好的处理:

sql 复制代码
select ADD_MONTHS(DATE '2024-2-29', 12) from dual;

ADD_MONTH
---------
28-FEB-25

select DATE '2024-2-29' +  INTERVAL '1' YEAR from dual;
SQL Error: ORA-01839: date not valid for month specified
01839. 00000 -  "date not valid for month specified"
*Cause:    
*Action:

Date Arithmetic with DATE Datatypes

DATE数据类型可以加减interval,也可以加减数值,数值的单位为"天"。以天为基础,可以换算成小时,分钟或秒。例如可以用1/24,1/24/60和1/24/60/60表示1小时,1分钟和1秒。

sql 复制代码
alter session set nls_date_format = 'dd-MON-yyyy hh24:mi:ss';
-- 加1天
select sysdate + 1 from dual;
-- 加1天又12小时
select sysdate + 1 + 12/24 from dual;

SYSDATE+1           
--------------------
21-MAR-2024 09:10:03


SYSDATE+1+12/24     
--------------------
21-MAR-2024 21:10:03

Computing the Interval Between Two Datetimes

两个datetime之间的差为INTERVAL DAY TO SECOND。

sql 复制代码
select systimestamp - TIMESTAMP '2024-01-01 00:00:00' from dual;
SYSTIMESTAMP-TIMEST
-------------------
+79 17:27:17.872690

interval可正可负,负的interval无法应用于ABS函数。

2个DATE数据类型之间的差为数值,其单位为24小时。

sql 复制代码
在这里插入代码片

MONTHS_BETWEEN用可以用于2个DATE,和ADD_MONTHS一样,也可以很好的处理月末的问题。其帮助见这里

sql 复制代码
在这里插入代码片

若MONTHS_BETWEEN 的结果带小数,小数部分要换算为天需乘以31,因为其假定每月为31天。

Mixing DATEs and TIMESTAMPs

两个TIMESTAMP的差,结果类型为INTERVAL DAY TO SECOND。两个DATE的差,结果类型是NUMBER。如果希望两个DATE的差为类型INTERVAL DAY to SECOND,则需要将DATE类型转换为TIMESTAMP类型,例如CAST(date as TIMESTAMP)。

如果在减法中混合了timestamp和date类型,则date会隐式转换为timestamp类型。总之,尽量显式转换。

Adding and Subtracting Intervals

interval之间是可以加减的,前提是他们是同一种interval类型。否则报错:

sql 复制代码
select INTERVAL '2 3:4:5.6' DAY TO SECOND - INTERVAL '1 1:1:1.1' DAY TO SECOND
from dual;

select INTERVAL '2-10' YEAR TO MONTH - INTERVAL '1-1' YEAR TO MONTH
from dual;

select INTERVAL '2-10' YEAR TO MONTH - INTERVAL '1 1:1:1.1' DAY TO SECOND
from dual;

输出为:

sql 复制代码
INTERVAL'23:4:5.6'D
-------------------
+01 02:03:04.500000


INTERV
------
+01-09

SQL Error: ORA-30081: invalid data type for datetime/interval arithmetic
30081. 00000 -  "invalid data type for datetime/interval arithmetic"
*Cause:    The data types of the operands are not valid for datetime/interval
           arithmetic.

Multiplying and Dividing Intervals

乘除操作支持interval,对于日期类型则无意义。

sql 复制代码
select INTERVAL '2-10' YEAR TO MONTH * 2
from dual;
INTERV
------
+05-08

Using Unconstrained INTERVAL Types

interal各元素可以指定精度,不同精度的值之间未必兼容。当编写接受interval值作为参数的过程和函数时,就可能出现问题。

先看一个例子:

sql 复制代码
set serveroutput on
DECLARE
   dts INTERVAL DAY(9) TO SECOND(9);

   FUNCTION double_my_interval (
      dts_in IN INTERVAL DAY TO SECOND) RETURN INTERVAL DAY TO SECOND
   IS
   BEGIN
      RETURN dts_in * 2;
   END;
BEGIN
   dts := '1 0:0:0.123456789';
   DBMS_OUTPUT.PUT_LINE(dts);
   DBMS_OUTPUT.PUT_LINE(double_my_interval(dts));
END;

输出为:

sql 复制代码
+000000001 00:00:00.123456789
+02 00:00:00.246914

显然,输出的小数秒损失了精度。如果把1天改为100天,甚至会报错。

sql 复制代码
01873. 00000 -  "the leading precision of the interval is too small"
*Cause:    The leading precision of the interval is too small to store the
           specified interval.
*Action:   Increase the leading precision of the interval or specify an
           interval with a smaller leading precision.

解决方案就是使用YMINTERVAL_UNCONSTRAINED和DSINTERVAL_UNCONSTRAINED,他们可以接受interval值,同时不损失精度。

以上的函数体改为如下即可:

sql 复制代码
dts_in IN DSINTERVAL_UNCONSTRAINED) RETURN DSINTERVAL_UNCONSTRAINED

新的输出如下:

sql 复制代码
+000000001 00:00:00.123456789
+000000002 00:00:00.246913578

Date/Time Function Quick Reference

注意:避免将 Oracle 的传统日期函数与新的 TIMESTAMP 类型一起使用。 相反,请尽可能使用新的 INTERVAL 功能。 仅对 DATE 值使用日期函数。

以下为内置的datetime函数:

函数名 描述
ADD_MONTHS 为日期加上整数月份。输入和输出均为DATE数据类型
CAST CAST 允许您将一种类型的内置数据类型或集合类型值转换为另一种内置数据类型或集合类型
CURRENT_DATE 返回会话时区中的当前日期,数据类型为DATE 的公历值
CURRENT_TIMESTAMP 返回会话时区中的当前日期和时间,数据类型为 TIMESTAMP WITH TIME ZONE
DBTIMEZONE 返回数据库时区的值
EXTRACT 从日期时间或间隔表达式中提取并返回指定日期时间字段的值
FROM_TZ 将时间戳值和时区转换为 TIMESTAMP WITH TIME ZONE 值
LAST_DAY 返回包含 date 的月份的最后一天的日期
LOCALTIMESTAMP 以数据类型 TIMESTAMP 的值返回会话时区中的当前日期和时间
MONTHS_BETWEEN 返回日期 date1 和 date2 之间的月数
NEW_TIME 当时区 timezone1 中的日期和时间为 date 时,返回时区 timezone2 中的日期和时间
NEXT_DAY 返回晚于日期 date 的 char 命名的第一个工作日的日期
NUMTODSINTERVAL 将 n 转换为 INTERVAL DAY TO SECOND 常数值
NUMTOYMINTERVAL 将数字 n 转换为 INTERVAL YEAR TO MONTH 常数值
ROUND 返回四舍五入到格式模型 fmt 指定的单位的日期
SESSIONTIMEZONE 返回当前会话的时区
SYS_EXTRACT_UTC 从带有时区偏移或时区区域名称的日期时间值中提取 UTC(协调世界时 - 以前称为格林威治标准时间)。 如果未指定时区,则日期时间与会话时区关联
SYSDATE 返回为数据库服务器所在的操作系统设置的当前日期和时间
SYSTIMESTAMP 返回数据库所在系统的系统日期,包括小数秒和时区
TO_CHAR (datetime) 将 DATE、TIMESTAMP、TIMESTAMP WITH TIME ZONE、TIMESTAMP WITH LOCAL TIME ZONE、INTERVAL DAY TO SECOND 或 INTERVAL YEAR TO MONTH 数据类型的日期时间或间隔值转换为 VARCHAR2 数据类型的值,格式为 日期格式 fmt
TO_DATE 将 char 转换为 DATE 数据类型的值
TO_DSINTERVAL 将其参数转换为 INTERVAL DAY TO SECOND 数据类型的值
TO_TIMESTAMP 将 char 转换为 TIMESTAMP 数据类型的值
TO_TIMESTAMP_TZ 将 char 转换为 TIMESTAMP WITH TIME ZONE 数据类型的值
TO_YMINTERVAL 将其参数转换为 INTERVAL MONTH TO YEAR 数据类型的值
TRUNC(date) 返回日期,其中一天中的时间部分被截断为格式模型 fmt 指定的单位
TZ_OFFSET 根据语句执行的日期返回与参数对应的时区偏移量
相关推荐
chenyang_884 小时前
MySQL——statement对象详解
数据库·mysql·oracle
六月的雨__6 小时前
房屋租赁管理小程序的设计
java·sql·学习·小程序
不爱洗脚的小滕7 小时前
【MySQL】SQL注入的介绍
数据库·sql·mysql
java6666688888 小时前
SQL游标的原理与在数据库操作中的应用
数据库·sql·oracle
baozongwi10 小时前
ctfshow sqli-libs web561--web568
数据库·经验分享·python·sql·mysql·web安全
六月的雨__10 小时前
跑腿平台小程序的设计
java·sql·学习·小程序
ItKevin爱java12 小时前
JDBC中如何处理数据库连接超时和SQL超时?
数据库·sql
2401_8576226612 小时前
【SQL Server高可用性全解】构建永不宕机的数据库解决方案
数据库·oracle
youhebuke22512 小时前
SQLAlchemy pool_pre_ping
数据库·oracle·sqlalchemy
白菜!!!12 小时前
SQL INSERT批量插入方式
数据库·sql·mysql·mybatis