Requête pour exclure des résultats sur des paires de valeurs

Des histoires de `in` et `not in`… ?

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous !

Je viens vers vous pour un problème qui tient en partie de l’algorithmique et aussi de ma compréhension des opérateurs IN et NOT IN en MySQL. Plus je tente de faire ça formellement, moins j’arrive à me convaincre que ce que je fais est correct, et que je m’en sortirai seul.

La situation est simple : j’ai des actifs financiers qui sont négociés avec différents comptes (qui eux sont des actifs d’un type particulier), le tout dans un portefeuille. Chaque jour, je dois calculer la nouvelle position (le montant total que ça représente) des différents actifs en fonction des quantité, prix à la clôture de la bourse et éventuel taux de change (aussi à la clôture de la bourse), et ce au niveau des comptes et du portefeuille.

L’idée que j’avais consiste à se baser sur les transactions de la journée. D’abord les renflouements sur les comptes (transactions de liquide), et ensuite les achats d’actifs (transactions d’actifs), qui se font toujours depuis un compte.
Reste que tous les actifs ne sont pas négociés tous les jours, et tous les comptes ne sont pas non plus utilisés tous les jours. Il me faut donc récupérer les positions de la veille pour ce qui n’a pas été concerné aujourd’hui, afin de les mettre à jour (en fait, je vais recréer un objet pour les nouvelles données, question d’historique, mais c’est un détail en ce qui nous occupe ici).

Ma question est la suivante : sachant que je garde trace des comptes et des actifs rencontrés dans les diverses transactions, comment récupérer les positions que je n’ai pas déjà mises à jour en traversant les transactions ?

Exemple : on a simplement deux comptes (c1 et c2) et deux actifs (a1 et a2), les actifs ayant été négociés avec les deux comptes dans le passé. Aujourd’hui, il y a eu une transaction pour chaque actif, mettons une transaction pour l’actif a1 avec le compte c1, et une transaction pour l’actif a2 avec le compte c2.

Comment récupérer les positions a1;c2 et a2;c1 ?

J’étais parti sur SELECT position p WHERE p.a NOT IN (a1, a2) AND p.c NOT IN (c1, c2), mais je me suis rendu compte que je n’allais rien récupérer. Avec un IN, je vais tout récupérer. J’étais en train de réfléchir à utiliser un XOR (si ça existe en SQL), mais je ne suis toujours pas sûr…

Est-ce que quelqu’un verrait une solution simple et efficace ?

Merci d’avance  :)

+0 -0

Tu commences ta requête par

1
select position p where 

J’imagine que c’est une faute d’étourderie, et que tu sais que ça doit commencer par quelque chose comme :

1
select * from position p where 

Ensuite, pour le not in, ce que tu cherches à faire n’est pas clair du tout. Mais voici peut-être une indication utile, une syntaxe autorisée :

1
select * from position p where (p.a, p.c) not in ( select a, c from autre_requete )

Par exemple :

1
2
select * from position p 
where (p.a, p.c) not in ( select a1, c1 from dual union  select a2, c2 from dual  )

Bon, j’avais déjà passé la journée dessus, je m’étais embrouillé, je pensais que j’arriverais à être clair quand j’ai écrit le message, mais en même temps j’étais tellement dans mon trip…

En fait, en sachant quels comptes et quels actifs sur quels comptes ont été négociés aujourd’hui (et donc leurs positions respectives calculées), j’aimerais pouvoir récupérer les positions qui n’étaient pas concernées par les transactions du jour.

J’étoffe un peu l’exemple, peut-être que ça pourra vous aider autant que moi.
Comme expliqué, j’ai deux actifs et deux comptes dans mon porte-monnaie. Les deux actifs ont été négociés avec les deux comptes par le passé, j’ai donc pour chaque jour précédent une position pour la paire a1;c1, et une autre pour la paire a1;c2. Similaire avec le second actif ou j’ai une position pour la paire a2;c1 et une pour a2;c2.
Dans les transactions d’aujourd’hui, il y a eu négoce de chacun des deux actifs, ce qui me génère les positions pour les paires a1;c1 et a2;c2.
Sachant que je dois mettre à jour le prix de l’actif et éventuellement le taux de change pour les positions a1;c2 et a2;c1, comment les récupérer ?

Initialement je pensais pouvoir garder les listes (séparées) des comptes et des actifs rencontrés lors du traitement des transactions, et avec ces listes exclure ce j’avais déjà. Mais mon raisonnement n’est pas correct, donc j’ai créé ce sujet  ;)

Tu commences ta requête par

1
select position p where 

J’imagine que c’est une faute d’étourderie

elegance

Non, c’était voulu et conscient, je ne souhaitais que donner l’idée de ma requête sans en écrire une qui soit "canonique"  ;)

Ensuite, pour le not in, ce que tu cherches à faire n’est pas clair du tout. Mais voici peut-être une indication utile, une syntaxe autorisée :

1
select * from position p where (p.a, p.c) not in ( select a, c from autre_requete )

Par exemple :

1
2
select * from position p 
where (p.a, p.c) not in ( select a1, c1 from dual union  select a2, c2 from dual  )
elegance

Merci, je pense que tu as saisi l’idée. J’avais imaginé quelque chose dans le genre en fin de compte, mais j’avoue que l’idée de boucler sur les paires pour construire la requête me semble gourmand en ressources. Je sais que je ne pourrai peut-être pas y couper, mais j’aimerais en être sûr.

Comment récupérer les positions a1;c2 et a2;c1 ?

Pas vraiment compris la question. Ce que tu veux faire, c’est ça :

1
2
SELECT * FROM position p
WHERE (p.a = a1 AND p.c = c2) OR (p.a = a2 AND p.c = c1);

?

yoch

C’est ça, tout en sachant que mon exemple est évidemment simplifié, parce que se limiter à deux actifs et deux comptes, ce n’est pas très réaliste  :D

Il nous manque un exemple de la structure de ta base de donnée.

A-312

Voilà pour la structure.

Et j’imagine que tu souhaites des données dedans ? Ça prendra un peu plus de temps, mais c’est possible.

+0 -0
1
2
3
4
5
6
7
8
SELECT p.prix, temp.prix, transactions
FROM position p
left join (
      SELECT tr.prix, tr.compte_id, tr.actif_id, COUNT(*) AS transactions
      FROM `transaction` tr
      GROUP BY tr.id
  ) temp on temp.compte_id = p.compte_id and temp.actif_id = p.actif_id
WHERE transactions IS NULL;

http://sqlfiddle.com/#!9/bd3b63/14

transactions c’est le nombre de transaction effectif pour ton couple (actif_id et compte_id). Cette solution est beaucoup plus rapide que d’utiliser IN().

Oui quelques entrées.

A-312

Si tu en as encore besoin, je peux en générer quelques unes ce soir.

Comment sais-tu que la transaction à lieu aujourd’hui ?

A-312

Il y a évidemment des dates (champs DATETIME) dans les deux tables transaction et position, mais je ne pensais pas que ça poserait un énorme problème que d’ajouter les critères à ce niveau avant de voir ta requête  ;)

Si je vulgarise : actif = Argent en banque ; position = Argent en bourse (EDIT) ?

EDIT : position = la valeur de tes actifs sur le marché ?

A-312

Actif = élément du portefeuille au sens large (ça peut être un compte bancaire, ou des actions, ou encore des devises – très similaires aux comptes, mais on ne peut pas utiliser le montant qu’elles représentent pour acheter autre chose —, des hypothèques… Bref, tout ce qui en fait fluctuer le montant total)
Position = valeur d’un actif (quel qu’il soit) sur le marché, en général à la clôture de la bourse principale de sa devise.

Je vais regarder ta requête, le fait que j’aie besoin de la relire pour la comprendre montre bien que je n’aurais pas pu y arriver avec ma manière de réfléchir. Merci beaucoup !

+0 -0

Il ne me restait pas beaucoup de temps pour expliquer, j’ai posté ma réponse rapidement pour ne pas perdre le résultat.

Je suis un peu rouillé en SQL, mes explications pourraient-être incorrecte/maladroite.

Comme tu l’as remarqué cette requête est composé d’une sous-requête (requête imbriquée) et d’une jointure.

1
2
3
SELECT tr.prix, tr.compte_id, tr.actif_id, COUNT(*) AS transactions
FROM `transaction` tr
GROUP BY tr.id

Je commence par éliminé tr.prix, il ne sert à rien, je l’ai juste mis à des fins de debug (pour vérifier que la transaction et la position correspondent), tu peux l’enlever.

tr.compte_id, tr.actif_id j’en ai besoin pour la jointure dans ma requête principale. Ces deux éléments servent à la condition qui joint la table position et transaction.

COUNT(*) AS transactions on ajoute un compteur qui compte chaque transaction de tes positions pour le couple actif_id et compte_id. Cet appel fonctionne en duo avec GROUP BY tr.id. Sans ce GROUP BY, le SGBDR (MySQL) va bêtement faire la somme de toute la table.

FROM {nom_table} {alias} permet de renommer la table car le mot transaction est réservé.

Ensuite :

1
2
3
4
5
6
SELECT p.prix, temp.prix, transactions
FROM position p
left join (
      -- requête imbriquée
) temp on temp.compte_id = p.compte_id and temp.actif_id = p.actif_id
WHERE transactions IS NULL;

left join ( est différent de inner join/join. Il permet de garder la ligne même si dans la table qu’on essaye de joindre, il n’y a pas de correspondance (donc les lignes qui sont "IS NULL). Tu as un beau schéma ici (à la fin de l’article) : https://www.codeproject.com/Articles/33052/Visual-Representation-of-SQL-Joins

) temp on temp.compte_id = p.compte_id and temp.actif_id = p.actif_id on nomme la requête imbriquée et on précise les conditions de jointure/correspondance.


J’ai mieux ! :magicien:

D’ailleurs, dans ton cas cette solution est meilleure que la précédente :

1
2
3
4
SELECT p.id, p.compte_id, p.actif_id
FROM position p
LEFT JOIN `transaction` tr ON tr.compte_id = p.compte_id AND tr.actif_id = p.actif_id
WHERE tr.id IS NULL;

La requête que je t’ai donné dans mon précédent message, est utile seulement si tu veux savoir combien de transaction ont eu lieu. Dans notre cas, tu souhaites simplement savoir si c’est IS NULL donc cette version suffit.

Alors celle-ci j’ai moins de peine à la comprendre, et punaise, c’est effectivement simple !

Je suis resté sur mes comptes et actifs sans réfléchir au fait que j’avais les transactions à disposition, transactions qui sont directement les paires que je dois exclure, et donc avec une jointure…

Pour le coup, avec les dates, j’aurais ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SELECT
    p.id,
    p.compte_id,
    p.actif_id
FROM
        `position` p
    LEFT JOIN
                `transaction` tr
        ON
                tr.compte_id = p.compte_id
            AND tr.actif_id = p.actif_id
            AND tr.`date` BETWEEN ? AND ? -- Aujourd'hui 00:00:00.000 et aujourd'hui 23:59:59.999
WHERE
            tr.id IS NULL
        AND p.`date` BETWEEN ? AND ?; -- Hier 00:00:00.000 et hier 23:59:59.999

Merci pour l’explication de ta précédente proposition !

Et merci à tous ceux qui ont pris du temps pour ce sujet  ^^

+0 -0

Personnellement, j’aime mieux cette formulation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SELECT
    p.id,
    p.compte_id,
    p.actif_id
FROM
        `position` p
WHERE
            tr.id IS NULL
        AND p.`date` BETWEEN ? AND ? -- Hier 00:00:00.000 et hier 23:59:59.999
        AND not exists 
           ( select * from transaction tr 
            where tr.compte_id = p.compte_id
              AND tr.actif_id = p.actif_id
              AND tr.`date` BETWEEN ? AND ? -- Aujourd'hui 00:00:00.000 et aujourd'hui 23:59:59.999
            ) ;

Je trouve que c’est plus lisible.

On peut aussi faire :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
SELECT
    p.id,
    p.compte_id,
    p.actif_id
FROM
        `position` p
WHERE
            tr.id IS NULL
        AND p.`date` BETWEEN ? AND ? -- Hier 00:00:00.000 et hier 23:59:59.999
        AND ( p.compte_id, p.actif_id) not in  
           ( select tr.compte_id, tr.actif_id from transaction tr 
            where tr.`date` BETWEEN ? AND ? -- Aujourd'hui 00:00:00.000 et aujourd'hui 23:59:59.999
            ) ;

En terme de performance, si il y a un index adapté sur cette table transaction (idéalement un index sur compte_id+actif_id+date), les requêtes avec l’outer-Join ou avec le not-exists seront très rapides.

La requête avec le not-in risque fort d’être plus lente.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte