Как я могу эффективно запросить таблицу с исправленными значениями? - PullRequest
2 голосов
/ 21 марта 2019

Мне нужно хранить таблицу элементов задач, где каждый элемент имеет уникальный идентификатор.Задачи могут приходить несколько раз, поэтому идентификатор не является первичным ключом.Однако меня волнует только последняя версия задачи, которую я определяю по последовательности.Каждый экземпляр задачи может быть 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

Для измененного оператора select я получаю следующий план, который кажется более эффективным, ноработает немного медленнее, чем вышеуказанный выбор:

enter image description here

Ответы [ 3 ]

3 голосов
/ 21 марта 2019

ОБНОВЛЕНО 3/22/19 на основе этого комментария

Пожалуйста, проверьте, обращается ли запрос к этому случаю из 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
;

В моей системе этот запрос выполняется с 4433 буферами.Это не очень хорошо, но на большинстве систем он должен выполняться за несколько секунд, если он выполняется достаточно часто, чтобы большая часть индекса находилась в кеше.Почти все данные буфера читают индекс.

Несколько замечаний:

1) Я добавил столбец TASK_DATA, чтобы избежать получения результатов, которые кажутся великолепными, поскольку индексы покрывали весь список SELECT.и / или было нереально большое количество строк в блоке, поэтому полное сканирование выглядит лучше, чем было бы на самом деле.

2) Этот подход выполняется относительно быстро, поскольку индекс охватывает все, что необходимо для удовлетворения lвстроенный вид, так что он может сделать эту работу, читая только индекс.Сортировка 100 000 строк, которые вернет l, довольно быстрая и достаточно мала, чтобы обычно делать ее в памяти.Наконец, надо только перейти к таблице с информацией TASK_DATA для 100 строк, которые вы действительно хотите вернуть.

1 голос
/ 26 марта 2019

После многих тестов производительности я пришел к выводу, что не существует хорошего решения, которое использует только индекс.В конце концов, Oracle необходимо определить максимальную ревизию каждого идентификатора, а затем отфильтровать эти ревизии в памяти.Невозможно перемещать дерево b * любого индекса к небольшому набору результатов, но всегда будет промежуточная материализация из-за того, что индекс не может упорядочить по совокупному значению.

Решение, которое я нашел сейчасоснован на использовании материализованных представлений.Сначала я создал журнал материализованного представления для базовой таблицы:

CREATE MATERIALIZED VIEW LOG ON TASKS 
WITH ROWID, SEQUENCE(ID, SEQ) 
INCLUDING NEW VALUES; 

Я создаю вспомогательное представление, которое всегда содержит максимальную редакцию для каждого идентификатора:

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

Из-за использования базовой таблицы только для добавления, сила обновления, кажется, всегда приводит кбыстрое обновление для нас, обеспечивающее производительность в миллисекундах за счет некоторого дискового пространства.Эта производительность сохраняется даже в таблице, содержащей миллиард записей задач.

1 голос
/ 22 марта 2019

На основе этого плана объяснения вы можете использовать индекс ниже для INNER JOIN

CREATE INDEX tasks_idx1 ON tasks (ID,SEQ);

Для внешнего запроса вы можете индексировать STATE и SEQ, чтобы этот индекс можно было использовать в плане объяснения

 CREATE INDEX tasks_idx2 ON tasks (STATE,SEQ); 

На основе предоставленного вами плана объяснения используйте приведенный ниже SQL и смотрите план объяснения

Я бы использовал тот факт, что есть индекс по STATE и SEQ

Примечание: -Я избежал БЫСТРОГО ПОЛНОГО ПРОВЕРКИ в плане объяснения в приведенном ниже SQL

Например, если в НОВОМ состоянии только 1000 строк, то необходимо отсканировать только те, которые имеют значение последовательности 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...