发表于

Oracle Magazine
2004年3/4月号
技术:向Tom咨询

关于格式、负0(零)和时间后移等问题
作者:Tom Kyte

我们的技术专家告诉你如何修改格式来让0显示0,以及如何避免时间后移。

日期和时间格式化

我在格式化日期时,在小时前面有一些前导符0(如09:30 a.m.)。 我怎么才能去掉它们呢?

这里有一个非常好的格式模型修改器--填充模式(FM),你可以把它用于日期和数字的修改。 它禁止出现空白及其他填充符字符(像这个前导符0)。 例如:

select to_char(dt,'hh:mi am'),
       to_char(dt,'fmhh:mi am'),
       to_char(dt,'fmhhfm:mi am')
  from (
select to_date('09:01 am','hh:mi am') dt
  from dual
       )
/
 
TO_CHAR(    TO_CHAR(    TO_CHAR(
--------    --------    --------

09:01 am    9:1 am      9:01 am

请注意这三种不同的输出结果。 第一列显示的可能就是你现在得到的: 需要的时候时间成分用前导符0填充为两个字符。 第二列显示出FM的作用: 前导符0被取消了。 但是那可能不是你想要的,因为值9:1的含义不太清楚。 第三列大概是你真正想得到的: 小时前面的前导符0不显示,但时间的其余成分的前导0不取消。 FM的作用类似于一个切换开关--第一次出现空白填充符时将其关闭(空格),再次出现时又打开让其显示。 在修改日期和时间格式时它非常方便。 考虑下面两种输出的区别:

select to_char(sysdate,'Day Month dd'),
      to_char(sysdate,'fmDay Month dd')
  from dual;
NO_FM
-------------------------------------
Sunday    November  02


FM
-------------------------------------
Sunday November 2
 

第二种输出使用了FM,从进行了精心格式化的日期中得到了希望的输出结果。 第一种输出没有使用FM,用空格填充了日期和月份的名字,以便得到统一格式的输出(一个固定长度的字符串)。

为什么是负0?

我在下面的查询中为什么得到负0,我感到迷惑:

select c1, c2, c1 - 
(trunc(to_date(c2,'YYYYMMDDHH24MISS') + 1) - to_date(c2,'YYYYMMDDHH24MISS')) * (24*3600) result 
from t where c3 != 'N05416776X0001'


C1      C2               RESULT
----    --------------    ------
...
1229    20030731235942      1211
 82    20030731235838       -0

我执行你的查询时得到了不同的结果。 但是,我能用SQL*Plus的命令set numformat重现你的问题,我设想你肯定用它格式化了输出结果。 问题出在结果列实际上不是0;它是一个非常小的数,而你的格式化命令掩盖了这一事实。请看下面的代码:

select c1,
      c2,
  c1-(trunc(to_date(c2,
       'YYYYMMDDHH24MISS')+1)
  -to_date(c2,

       'YYYYMMDDHH24MISS'))
  * (24*3600) result
  from (
select 82 c1, 20030731235838 c2
  from dual
       )
/
 
        C1          C2         RESULT
-----------   ----------     ----------        	      
        82   2.0031E+13     -2.240E-36

请注意默认情况下SQL*Plus是如何用指数计数法格式化数字的(它最适合于这种格式)。 你过去用了固定格式的显示方式,因此在显示结果中没有小数位:

SQL> set numformat 99999999999999


SQL> /
 
      C1               C2      RESULT
 --------   ---------------  ----------
      82    20030731235838          -0

所以,结果其实不是负0,而是一个非常小的负数。 用格式时要十分小心;它们也许能很好地格式化信息,但它们也可能对你隐藏信息。

作业后移

我注意到DBA_JOBS.NEXT_DATE的值每天后移一点儿。 我如何将这种机制用于业务关键的操作呢? 我有一个服务器,现在的时间已经后移了10多分钟。 这种情况下是不是有什么出了问题?

这是预料中的、确定的行为。 很容易就可以让DBA_JOBS.NEXT_DATE值不"后移"。

在数据库中,作业是"按照或大致按照预定时间"运行的。 当作业运行时,数据库只是简单地把你指定的字符串作为时间间隔并计算它的值(例如,从DUAL中选择它)。 例如,假设你安排的一项作业的时间是从今天晚上午夜开始,间隔是SYSDATE+1。另外,假设安排在午夜开始的作业实际上是在12:00:20 a.m.(午夜后20秒,虽然它离午夜时间很近,但按秒计算还是晚了一些的)开始运行的。 作业队列工具将接受字符串SYSDATE+1并计算它的值。 安排的下一次开始时间因此将变为明天的12:00:20 a.m.! 假设该过程自己不断重复(总是差20秒)。 两周之内,作业将在大约12:05 a.m.运行。一个月之内就是12时过10分钟,如此等等。

解决的办法就是使用日期函数,它计算将来的一个固定时间。 例如,假设你想让作业在每天晚上1 a.m.运行。 时间间隔应该是:

trunc(sysdate)+1+1/24

不管你什么时候计算它,这个日期函数都会计算出明天1 a.m.。

设置模式

我有一个Oracle8i第3版(8.1.7)数据库的模式(schema),借助它我使用Oracle Forms 6i作为用户界面。 当时,它只有一个可以用来创建表或其他东西的用户模式。

我不知道哪种方法是最好、最有效的方法,它使以不同身份登陆的用户能够通过Oracle数据库的内置程序来使用我的应用程序。 有人告诉我说,如果要使多个登陆用户能够自己运行这个应用程序,那么我必须使用同义词,因为他们必须有<schema>.<procedure>权限来执行一个过程。 这是真的吗?

好的,如果你问我,那么我说你的应用程序使?lt;schema>.<procedure>绝对没错。 事实上,我认为这是一条好的经验。 但是,如果你没这么做,而且也不想或不允许这么做,那么你就可以在你的代码中添加以下代码:

alter session set 
current_schema=schema_name;

例如,考虑用户以SCOTT以外的身份登陆后的情况:

SQL> desc dept

ERROR:
ORA-04043: object dept does not exist

SQL> alter session set
  2  current_schema=scott;
Session altered.
 
SQL> desc dept

Name        Null?     Type

------  	   --------   -------- 	          
DEPTNO      NOT NULL   NUMBER(2)
DNAME                 VARCHAR2(14)
LOC             	     VARCHAR2(13)

该会话的CURRENT_SCHEMA设置用于提供会话的默认模式名。 因此,无论数据库何时要把你的当前用户名放到查询中,它都将放入SCOTT(或者设置给它的什么模式)。 请注意这不会以任何方式和形式影响安全性;它只影响所使用的默认模式名。 如果我没有访问SCOTT.DEPT,则上面的说法就没什么用。

限定对象的数目

我很想知道对于某一特定用户,Oracle数据库是否可以限定所创建的对象的数目(例如,限定为10)。 是否有可能实现这样的要求?

系统事件触发器是用于实现这些独特的定制规则的优秀工具。 你可以使用BEFORE CREATE触发器统计模式中已有对象的数目,如果对象数超过你设定的最大值则使创建对象的过程失败。代码清单1 显示出了这样一个触发器的例子。

另一种方法是你可以把触发器放到想要实现这一要求的每个模式中,而不是在数据库一级上。 为了在运行过程中看到它,我简单地建立一个测试账户:

SQL> drop user a cascade;
User dropped.


SQL> create user a identified by a;
User created.

SQL> grant create session, 
  2    create table to a;
Grant succeeded.

现在我以这个新用户的身份连接到数据库并创建10个对象:

SQL> connect a/a

SQL> begin
  2    for i in 1 .. 10
  3    loop
  4      execute immediate 
         'create table t' || i || 
             ' (x int)';
  5    end loop;
  6  end;
  7  /


PL/SQL过程已成功完成。

到目前为止一切正常,但当我试着创建第11个对象时,将看到:

SQL> create table t11 ( x int );
create table t11 ( x int )
*
ERROR at line 1:
ORA-00604: error occurred at 
           recursive SQL level 1
ORA-20001: You can only do 10 things!
ORA-06512: at line 13

你自己可能会问,为什么没有一条刚好能做这件事的命令呢? 事实是数据库不可能预见到数据库之外的每一项独特的定制或要求,但它能向我们提供必需的工具来自己实现定制要求。 例如,假设有一组像下面这样的客户要求,而不是一条简单的"限定到10"的规则:

1. "我希望Oracle数据库使我能够将'alter these 20 users only'权限授予某个用户。"

2. "正常情况下,ALTER USER权限将允许某人可以修改其他任何人的密码,包括DBA的密码。 我希望有一张列表,其中说明了哪个用户可以更改其他哪些用户的密码,这样就可以使帮助台的分析员只更改这5个用户而不是任何用户的密码。 因此我希望得到授权能真正检查这张表,看看George是否能更改Mary的密码。"

3. "我希望Oracle数据库能使这个用户可以更改任何以APP_开头的账户。"

这些都是基于帮助台的正当要求(允许权限相对较低的帮助台技术人员重新设置密码,但不是DBA的密码!),但是这些要求的独特性和定制性太强,以至于我们不能期望实现所有这些要求。 但是鉴于数据库存储了用定义者权限可以运行的过程,那么我就可以轻松执行世界上大多数定制的、独特的安全性协议。 例如,为了满足第一个客户要求,我可以创建一个过程,如代码清单2所示。

因为该过程用定义者的基本权限来执行(因此该过程的拥有者,不是GEORGE,必须有ALTER USER权限),所以我可以在几秒钟之内实现第一个客户要求! 满足客户的第二个和第三个要求同样简单。 事实上,用这种方法任何一种变化都是有可能的。 而且它们和你想要的一样安全和可审计。

RR格式

我正在尝试使用Oracle9i支持的RR格式。 按照RR的格式,如果当前年份和指定的年份都在0-49的范围内,那么日期应该是在当前世纪内的。 假定当前是2003年,指定的日期是27-oct-17。如果用RR格式那么它是不是代表2017年,而如果用YYYY格式,那是不是1917年? 对于同一个日期是不是会显示两个不同的值?

在这种情况下,答案应该是用RR格式和用YY格式都是2017年。 用YYYY格式则是17年。来看下面的代码:

alter session set 
nls_date_format='yyyy';
select 
 to_date('27-oct-17','dd-mon-yy') c1,
 to_date('27-oct-17','dd-mon-rr') c2,
 to_date('27-oct-17','dd-mon-yyyy') c3
from dual
 /
C1      C2      C3
----    ----    ----
2017    2017    0017

把它在旁边先放一会儿,我要指明非常重要的一点。 要做的惟一一件正确的、适当的、专业的、巧妙的、智能的事情是1999年我们学会的。 用4个字符正确地输入年份。就是这样。用户接口会正确接收4位数字--不多不少。 你不应该用2位数字去表示需要4位数字的数据。 含义太不清楚了。 当看到27-oct-17时,你不知道日期到底是什么。

但在回答你的问题时,如果你把数据拆开,数据就会丢失。 如果你要求使用2个字符来显示最少需要4个字符的内容,你将丢失数据;有时你还会误解它。

使用YYYY格式,你绝对不用担心这一点!

对求救问题的分析(再一次)

我有这样的一些记录:

Time                  Amount
-------------------    ------
11/22/2003 12:22:01       100
11/22/2003 12:22:03       200
11/22/2003 12:22:04       300
11/22/2003 12:22:06       200
11/22/2003 12:22:45       100
11/22/2003 12:22:46       200
11/22/2003 12:23:12       100
11/22/2003 12:23:12       200

我需要得到那些与其前一条记录或后一条记录的时间值之差在3秒之内的记录的数量之和。 根据上面的输入,我预期会得到3行: 前4条记录应该放在一起,然后是接下来的2条,最后是最后的2条记录。 我希望得到这样几个"实例",即一个实例内的各个记录的时间值前后相差不超过3秒。 我认为应该使用LAGLEAD函数。

这是一个有趣的问题,你是对的--使用分析函数是正确的方法。 我把我的方法写成一个任务列表:

  • 取1行,如果与前1行的时间差在3秒之内,则把它放在这组中。 如超过3秒那我就开始建立一个新组。 我可以使用LAG向回看1行,并得到当前行与前1行的时间差。
  • 一旦我给出现时间差值超过3秒的记录做上标记,我就必须沿用这个组标识。
  • 然后我就可以对有这个组标识的数据求和了。

我将逐个进行每一步,看看会出现什么情况。 我把你的数据加载到一个小表T中,并且为了节省空间我将使用hh24:mi:ss 这样的NLS_DATE_FORMAT格式,这样我就可以只看到日期的时间部分了。 先解决第一个要求,我给每个新组(也就是你所说的"实例")的开始部分做上标记:

select time, amount,
       case
       when time-ltime > 3/24/60/60
            or ltime is null
       then row_num
        end rn
  from (
select time, amount,
       lag(time) 
          over (order by time) ltime,

       row_number() 
          over (order by time) row_num
  from t
       )
 order by 1
/
TIME       AMOUNT    RN 
--------   ------     --  
12:22:01      100     1
12:22:03      200
12:22:04      300
12:22:06      200
12:22:45      100     5
12:22:46      200
12:23:12      100     7
12:23:12      200
 
8 rows selected.

在每个组的一开始,我输出ROW_NUMBER()分析函数的返回值。 通过从当前行的时间值中减去前1行的时间值(用LAG()得到),就可以给每一组的第1行做标记了。 如果结果值是NULL--说明这是结果集的第1行--或大于3秒,我就给这行做标记。 现在我必须沿用这些独特的值,从而使所有行都在同一个组中。 我可以使用分析函数MAX()完成这件事。 我利用上面的查询(QUERY-FROM-ABOVE)--去掉ORDER BY 1子句--并执行该查询:

select time, amount,
       max(rn) 
         over(order by time) max_rn
  from (QUERY-FROM-ABOVE)

order by 1
/

TIME       AMOUNT   MAX_RN
--------   ------    ------
12:22:01      100        1
12:22:03      200        1
12:22:04      300        1
12:22:06      200        1 
12:22:45      100        5
12:22:46      200        5
12:23:12      100        7
12:23:12      200        7
 
8 rows selected.

它沿用了组标识符。之所以能做到这一点是因为MAX(RN) OVER (ORDER BY TIME)使用了"当前行与之前所有行之间"这样一个隐含范围窗口。 这就意味着计算第一行的
下一步

向Tom咨询
Oracle副总裁Tom Kyte会为你解答你所遇到的最困难的技术问题。本专栏从该论坛中精选了部分重要内容。

阅读Tom的更多著述
《高超的设计造就了高效的Oracle产品》(Effective Oracle by Design)

MAX(RN)时,所考虑的行只是第一行以及"按照时间排序后在其前边的那些行",而在此情况下根本就没有任何一行了。 对于结果集中的第2行,从当前行和前一行中选择MAX(RN),因此,在这种情况下值1被沿用下去。 同样对于第3行和第4行,再次从当前行和前一行中选择MAX(RN)。 到第5行时,考虑新的当前行和前面的所有行,当然现在MAX(RN)的值是5,而且在遇到下一个非空MAX(RN)之前它一直是MAX(RN)的值。

现在要做的最后一件事是完成总计。 我已经给所有组加了标记;进行总计非常简单。 使用上面的查询(不带ORDER BY),我可以完成:

select min(time), 
       max(time), 
       sum(amount)
  from (QUERY-FROM-ABOVE)
 group by max_rn
 order by 1
/

MIN(TIME    MAX(TIME    SUM(AMOUNT)
--------    --------    -----------
12:22:01    12:22:06           800
12:22:45    12:22:46           300
12:23:12    12:23:12           300
 
3 rows selected.

你完全得到了你想要的。 一旦你开始使用分析函数,就会发现它的功能极为强大。


Tom Kyte thomas.kyte@oracle.com)从1993年起一直为Oracle工作。Kyte是Oracle Government、Education和Healthcare集团的副总裁,是"Effective Oracle by Design(高超的设计造就了高效的Oracle产品)"(Oracle Press出版)和"Expert One-on-One(一对一的专家): Oracle(Apress出版,2003年)两书的作者。


请评价这篇文章:

很差

E-mail this page