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

时间:2011-05-24 21:40:49

标签: sql sql-server oracle queue

有一个few questions关于如何在Oracle和SQL Server中实现类似队列的表(锁定特定行,选择一定数量的行,以及跳过当前锁定的行)。 / p>

假设至少N行符合条件,我如何保证检索到一定数量(N)行?

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

这与SQL Server似乎处理UPDLOCKROWLOCKREADPAST锁定提示的方式相反。在SQL Server中,TOP在成功获得锁定后,神奇地似乎限制了的记录数。

请注意,有两篇有趣的文章herehere

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);

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

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

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

第一节

 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);

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

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

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

第一节

 ID
----
  4

第二节

 ID
----
  3

如何在Oracle中获得类似的行为?

6 个答案:

答案 0 :(得分: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;

答案 1 :(得分: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;

更新

根据以下评论,建议使用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;
/

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

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

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

结果如下:

C1
--
6
7
8
9 
10

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


| 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)

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

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

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

这会出错:

ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc

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

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

唯一可行的是以下,给出了错误的结果

  select *
  from lock_test
  where rownum <= 5
  order by c1 asc
  for update skip locked;

事实上,如果你在会话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;
/    

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

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

答案 2 :(得分:1)

在第一个会话中,执行时:

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

你的内部选择尝试只抓取id = 4并锁定它。这是成功的,因为此单行尚未锁定。

在第二个会话中,您的内部选择STILL尝试抓住 ONLY id = 4 并锁定它。这不成功,因为第一个会话仍然锁定了该行。

现在,如果您在第一个会话中更新了“已锁定”字段,则运行该选择的下一个会话将获取id = 3.

基本上,在您的示例中,您依赖于未设置的标志。要使用锁定的标志,您可能需要执行以下操作:

  1. 根据某些条件选择所需的ID。
  2. 立即更新这些ID的锁定标志= 1(如果资源繁忙,另一个会话将您击败此步骤以获取1个或更多ID,再次转到1)
  3. 对这些ID做任何事情
  4. 将锁定标志更新回null
  5. 然后,您可以使用select for update skip locked语句,因为您正在维护锁定的标志。

    就个人而言,我不喜欢所有标志的更新(你的解决方案可能因任何原因需要它们),所以我可能只是尝试来选择我想要更新的ID(通过每个会话中的任何标准:

    select * from queuetest where ... for update skip locked;

    例如(实际上,我的标准不是基于id列表,但是queuetest表过于简单化):

    • sess 1:select * from queuetest where (4,3)中的id用于更新已跳过锁定;

    • sess 2:select * from queuetest where id(4,3,2)中的更新已跳过锁定;

    这里sess1会锁定4,3而sess2只会锁定2。

    你不能在我的知识中做一个top -n或在select for update语句中使用group_by / order_by等,你会得到一个ORA-02014。

答案 3 :(得分:1)

我的解决方案 - 就是编写这样的存储过程:

CREATE OR REPLACE FUNCTION selectQueue 
RETURN SYS_REFCURSOR
AS
  st_cursor SYS_REFCURSOR;
  rt_cursor SYS_REFCURSOR;
  i number(19, 0);

BEGIN

  open st_cursor for
  select id
  from my_queue_table
  for update skip locked;

  fetch st_cursor into i;
  close st_cursor;

  open rt_cursor for select i as id from dual;
  return  rt_cursor;

 END;

这是一个简单的例子 - 返回TOP FIRST非阻塞行。要检索TOP N行 - 将单个fetch替换为局部变量(“i”),并将循环提取替换为临时表。

PS:返回光标 - 用于休眠友谊。

答案 4 :(得分:1)

我遇到了这个问题,我们花了很多时间解决它。某些使用for update for update skip locked,在oracle 12c中,一种新方法是使用fetch first n rows only。但是我们使用的是oracle 11g。

最后,我们尝试这种方法,发现效果很好。

CURSOR c_1 IS  
   SELECT *
     FROM QueueTest qt
     WHERE Locked IS NULL
     ORDER BY PRIORITY;
   myRow c_1%rowtype;
   i number(5):=0;
   returnNum := 10;
BEGIN
  OPEN c_1;
  loop 
    FETCH c_1 into myRow 
    exit when c_1%notFOUND 
    exit when i>=returnNum;
    update QueueTest set Locked='myLock' where id=myrow.id and locked is null;
    i := i + sql%rowcount;
  END
  CLOSE c_1;
  commit;
END;

我在记事本中写了它,所以可能出了点问题,您可以将其作为一个过程进行修改,否则。

答案 5 :(得分:0)

首先感谢您对前2个问题的回答。从中学到很多。我测试了以下代码,并在运行Practicedontdel.java main方法后发现,这两个类每次都打印不同的行。请让我知道该代码在任何情况下都可能失败。(P.S:由于堆栈溢出)

Practicedontdel.java:

    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs =null;
    String val="";
    int count =0;

        conn = getOracleConnection();
        conn.setAutoCommit(false);
        ps = prepareStatement(conn,"SELECT /*+FIRST_ROWS_3*/ t.* from 
        REPROCESS_QUEUE t FOR UPDATE SKIP LOCKED");
        ps.setFetchSize(3);
        boolean rss = ps.execute();
        rs = ps.getResultSet();
        new Practisethread().start();
        while(count<3 && rs.next())
        {
            val = rs.getString(1);
            System.out.println(val);
            count++;
            Thread.sleep(10000);
        }
       conn.commit();
            System.out.println("end of main program");

Practisethread.java:在run()中:

            conn = getOracleConnection();
            conn.setAutoCommit(false);
            ps = prepareStatement(conn,"SELECT /*+FIRST_ROWS_3*/ t.* from REPROCESS_QUEUE t FOR UPDATE SKIP LOCKED");
            ps.setFetchSize(3);
            boolean rss = ps.execute();
            rs = ps.getResultSet();
            while(count<3 && rs.next())
            {
                val = rs.getString(1);
                System.out.println("******thread******");
                System.out.println(val);
                count++;
                Thread.sleep(5000);
            }
            conn.commit();
            System.out.println("end of thread program");