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:
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:
3 Réponses :
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:
(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 ;
STATE
,ID
,SEQ
XXX
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.
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.
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);
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
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.
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 numberJe 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.