如何有效查询带有修订值的表?

时间:2019-03-21 12:44:20

标签: sql oracle indexing

我需要存储任务项目表,其中每个项目都有唯一的标识符。任务可以多次到达,因此标识符不是主键。但是,我只关心使用序列确定的任务的最新版本。任务的每个实例都可以是 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); 

我得到以下计划:

Query plan first suggestion

对于更改后的选择语句,我得到以下计划,该计划似乎更有效,但运行速度比上述选择慢了很多

enter image description here

3 个答案:

答案 0 :(得分:3)

根据此评论更新的时间19/3/22

  
    

请检查查询是否从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;

STATEIDSEQ上创建索引

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

由于基表仅用于追加操作,刷新力似乎总是导致快速刷新,这给我们提供了毫秒级的性能,但需要付出一些磁盘开销。即使在包含十亿个任务条目的表中,也可以保持这种性能。