There are a fewquestions on how to implement a queue-like table (lock specific rows, selecting a certain number of them, and skipping currently locked rows) in Oracle and SQL Server.
How can I guarantee that I retrieve a certain number ( N
) rows, assuming there are at least N
rows eligible?
From what I have seen, Oracle applies the WHERE
predicate before determining what rows to skip. This means that if I want to pull one row from a table, and two threads concurrently execute the same SQL, one will receive the row and the other an empty result set (even if there are more eligible rows).
This is contrary to how SQL Server appears to handle the UPDLOCK
, ROWLOCK
and READPAST
lock hints. In SQL Server, TOP
magically appears to limit the number of records after successfully attaining locks.
Note, two interesting articles here and here .
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);
In two separate sessions, execute:
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
Note that the first returns a row, and the second session does not return a row:
Session 1
ID
----
4
Session 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);
In two separate sessions, execute:
BEGIN TRANSACTION
SELECT TOP 1 qt.ID
FROM QueueTest qt
WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE Locked IS NULL
ORDER BY Priority;
Note that both sessions return a different row.
Session 1
ID
----
4
Session 2
ID
----
3
How can I get similar behavior in Oracle?
7条答案
按热度按时间kzipqqlq1#
"From what I have seen, Oracle applies the WHERE predicate before determining what rows to skip."
Yup. It is the only possible way. You can't skip a row from a resultset until you have determined the resultset.
The answer is simply not to limit the number of rows returned by the SELECT statement. You can still use the FIRST_ROWS_n hints to direct the optimizer that you won't be grabbing the full data set.
The software calling the SELECT should only select the first n rows. In PL/SQL, it would be
bnl4lu3b2#
The solution Gary Meyers posted is about all I can think of, short of using AQ, which does all this for you and much more.
If you really want to avoid the PLSQL, you should be able to translate the PLSQL into Java JDBC calls. All you need to do is prepare the same SQL statement, execute it and then keep doing single row fetches on it (or N row fetches).
The Oracle documentation at http://download.oracle.com/docs/cd/B10501_01/java.920/a96654/resltset.htm#1023642 gives some clue how to do this at the statement level:
To set the fetch size for a query, call setFetchSize() on the statement object prior to executing the query. If you set the fetch size to N, then N rows are fetched with each trip to the database.
So you could code up something in Java that looks something like (in Pseudo code):
UPDATE
Based on the comments below, a suggestion was made to use ROWNUM to limit the results received, but that won't work in this case. Consider the example:
Now we have a table with 10 rows. Note that I have carefully inserted the rows in reverse order, the row containing 10 is first, then 9 etc.
Say you want the first 5 rows, ordered ascending - ie 1 to 5. Your first try is this:
Which gives the results:
That is clearly wrong, and is a mistake almost everyone makes! Look at the explain plan for the query:
Oracle executes the plan from the bottom up - notice that the filter on rownum is carried out before the sort, Oracle takes the rows in the order it finds them (order they were inserted here { 10, 9, 8, 7, 6}), stops after it gets 5 rows, and then sorts that set.
So, to get the correct first 5 you need to do the sort first and then the order by using an inline view:
Now, to finally get to the point - can you put a for update skip locked in the correct place?
This gives an error:
Trying to move the for update into the view gives a syntax error:
The only thing that will work is the following, which GIVES THE WRONG RESULT:
Infact, if you run this query in session 1, and then run it again in session two, session two will give zero rows, which is really really wrong!
So what can you do? Open the cursor and fetch how many rows you want from it:
If you run that block in session 1, it will print out '1' as it got a locked the first row. Then run it again in session 2, and it will print '2' as it skipped row 1 and got the next free one.
This example is in PLSQL, but using the setFetchSize in Java you should be able get the exact same behaviour.
nnvyjq4y3#
In your first session, when you execute:
Your inner select attempt to grab only id=4 and lock it. This is successful because this single row is not yet locked.
In second session, your inner select STILL tries to grab ONLY id=4 and lock it. This is not successful because that single row is still locked by first session.
Now, if you updated the "locked" field in first session, the next session to run that select will grab id=3.
Basically, in your example you are depending on a flag that isn't being set. To use your locked flag, you probably mean to do something like:
You can then use your select for update skip locked statement since your locked flag is being maintained.
Personally, I don't like all the updates to flags (your solution may require them for whatever reason), so I'd probably just try to select the IDs I want to update (by whatever criteria) in each session:
select * from queuetest where ... for update skip locked;
For example (in reality, my criteria wouldn't be based on a list of ids, but queuetest table is overly simplistic):
Here sess1 would lock 4,3 and sess2 would lock only 2.
You cannot to my knowledge do a top-n or use group_by/order_by etc in the select for update statement, you'll get a ORA-02014.
px9o7tmv4#
My solution - is to write stored procedure like this:
This is simple example - returning TOP FIRST non blocked row. To retrieve TOP N rows - replace single fetch into local variable ("i") with loop fetch into temp table.
PS: returning cursor - is for hibernate friendship.
sg24os4d5#
I met this problem, we spend a lot of time to solve it. Some use
for update
for update skip locked
, in oracle 12c, a new method is to usefetch first n rows only
. But we use oracle 11g.Finally, we try this method, and found works well.
I wirte it in notepad, so something maybe wrong, you can modify it as a procedure or else.
1mrurvl16#
Firstly thanks for top 2 answers ..Learnt a lot from them.I have tested the following code and after running Practicedontdel.java main method ,I found that the two classes prints different rows every time. Please let me know if in any case this code might fail.(P.S : thanks to stack overflow)
Practicedontdel.java:
Practisethread.java: in run():
kxeu7u2r7#
Worked on similar task, here are my findings
Approach 1: Function with cursor
SQL
Java
Did not notice much change when removing
FIRST_ROWS
optimizer hint andsetFetchSize
.Approach 2: Prepared Statement
Java
The test
Record (log) all occurrences where count of rows returned is less than topN. These events are only noticed at the end - some workers getting less rows than they asked for.
If the prepared statement would indeed lock all rows in the table there would be a lot more events.
ROWNUM
If using ROWNUM with the PreparedStatement -
where <...> and ROWNUM < topN
- there are a lot of events where the workers get less rows than they ask for.Conclusion
I coded the
PreparedStatement
implementation just to validate that it does not work - 1 worker would lock all rows (as hinted in other answers) leading to poor concurrency.Given the findings I conclude the cursor is not needed,
PreparedStatement
works just as fine.