web-dev-qa-db-fra.com

Fonction SQL agrégée pour ne saisir que le premier de chaque groupe

J'ai 2 tables - une table de compte et une table d'utilisateurs. Chaque compte peut avoir plusieurs utilisateurs. J'ai un scénario dans lequel je veux exécuter une requête/jointure unique sur ces deux tables, mais je veux toutes les données du compte (Account. *) Et seulement le premier ensemble de données utilisateur (spécifiquement leur nom).

Au lieu de faire un "min" ou un "max" sur mon groupe agrégé, je voulais faire un "premier". Mais, apparemment, il n'y a pas de "première" fonction d'agrégat dans TSQL.

Des suggestions sur la façon d'obtenir cette requête? Évidemment, il est facile d’obtenir le produit cartésien de Account x Users:

 SELECT User.Name, Account.* FROM Account, User
 WHERE Account.ID = User.Account_ID

Mais comment pourrais-je obtenir seulement le premier utilisateur du produit en fonction de l'ordre de leur User.ID?

28
Matt

Plutôt que de grouper, agissez comme ça ...

select
    *

from account a

join (
    select 
        account_id, 
        row_number() over (order by account_id, id) - 
            rank() over (order by account_id) as row_num from user
     ) first on first.account_id = a.id and first.row_num = 0
24
Adam Robinson

Je sais que ma réponse est un peu tardive, mais cela pourrait aider les autres. Il existe un moyen de réaliser un First () et un Last () dans SQL Server, et le voici:

Stuff(Min(Convert(Varchar, DATE_FIELD, 126) + Convert(Varchar, DESIRED_FIELD)), 1, 23, '')

Utilisez Min () pour First () et Max () pour Last (). DATE_FIELD doit être la date qui détermine s'il s'agit du premier ou du dernier enregistrement. Le DESIRED_FIELD est le champ que vous voulez la première ou la dernière valeur. Qu'est-ce qu'il fait est:

  1. Ajoutez la date au format ISO au début de la chaîne (23 caractères)
  2. Ajouter le DESIRED_FIELD à cette chaîne
  3. Obtenir la valeur MIN/MAX pour ce champ (puisqu'il commence par la date, vous obtiendrez le premier ou le dernier enregistrement)
  4. Farcissez cette chaîne pour supprimer les 23 premiers caractères (la partie date)

Voici!

EDIT: J'ai eu des problèmes avec la première formule: lorsque le champ DATE_FIELD a une valeur de millisecondes, SQL Server renvoie la date sous forme de chaîne avec AUCUN millisecondes, supprimant ainsi les 4 premiers caractères du DESIRED_FIELD. J'ai simplement changé le format à "20" (sans millisecondes) et cela fonctionne très bien. Le seul inconvénient est que si vous avez deux champs créés à la même seconde, le tri peut éventuellement être compliqué ... Dans ce cas, vous pouvez revenir à "126" pour le format.

Stuff(Max(Convert(Varchar, DATE_FIELD, 20) + Convert(Varchar, DESIRED_FIELD)), 1, 19, '')

EDIT 2: Mon intention initiale était de renvoyer la dernière (ou la première) ligne NON NULL. On m'a demandé comment retourner la dernière ou la première ligne, qu'elle soit nulle ou non. Ajoutez simplement un ISNULL au DESIRED_FIELD. Lorsque vous concaténez deux chaînes avec un opérateur +, lorsque l'une d'entre elles est NULL, le résultat est NULL. Alors utilisez les éléments suivants:

Stuff(Max(Convert(Varchar, DATE_FIELD, 20) + IsNull(Convert(Varchar, DESIRED_FIELD), '')), 1, 19, '')
9
Dominic Goulet
Select *
From Accounts a
Left Join (
    Select u.*, 
    row_number() over (Partition By u.AccountKey Order By u.UserKey) as Ranking
    From Users u
  ) as UsersRanked
  on UsersRanked.AccountKey = a.AccountKey and UsersRanked.Ranking = 1

Ceci peut être simplifié en utilisant la clause Partition By. Dans ce qui précède, si un compte a trois utilisateurs, la sous-requête est numérotée 1, 2 et 3 et, pour une autre clé de compte, elle réinitialisera la numérotation. Cela signifie que pour chaque clé de compte unique, il y aura toujours un 1, et potentiellement 2,3,4, etc.

Vous filtrez donc sur Ranking = 1 pour récupérer le premier de chaque groupe.

Cela vous donnera une ligne par compte, et s'il y a au moins un utilisateur pour ce compte, il vous donnera l'utilisateur avec la clé la plus basse (parce que j'utilise une jointure à gauche, vous obtiendrez toujours une liste de comptes même si aucun l'utilisateur existe). Remplacez Order By u.UserKey par un autre champ si vous préférez que le premier utilisateur soit choisi par ordre alphabétique ou par un autre critère.

7
AaronLS

La réponse de STUFF de Dominic Goulet est lisse. Mais, si votre champ DATE_FIELD est SMALLDATETIME (au lieu de DATETIME), la longueur ISO 8601 sera 19, au lieu de 23 (SMALLDATETIME n’ayant pas de millisecondes), ajustez donc le paramètre STUFF en conséquence ou la valeur renvoyée par la fonction STUFF sera incorrecte ( manque les quatre premiers caractères).

3
mweaver

Vous pouvez utiliser OUTER APPLY, voir documentation .

SELECT User1.Name, Account.* FROM Account
OUTER APPLY 
    (SELECT  TOP 1 Name 
    FROM [User]
    WHERE Account.ID = [User].Account_ID
    ORDER BY Name ASC) User1
2
Tomas Kubes

First et Last n'existent pas dans Sql Server 2005 ou 2008, mais il existe une fonction First_Value, Last_Value dans Sql Server 2012. J'ai essayé d'implémenter l'agrégat First and Last pour SQL Server 2005 et je suis tombé sur l'obstacle voulant que SQL Server garantisse le calcul de l'agrégat dans un ordre défini. (Voir l'attribut SqlUserDefinedAggregateAttribute.IsInvariantToOrder, qui n'est pas implémenté.) Cela peut être dû au fait que l'analyseur de requête tente d'exécuter le calcul de l'agrégat sur plusieurs threads et combine les résultats, ce qui accélère l'exécution, mais ne garantit pas un ordre. quels éléments sont agrégés. 

2
Christoph K
SELECT (SELECT TOP 1 Name 
        FROM User 
        WHERE Account_ID = a.AccountID 
        ORDER BY UserID) [Name],
       a.*
FROM Account a
1
Jimmie R. Houts

J'ai référencé toutes les méthodes, la méthode la plus simple et la plus rapide pour y parvenir consiste à utiliser une application externe/croisée.

SELECT u.Name, Account.* FROM Account
OUTER APPLY (SELECT TOP 1 * FROM User WHERE Account.ID = Account_ID ) as u

CROSS APPLY fonctionne comme INNER JOIN et extrait les lignes où les deux tables sont liées, tandis que OUTER APPLY fonctionne comme LEFT OUTER JOIN et extrait toutes les lignes de la table de gauche (Compte ici)

1
Fire in the Hole

Il y a plusieurs façons de procéder, voici une solution rapide et sale.

Select (SELECT TOP 1 U.Name FROM Users U WHERE U.Account_ID = A.ID) AS "Name,
    A.*
FROM Account A
0
Mitchel Sellers

Définir "premier". Ce que vous pensez en premier lieu est une coïncidence qui concerne normalement l'ordre des index en cluster, mais ne doit pas être invoqué (vous pouvez inventer des exemples qui le cassent). 

Vous avez raison de ne pas utiliser MAX () ou MIN (). Bien que tentant, considérons le scénario dans lequel le prénom et le nom de famille sont dans des champs distincts. Vous pourriez obtenir des noms de différents enregistrements. 

Comme il semble que tout ce qui vous préoccupe est que vous obteniez exactement un enregistrement arbitraire pour chaque groupe, vous pouvez simplement simplement MIN ou MAX un champ ID pour cet enregistrement, puis associer la table à la requête portant sur cet ID.

0
Joel Coehoorn

Créer et rejoindre une sous-sélection 'FirstUser' qui renvoie le premier utilisateur pour chaque compte

SELECT User.Name, Account.* 
FROM Account, User, 
 (select min(user.id) id,account_id from User group by user.account_id) as firstUser
WHERE Account.ID = User.Account_ID 
 and User.id = firstUser.id and Account.ID = firstUser.account_id
0
Leon Droog

(Un peu hors sujet, mais) Je lance souvent des requêtes globales pour répertorier les résumés des exceptions, puis je veux savoir POURQUOI un client figure dans les résultats, utilisez donc MIN et MAX pour donner 2 échantillons semi-aléatoires que je peux consulter dans détails par exemple.

SELECT Customer.Id, COUNT(*) AS ProblemCount
      , MIN(Invoice.Id) AS MinInv, MAX(Invoice.Id) AS MaxInv
FROM Customer
INNER JOIN Invoice on Invoice.CustomerId = Customer.Id
WHERE Invoice.SomethingHasGoneWrong=1
GROUP BY Customer.Id
0
brewmanz