我需要存储任务项目表,其中每个项目都有唯一的标识符。任务可以多次到达,因此标识符不是主键。但是,我只关心使用序列确定的任务的最新版本。任务的每个实例都可以是 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
。我想先阅读“旧”任务,以避免动态锁定。我正在获取给定批处理大小的任务块。
select语句看起来像这样:
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);
对顺序约束没有技巧,我想知道如何更改或添加索引以实现目标。我还想知道,物化视图是否是在其上定义索引的更好选择。
更新:至于建议的解决方案,这是添加时执行语句的查询计划
CREATE UNIQUE INDEX tasks_idx1 ON tasks (ID ASC, SEQ DESC);
CREATE UNIQUE INDEX tasks_idx2 ON tasks (STATE, SEQ);
我得到以下计划:
对于更改后的选择语句,我得到以下计划,该计划似乎更有效,但运行速度比上述选择慢了很多
答案 0 :(得分:3)
请检查查询是否从OP中解决了这种情况:“在这种情况下,我希望轮询包含(B,4)和(C,6),但不包含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,443个缓冲区。这不是很好,但是如果它足够频繁地运行以使大多数索引位于高速缓存中,那么它应该在大多数系统上运行几秒钟。几乎所有获得的缓冲区都在读取索引。
一些注意事项:
1)我添加了TASK_DATA列,以避免获得仅看起来不错的结果,因为索引覆盖了整个SELECT列表和/或每个块的行数高得不切实际,使得完整扫描看起来比它们要好真的。
2)这种方法运行相对较快,因为索引涵盖了满足l
内联视图所需的所有内容,因此它可以通过仅读取索引来完成这项工作。对l
将返回的100,000行进行排序非常快,而且足够小,通常可以在内存中进行。最后,它只会麻烦您进入表以获取您实际要返回的100行的TASK_DATA
信息。
答案 1 :(得分:1)
基于此解释计划,您可以在索引下使用INNER JOIN
CREATE INDEX tasks_idx1 ON tasks (ID,SEQ);
对于外部查询,您可以索引STATE和SEQ,以便可以在解释计划中使用该索引
CREATE INDEX tasks_idx2 ON tasks (STATE,SEQ);
根据您提供的说明计划,使用以下SQL并查看说明计划
我会利用STATE和SEQ有索引的事实
注意:-我在下面的SQL解释计划中避免了FAST FULL SCAN
例如,如果只有1000行处于NEW状态,那么仅需要扫描这些行的MAX序列值
with STATE1 as (select * from TASKS where state='NEW')
, STATE2 as (select * from tasks where state='DONE')
SELECT * FROM
(
SELECT L.ID, L.SEQ
FROM STATE1 L
INNER JOIN (
SELECT ID, MAX(SEQ) MAXSEQ
FROM STATE1
GROUP BY ID
) R
ON L.ID = R.ID
AND L.SEQ = R.MAXSEQ
Where NOT EXISTS (Select 1 from STATE2 where L.id=STATE2.ID and L.SEQ <
STATE2.SEQ)
ORDER BY L.SEQ)
WHERE ROWNUM <=100
我对您的数据和以下接缝进行了进一步测试,以获取最大利益
更新:-删除子查询重构使性能提高了一倍(结果从1秒返回到1/2秒)
CREATE INDEX tasks_idx1 ON tasks (state,id,SEQ);
SELECT * FROM
(
SELECT L.ID, L.SEQ
FROM TASKS L
INNER JOIN (
SELECT ID, MAX(SEQ) MAXSEQ
FROM TASKS
WHERE STATE='NEW'
GROUP BY ID
) R
ON L.ID = R.ID
AND L.SEQ = R.MAXSEQ
Where L.STATE='NEW'
AND NOT EXISTS (Select 1 from TASKS where TASKS.STATE='DONE' AND L.id=TASKS.ID and L.SEQ <
TASKS.SEQ)
ORDER BY L.SEQ)
WHERE ROWNUM <=100
答案 2 :(得分:1)
经过许多性能测试后,我得出结论,没有好的解决方案仅使用索引。最后,Oracle需要解析每个id的最大修订,然后在内存中过滤这些修订。无法将任何索引的b * tree导航到较小的结果集,但由于索引无法按汇总值排序,因此始终会出现中间实现。
我现在发现的解决方案基于使用物化视图。首先,我为基表创建了一个物化视图日志:
CREATE MATERIALIZED VIEW LOG ON TASKS
WITH ROWID, SEQUENCE(ID, SEQ)
INCLUDING NEW VALUES;
我创建了一个助手视图,该视图始终包含每个id的最高修订版本:
CREATE MATERIALIZED VIEW LATEST_REVISION
REFRESH FAST ON COMMIT
AS
SELECT ID, MAX(SEQ) MAXSEQ
FROM TASKS
GROUP BY ID;
CREATE UNIQUE INDEX LATEST_REVISION_IDX ON LATEST_REVISION (ID, MAXSEQ);
使用此表,我现在可以创建一个物化视图,该视图以可索引的方式包含所需的数据:
CREATE MATERIALIZED VIEW LOG ON LATEST_REVISION
WITH ROWID, SEQUENCE(ID, MAXSEQ)
INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW LATEST_ENTRIES
REFRESH FORCE ON COMMIT
AS
SELECT T.ID, T.SEQ, T.STATE
FROM TASKS T
INNER JOIN LATEST_REVISION R
ON T.ID = R.ID AND T.SEQ = R.MAXSEQ;
CREATE UNIQUE INDEX LATEST_ENTRIES_IDX ON LATEST_ENTRIES (STATE, SEQ);
由于基表仅用于追加操作,刷新力似乎总是导致快速刷新,这给我们提供了毫秒级的性能,但需要付出一些磁盘开销。即使在包含十亿个任务条目的表中,也可以保持这种性能。