Existe-t-il un moyen de rendre une requête SQL NOT IN plus rapide?

I want to get the number of unique mobile phone entries per day that have been logged to a database and have never appeared in the log. I thought it was a trivial query but shock when the query took 10 minutes on a table with about 900K entries. A sample Select is getting the number of unique mobile phones that were logged on the 9th of April 2015 and had never been logged before. Its like getting who are the truly new visitors to you site on a specific day. SQL Fiddle Link

SELECT COUNT(DISTINCT mobile_number)
FROM log_entries
WHERE created_at BETWEEN '2015-04-09 00:00:00'
    AND '2015-04-09 23:59:59'
    AND mobile_number NOT IN (
        SELECT mobile_number
        FROM log_entries
        WHERE created_at < '2015-04-09 00:00:00'
        )

J'ai des index individuels sur created_at et sur numéro_mobile .

Y a-t-il un moyen de le rendre plus rapide? Je vois une question très similaire ici sous SO , mais cela fonctionnait avec deux tables.

3
Voici le Explain, Analyze pour le code @a_horse_with_no_name. exposé.depesz.com/s/tnWG pour le précédent posté dans cette question, c'est toujours en cours d'exécution 45 minutes!
ajouté l'auteur lukik, source
Aah, c'est juste fini! Voici le Explain Analyze pour la requête de la question define.depesz.com/ s/3Bb6
ajouté l'auteur lukik, source
@FrankHeikens J'ai ajouté les liens d'explication de la requête que j'ai fournie dans ma question au lien explique.depesz. com/s/eup9 et pour la solution fournie par a_horse_with_no_name sur le lien explique.depesz.com/s/B64O pardonnez-moi si elles ne sont pas correctes. c'est la première fois que je l'utilise
ajouté l'auteur lukik, source
@CraigRinger je l'ai déjà fait? juste avant le code échantillon
ajouté l'auteur lukik, source
D'accord. Et ici, ils ne sont pas anonymisés: De CraigRinger: explique.depesz.com/s/NC3 . a_horse_with_no_name: define.depesz.com/s/DcZw Ma requête d'origine: explique.depesz.com/s/WNE2 Veuillez expliquer les résultats.
ajouté l'auteur lukik, source
@a_horse_with_no_name quoi? résultats renvoyés en moins de 0,5 ms. Maintenant, je doute même si c'est correct :-) Peut-être que vous pouvez mettre votre solution dans les réponses pour que les autres puissent l'examiner.
ajouté l'auteur lukik, source
retourne 641K + entrées en 0.1ms
ajouté l'auteur lukik, source
@unique_id Je pense que votre seule modification a été la syntaxe entre , ce qui, à mon avis, est plus élégant. Cependant, la requête est en cours d'exécution et en est à sa 7e minute et n'a pas eu de résultat alors ne pensez pas que cela résout le problème
ajouté l'auteur lukik, source
Essayez une sous-requête connexe NOT EXISTS
ajouté l'auteur a_horse_with_no_name, source
Merci. Pourriez-vous ajouter le plan de la solution de Craig également (veuillez ne pas cocher l'option "anonymisée")
ajouté l'auteur a_horse_with_no_name, source
@lukik: serait bien si vous utilisiez expliquez (analysez, commentez) au lieu de expliquez pour générer le plan d'exécution afin que les temps d'exécution réels de chaque étape soient visibles.
ajouté l'auteur a_horse_with_no_name, source
@unique_id: le distinct de la sous-requête est inutile et rendra en fait la requête plus lente.
ajouté l'auteur a_horse_with_no_name, source
Le n'existe pas améliore-t-il les performances? sqlfiddle.com/#!15/9ee8e/24
ajouté l'auteur a_horse_with_no_name, source
vraiment vous aide si vous fournissez un exemple de schéma et de données pour des questions comme celle-ci. Idéalement via sqlfiddle.com
ajouté l'auteur Craig Ringer, source
@lukik Gah. Je suis aveugle. Pardon
ajouté l'auteur Craig Ringer, source
Je me demande s’il est justifié de procéder à une nouvelle conception, de telle sorte que la première entrée d’un numéro de téléphone portable dans la table de journalisation soit elle-même enregistrée séparément dans la table contenant les numéros de téléphone mobile, ou une autre uniquement à cet effet.
ajouté l'auteur David Aldridge, source
Pourriez-vous nous montrer les résultats (pour votre requête actuelle et nouvelle) de EXPLAIN ANALYSE? Veuillez utiliser define.depesz.com pour publier les résultats.
ajouté l'auteur Frank Heikens, source
@lukik: Pourriez-vous utiliser EXPLAIN ANALYSE? Sans ANALYSE, la requête n'est pas exécutée et il n'y a pas de minutage disponible. Sans timing, vous ne pouvez voir aucune amélioration, juste un plan différent. Et "différent" ne veut pas dire "meilleur"
ajouté l'auteur Frank Heikens, source
@a_horse_with_no_name oui bien sûr !!
ajouté l'auteur w͏̢in̡͢g͘̕ed̨p̢͟a͞n͏͏t̡͜͝he̸r̴, source
combien de temps cela prend-il SELECT numéro_mobile FROM log_entries WHERE created_at <'2015-04-09 00:00:00' ?
ajouté l'auteur w͏̢in̡͢g͘̕ed̨p̢͟a͞n͏͏t̡͜͝he̸r̴, source
@lukik J'ai ajouté SELECT DISTINCT numéro_mobile au lieu de SELECT numéro_mobile dans NOT IN également.
ajouté l'auteur w͏̢in̡͢g͘̕ed̨p̢͟a͞n͏͏t̡͜͝he̸r̴, source
@lukik essayer this
ajouté l'auteur w͏̢in̡͢g͘̕ed̨p̢͟a͞n͏͏t̡͜͝he̸r̴, source
note latérale: WHERE created_at entre '2015-04-09 00: 00: 00' et '2015-04-09 23:59:59' au lieu de WHERE created_at> = '2015- 04-09 00:00:00 'AND created_at <=' 2015-04-09 23:59:59 '
ajouté l'auteur w͏̢in̡͢g͘̕ed̨p̢͟a͞n͏͏t̡͜͝he̸r̴, source

6 Réponses

Un NOT IN peut être réécrit en tant que requête NOT EXISTS qui est très souvent plus rapide (malheureusement, l'optimiseur Postgres n'est pas assez intelligent pour le détecter).

SELECT COUNT(DISTINCT l1.mobile_number) 
FROM log_entries as l1
WHERE l1.created_at >= '2015-04-09 00:00:00' 
  AND l1.created_at <= '2015-04-09 23:59:59' 
  AND NOT EXISTS (SELECT * 
                  FROM log_entries l2
                  WHERE l2.created_at < '2015-04-09 00:00:00'
                    AND l2.mobile_number = l1.mobile_number);

Un index sur (numéro_mobile, created_at) devrait encore améliorer les performances.


A side note: created_at <= '2015-04-09 23:59:59' will not include rows with fractional seconds, e.g. 2015-04-09 23:59:59.789. When dealing with timestamps it's better to use a "lower than" with the "next day" instead of a "lower or equal" with the day in question.

So better use: created_at < '2015-04-10 00:00:00' instead to also "catch" rows on that day with fractional seconds.

4
ajouté
J'aime celui la. NOT EXISTS est sémantiquement correct et l'anti-semijoin est très rapide.
ajouté l'auteur David Aldridge, source

J'ai tendance à suggérer de transformer NOT IN en une anti-jointure gauche (c'est-à-dire une jointure gauche qui conserve uniquement les lignes de gauche qui ne ne correspondent pas au côté droit). C'est un peu compliqué dans le cas présent car il s'agit d'une auto-jointure contre deux plages distinctes de la même table. Vous joignez donc deux sous-requêtes:

SELECT COUNT(n.mobile_number)
FROM (
  SELECT DISTINCT mobile_number
  FROM log_entries
  WHERE created_at BETWEEN '2015-04-09 00:00:00' AND '2015-04-09 23:59:59'
) n
LEFT OUTER JOIN (
  SELECT DISTINCT mobile_number
  FROM log_entries
  WHERE created_at < '2015-04-09 00:00:00'
) o ON (n.mobile_number = o.mobile_number)
WHERE o.mobile_number IS NULL;

Je serais intéressé par les performances de ceci par rapport à la formulation type NOT EXISTS fournie par @a_horse_with_no_name.

Notez que j'ai également introduit la vérification DISTINCT dans la sous-requête.

Your query seems to be "how many newly seen mobile numbers are there in

1
ajouté
Obtention d'une erreur [Err] ERREUR: entrée manquante de la clause FROM pour la table "l" LIGNE 5: WHERE l.created_at ENTRE '2015-04-09 00:00:00' ET '2015 ...
ajouté l'auteur lukik, source
A pris 8,63 secondes. Ajoute un lien EXPLAIN comme demandé dans les commentaires ci-dessus, vous pouvez voir ce qui se passe
ajouté l'auteur lukik, source
@DavidAldridge Oui, j'ai tendance à être d'accord. S'il existe un index approprié, la sous-requête not exist fonctionnera probablement mieux. J'étais curieux, car ils arrivent souvent à produire des plans similaires, mais le distinct ici jette un peu une ride dans les choses.
ajouté l'auteur Craig Ringer, source
@lukik Merci. Cela vaut également la peine de supprimer les distincts sur les requêtes internes et de restaurer le nombre.
ajouté l'auteur Craig Ringer, source
@lukik Oops. Erreur d'édition. Fixation. Fixé. (C’est pourquoi il est intéressant de disposer d’un sqlfiddle.com )
ajouté l'auteur Craig Ringer, source
Je suppose que si cette méthode présente un point faible, elle réside dans le nombre potentiellement élevé de valeurs à accéder à la deuxième sous-requête, qui doit analyser l'historique des journaux (dans le cas contraire, il pourrait s'agir simplement d'un index). Accéder à quelques milliers de lignes dans la première sous-requête n’est pas si grave, mais il peut y avoir des millions/milliards dans la seconde. Si tel est le cas, et si relativement peu d'enregistrements sont renvoyés de la première sous-requête, je m'attendrais à ce que la formulation NOT EXISTS fonctionne mieux. Très difficile à dire sans informations de cardinalité à partir des données réelles.
ajouté l'auteur David Aldridge, source

Essayez quelque chose comme ça:

SELECT mobile_number, min(created_at)
FROM log_entries
GROUP BY mobile_number
HAVING min(created_at) between '2015-04-09 00:00:00' and '2015-04-09 23:59:59'

L'ajout d'un index unique couvrant mobile_number et created_at améliorera légèrement les performances, en supposant qu'il existe d'autres colonnes dans la table, car seul cet index devra être analysé.

0
ajouté
Comment votre requête prend-elle soin de NE PAS renvoyer les numéros de téléphone mobile qui ont été enregistrés à l'heure sélectionnée mais qui ont été entrés dans la table avant le 9 avril 2015
ajouté l'auteur lukik, source
Cela prend 8,5 secondes. C'est une amélioration mais il y a des options plus rapides données dans les commentaires de la question
ajouté l'auteur lukik, source
Si un numéro de mobile a été vu pour la première fois le 2015-04-01, min (created_at) sera cette date et la clause HAVING l'exclura.
ajouté l'auteur TobyLL, source

Isn't WHERE created_at >= '2015-04-09 00:00:00' AND created_at <= '2015-04-09 23:59:59' taking care of WHERE created_at < '2015-04-09 00:00:00'? Am I missing something here?

0
ajouté
J'ai modifié la question pour plus de clarté. S'il vous plaît voir si cela répond. J'essaie d'obtenir des numéros de téléphone mobile qui ont été enregistrés à une date spécifique et qui ne sont jamais apparus auparavant dans ce journal.
ajouté l'auteur lukik, source
Je suis sûr de la performance, mais vous pouvez essayer ceci: SELECT COUNT (DISTINCT numéro_table) FROM log_entries x WHERE created_at> = '2015-04-09 00:00:00' AND created_at <= '2015-04 -09 23:59:59 'AND (nombre SELECT (numéro_table) DE FROM entrées_journal y WHERE created_at <' 2015-04-09 00:00:00 'et x.mobile_number = y.mobile_number) = 0;
ajouté l'auteur Phoenix, source

NOT IN n'est pas rapide du tout. Et votre sous-requête renvoie beaucoup d'enregistrements répétés. Peut-être devriez-vous mettre des numéros uniques dans une table dédiée (car GROUP BY sera également lent).

0
ajouté

Try use WITH(if your sql support it). Here is help(postgres):http://www.postgresql.org/docs/current/static/queries-with.html

Et votre requête devrait ressembler à ça:

WITH  b as
(SELECT distinct mobile_number
        FROM log_entries
        WHERE created_at < '2015-04-09 00:00:00') 
SELECT COUNT(DISTINCT a.mobile_number)
FROM log_entries a   
left join b using(mobile_number)
where created_at >= '2015-04-09 00:00:00'
   AND created_at <= '2015-04-09 23:59:59' and b.mobile_number is null;
0
ajouté
Ne pensez pas que le code SQL est correct, car je n’obtiens AUCUN résultat.
ajouté l'auteur lukik, source
Cette fois, cela a fonctionné et cela a pris 10,26 ms. Consultez l'analyse d'expliquer pour les autres questions dans les commentaires pour les questions qui pourraient peut-être aider
ajouté l'auteur lukik, source
vous aurez besoin d'une join gauche et non d'une join (qui est une jointure interne), sinon la condition b.mobile_number est null ne sera jamais vraie.
ajouté l'auteur a_horse_with_no_name, source
Oui laissé rejoindre;) mon erreur :)
ajouté l'auteur Wiol, source