Заставить Oracle вернуть TOP N строк с SKIP LOCKED - PullRequest
21 голосов
/ 25 мая 2011

Существует несколько вопросов о том, как реализовать таблицу, подобную очереди (блокировка определенных строк, выбор определенного количества и пропуск текущих заблокированных строк) в Oracle иSQL Server.

Как я могу гарантировать, что я получу определенное количество (N) строк, при условии, что есть хотя бы N подходящих строк?

Из того, что я видел, Oracleприменяет предикат WHERE, прежде чем определять, какие строки пропустить.Это означает, что если я хочу извлечь одну строку из таблицы, и два потока одновременно выполняют один и тот же SQL-запрос, один получит строку, а другой - пустой набор результатов (даже если имеется больше подходящих строк).

Это противоречит тому, как SQL Server обрабатывает подсказки блокировки UPDLOCK, ROWLOCK и READPAST.В 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);

В двух отдельных сеансах выполните:

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

Обратите внимание, что первый возвращает строку, ивторой сеанс не возвращает строку:

сеанс 1

 ID
----
  4

сеанс 2

 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;

Обратите внимание, что оба сеанса возвращают разные строки.

Сессия 1

 ID
----
  4

Сессия2

 ID
----
  3

Как я могу получить подобное поведение в Oracle?

Ответы [ 6 ]

14 голосов
/ 25 мая 2011

"Из того, что я видел, 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;
10 голосов
/ 11 июля 2011

Решение, опубликованное Гэри Мейерсом, - это почти все, что я могу придумать, кроме использования AQ, которое делает все это для вас и многое другое.

Если вы действительно хотите избежать PLSQL, вы должны иметь возможностьперевести PLSQL в вызовы Java JDBC.Все, что вам нужно сделать, это подготовить один и тот же оператор SQL, выполнить его, а затем продолжать выполнять выборки из одной строки (или N выборок строк).

Документация Oracle по http://download.oracle.com/docs/cd/B10501_01/java.920/a96654/resltset.htm#1023642 дает некоторое представление о том, какчтобы сделать это на уровне оператора:

Чтобы установить размер выборки для запроса, вызовите 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

Попытка переместить для обновления в представление приводит к синтаксической ошибке:

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;

Infact, если вы запустите этот запрос в сеансе 1, а затем снова запустите его во втором сеансе, сеанс два даст ноль строк, что на самом деле действительно неправильно!

Так что вы можете сделать?Откройте курсор и извлеките из него желаемое количество строк:

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, и он напечатает '2', пропустив строку 1 и получив следующую свободную.

Этот пример в PLSQL, но используя setFetchSize в Java, вы сможете получитьточно такое же поведение.

1 голос
/ 03 августа 2018

Я встретил эту проблему, мы тратим много времени на ее решение. Некоторые используют for update for update skip locked, в Oracle 12c новый метод должен использовать fetch first n rows only. Но мы используем оракул 11 г.

Наконец, мы попробуем этот метод, и нашёл, что работает хорошо.

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;

Я записал его в блокноте, так что что-то может быть не так, вы можете изменить его как процедуру или как-то еще.

1 голос
/ 14 октября 2011

Мое решение - написать хранимую процедуру следующим образом:

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 - замените одиночную выборку в локальную переменную ("i") циклической выборкой во временную таблицу.

PS: возвращение курсора - для спящего режима дружбы.

1 голос
/ 08 июля 2011

В вашем первом сеансе, когда вы выполняете:

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. Выберите нужные идентификаторы на основе некоторых критериев.
  2. Немедленное обновление заблокированного флага = 1 для этих идентификаторов (если ресурс занят, другой сеанс побьет вас к этому шагу для 1 или более идентификаторов, снова перейдите к 1)
  3. Делайте что-нибудь с этими идентификаторами
  4. обновить заблокированный флаг обратно до нуля

Затем вы можете использовать свой выбор для обновления, чтобы пропустить заблокированный оператор, поскольку ваш флаг блокировки сохраняется.

Лично мне не нравятся все обновления флагов (ваше решение может потребовать их по любой причине), поэтому я, вероятно, просто попытаюсь выбрать идентификаторы, которые я хочу обновить (какими бы критерии) в каждой сессии:

выберите * из очереди, где ... для обновления пропущено заблокировано;

Например (на самом деле мои критерии не основаны на списке идентификаторов, но таблица очередей слишком упрощена):

  • sess 1: выберите * из очереди, где идентификатор в (4,3) для обновления пропущен заблокирован;

  • sess 2: выберите * из очереди, где идентификатор в (4,3,2) для обновления пропущен заблокирован;

Здесь sess1 заблокирует 4,3, а sess2 заблокирует только 2.

Насколько мне известно, вы не можете использовать top-n или использовать group_by / order_by и т. Д. В операторе select for update, вы получите ORA-02014.

0 голосов
/ 21 июня 2019

Во-первых, спасибо за первые 2 ответа. Изучите много из них. Я проверил следующий код и после запуска основного метода Practicedontdel.java обнаружил, что эти два класса каждый раз выводят разные строки. Пожалуйста, дайте мне знать, если в любом случае этот код может потерпеть неудачу. (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: in 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");
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...