【问题标题】:How can I efficiently query a table with revisioned values?如何有效地查询具有修订值的表?
【发布时间】:2019-03-21 12:44:19
【问题描述】:

我需要存储一个任务项目表,其中每个项目都有一个唯一标识符。任务可以多次到达,因此标识符不是主键。然而,我只关心我使用序列识别的任务的最新版本。任务的每个实例都可以是NEWDONE。这些表看起来有点像这样:

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

我得到以下计划:

对于更改后的选择语句,我得到以下计划,它看起来更有效,但运行速度比上面的选择慢很多:

【问题讨论】:

  • 为什么需要子查询?还是您不是要在连接中输入AND L.SEQ = R.MAXSEQ?此查询将仅返回 state = new 和最大序列号的项目
  • 我添加它以过滤任务列表以仅包含每个任务的最高序列号的任务。例如,如果表包含 id "A" 的序列为 1,2,3 和 "B" 的序列为 4,5,我只想要 A,3 和 B,5。
  • 之前和之后的数据真的很有帮助。我无法弄清楚你对数据做了什么。新任务即将到来。任务的旧数据会发生什么变化?如果新任务中有重复项怎么办?这是一次性加载还是新任务一直在不断增加?
  • 我用一些示例数据扩展了这个问题。
  • 只在 ID 和 SEQ 上创建一个索引,看看是否有帮助

标签: sql oracle indexing


【解决方案1】:

根据此评论于 2019 年 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;

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 列表和/或每个块的行数不切实际,这使得全扫描看起来比实际更好真的。

2) 这种方法运行相对较快,因为索引涵盖了满足l 内联视图所需的所有内容,因此它可以通过仅读取索引来完成这项工作。对l 将返回的 100,000 行进行排序非常快且足够小,通常可以在内存中完成。最后,它只需要到表中查找您实际想要返回的 100 行的 TASK_DATA 信息。

【讨论】:

  • 请检查查询是否来自 OP “在这种情况下,我希望轮询将包含 (B,4) 和 (C,6) 但不包含 A”
  • @psaraj12 我很欣赏你措辞温和的方式,因为我很确定你很清楚我的回答并没有解决这种情况。 )无论如何,我已经更新了我的答案,我认为它现在覆盖得更好。性能遭受了解决它,但我认为它仍然没有那么糟糕。我有兴趣看到其他答案。如果我有时间,我稍后会尝试想出更好的东西。
  • 连我都忽略了,所以我通知了你
  • 感谢马修分享您的想法。查询计划似乎确实非常有效,但运行查询仍然需要大约十秒钟,这超出了我的承受能力。我想得越多,我就越认为物化视图可能是最好的方法,实际上,我不知道 Oracle 是如何直接从索引中读取这个查询的。我会尝试并告诉你。
  • 我认为您的备份计划应该是主要计划。假设你得到了一个看似完美的答案。它会随着时间的推移而降级,因为每次对于具有较早“NEW”记录的任务有“DONE”记录时,每次运行查询时都必须读取并忽略这些“NEW”记录(因为它们可能会SEQ 的最旧值仍处于“新”状态)。随着时间的推移,您的查询会变得越来越慢。最好根据应用规则保持数据准确。如果“DONE”记录使其他记录不再“NEW”,那么最好更新它们。
【解决方案2】:

根据这个解释计划,您可以使用下面的索引进行 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

【讨论】:

  • 我在我的问题中添加了查询计划。
  • 我已根据您提供的解释计划编辑了答案
【解决方案3】:

经过多次性能测试,我得出的结论是,没有只使用索引的好的解决方案。最后,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);

由于基表仅追加使用,刷新力似乎总是为我们带来快速刷新,以牺牲一些磁盘开销为代价为我们提供毫秒级的性能。即使在包含十亿个任务条目的表中也保留了这种性能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-02
    • 2013-11-29
    • 2017-12-23
    • 2017-07-30
    • 1970-01-01
    相关资源
    最近更新 更多