我需要存储一个任务项目表,其中每个项目都有一个唯一的标识符。任务可以多次到达,因此标识符不是主键。然而,我只关心我使用序列识别的任务的最新版本。任务的每个实例都可以是NEW或DONE。这些表格看起来有点像这样:
CREATE SEQUENCE TASKSEQ;
CREATE TABLE TASKS (
ID VARCHAR2(100),
STATE VARCHAR2(50),
SEQ NUMBER(20)
);
作为数据模拟,请考虑该表包含一百万个完整任务,但在将状态设置为 NEW
后,一批新的先前存在的任务立即到达。
BEGIN
FOR IDX IN 1..1000000
LOOP
INSERT INTO TASKS (ID, STATE, SEQ)
VALUES (IDX, 'DONE', TASKSEQ.NEXTVAL);
END LOOP;
FOR IDX IN 900001..1000000
LOOP
INSERT INTO TASKS (ID, STATE, SEQ)
VALUES (IDX, 'NEW', TASKSEQ.NEXTVAL);
END LOOP;
END;
我现在尝试选择在最新版本中标记为NEW
的任务。我并不真正关心处理这些任务的顺序,只关心这些任务在其各自的最新版本中被标记为“NEW”。我想先阅读“旧”任务以避免活锁。我正在获取给定批量大小的任务 block 。
选择语句看起来像这样:
SELECT L.ID, L.SEQ
FROM TASKS L
INNER JOIN (
SELECT ID, MAX(SEQ) MAXSEQ
FROM TASKS
GROUP BY ID
) R
ON L.ID = R.ID
AND L.SEQ = R.MAXSEQ
WHERE L.STATE = 'NEW'
ORDER BY L.SEQ
FETCH FIRST 100 ROWS ONLY;
任务到达应用程序后,它们将通过以下方式在数据库中进行处理和更新:
UPDATE TASKS
SET STATE = 'DONE'
WHERE ID = ?
AND SEQ = ?;
此更新完成后,将轮询下一批任务。在处理任务时可能会并行写入表,但除上述语句外,不会从表中删除任何任务。
表中的数据例如是:
ID|STATE|SEQ
A |NEW |1
A |DONE |2
B |DONE |3
B |NEW |4
C |NEW |5
C |NEW |6
在这种情况下,我预计轮询将包含 (B,4) 和 (C,6),但不包含 A。将这些元组状态更新为 DONE 后,我预计除非在表中插入更多数据,否则后续轮询将不会包含任何数据。
我想知道这个表设计是否可以通过索引有效地实现,以及这个索引会是什么样子。一个简单的索引,例如
CREATE UNIQUE INDEX NEW_TASK_INDEX ON TASKS (ID, SEQ, STATE);
并没有对排序约束起到作用,我想知道如何更改或添加索引来实现我的目标。我还想知道物化 View 是否是在其上定义索引的更好选择。
更新:至于建议的解决方案,这里是添加时执行语句的查询计划
CREATE UNIQUE INDEX tasks_idx1 ON tasks (ID ASC, SEQ DESC);
CREATE UNIQUE INDEX tasks_idx2 ON tasks (STATE, SEQ);
我得到以下计划:
对于更改后的 select 语句,我得到以下计划,该计划似乎更有效,但运行速度比上述选择慢得多:
最佳答案
根据此评论于 2019 年 3 月 22 日更新
block 引用>Please check whether the query addresses this case from OP "In this case, I would expect that a polling would contain (B,4) and (C,6) but not A"
我会从这个开始:
设置
(与您的相同,但我添加了
TASK_DATA
列以获得更准确的结果)CREATE SEQUENCE TASKSEQ; DROP TABLE TASKS; CREATE TABLE TASKS ( ID VARCHAR2(100), STATE VARCHAR2(50), SEQ NUMBER(20), TASK_DATA VARCHAR2(500) ); BEGIN FOR IDX IN 1..1000000 LOOP INSERT INTO TASKS (ID, STATE, SEQ, TASK_DATA) VALUES (IDX, 'DONE', TASKSEQ.NEXTVAL, LPAD('.',500,'.')); END LOOP; FOR IDX IN 900001..1000000 LOOP INSERT INTO TASKS (ID, STATE, SEQ, TASK_DATA) VALUES (IDX, 'NEW', TASKSEQ.NEXTVAL, LPAD('.',500,'.')); END LOOP; END;
在
STATE
上创建索引,ID
,SEQ
CREATE INDEX tasks_n1 ON tasks ( STATE, ID, SEQ ); EXEC DBMS_STATS.GATHER_TABLE_STATS(user,'TASKS');
查询
SELECT l.id, l.seq, l2.task_data FROM ( SELECT l.rowid row_id, l.id, l.seq, max(l.seq) keep ( dense_rank first order by l.seq desc) over ( partition by l.id) maxseq FROM tasks l WHERE l.state = 'NEW' AND NOT EXISTS ( SELECT 'later, completed task for ID' FROM tasks l3 WHERE l3.id = l.id AND l3.state = 'DONE' AND l3.seq > l.seq ) ORDER BY l.seq ) l INNER JOIN tasks l2 ON l2.rowid = l.row_id WHERE l.seq = l.maxseq AND ROWNUM <= 100 ;
在我的系统上,该查询运行时获取了 4,433 个缓冲区。这不太好,但如果它运行得足够频繁以至于大部分索引都在缓存中,那么它在大多数系统上可能会在几秒钟内运行。几乎所有的缓冲区获取都在读取索引。
一些注意事项:
1) 我添加了一个 TASK_DATA 列,以避免获得看起来很棒的结果,因为索引覆盖了整个 SELECT 列表和/或每个 block 的行数不切实际,使得完整扫描看起来比实际情况要好真的。
2) 这种方法运行得相对较快,因为索引涵盖了满足
l
所需的所有内容。内联 View ,因此它可以通过仅读取索引来完成该工作。对l
的 100,000 行进行排序返回的速度相当快且小,通常可以在内存中完成。最后,只费心去表了TASK_DATA
您实际想要返回的 100 行的信息。
关于sql - 如何有效地查询具有修订值的表?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55280743/