web-dev-qa-db-fra.com

Comment sélectionner la première ligne pour chaque groupe dans MySQL?

En C # ce serait comme ça:

table
   .GroupBy(row => row.SomeColumn)
   .Select(group => group
       .OrderBy(row => row.AnotherColumn)
       .First()
   )

Linq-To-Sql le traduit en code T-SQL suivant:

SELECT [t3].[AnotherColumn], [t3].[SomeColumn]
FROM (
    SELECT [t0].[SomeColumn]
    FROM [Table] AS [t0]
    GROUP BY [t0].[SomeColumn]
    ) AS [t1]
OUTER APPLY (
    SELECT TOP (1) [t2].[AnotherColumn], [t2].[SomeColumn]
    FROM [Table] AS [t2]
    WHERE (([t1].[SomeColumn] IS NULL) AND ([t2].[SomeColumn] IS NULL))
      OR (([t1].[SomeColumn] IS NOT NULL) AND ([t2].[SomeColumn] IS NOT NULL)
        AND ([t1].[SomeColumn] = [t2].[SomeColumn]))
    ORDER BY [t2].[AnotherColumn]
    ) AS [t3]
ORDER BY [t3].[AnotherColumn]

Mais c'est incompatible avec MySQL.

55
Jader Dias

Quand j'écris

SELECT AnotherColumn
FROM Table
GROUP BY SomeColumn
;

Ça marche. IIRC dans un autre SGBDR, une telle déclaration est impossible, car une colonne qui n'appartient pas à la clé de regroupement est référencée sans aucune sorte d'agrégation.

Cette "bizarrerie" se comporte de très près à ce que je veux. Je l'ai donc utilisé pour obtenir le résultat souhaité:

SELECT * FROM 
(
 SELECT * FROM `table`
 ORDER BY AnotherColumn
) t1
GROUP BY SomeColumn
;
20
Jader Dias

J'ai basé ma réponse sur le titre de votre message uniquement, car je ne connais pas C # et je n'ai pas compris la requête donnée. Mais dans MySQL, je vous suggère d’essayer les sous-sélections. Commencez par obtenir un ensemble de clés primaires de colonnes intéressantes, puis sélectionnez des données dans ces lignes:

SELECT somecolumn, anothercolumn 
  FROM sometable 
 WHERE id IN (
               SELECT min(id) 
                 FROM sometable 
                GROUP BY somecolumn
             );
69
lfagundes

Voici une autre façon d'essayer, qui n'a pas besoin de ce champ ID.

select some_column, min(another_column)
  from i_have_a_table
 group by some_column

Pourtant, je suis d’accord avec lfagundes pour ajouter une clé primaire.

Notez également qu'en faisant cela, vous ne pouvez pas (facilement) accéder aux autres valeurs qui se trouvent sur la même ligne que la paire some_colum, another_column résultante! Il faudrait que lfagundes apprach et un PK pour le faire!

15
lexu

Vous devez utiliser une fonction d'agrégat pour obtenir la valeur de AnotherColumn souhaitée. Autrement dit, si vous voulez la valeur la plus basse de AnotherColumn pour chaque valeur de SomeColumn (numériquement ou lexicographiquement), vous pouvez utiliser:

SELECT SomeColumn, MIN(AnotherColumn)
FROM YourTable
GROUP BY SomeColumn

Quelques liens utiles:

http://dev.mysql.com/doc/refman/5.1/en/group-by-functions.html

http://www.oreillynet.com/databases/blog/2007/05/debunking_group_by_myths.html

6
David M

À partir de documentation MySQL 5.7

MySQL 5.7.5 et supérieur implémente la détection de la dépendance fonctionnelle. Si le mode SQL ONLY_FULL_GROUP_BY est activé (ce qu'il est par défaut), MySQL rejette les requêtes pour lesquelles la liste de sélection, la condition HAVING ou la liste ORDER BY font référence à des colonnes non agrégées qui ne sont ni nommées dans la clause GROUP BY ni fonctionnellement dépendantes. .

Cela signifie que la solution de @Jader Dias ne fonctionnerait pas partout.

Voici une solution qui fonctionnerait quand ONLY_FULL_GROUP_BY est autorisé:

SET @row := NULL;
SELECT
    SomeColumn,
    AnotherColumn
FROM (
    SELECT
        CASE @id <=> SomeColumn AND @row IS NOT NULL 
            WHEN TRUE THEN @row := @row+1 
            ELSE @row := 0 
        END AS rownum,
        @id := SomeColumn AS SomeColumn,
        AnotherColumn
    FROM
        SomeTable
    ORDER BY
        SomeColumn, -AnotherColumn DESC
) _values
WHERE rownum = 0
ORDER BY SomeColumn;
4
Nicolai

Parmi les réponses, je n’ai pas vu la solution suivante, alors j’ai pensé la mettre en avant.

Le problème est de sélectionner les lignes qui sont les premières lignes quand elles sont classées par AnotherColumn dans tous les groupes groupés par SomeColumn.

La solution suivante le fera dans MySQL. id doit être une colonne unique qui ne doit pas contenir de valeurs contenant - (que j'utilise comme séparateur).

select t1.*
from mytable t1
inner join (
  select SUBSTRING_INDEX(
    GROUP_CONCAT(t3.id ORDER BY t3.AnotherColumn DESC SEPARATOR '-'),
    '-', 
    1
  ) as id
  from mytable t3
  group by t3.SomeColumn
) t2 on t2.id = t1.id


-- Where 
SUBSTRING_INDEX(GROUP_CONCAT(id order by AnotherColumn desc separator '-'), '-', 1)
-- can be seen as:
FIRST(id order by AnotherColumn desc)

-- For completeness sake:
SUBSTRING_INDEX(GROUP_CONCAT(id order by AnotherColumn desc separator '-'), '-', -1)
-- would then be seen as:
LAST(id order by AnotherColumn desc)

Il y a un demande de fonctionnalité pour FIRST() et LAST() dans le suivi des bogues MySQL, mais il a été fermé il y a de nombreuses années.

2
Lars Nyström

Une autre façon de le faire (sans la clé primaire) serait d'utiliser les fonctions JSON:

select somecolumn, json_unquote( json_extract(json_arrayagg(othercolumn), "$[0]") )
  from sometable group by somecolumn

ou avant le 5.7.22

select somecolumn, 
  json_unquote( 
    json_extract( 
      concat('["', group_concat(othercolumn separator '","') ,'"]') 
    ,"$[0]" ) 
  ) 
  from sometable group by somecolumn

La commande (ou le filtrage) peut être effectuée avant de regrouper:

select somecolumn, json_unquote( json_extract(json_arrayagg(othercolumn), "$[0]") ) 
  from (select * from sometable order by othercolumn) as t group by somecolumn

... ou après regroupement (bien sûr):

select somecolumn, json_unquote( json_extract(json_arrayagg(othercolumn), "$[0]") ) as other 
  from sometable group by somecolumn order by other

Certes, il est plutôt compliqué et les performances ne sont probablement pas géniales (je ne l'ai pas testé sur des données volumineuses, cela fonctionne bien sur mes jeux de données limités).

1
Iikka
SELECT
    t1.*

FROM
    table_name AS t1

    LEFT JOIN table_name AS t2 ON (
        t2.group_by_column = t1.group_by_column
        -- group_by_column is the column you would use in the GROUP BY statement
        AND
        t2.order_by_column < t1.order_by_column
        -- order_by_column is column you would use in the ORDER BY statement
        -- usually is the autoincremented key column
    )

WHERE
    t2.group_by_column IS NULL;

Avec MySQL v8 +, vous pouvez utiliser des fonctions de fenêtre

1

Encore une autre façon de le faire

Sélectionnez max dans le groupe qui fonctionne dans les vues

SELECT * FROM action a 
WHERE NOT EXISTS (
   SELECT 1 FROM action a2 
   WHERE a2.user_id = a.user_id 
   AND a2.action_date > a.action_date 
   AND a2.action_type = a.action_type
)
AND a.action_type = "CF"
0
Timo Huovinen

Que dis-tu de ça:

SELECT SUBSTRING_INDEX(
      MIN(CONCAT(OrderColumn, '|', IFNULL(TargetColumn, ''))
    ), '|', -1) as TargetColumn
FROM table
GROUP BY GroupColumn
0
Yura Fedoriv