web-dev-qa-db-fra.com

Utiliser une table de base de données comme file d'attente

Je veux utiliser une table de base de données en tant que file d'attente. Je veux y insérer des éléments et en extraire des éléments dans l’ordre inséré (FIFO). Ma considération principale est la performance, car j’ai des milliers de transactions à la seconde. Donc, je veux utiliser une requête SQL qui me donne le premier élément sans chercher dans la table entière. Je ne supprime pas une ligne lorsque je le lis . Est-ce que SELECT TOP 1 ..... aide ici? Devrais-je utiliser des index spéciaux?

38
Shayan

J'utiliserais un champ IDENTITY en tant que clé primaire pour fournir l'identifiant unique incrémentant pour chaque élément mis en file d'attente et y coller un index en cluster. Cela représenterait l'ordre dans lequel les éléments ont été mis en file d'attente.

Pour conserver les éléments dans la table de file d'attente pendant que vous les traitez, vous avez besoin d'un champ "statut" pour indiquer le statut actuel d'un élément particulier (par exemple, 0 = en attente, 1 = en cours de traitement, 2 = en traitement). Cela est nécessaire pour éviter qu'un élément ne soit traité deux fois.

Lors du traitement d'éléments dans la file d'attente, vous devez rechercher l'élément suivant dans la table, NON en cours de traitement. Cela devrait être fait de manière à empêcher que plusieurs processus prennent le même article à traiter en même temps, comme indiqué ci-dessous. Notez les indications de la table UPDLOCK et READPAST dont vous devez être conscient lors de la mise en œuvre des files d'attente.

par exemple. dans un sproc, quelque chose comme ceci:

DECLARE @NextID INTEGER

BEGIN TRANSACTION

-- Find the next queued item that is waiting to be processed
SELECT TOP 1 @NextID = ID
FROM MyQueueTable WITH (UPDLOCK, READPAST)
WHERE StateField = 0
ORDER BY ID ASC

-- if we've found one, mark it as being processed
IF @NextId IS NOT NULL
    UPDATE MyQueueTable SET Status = 1 WHERE ID = @NextId

COMMIT TRANSACTION

-- If we've got an item from the queue, return to whatever is going to process it
IF @NextId IS NOT NULL
    SELECT * FROM MyQueueTable WHERE ID = @NextID

Si le traitement d'un élément échoue, voulez-vous pouvoir le réessayer plus tard? Si tel est le cas, vous devrez réinitialiser le statut sur 0 ou quelque chose du genre. Cela nécessitera plus de réflexion.

Sinon, n'utilisez pas une table de base de données comme file d'attente, mais quelque chose comme MSMQ - vous avez seulement pensé que je mettrais cela dans le mélange!

30
AdaTheDev

Si vous ne supprimez pas vos lignes traitées, vous aurez besoin d'une sorte d'indicateur indiquant qu'une ligne a déjà été traitée.

Placez un index sur ce drapeau et sur la colonne à commander.

Partitionnez votre table sur cet indicateur, afin que les transactions supprimées de la file d'attente n'engorgent pas vos requêtes.

Si vous obteniez réellement des messages 1.000 toutes les secondes, cela produirait 86.400.000 lignes par jour. Vous voudrez peut-être penser à un moyen de nettoyer les anciennes lignes.

7
Peter Lang

Tout dépend de votre moteur de base de données/mise en œuvre.

Pour moi, de simples files d'attente sur des tables avec les colonnes suivantes:

id / task / priority / date_added

fonctionne habituellement.

J'ai utilisé priorité et tâche pour regrouper les tâches et, en cas de tâche double, j'ai choisi celle dont la priorité est la plus grande.

Et ne vous inquiétez pas, pour les bases de données modernes, "des milliers" n’ont rien de spécial.

4
bluszcz

Créez un index en cluster sur une colonne de date (ou d'auto-incrémentation). Cela maintiendra les lignes de la table approximativement dans l'ordre d'index et permettra un accès rapide basé sur l'index lorsque vous ORDER BY la colonne indexée. L'utilisation de TOP X (ou LIMIT X, en fonction de votre RDMBS) ne récupérera alors que les x premiers éléments de l'index.

Avertissement de performance: vous devez toujours consulter les plans d'exécution de vos requêtes (sur des données réelles) pour vous assurer que l'optimiseur ne fait pas des choses inattendues. Essayez également de référencer vos requêtes (à nouveau sur des données réelles) pour pouvoir prendre des décisions éclairées.

3
David Schmitt

peut-être qu’ajouter un LIMIT = 1 à votre déclaration choisie pourrait aider.

2
Reed Debaets

Ce ne sera pas du tout un problème tant que vous utilisez quelque chose pour garder une trace de la date/heure de l'insertion. Voir ici pour les options mysql . La question est de savoir si vous avez besoin uniquement du dernier élément absolu soumis ou si vous devez effectuer une itération. Si vous devez effectuer une itération, vous devez saisir un bloc avec une instruction ORDER BY, le boucler et rappeler le dernier datetime afin que vous puissiez l'utiliser lors de votre prochain bloc.

2
David Berger

Puisque vous ne supprimez pas les enregistrements de la table, vous devez avoir un index composite sur (processed, id), où processed est la colonne qui indique si l'enregistrement actuel a été traité.

La meilleure chose à faire serait de créer une table partitionnée pour vos enregistrements et de définir le champ PROCESSED comme clé de partitionnement. De cette façon, vous pouvez conserver trois index locaux ou plus.

Cependant, si vous traitez toujours les enregistrements dans l'ordre id et que vous n'avez que deux états, la mise à jour de l'enregistrement signifierait simplement extraire l'enregistrement de la première feuille de l'index et l'ajouter à la dernière feuille.

L'enregistrement actuellement traité comportera toujours la plus petite id de tous les enregistrements non traités et la plus grande id de tous les enregistrements traités.

2
Quassnoi

J'avais la même question générale de "comment transformer une table en file d'attente" et ne pouvais trouver la réponse que je voulais nulle part. 

Voici ce que j'ai proposé pour Node/SQLite/better-sqlite3. En gros, il suffit de modifier les clauses WHERE et ORDER BY internes pour votre cas d'utilisation.

module.exports.pickBatchInstructions = (db, batchSize) => {
  const buf = crypto.randomBytes(8); // Create a unique batch identifier

  const q_pickBatch = `
    UPDATE
      instructions
    SET
      status = '${status.INSTRUCTION_INPROGRESS}',  
      run_id = '${buf.toString("hex")}',
      mdate = datetime(datetime(), 'localtime')
    WHERE
      id IN (SELECT id 
        FROM instructions 
        WHERE 
          status is not '${status.INSTRUCTION_COMPLETE}'
          and run_id is null
        ORDER BY
          length(targetpath), id
        LIMIT ${batchSize});
  `;
  db.run(q_pickBatch); // Change the status and set the run id

  const q_getInstructions = `
    SELECT
      *
    FROM
      instructions
    WHERE
      run_id = '${buf.toString("hex")}'
  `;
  const rows = db.all(q_getInstructions); // Get all rows with this batch id

  return rows;
};
1
Daniel Kaplan

Une solution très simple pour éviter les transactions, les verrous, etc. consiste à utiliser les mécanismes de suivi des modifications (et non la capture de données). Il utilise le contrôle de version pour chaque ligne ajoutée/mise à jour/supprimée afin que vous puissiez suivre les modifications apportées après une version spécifique.

Ainsi, vous conservez la dernière version et interrogez les nouvelles modifications. 

Si une requête échoue, vous pouvez toujours revenir en arrière et interroger les données de la dernière version . En outre, si vous ne souhaitez pas obtenir toutes les modifications avec une seule requête, vous pouvez obtenir le n premier ordre par dernière version et stocker la version la plus complète. 'vous devez interroger à nouveau.

Voir ceci par exemple Utilisation du suivi des modifications dans SQL Server 2008

0
George Mavritsakis