2
votes

Comment puis-je interroger efficacement une table avec des valeurs révisées?

J'ai besoin de stocker une table d'éléments de tâche où chaque élément a un identifiant unique. Les tâches peuvent arriver plusieurs fois, l'identifiant n'est donc pas une clé primaire. Je ne me soucie cependant que de la dernière version d'une tâche que j'identifie à l'aide d'une séquence. Chaque instance d'une tâche peut être NEW ou DONE . Les tables ressemblent un peu à ceci:

CREATE UNIQUE INDEX tasks_idx1 ON tasks (ID ASC, SEQ DESC);
CREATE UNIQUE INDEX tasks_idx2 ON tasks (STATE, SEQ); 

En tant que simulation de données, considérez que la table contient un million de tâches complètes mais qu'un nouveau lot de tâches précédemment existantes est arrivé juste après avoir défini l'état sur NOUVEAU .

CREATE UNIQUE INDEX NEW_TASK_INDEX ON TASKS (ID, SEQ, STATE);

J'essaie maintenant de sélectionner des tâches marquées comme NEW dans leur dernière révision. Je ne me soucie pas vraiment de l'ordre dans lequel je traite ces tâches, juste du fait que ces tâches sont marquées NEW dans leur dernière révision individuelle. Je voudrais d'abord lire les «anciennes» tâches pour éviter les verrous en direct. Je récupère des blocs de tâches d'une taille de lot donnée.

L'instruction select ressemble à ceci:

ID|STATE|SEQ
A |NEW  |1
A |DONE |2
B |DONE |3
B |NEW  |4
C |NEW  |5
C |NEW  |6

Une fois les tâches arrivées dans l'application, elles sont traitées et mises à jour dans la base de données via:

UPDATE TASKS
SET STATE = 'DONE'
WHERE ID = ? 
AND SEQ = ?;

Une fois cette mise à jour terminée, le prochain lot de tâches est interrogé. Il peut y avoir eu des écritures parallèles dans la table lors du traitement des tâches, mais à part les instructions ci-dessus, aucune tâche n'est jamais supprimée de la table.

Les données de la table seraient par exemple:

XXX

Dans ce cas, je m'attendrais à ce qu'une interrogation contienne (B, 4) et (C, 6) mais pas A. Après la mise à jour de ces états de tuple sur DONE em >, Je m'attendrais à ce que le sondage suivant ne contienne aucune donnée à moins que davantage de données ne soient insérées dans la table.

Je me demande si cette conception de table peut être mise en œuvre efficacement avec un index et à quoi ressemblerait cet index comme. Un simple index tel que

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;

ne fait pas l'affaire pour la contrainte de tri et je me demande comment je pourrais changer ou ajouter un index pour accomplir mon objectif. Je me demande également si une vue matérialisée serait une meilleure option pour définir un index dessus.


Mise à jour: En ce qui concerne les solutions suggérées, voici les plans de requête pour exécuter les instructions, lors de l'ajout de

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;

J'obtiens le plan suivant:

Première suggestion du plan de requête

Pour la sélection modifiée déclaration, j'obtiens le plan suivant qui semble plus efficace mais fonctionne un peu plus lentement que la sélection ci-dessus:

entrez la description de l'image ici


12 commentaires

Pourquoi avez-vous besoin de la sous-requête? Ou n'avez-vous pas l'intention de mettre AND L.SEQ = R.MAXSEQ dans la jointure? Cette requête ne retournera que les éléments avec state = new et max of sequence number


Je l'ajoute pour filtrer la liste des tâches afin de ne contenir que les tâches avec le numéro de séquence le plus élevé pour chaque tâche individuelle. Par exemple, si la table contient id "A" avec les séquences 1, 2, 3 et "B" avec 4,5, je veux seulement A, 3 et B, 5.


Les données avant et après aideraient vraiment. Je ne peux pas comprendre ce que vous faites aux données. De nouvelles tâches arrivent. Que deviennent les anciennes données pour les tâches? Que faire s'il y a des doublons dans les nouvelles tâches? S'agit-il d'une charge ponctuelle ou de nouvelles tâches arrivent-elles tout le temps?


J'ai étendu la question avec quelques exemples de données.


créer un INDEX uniquement sur ID et SEQ et voir si cela aide


Cela semble aider. Pouvez-vous expliquer pourquoi? J'étais convaincu que le champ supplémentaire dans l'index permettrait de réduire les recherches.


Je l'ai mentionné sur la base de la compréhension de cet article use- the-index-luke.com/sql/sorting-grouping/indexed-order-by vous pouvez également inclure ID, SEQ ASC dans votre INDEX


Après avoir suivi le conseil dans la réponse où j'ai ajouté une colonne factice, la performance a de nouveau trébuché.


@RafaelWinterhalter Avez-vous essayé l'index et la requête suggérés dans la réponse?


@RafaelWinterhalter Toute amélioration avec ma réponse également Veuillez poster le plan d'explication de votre requête


toute mise à jour sur la situation actuelle tout gain avec la dernière série de réponses


Je travaille activement sur une solution en essayant actuellement un détour via des vues matérialisées.


3 Réponses :


3
votes

MISE À JOUR 22/03/2019 sur la base de ce commentaire

Veuillez vérifier si la requête répond à ce cas depuis OP "Dans ce cas, je m'attendrais à ce qu'un sondage contienne (B, 4) et (C, 6) mais pas A"

Je commencerais par ceci:

Configuration

(identique à la vôtre, mais j'ai ajouté une colonne TASK_DATA pour des résultats plus précis )

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
;

Créer un index sur STATE,ID,SEQ

XXX

Requête

CREATE INDEX tasks_n1 ON tasks ( STATE, ID, SEQ );
EXEC DBMS_STATS.GATHER_TABLE_STATS(user,'TASKS');

Sur mon système, cette requête s'exécute avec 4 433 obtentions de tampon. Ce n'est pas génial, mais il devrait s'exécuter en peut-être quelques secondes sur la plupart des systèmes s'il s'exécute suffisamment souvent pour que la majeure partie de l'index se trouve dans le cache. Presque tous les tampons récupérés lisent l'index.

Quelques notes:

1) J'ai ajouté une colonne TASK_DATA pour éviter d'obtenir des résultats qui ne semblent excellents que parce que les index couvraient l'ensemble SELECT list et / ou il y avait un nombre irréaliste de lignes par bloc, ce qui donne l'impression que les analyses complètes semblent meilleures qu'elles ne le seraient réellement.

2) Cette approche fonctionne relativement rapidement car l'index couvre tout ce qui est nécessaire pour satisfaire le l vue en ligne, donc il peut faire ce travail en ne lisant que l'index. Le tri des 100 000 lignes que l renverra est assez rapide et assez petit pour être généralement mis en mémoire. Enfin, cela ne dérange que d'aller dans la table des informations TASK_DATA pour les 100 lignes que vous voulez réellement renvoyer.


8 commentaires

Veuillez vérifier si la requête aborde ce cas de OP "Dans ce cas, je m'attendrais à ce qu'un sondage contienne (B, 4) et (C, 6) mais pas A"


@ psaraj12 J'apprécie la gentillesse avec laquelle vous avez formulé cela, puisque je suis presque sûr que vous saviez très bien que ma réponse ne traitait pas de ce cas. ) Quoi qu'il en soit, j'ai mis à jour ma réponse et je pense qu'elle est mieux couverte maintenant. La performance a souffert pour y remédier, mais je pense que ce n'est toujours pas si mal. Je serai intéressé de voir d'autres réponses. Si j'ai le temps, j'essaierai de trouver quelque chose de mieux plus tard.


Même moi, je l'avais négligé, donc je vous ai informé


Merci de partager vos pensées Matthew. Le plan de requête semble en effet assez efficace, mais l'exécution de la requête prend encore environ dix secondes, ce que je peux me permettre. Plus j'y pense, plus je pense qu'une vue matérialisée pourrait être le meilleur moyen de le faire, en fait, je ne saurais pas comment Oracle pourrait lire cette requête directement à partir d'un index. Je vais essayer et vous le faire savoir.


HI Rafael dans le plan Explain Je vérifierais le coût de chaque opération HASH_JOIN, SORT etc. sinon le coût total peut être assez trompeur


@RafaelWinterhalter Votre plan d'explication indique que votre tableau contient 10 millions de lignes, et non 1 million. Et combien de secondes pouvez-vous «vous permettre»? Il existe des mesures de réglage plus agressives. Mais, avant de les explorer, je vous suggère de modifier votre application afin que, lorsqu'elle met à jour une tâche sur "DONE", elle trouve les tâches "NEW" précédentes pour le même ID et les marque sur "DONE" (ou "SKIPPED" ) ou quelque chose. Mieux vaut ajouter un peu de frais généraux au processus de mise à jour plutôt que de gaspiller des ressources informatiques en recherchant encore et encore des enregistrements "NOUVEAUX" que vous n'avez pas vraiment besoin de traiter.


J'ai fait différents tests, donc la divergence. Oui, la mise à jour de ces champs est mon plan de sauvegarde. Je me demandais simplement si j'aurais pu l'éviter grâce à un index intelligent, mais en pensant à ce à quoi ressemblerait les arbres, je commence à voir comment cela ne fonctionnerait pas.


Je pense que votre plan de sauvegarde devrait être le plan principal. Supposons que vous ayez une réponse apparemment parfaite. Cela se dégraderait avec le temps, car chaque fois qu'il y avait un enregistrement «DONE» pour une tâche ayant des enregistrements «NEW» antérieurs, ces enregistrements «NEW» devraient être lus et ignorés à chaque fois que votre requête s'exécutait (car ils seraient vraisemblablement le valeurs les plus anciennes pour SEQ toujours à l'état "NEW"). Votre requête deviendrait de plus en plus lente avec le temps. Il est préférable de garder les données exactes conformément aux règles d'application. Si un enregistrement "DONE" rend les autres "NOUVEAU", alors il est préférable de les mettre à jour.



1
votes

Sur la base de ce plan d’explication, vous pouvez utiliser l’index ci-dessous pour INNER JOIN

  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

Pour votre requête externe, vous pouvez indexer STATE et SEQ afin que l’index puisse être utilisé dans le plan Explain p>

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

Sur la base du plan d'explication que vous avez fourni, utilisez le SQL ci-dessous et voyez le plan d'explication

J'utiliserais le fait qu'il existe un index sur STATE et SEQ

Remarque: -J'ai évité le FAST FULL SCAN dans le plan d'explication dans le SQL ci-dessous

Par exemple, s'il n'y a que 1000 lignes dans l'état NEW, seules celles-ci doivent être analysées la valeur de séquence MAX

 CREATE INDEX tasks_idx2 ON tasks (STATE,SEQ); 

J'ai effectué des tests supplémentaires sur vos données et les coutures suivantes pour obtenir un bénéfice maximal

Mise à jour: -La suppression de la refactorisation des sous-requêtes a doublé les performances (résultats renvoyés de 1 s à 1/2 s)

CREATE INDEX tasks_idx1 ON tasks (ID,SEQ);


2 commentaires

J'ai ajouté le plan de requête que j'ai obtenu à ma question.


J'ai modifié la réponse en fonction du plan d'explication que vous avez fourni



1
votes

Après de nombreux tests de performances, je conclus qu'il n'y a pas de bonne solution qui utilise uniquement un index. En fin de compte, Oracle doit résoudre la révision maximale de chaque identifiant, puis filtrer ces révisions en mémoire. Il n'y a aucun moyen de naviguer dans l'arbre b * d'un index vers un petit jeu de résultats, mais il y aura toujours une matérialisation intermédiaire en raison du fait que l'index ne peut pas commander sur une valeur agrégée.

Une solution que j'ai trouvée maintenant est basée sur en utilisant des vues matérialisées. Tout d'abord, j'ai créé un journal de vues matérialisées pour la table de base:

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

Le Je crée une vue d'aide qui contient toujours la révision maximale pour chaque 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);

En utilisant cette table, je peux maintenant créer une vue matérialisée qui contient les données que je veux de manière indexable:

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

En raison de la utilisation de la table de base uniquement en append, la force d'actualisation semble toujours se traduire par une actualisation rapide pour nous, ce qui nous donne des performances à la milliseconde au prix d'une surcharge de disque. Cette performance est conservée même dans une table contenant un milliard d'entrées de tâches.


0 commentaires