Comment (ou puis-je) sélectionner SELECT DISTINCT sur plusieurs colonnes?

415

J'ai besoin de récupérer toutes les lignes d'une table où 2 colonnes combinées sont toutes différentes. Je veux donc toutes les ventes qui n'ont pas d'autres ventes qui ont eu lieu le même jour pour le même prix. Les ventes qui sont uniques en fonction du jour et du prix seront mises à jour à un statut actif.

Je pense donc:

UPDATE sales
SET status = 'ACTIVE'
WHERE id IN (SELECT DISTINCT (saleprice, saledate), id, count(id)
             FROM sales
             HAVING count = 1)

Mais mon cerveau me fait mal d'aller plus loin que ça.

sheats
la source

Réponses:

436
SELECT DISTINCT a,b,c FROM t

est à peu près équivalent à:

SELECT a,b,c FROM t GROUP BY a,b,c

C'est une bonne idée de s'habituer à la syntaxe GROUP BY, car elle est plus puissante.

Pour votre requête, je le ferais comme ceci:

UPDATE sales
SET status='ACTIVE'
WHERE id IN
(
    SELECT id
    FROM sales S
    INNER JOIN
    (
        SELECT saleprice, saledate
        FROM sales
        GROUP BY saleprice, saledate
        HAVING COUNT(*) = 1 
    ) T
    ON S.saleprice=T.saleprice AND s.saledate=T.saledate
 )
Joel Coehoorn
la source
117
Cette requête, bien que correcte et acceptée depuis un an, est extrêmement inefficace et inutilement. Ne l'utilisez pas. J'ai fourni une alternative et quelques explications dans une autre réponse.
Erwin Brandstetter
1
SELECT DISTINCT a, b, c FROM t n'est-il pas exactement la même chose que SELECT a, b, c FROM t GROUP BY a, b, c?
famargar
8
@famargar pour le cas simple, pour l'instant, mais ils ont des significations sémantiques différentes et ils sont différents en termes de ce que vous pouvez faire pour l'étape lors de la création d'une requête plus grande. De plus, les gens sur les forums technologiques peuvent souvent être extrêmement pédants sur les choses, je trouve souvent utile d'ajouter des mots de belette à mes messages dans ce contexte.
Joel Coehoorn
344

Si vous réunissez les réponses jusqu'à présent, nettoyez et améliorez, vous arriverez à cette requête supérieure:

UPDATE sales
SET    status = 'ACTIVE'
WHERE  (saleprice, saledate) IN (
    SELECT saleprice, saledate
    FROM   sales
    GROUP  BY saleprice, saledate
    HAVING count(*) = 1 
    );

Ce qui est beaucoup plus rapide que l'un ou l'autre. Détruit les performances de la réponse actuellement acceptée par le facteur 10 - 15 (dans mes tests sur PostgreSQL 8.4 et 9.1).

Mais c'est encore loin d'être optimal. Utilisez une NOT EXISTS(anti) semi-jointure pour des performances encore meilleures. EXISTSest du SQL standard, existe depuis toujours (au moins depuis PostgreSQL 7.2, bien avant que cette question ne soit posée) et correspond parfaitement aux exigences présentées:

UPDATE sales s
SET    status = 'ACTIVE'
WHERE  NOT EXISTS (
   SELECT FROM sales s1                     -- SELECT list can be empty for EXISTS
   WHERE  s.saleprice = s1.saleprice
   AND    s.saledate  = s1.saledate
   AND    s.id <> s1.id                     -- except for row itself
   )
AND    s.status IS DISTINCT FROM 'ACTIVE';  -- avoid empty updates. see below

db <> violon ici
Old SQL Fiddle

Clé unique pour identifier la ligne

Si vous n'avez pas de clé primaire ou unique pour la table ( iddans l'exemple), vous pouvez remplacer par la colonne système ctidaux fins de cette requête (mais pas à d'autres fins):

   AND    s1.ctid <> s.ctid

Chaque table doit avoir une clé primaire. Ajoutez-en un si vous n'en avez pas encore. Je suggère une serialou une IDENTITYcolonne dans Postgres 10+.

En relation:

Comment est-ce plus rapide?

La sous-requête dans l' EXISTSanti-semi-jointure peut cesser d'être évaluée dès que la première dupe est trouvée (inutile de chercher plus loin). Pour une table de base avec peu de doublons, cela n'est que légèrement plus efficace. Avec beaucoup de doublons, cela devient beaucoup plus efficace.

Exclure les mises à jour vides

Pour les lignes qui ont déjà status = 'ACTIVE'cette mise à jour, cela ne changera rien, mais insérez toujours une nouvelle version de ligne à plein coût (des exceptions mineures s'appliquent). Normalement, vous ne le souhaitez pas. Ajoutez une autre WHEREcondition comme illustré ci-dessus pour éviter cela et le rendre encore plus rapide:

Si statusest défini NOT NULL, vous pouvez simplifier pour:

AND status <> 'ACTIVE';

Le type de données de la colonne doit prendre en charge l' <>opérateur. Certains types aiment jsonpas. Voir:

Différence subtile dans la gestion NULL

Cette requête (contrairement à la réponse actuellement acceptée par Joel ) ne traite pas les valeurs NULL comme égales. Les deux lignes suivantes pour (saleprice, saledate)seraient qualifiées de "distinctes" (bien qu'elles semblent identiques à l'œil humain):

(123, NULL)
(123, NULL)

Passe également dans un index unique et presque partout ailleurs, car les valeurs NULL ne sont pas comparables égales selon la norme SQL. Voir:

OTOH, GROUP BY, DISTINCTou DISTINCT ON ()traiter les valeurs NULL comme égales. Utilisez un style de requête approprié en fonction de ce que vous souhaitez réaliser. Vous pouvez toujours utiliser cette requête plus rapide avec IS NOT DISTINCT FROMau lieu de =pour une ou toutes les comparaisons pour que la comparaison NULL soit égale. Plus:

Si toutes les colonnes comparées sont définies NOT NULL, il n'y a pas de place pour le désaccord.

Erwin Brandstetter
la source
16
Bonne réponse. Je suis un serveur SQL Server, donc la première suggestion d'utiliser un tuple avec une vérification IN () ne me vient pas à l'esprit. La suggestion n'existe pas va généralement se retrouver avec le même plan d'exécution dans le serveur SQL que la jointure interne.
Joel Coehoorn
2
Agréable. L'explication augmente considérablement la valeur de la réponse. Je suis presque tenté d'exécuter des tests avec Oracle pour voir comment les plans se comparent à Postgres et SQLServer.
Peter
2
@alairock: Où avez-vous obtenu cela? Pour Postgres, l' inverse est vrai. Tout en comptant toutes les lignes, count(*)est plus efficace que count(<expression>). Essayez-le. Postgres a une implémentation plus rapide pour cette variante de la fonction d'agrégation. Peut-être confondez-vous Postgres avec un autre SGBDR?
Erwin Brandstetter
6
@alairock: Il se trouve que je suis co-auteur de cette page et qu'elle ne dit rien de la sorte.
Erwin Brandstetter
2
@ErwinBrandstetter, vous êtes toujours sur la bonne voie avec vos réponses à travers la pile. Vous avez aidé au fil des ans d'une manière presque inimaginable. Quant à cet exemple, je connaissais plusieurs façons de résoudre mon problème, mais je voulais voir que quelqu'un avait testé l'efficacité entre les possibilités. Je vous remercie.
WebWanderer
24

Le problème avec votre requête est que lorsque vous utilisez une clause GROUP BY (ce que vous faites essentiellement en utilisant distinct), vous ne pouvez utiliser que des colonnes que vous regroupez ou agrégez des fonctions. Vous ne pouvez pas utiliser l'ID de colonne car il existe des valeurs potentiellement différentes. Dans votre cas, il n'y a toujours qu'une seule valeur en raison de la clause HAVING, mais la plupart des SGBDR ne sont pas assez intelligents pour le reconnaître.

Cela devrait cependant fonctionner (et n'a pas besoin d'une jointure):

UPDATE sales
SET status='ACTIVE'
WHERE id IN (
  SELECT MIN(id) FROM sales
  GROUP BY saleprice, saledate
  HAVING COUNT(id) = 1
)

Vous pouvez également utiliser MAX ou AVG au lieu de MIN, il est uniquement important d'utiliser une fonction qui renvoie la valeur de la colonne s'il n'y a qu'une seule ligne correspondante.

Christian Berg
la source
1

Je veux sélectionner les valeurs distinctes d'une colonne «GrondOfLucht» mais elles doivent être triées dans l'ordre indiqué dans la colonne «tri». Je ne peux pas obtenir les valeurs distinctes d'une seule colonne en utilisant

Select distinct GrondOfLucht,sortering
from CorWijzeVanAanleg
order by sortering

Il donnera également la colonne «tri» et parce que «GrondOfLucht» ET «tri» n'est pas unique, le résultat sera TOUTES les lignes.

utiliser le GROUPE pour sélectionner les enregistrements de 'GrondOfLucht' dans l'ordre donné par 'tri

SELECT        GrondOfLucht
FROM            dbo.CorWijzeVanAanleg
GROUP BY GrondOfLucht, sortering
ORDER BY MIN(sortering)
frans eilering
la source
Cela explique essentiellement ce que fait la réponse acceptée, mais je recommande de ne pas utiliser de tels noms pour un exemple (au moins les traduire). PS: Je recommande de toujours tout nommer en anglais dans tous les projets, même si vous êtes néerlandais.
Kerwin Sneijders
0

Si votre SGBD ne prend pas en charge distinct avec plusieurs colonnes comme ceci:

select distinct(col1, col2) from table

La sélection multiple en général peut être exécutée en toute sécurité comme suit:

select distinct * from (select col1, col2 from table ) as x

Comme cela peut fonctionner sur la plupart des SGBD et que cela devrait être plus rapide que le regroupement par solution, vous évitez la fonctionnalité de regroupement.

Abdulhafeth Sartawi
la source