强制Oracle使用SKIP LOCKED返回TOP N行

Tra*_*vis 21 sql sql-server oracle queue

关于如何在Oracle和SQL Server中实现类似队列的表(锁定特定行,选择一定数量的行,以及跳过当前锁定的行),有几个 问题.

N假设至少有N行符合条件,我怎样才能保证检索到某些number()行?

从我所看到的,Oracle WHERE在确定要跳过哪些行之前应用谓词.这意味着如果我想从表中拉出一行,并且两个线程同时执行相同的SQL,则一个将接收该行,另一个将接收一个空结果集(即使有更多符合条件的行).

这违背了SQL Server如何显示处理UPDLOCK,ROWLOCKREADPAST锁定提示.在SQL Server中,TOP神奇地似乎限制了成功获得锁定的记录数.

请注意,这里这里有两篇有趣的文章.

ORACLE

CREATE TABLE QueueTest (
    ID NUMBER(10) NOT NULL,
    Locked NUMBER(1) NULL,
    Priority NUMBER(10) NOT NULL
);

ALTER TABLE QueueTest ADD CONSTRAINT PK_QueueTest PRIMARY KEY (ID);

CREATE INDEX IX_QueuePriority ON QueueTest(Priority);

INSERT INTO QueueTest (ID, Locked, Priority) VALUES (1, NULL, 4);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (2, NULL, 3);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (3, NULL, 2);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (4, NULL, 1);
Run Code Online (Sandbox Code Playgroud)

在两个单独的会话中,执行:

SELECT qt.ID
FROM QueueTest qt
WHERE qt.ID IN (
    SELECT ID
    FROM
        (SELECT ID FROM QueueTest WHERE Locked IS NULL ORDER BY Priority)
    WHERE ROWNUM = 1)
FOR UPDATE SKIP LOCKED
Run Code Online (Sandbox Code Playgroud)

请注意,第一个返回一行,第二个会话不返回一行:

第1节

 ID
----
  4

第二节

 ID
----

SQL SERVER

CREATE TABLE QueueTest (
    ID INT IDENTITY NOT NULL,
    Locked TINYINT NULL,
    Priority INT NOT NULL
);

ALTER TABLE QueueTest ADD CONSTRAINT PK_QueueTest PRIMARY KEY NONCLUSTERED (ID);

CREATE INDEX IX_QueuePriority ON QueueTest(Priority);

INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 4);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 3);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 2);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 1);
Run Code Online (Sandbox Code Playgroud)

在两个单独的会话中,执行:

BEGIN TRANSACTION
SELECT TOP 1 qt.ID
FROM QueueTest qt
WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE Locked IS NULL
ORDER BY Priority;
Run Code Online (Sandbox Code Playgroud)

请注意,两个会话都返回不同的行.

第1节

 ID
----
  4

第二节

 ID
----
  3

我怎样才能在Oracle中获得类似的行为?

Gar*_*ers 14

"根据我的看法,Oracle在确定要跳过的行之前应用WHERE谓词."

对.这是唯一可能的方式.在确定结果集之前,不能跳过结果集中的行.

答案是不要限制SELECT语句返回的行数.您仍然可以使用FIRST_ROWS_n提示来指示优化器,您将不会获取完整的数据集.

调用SELECT的软件应该只选择前n行.在PL/SQL中,它会是

DECLARE
  CURSOR c_1 IS  
    SELECT /*+FIRST_ROWS_1*/ qt.ID
    FROM QueueTest qt
    WHERE Locked IS NULL
    ORDER BY PRIORITY
    FOR UPDATE SKIP LOCKED;
BEGIN
  OPEN c_1;
  FETCH c_1 into ....
  IF c_1%FOUND THEN
     ...
  END IF;
  CLOSE c_1;
END;
Run Code Online (Sandbox Code Playgroud)

  • 再看一遍,由于您使用的是游标,因此在根据我在 http://rwijk.blogspot.com/2009/02/for-update-skip-locked.html 找到的内容从游标中获取之前,该行不会被锁定。我试图在没有 PL/SQL 的情况下解决这个问题(我想从 JDBC 执行一个简单的 SQL 批处理)。 (2认同)

Ste*_*ell 10

Gary Meyers发布的解决方案是关于我能想到的所有,而不是使用AQ,它可以为您提供所有这些以及更多功能.

如果你真的想避开PLSQL,你应该能够将PLSQL转换为Java JDBC调用.您需要做的就是准备相同的SQL语句,执行它然后继续对其进行单行读取(或N行读取).

http://download.oracle.com/docs/cd/B10501_01/java.920/a96654/resltset.htm#1023642上的Oracle文档提供了一些如何在语句级别执行此操作的线索:

要设置查询的提取大小,请在执行查询之前在语句对象上调用setFetchSize().如果将获取大小设置为N,则每次访问数据库时都会获取N行.

所以你可以在Java中编写类似的内容(在伪代码中):

stmt = Prepare('SELECT /*+FIRST_ROWS_1*/ qt.ID
FROM QueueTest qt
WHERE Locked IS NULL
ORDER BY PRIORITY
FOR UPDATE SKIP LOCKED');

stmt.setFetchSize(10);
stmt.execute();

batch := stmt.fetch();
foreach row in batch {
  -- process row
}
commit (to free the locks from the update)
stmt.close;
Run Code Online (Sandbox Code Playgroud)

UPDATE

根据以下评论,建议使用ROWNUM来限制收到的结果,但在这种情况下不起作用.考虑这个例子:

create table lock_test (c1 integer);

begin
  for i in 1..10 loop
    insert into lock_test values (11 - i);
  end loop;
  commit;
end;
/
Run Code Online (Sandbox Code Playgroud)

现在我们有一个包含10行的表.请注意,我已经以相反的顺序小心地插入了行,包含10的行是第一行,然后是9等.

假设您想要前5行,按升序排序 - 即1到5.您的第一次尝试是这样的:

select *
from lock_test
where rownum <= 5
order by c1 asc;
Run Code Online (Sandbox Code Playgroud)

结果如下:

C1
--
6
7
8
9 
10
Run Code Online (Sandbox Code Playgroud)

这显然是错误的,几乎每个人都犯了一个错误!查看查询的解释计划:


| Id  | Operation           | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |           |     5 |    65 |     4  (25)| 00:00:01 |
|   1 |  SORT ORDER BY      |           |     5 |    65 |     4  (25)| 00:00:01 |
|*  2 |   COUNT STOPKEY     |           |       |       |            |          |
|   3 |    TABLE ACCESS FULL| LOCK_TEST |    10 |   130 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter(ROWNUM<=5)
Run Code Online (Sandbox Code Playgroud)

Oracle从下到上执行计划 - 请注意rownum上的过滤器是在排序之前执行的,Oracle按照它找到的顺序获取行(在这里插入它们的顺序{10,9,8,7,6}) ,在获得5行后停止,然后对该集进行排序.

因此,要获得正确的前5个,您需要先进行排序,然后使用内联视图进行排序:

select * from
(
  select *
  from lock_test
  order by c1 asc
)
where rownum <= 5;

C1
--
1
2
3
4
5
Run Code Online (Sandbox Code Playgroud)

现在,为了最终达到目的 - 你能将更新跳过锁定在正确的位置吗?

select * from
(
  select *
  from lock_test
  order by c1 asc
)
where rownum <= 5
for update skip locked;
Run Code Online (Sandbox Code Playgroud)

这给出了一个错误:

ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc
Run Code Online (Sandbox Code Playgroud)

尝试将for update移动到视图中会出现语法错误:

select * from
(
  select *
  from lock_test
  order by c1 asc
  for update skip locked
)
where rownum <= 5;
Run Code Online (Sandbox Code Playgroud)

唯一可行的是以下内容,它会带来错误的结果:

  select *
  from lock_test
  where rownum <= 5
  order by c1 asc
  for update skip locked;
Run Code Online (Sandbox Code Playgroud)

事实上,如果您在会话1中运行此查询,然后在第二个会话中再次运行它,会话2将给出零行,这真的是错误的!

所以,你可以做什么?打开游标并从中获取所需的行数:

set serveroutput on

declare
  v_row lock_test%rowtype;
  cursor c_lock_test
  is
  select c1
  from lock_test
  order by c1
  for update skip locked;
begin
  open c_lock_test;
  fetch c_lock_test into v_row;
  dbms_output.put_line(v_row.c1);
  close c_lock_test;
end;
/    
Run Code Online (Sandbox Code Playgroud)

如果在会话1中运行该块,它将在第一行锁定时打印出"1".然后在会话2中再次运行它,它会在跳过第1行时打印"2"并获得下一个免费的.

这个例子在PLSQL中,但是在Java中使用setFetchSize你应该能够得到完全相同的行为.