2009-04-21 42 views
26

Tengo 2 tablas: una tabla de cuentas y una tabla de usuarios. Cada cuenta puede tener múltiples usuarios. Tengo un escenario en el que quiero ejecutar una sola consulta/combinación en estas dos tablas, pero quiero todos los datos de la cuenta (Cuenta. *) Y solo el primer conjunto de datos de usuario (específicamente su nombre).Función SQL agregada para obtener solo la primera de cada grupo

En lugar de hacer un "mínimo" o "máximo" en mi grupo agregado, quería hacer una "primera". Pero, aparentemente, no hay una función agregada "Primera" en TSQL.

¿Alguna sugerencia sobre cómo conseguir esta consulta? Obviamente, es fácil de obtener el producto cartesiano de Cuenta x Usuarios:

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

Pero, ¿cómo podría yo nos acerca sólo para conseguir el primer usuario del producto basado en el orden de su User.ID?

+0

SQL Server es peor porque no tiene PRIMERO. No escuché una explicación convincente de por qué no existe en SQL Server. A veces no importa en qué orden se encuentren (si todos tienen el mismo valor en una columna para un grupo en particular) y a veces sí (y están ordenados). De cualquier forma, FIRST() tendría un uso. – micahhoover

Respuesta

22

En lugar de agrupación, de hacerlo así ...

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 
+0

interesante, no me di cuenta de que podría hacer algo como first.row_num = 0 – Matt

+1

Veo que utilizaste Rank() aquí, luego la restaste de Row_Number() y busqué 0. Hubiera utilizado ÚNICAMENTE Row_Number() (con Partitioned by Account_ID) y filtrado en Row_Num = 1. Los resultados serían los mismos (y quizás técnicamente más rápidos). Vea el ejemplo de @AaronLS: http://stackoverflow.com/a/9220232/555798 – MikeTeeVee

+2

@MikeTeeVee Agreeed; esa es una mejor solución, y es lo que habría surgido si resolviera ese problema hoy. –

1
SELECT (SELECT TOP 1 Name 
     FROM User 
     WHERE Account_ID = a.AccountID 
     ORDER BY UserID) [Name], 
     a.* 
FROM Account a 
+0

Sin embargo, este enfoque ejecutará otra instrucción de selección para cada fila de cuenta. Si tiene 1000 cuentas, su consulta ejecutará 1001 declaraciones de selección independientes) –

+0

No es un gran problema para las tablas pequeñas, pero su solución es mejor :) –

0

Hay un número de maneras de hacer esto, he aquí un uno rápido y sucio.

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

Definir "Primero". Lo que piensas primero es una coincidencia que normalmente tiene que ver con el orden de índice agrupado, pero no se debe confiar en ella (puedes encontrar ejemplos que la rompan).

Tiene usted razón de no usar MAX() o MIN(). Si bien es tentador, considera el escenario en el que el primer nombre y el apellido se encuentran en campos separados. Puede obtener nombres de diferentes registros.

Como parece que todo lo que realmente importa es obtener exactamente un registro arbitrario para cada grupo, lo que puede hacer es MIN o MAX un campo ID para ese registro, y luego unir la tabla a la consulta en ese CARNÉ DE IDENTIDAD.

+0

Dijo primero basándose en su identificación de usuario –

2

Primero y último no existen en Sql Server 2005 o 2008, pero en Sql Server 2012 hay una función First_Value, Last_Value. Traté de implementar el agregado First and Last para Sql Server 2005 y llegué al obstáculo de que sql server garantiza el cálculo del agregado en un orden definido. (Ver atributo SqlUserDefinedAggregateAttribute.IsInvariantToOrder Property, que no está implementado). Esto podría deberse a que el analizador de consultas intenta ejecutar el cálculo del agregado en múltiples hilos y combinar los resultados, lo que acelera la ejecución, pero no garantiza un orden en qué elementos se agregan

+1

¡Bienvenido a Stack Overflow! Tenga cuidado cuando publique copiar y pegar respuestas al pie de página o verbatim a preguntas múltiples, estas tienden a ser señaladas como "basura" por la comunidad. Si está haciendo esto, generalmente significa que las preguntas son duplicadas, por lo tanto, márquelas como tales. – Kev

6
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 

Esto se puede simplificar mediante el uso de la cláusula Partition By. En lo anterior, si una cuenta tiene tres usuarios, la subconsulta los numera 1,2, y 3, y para una AccountKey diferente, restablecerá el numnbering. Esto significa que para cada AccountKey único, siempre habrá un 1, y potencialmente 2,3,4, etc.

Por lo tanto, filtra en Ranking = 1 para obtener el primero de cada grupo.

Esto le dará una fila por cuenta, y si hay al menos un usuario para esa cuenta, entonces le dará al usuario la clave más baja (porque utilizo una combinación izquierda, siempre obtendrá una cuenta listado incluso si no existe un usuario). Reemplace Order By u.UserKey con otro campo si prefiere que el primer usuario sea elegido alfabéticamente o algún otro criterio.

9

Sé que mi respuesta es un poco tarde, pero eso podría ayudar a otros. Hay una manera de conseguir un primero() y Last() en SQL Server, y aquí está:

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

Uso Min() para el primero() y Max() para Last(). El DATE_FIELD debe ser la fecha que determina si es el primer o el último registro. El DESIRED_FIELD es el campo que desea el primer o el último valor. Lo que hace es:

  1. Añadir la fecha en formato ISO en el inicio de la cadena (23 caracteres)
  2. anexar el DESIRED_FIELD a esa cadena
  3. Obtener el valor MIN/MAX para ese campo (ya que se inicia con la fecha, obtendrá el primer o último registro)
  4. cosas que concatened cadena para eliminar los primeros 23 caracteres (la parte de fecha)

Aquí tienes!

EDIT: Obtuve problemas con la primera fórmula: cuando DATE_FIELD tiene .000 como milisegundos, SQL Server devuelve la fecha como cadena sin ningún milisegundo en absoluto, eliminando así los primeros 4 caracteres de DESIRED_FIELD. Simplemente cambié el formato a "20" (sin milisegundos) y funciona todo genial. El único inconveniente es que si tiene dos campos creados en los mismos segundos, el tipo puede ser desordenado ... en cuyo caso puede volver a "126" para el formato.

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

EDIT 2: Mi intención original era para devolver la última (o primera) fila NO NULO. Me preguntaron cómo devolver la última o primera fila, ya sea nula o no. Simplemente agregue un ISNULL al DESIRED_FIELD. Cuando concatena dos cadenas con un operador +, cuando una de ellas es NULL, el resultado es NULL. Por lo tanto, use lo siguiente:

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

Observé un rendimiento significativo al usar min() en lugar de una selección anidada con un top 1. Creo que la razón es que min itera todo el conjunto de datos, donde el top 1 simplemente toma el primero que aparece. –

3

La respuesta STUFF de Dominic Goulet es astuta. Pero, si su DATE_FIELD es SMALLDATETIME (en lugar de DATETIME), la longitud de ISO 8601 será 19 en lugar de 23 (porque SMALLDATETIME no tiene milisegundos); por lo tanto, ajuste el parámetro STUFF en consecuencia o el valor de retorno de la función STUFF será incorrecto (falta los primeros cuatro caracteres).

+0

¡Gracias por el comentario! Me di cuenta de que también hace algunas semanas, actualicé mi respuesta. Esto también ocurre cuando su fecha y hora tiene .000 como milisegundos, simplemente se eliminan y pierde los primeros 4 caracteres. Cambié el formato de 126 a 20 para cortar siempre los milisegundos, ¡ahora está funcionando genial! –

2

Puede utilizar APLICACIÓN EXTERIOR, consulte 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 
1

He Benchmarked todos los métodos, el método más rápido y simpelest para lograr esto es mediante el uso exterior/cruz aplicar

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

CROSS APPLY funciona igual que INNER JOIN y capta las filas donde ambos las tablas están relacionadas, mientras APLICACIÓN EXTERNA funciona como ENJUEGUE EXTERIOR IZQUIERDO y recupera todas las filas de la tabla izquierda (Cuenta aquí)

+0

Esta consulta puede dar resultados inconsistentes. SELECCIONAR TOP 1 sin ORDENAR puede devolver cualquiera de coincidencia de consulta, depende de SqlServer Engine. Y por lo tanto, tal resultado puede dar "resultados aleatorios". – qub1n

0

(ligeramente fuera de tema, pero) A menudo ejecuto consultas globales para enumerar resúmenes de excepciones y luego deseo saber POR QUÉ un cliente está en los resultados, así que use MIN y MAX para dar 2 muestras semi-aleatorias que puedo ver en detalles, por ej.

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

crear y unirse con una subselección 'firstUser' que devuelve el primer usuario para cada cuenta

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 
Cuestiones relacionadas