Oracle 序列:基础知识
Oracle 序列可用于为表创建人工 ID。在这篇文章中,乔纳森刘易斯解释了它们是如何在幕后工作的。
迄今为止的系列:
许多数据库应用程序使用“无意义的 id”或“合成键”,而不是使用用户数据列来构建表的主键。很长一段时间以来,Oracle RDBMS 提供了“序列”机制来有效地生成这样的值,并尽量减少事务之间的争用。尽管有几十年的历史,序列经常被误解和误用——甲骨文公司仍在努力减少一些涉及序列的威胁。
在这个简短的系列文章中,我的目标是消除最典型的误解,警告您注意威胁并提供解决方法的想法。我将首先讨论序列在 12c 之前对单实例 Oracle 的使用,然后我将继续讨论与多实例 RAC 相关的关键更改,最后对出现更多的功能进行一些评论最近在 12.1、18c 和 19c 中专门处理“应用程序连续性”、RAC 和“分片”。
定义序列
直到 12c,create sequence命令的完整语法如下:
1 2 3 4 5 6 7 8 | CREATE SEQUENCE [schema_name.]{sequence_name} START WITH n INCREMENT BY n [MINVALUE n | NOMINVALUE ] [MAXVALUE n | NOMAXVALUE ] [CACHE n | NOCACHE] [ORDER | NOORDER] [CYCLE | NOCYCLE] |
但是,您可以用来创建序列的最短语句很简单:
1 | CREATE SEQUENCE sequence_name; |
此命令等效于:
1 2 3 4 5 6 7 8 | CREATE SEQUENCE sequence_name START WITH 1 INCREMENT BY 1 MINVALUE 1 NOMAXVALUE -- which is effectively 1e28 - 1 CACHE 20 -- which is usually a bad idea, as we shall see later NOORDER NOCYCLE |
Oracle 发明了序列,使按需提供(几乎)无限的数字序列成为可能,这些数字序列保证是唯一的。最简单的方法是保持一个全局可见的计数器,并在每次会话说:“给我一个数字”(或者更准确地说,“告诉我 ”的值sequence_name.nextval
)时递增它。
但是 Oracle 必须跟踪提供的最后一个数字,以避免两次提供相同数字的风险——这对于数据库系统来说很容易:只需在表中保留最近的数字。但是,如果每次有人需要下一个数字时都必须更新表,这会使事情变得非常缓慢。您应该使用自治事务,以便在生成下一个数字之前没有人必须等待第一个会话提交其当前事务。因此,Oracle 使用递归的、自治的事务来处理表更新,并且nextval
通过在内存中计数并记住下次需要更新表的时间来避免每次调用时更新表。不幸的是,Oracle 将这种“内存中”机制称为缓存——这是非常具有误导性的。
“自制”序列的一个常见策略是在表中包含列(序列名称,最近使用的序列号)的行,然后编写一个函数,从表中选择相关行进行更新,增加编号,更新表,并将号码提供给最终用户。在幕后,这正是 Oracle 软件所做的,尽管它是一种内化机制,但它可以以本土代码无法做到的方式“作弊”。
您可以通过查询视图来查看序列的数据库图像user_sequences
。假设我刚刚创建了一个名为s1的序列,这里有一个合适的查询,然后是它在我创建序列后立即给我的结果,然后是我在调用 fetch 后得到的结果s1.nextval
:
1 2 3 4 5 6 7 8 9 | select min_value, max_value, increment_by, cache_size, last_number, cycle_flag, order_flag from user_sequences where sequence_name = 'S1' / |
如果您有 DBA 权限,您可以更进一步,查找object_id
序列的 ,然后查询seq$
视图所基于的字典表 ( )。同样,我已经运行了两次查询,一次是在第一次调用之后,一次是在第一次调用之后s1.nextval):
1 2 3 4 5 6 7 | select minvalue, maxvalue, increment$, cache, highwater, cycle#, order$ from seq$ where obj# = 124874 ; |
检查序列状态的最后一个选项是特权帐户通过动态性能视图可见的内存信息v$_sequences
(意外的下划线不是拼写错误)。在这种情况下,我只查询了一次视图,在第一次调用之后s1.nextval
(序列甚至不会在视图中,直到第一次调用,nextval),
并且我已经包含了字典表中不可用的列:
1 2 3 4 5 6 7 8 9 | select min_value, max_value, increment_by, cache_size, highwater, cycle_flag, order_flag, nextvalue from v$_sequences where object# = 124874 / |
正如您所看到的,动态性能视图有一列nextvalue
保存下一次用户调用时将提供的值s1.nextval
。
所有三个视图都有一个“highwater”值(尽管它在 view 中被称为“ last_number
”user_sequences
)。当对s1.nextval的调用返回highwater
(highwater
和nextvalue
相同) 中显示的值时,进行调用的会话将执行并提交递归事务以将该increment
值添加到当前值highwater
并更新seq$
表。
“缓存”不是传统意义上的缓存,它只是一对数字(无论您设置序列缓存大小有多大):当前值和您碰撞该highwater
值的值。为序列设置大缓存没有资源损失,关于序列的最重要的事实是,在大多数情况下,您可能应该使用大缓存大小。
性能问题(单实例)
正如我在本文开头所说,序列最常见的用途是生成合成密钥。如果您以这种方式将序列用于受大量活动影响的表——特别是如果它受高并发活动影响——您希望尽快提供序列号。您不想在每 20次调用时暂停以更新数据字典表,并且您不想等待,因为其他 50 个会话都在同一序列维护上发生冲突。
缓存大小
在单实例 Oracle 中很容易识别此问题,因为您将看到在模式 4中v$lock
等待“ SQ
”入队(锁定)的会话,并且报告的等待事件将为“ enq: SQ - contention
”,并p1
设置为object_id
它们的序列正在等待。解决这个问题的方法很简单,就是增加缓存大小,在非常繁忙的系统上看到有数千个缓存的序列并不少见:
1 | alter sequence s1 cache 10000; |
鉴于将序列的缓存大小从默认的 20 增加通常对性能和并发性很重要,您应该意识到创建单个序列来为需要唯一无意义值的每一列提供值是一个坏主意。安全操作 – 为每个合成键指定自己的序列。(其他一些 RDBMS 通过“身份”数据类型自动执行此操作,但该功能仅在最新版本的 Oracle 中引入。)
这种默认的性能问题有时会因最终用户的要求而变得更糟,即“不应有丢失的数字”,这导致 DBA 将序列设置为nocache
. 换句话说,每次调用都会nextval
导致highwater
递增和seq$
更新表。这可能是一个巨大的争用源,很容易成为系统中的关键瓶颈——更糟糕的是,它不会阻止数字丢失(例如,会话可能必须回滚或在调用后崩溃到nextval
,然后该值将丢失)。
Oracle 序列并非旨在提供不间断的值序列;它们仅用于提供一组唯一的数字。如果您需要某种形式的顺序审计跟踪(并且您希望它具有可扩展性),那么您将必须进行一些相当仔细的编码来创建自己的——这不是一项微不足道的练习。
索引争用
另一个常见的性能问题是使用序列生成唯一 ID 的副产品:如果唯一性很重要,您应该在相关列上声明一个约束,唯一(或主键)约束将自动导致 Oracle 创建一个相应的索引。索引将成为一个争论点,因为所有尝试插入新行的会话都将生成 (a) 彼此非常接近的序列号,以及 (b) 迄今为止生成的最高值;这意味着您将有多个会话尝试将新值插入索引的同一个(所谓的“右手”)叶块中。
在这些情况下,您可能会在索引上看到大量等待“缓冲区忙等待”;您可能还会看到等待“enq:TX – 索引争用”,甚至“enq:TX – 分配 ITL 条目”,因为会话排队等待索引叶块拆分完成。不幸的是,这个问题没有好的解决方案,尽管您可以实施损坏限制机制,而且 Oracle 18c 提供的增强功能之一是其中一种通用机制的声明性版本。
曾几何时,避免索引热点的常见建议是将索引重新创建为“反向键”索引,但对于不断增长的表,这是一个坏主意,因为它通常会取代几秒钟的“缓冲区繁忙”等待几分钟(或更多)额外的“数据库文件顺序读取”等待。(有关此评论的解释,请参阅:https : //jonathanlewis.wordpress.com/2015/06/17/reverse-key-2/)
如果您获得了分区选项的许可,则删除热点的更好方法是将索引重新创建为全局散列分区索引 - 通常的条件是分区数应为 2 的幂以确保分区大小相同. 以这种方式对索引进行分区存在一些与优化器相关的缺点,但由于我们正在讨论将序列用于合成(无意义)键,因此您希望看到的针对索引的唯一查询可能是以下形式:unique_key = {constant},
并且优化器不太可能被具有该类型谓词的查询所误导。
当然,分区选项很昂贵,并且在标准版上不可用,因此手动编码的回退通过在您获取序列号后操纵序列号来传播热点。首先,您必须选择一个大到您认为序列永远不需要到达它的数字(例如 1e10)。然后,您将一个会话相关的值添加到序列号,使彼此接近的序列值最终在索引中广泛分离,因为它们是由不同的会话插入的。例如,您可以添加(1 + mod(sid, 16)) * 1e10
到序列值;这将为您提供散列将索引划分为 16 个分区的大部分性能优势 - 将您的一个热点划分为 16 个“稍微温暖”的点。
这种扩展机制不可避免地要付出代价。在一个理想的世界,由序列生成数据的索引将使用接近每个索引叶块的可用空间的100%,因为甲骨文会做它所谓的“叶节点90-10分裂”每次右手(高值)叶块分裂。如果您将插入分布在索引中的 N 个不同插入点上(使用1+mod(sid,N)
如上),那么除了最高插入点之外的所有插入点都将进行“50-50”叶节点拆分。
尽管 Oracle 在实例和会话活动中statistics (v$sysstat / v$sesstat)
为“叶节点 90-10 次拆分”提供了统计信息,但更好的名称是“叶节点 100-0 次拆分”。“拆分”只是在索引的右侧(高值)端添加一个叶块并继续执行;它不会从前面的叶块中复制任何数据。相比之下,“正常”中索引叶节点拆分将新叶块链接到正确的位置,然后将大约一半的数据从现有叶块移动到新叶块中。
以这种方式手动“伪分区”索引的副作用是它可能以大约两倍于以前的大小运行。(而且这不允许您现在拥有一个索引,其中最小条目比以前的最大条目长几个字节。)在现实生活中,有一些奇怪的并发效应可以使任何索引热点浪费了大量空间,因此变化可能没有理论预测的那么剧烈。
各种各样的
我将简要总结 12c 之前序列的其余不太常用的功能来结束本文。
开始:您可以指定任何合法的整数值作为要使用的第一个值,正数或负数;默认值为 1。
Increment by:虽然我一直在讨论增加 1 的序列,但您可以将增量设置为任何合法的整数值(正数或负数)。因此,如果您想要一个正偶数序列,您可以指定“以 2 递增 2 开始”。默认增量为 1。
Minvalue:如果您的增量为负,您可以设置一个值,低于该值Oracle 会引发错误(ORA-08004:序列低于 MINVALUE
)。
Maxvalue :镜像到minvalue
– 在适当的位置增加正增量,您可以设置一个值,高于该值Oracle 将引发错误(ORA-08004:序列超出 MAXVALUE
)。
Cycle :我认为我从未见过在野外使用过这种方法,但是您可以设置一个序列来重复,如果它具有maxvalue
正增量或minvalue
负增量。如果调用nextval
将推动序列超过其最小值/最大值,则它会返回到其起始值并从那里继续。
Order:这是一个非常适用于 RAC 如何处理序列的选项,我将把它留给本系列的第二篇文章。
如果您开始使用序列,您会发现有多种组合选项的方法可以产生不言自明的 Oracle 错误。例如:
1 2 3 | SQL> create sequence s4 start with 10 increment by 15 nomaxvalue cycle; create sequence s4 start with 10 increment by 15 nomaxvalue cycle * |
有如此多的可能性,试图列出所有这些可能性是没有意义的。如果您想阅读 Oracle 错误消息文件 ($ORACLE_HOME/rdbms/mesg/oraus.msg),则与创建序列相关的错误编号从 ORA-04001 到 ORA-04016。
概括
在第一篇文章中,我描述了序列的基本和最常见的用法,并强调了选择合理的缓存大小以避免序列本身的并发问题的必要性。特别是,我强调了将序列定义为nocache的威胁,同时指出,与流行的假设相反,此选项不会阻止您“丢失”序列号。
我已经指出了在创建索引以强制序列值的唯一性时出现的并发问题,并讨论了最小化威胁的策略,同时指出无论你做什么都会有副作用。顺便说一下,我已经提到基于时间的索引将表现出与基于序列的索引相似的行为。
最后,我简要介绍了用于定义序列的不太常用的选项,将有关order选项的任何评论留在下一篇文章中,该文章将开始讨论 RAC 以及处理序列时所需的额外注意多实例系统。
乔纳森·刘易斯
文章来源:https://www.red-gate.com/simple-talk/sql/oracle/oracle-sequences-the-basics/