web-dev-qa-db-de.com

Aggregat-SQL-Funktion, um nur die erste aus jeder Gruppe zu holen

Ich habe 2 Tabellen - eine Kontentabelle und eine Benutzertabelle. Jedes Konto kann mehrere Benutzer haben. Ich habe ein Szenario, in dem ich eine einzelne Abfrage/einen einzigen Join für diese beiden Tabellen ausführen möchte, aber ich möchte alle Kontodaten (Account. *) Und nur die first - Benutzerdaten (insbesondere deren Namen).

Anstelle eines "min" oder "max" für meine aggregierte Gruppe wollte ich ein "erstes" machen. Anscheinend gibt es in TSQL jedoch keine Aggregatfunktion "First".

Irgendwelche Vorschläge, wie Sie diese Abfrage erhalten? Natürlich ist es einfach, das kartesische Produkt von Konto x Benutzer zu erhalten:

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

Aber wie kann ich erreichen, dass ich nur den ersten Benutzer des Produkts in der Reihenfolge seiner User.ID erhält?

28
Matt

Statt zu gruppieren, gehen Sie so vor ...

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

Ich weiß, dass meine Antwort etwas spät ist, aber das könnte anderen helfen. Es gibt eine Möglichkeit, First () und Last () in SQL Server zu erreichen, und hier ist es:

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

Verwenden Sie Min () für First () und Max () für Last (). DATE_FIELD sollte das Datum sein, das bestimmt, ob es sich um den ersten oder letzten Datensatz handelt. DESIRED_FIELD ist das Feld, in dem der erste oder der letzte Wert eingegeben werden soll. Was es tut, ist:

  1. Füge das Datum im ISO-Format am Anfang der Zeichenfolge hinzu (23 Zeichen lang)
  2. Hängen Sie DESIRED_FIELD an diese Zeichenfolge an
  3. Rufen Sie den MIN/MAX-Wert für dieses Feld ab (da es mit dem Datum beginnt, erhalten Sie den ersten oder letzten Datensatz)
  4. Stuff diese verkettete Zeichenfolge, um die ersten 23 Zeichen (den Datumsteil) zu entfernen

Bitte schön!

BEARBEITEN: Ich habe Probleme mit der ersten Formel: Wenn DATE_FIELD .000 als Millisekunden hat, gibt SQL Server das Datum als Zeichenfolge ohne NO Millisekunden zurück und entfernt so die ersten 4 Zeichen aus DESIRED_FIELD. Ich habe einfach das Format auf "20" (ohne Millisekunden) geändert und es funktioniert alles super. Der einzige Nachteil ist, wenn Sie zwei Felder haben, die in den gleichen Sekunden erstellt wurden. Die Sortierung kann möglicherweise unordentlich sein ... in welchem ​​Fall Sie für das Format auf "126" zurückkehren können.

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

EDIT 2: Meine ursprüngliche Absicht war es, die letzte (oder erste) NON NULL-Zeile zurückzugeben. Ich wurde gefragt, wie ich die letzte oder erste Reihe zurückgeben soll, ob sie null ist oder nicht. Fügen Sie dem DESIRED_FIELD einfach eine ISNULL hinzu. Wenn Sie zwei Zeichenfolgen mit einem + -Operator verketten und einer von ihnen NULL ist, ist das Ergebnis NULL. Verwenden Sie also Folgendes:

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

Dies kann durch Verwendung der Partition By-Klausel vereinfacht werden. Wenn in einem Konto drei Benutzer vorhanden sind, werden in der Unterabfrage die Nummern 1, 2 und 3 angegeben. Bei einem anderen AccountKey wird die Nummerierung zurückgesetzt. Dies bedeutet für jeden eindeutigen AccountKey, dass es immer eine 1 und möglicherweise 2,3,4 usw. gibt.

So filtern Sie nach Ranking = 1, um aus jeder Gruppe den ersten zu holen.

Dadurch erhalten Sie eine Zeile pro Konto. Wenn für dieses Konto mindestens ein Benutzer vorhanden ist, erhalten Sie den Benutzer mit dem niedrigsten Schlüssel (da ich einen Links-Join verwende, erhalten Sie immer eine Kontoauflistung, auch wenn nein.) Benutzer existiert). Ersetzen Sie Order By u.UserKey durch ein anderes Feld, wenn Sie es vorziehen, dass der erste Benutzer alphabetisch oder nach anderen Kriterien ausgewählt wird.

7
AaronLS

Die STUFF-Antwort von Dominic Goulet ist glatt. Wenn Ihr DATE_FIELD jedoch SMALLDATETIME ist (anstelle von DATETIME), dann ist die Länge von ISO 8601 19 statt 23 (da SMALLDATETIME keine Millisekunden enthält). die ersten vier Zeichen fehlen).

3
mweaver

Sie können OUTER APPLY verwenden, siehe 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 und Last sind in SQL Server 2005 oder 2008 nicht vorhanden, aber in SQL Server 2012 gibt es eine First_Value-, Last_Value-Funktion. Ich habe versucht, das Aggregat First und Last für SQL Server 2005 zu implementieren, und kam zu dem Hindernis, dass SQL Server die Berechnung des Aggregats in einer definierten Reihenfolge garantiert. (Siehe Attribut SqlUserDefinedAggregateAttribute.IsInvariantToOrder-Eigenschaft, das nicht implementiert ist.) Dies kann daran liegen, dass der Abfrageanalyse versucht, die Berechnung des Aggregats in mehreren Threads auszuführen und die Ergebnisse zu kombinieren, was jedoch die Reihenfolge nicht beschleunigt welche Elemente werden aggregiert. 

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

Ich habe alle Methoden im Benchmarking getestet. Die einfachste und schnellste Methode, um dies zu erreichen, ist die Verwendung von äußeren/kreuzweisen Anwenden

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

CROSS APPLY funktioniert genauso wie INNER JOIN und ruft die Zeilen ab, in denen beide Tabellen in Beziehung stehen, während OUTER APPLY wie LEFT OUTER JOIN funktioniert und alle Zeilen aus der linken Tabelle abruft (Konto hier).

1

Es gibt verschiedene Möglichkeiten, dies zu tun, hier eine schnelle und schmutzige.

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

Definieren Sie "zuerst". Was Sie als erstes meinen, ist ein Zufall, der normalerweise mit der Cluster-Index-Reihenfolge zu tun hat, auf den Sie sich aber nicht verlassen sollten (Sie können Beispiele dafür finden, die sie brechen). 

Es ist richtig, dass Sie MAX () oder MIN () nicht verwenden. Berücksichtigen Sie während der Versuchung das Szenario, in dem sich Vorname und Nachname in separaten Feldern befinden. Möglicherweise erhalten Sie Namen aus verschiedenen Datensätzen. 

Da es sich so anhört, als ob Sie wirklich genau darauf achten, dass Sie für jede Gruppe genau einen beliebigen Datensatz erhalten, können Sie nur ein ID-Feld für diesen Datensatz MIN oder MAX einstellen und dann die Tabelle mit der Abfrage dieser ID verknüpfen.

0
Joel Coehoorn

Erstellen Sie eine Subselect 'FirstUser', die den ersten Benutzer für jedes Konto zurückgibt

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

(Etwas außerhalb des Themas, aber) Ich führe häufig aggregierte Abfragen aus, um Ausnahmezusammenfassungen aufzulisten, und dann möchte ich wissen, WARUM ein Kunde in den Ergebnissen ist. Verwenden Sie also MIN und MAX, um zwei semi-random-Samples zu geben, die ich mir ansehen kann Details z.

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