Funzione di aggregazione SQL per afferrare solo la prima di ogni gruppo
-
13-09-2019 - |
Domanda
Ho 2 tavoli - una tabella di account e una tabella Utenti. Ogni account può avere più utenti. Ho uno scenario in cui voglio eseguire una singola query / unirsi contro queste due tabelle, ma voglio tutti i dati conto (. *) E solo il prima insieme dei dati degli utenti (in particolare il loro nome ).
Invece di fare un "min" o "max" sul mio gruppo aggregato, ho voluto fare un "prima". Ma, a quanto pare, non c'è un "prima" funzione di aggregazione in TSQL.
Qualche suggerimento su come fare per ottenere questa ricerca? Ovviamente, è facile ottenere il prodotto cartesiano di conto utenti x:
SELECT User.Name, Account.* FROM Account, User
WHERE Account.ID = User.Account_ID
Ma come potrei ottenuto circa ottenendo soltanto il primo utente dal prodotto in base all'ordine del loro User.ID?
Soluzione
Invece di raggruppamento, andare su di esso come questo ...
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
Altri suggerimenti
So che la mia risposta è un po 'in ritardo, ma che potrebbe aiutare gli altri. C'è un modo per ottenere un First () e Last () in SQL Server, e qui è:
Stuff(Min(Convert(Varchar, DATE_FIELD, 126) + Convert(Varchar, DESIRED_FIELD)), 1, 23, '')
Utilizzare Min () per First () e Max () per Last (). Il DATE_FIELD dovrebbe essere la data che determina se è il primo o l'ultimo record. Il DESIRED_FIELD è il campo che si desidera che il primo o l'ultimo valore. Ciò che fa è:
- Aggiungi la data in formato ISO, all'inizio della stringa (lunga 23 caratteri)
- Aggiungere il DESIRED_FIELD a quella stringa
- Prendi il valore MIN / MAX per quel campo (dal momento che inizia con la data, si otterrà il primo o l'ultimo record)
- Roba che concatened stringa per rimuovere i primi 23 caratteri (la parte data)
Ecco a te!
EDIT: ho avuto problemi con la prima formula: quando il DATE_FIELD ha .000 come millisecondi, SQL Server restituisce la data come stringa con NO millisecondi a tutti, eliminando così i primi 4 caratteri del DESIRED_FIELD. Ho semplicemente cambiato il formato a "20" (senza millisecondi) e funziona tutto fantastico. L'unico lato negativo è che se si dispone di due campi che sono stati creati negli stessi secondi, il genere può eventualmente essere disordinato ... in cui CAS si può tornare a "126" per il formato.
Stuff(Max(Convert(Varchar, DATE_FIELD, 20) + Convert(Varchar, DESIRED_FIELD)), 1, 19, '')
EDIT 2: Il mio intento originale era quello di restituire l'ultima (o la prima) fila NON NULL. Mi sono chiesto come restituire l'ultima o la prima fila, montone castrato che sia nullo o non è. Basta aggiungere un ISNULL al DESIRED_FIELD. Quando è concatenare due stringhe con un operatore di +, quando uno di loro è NULL, il risultato è nullo. Quindi utilizzare il seguente:
Stuff(Max(Convert(Varchar, DATE_FIELD, 20) + IsNull(Convert(Varchar, DESIRED_FIELD), '')), 1, 19, '')
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
Questo può essere semplificata utilizzando la partizione clausola. In precedenza, se un account ha tre utenti, allora i numeri subquery loro 1,2 e 3, e per un AccountKey diversa, si resetta la numnbering. Questo significa che per ogni AccountKey unica, ci sarà sempre un 1, e potenzialmente 2,3,4, ecc.
In questo modo di filtrare il Ranking = 1 per afferrare il primo di ogni gruppo.
Questo vi darà una riga per conto, e se c'è almeno un utente per l'account, allora vi darà l'utente con il tasto più basso (perché io uso una sinistra unirsi, è sempre ottenere un elenco conto anche se esiste nessun utente). Sostituire Order By u.UserKey
con un altro campo, se si preferisce che il primo utente essere scelto in ordine alfabetico o altri criteri.
La risposta STUFF da Dominic Goulet è chiazza di petrolio. Ma, se il vostro DATE_FIELD è SMALLDATETIME (invece di DATETIME), allora la lunghezza ISO 8601 saranno 19 invece di 23 (perché SMALLDATETIME ha millisecondi) - in modo da regolare il parametro STUFF conseguenza o il valore restituito dalla funzione STUFF non sarà corretto ( mancano i primi quattro caratteri).
Cognome e non esistono in SQL Server 2005 o 2008, ma in SQL Server 2012 è disponibile una funzione di FIRST_VALUE, LAST_VALUE. Ho cercato di implementare l'aggregato Cognome e per SQL Server 2005 e sono giunto alla ostacolo che SQL Server non garantisce il calcolo del totale in un ordine definito. (Vedere attributo SqlUserDefinedAggregateAttribute.IsInvariantToOrder immobile, che non è implementata.) Questo potrebbe essere dovuto alla Query Analyzer tenta di eseguire il calcolo dell'aggregato sul più thread e combinare i risultati, che accelera l'esecuzione, ma non garantisce un ordine in quali elementi sono aggregati.
È possibile utilizzare ESTERNO APPLICA, vedi documentazione .
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
Ho benchmark tutti i metodi, il metodo simpelest e veloce per raggiungere questo obiettivo è quello di utilizzare esterno / cross applicare
SELECT u.Name, Account.* FROM Account
OUTER APPLY (SELECT TOP 1 * FROM User WHERE Account.ID = Account_ID ) as u
CROSS APPLY funziona proprio come INNER JOIN e recupera le righe in cui entrambe le tabelle sono legate, mentre OUTER APPLY opere come LEFT OUTER JOIN e recupera tutte le righe della tabella di sinistra (account qui)
SELECT (SELECT TOP 1 Name
FROM User
WHERE Account_ID = a.AccountID
ORDER BY UserID) [Name],
a.*
FROM Account a
Ci sono un certo numero di modi di fare questo, qui a a una rapida e sporca.
Select (SELECT TOP 1 U.Name FROM Users U WHERE U.Account_ID = A.ID) AS "Name,
A.*
FROM Account A
Definisci "First". Cosa ne pensi di come in primo luogo è un caso che normalmente ha a che fare con ordine indice cluster ma non dovrebbe essere fatta valere (si può escogitare esempi che romperlo).
Hai ragione di non usare MAX () o MIN (). Mentre allettante, prendere in considerazione lo scenario in cui si il nome e il cognome sono in campi separati. Si potrebbe ottenere i nomi di diversi record.
Dal momento che suona come tutti i tuoi interessa davvero è che si ottiene esattamente un record arbitrario per ogni gruppo, che cosa si può fare è solo MIN o MAX un campo ID per il record, e poi unirsi al tavolo nella query su quel ID .
(Leggermente off-topic, ma) spesso eseguire query di aggregazione per elencare sommari di eccezione, e poi voglio sapere perché un cliente è nei risultati, in modo da utilizzare MIN e MAX per dare 2 campioni semi-casuali che posso guardare in dettaglio ad esempio
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
Crea e unirsi a una selezione secondaria 'FirstUser' che restituisce il primo utente per ogni account
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