Агрегировать функцию SQL, чтобы получить только первое из каждой группы - PullRequest
28 голосов
/ 21 апреля 2009

У меня есть 2 таблицы - таблица учетных записей и таблица пользователей. Каждая учетная запись может иметь несколько пользователей. У меня есть сценарий, в котором я хочу выполнить один запрос / объединение для этих двух таблиц, но мне нужны все данные учетной записи (Account. *) И только набор first пользовательских данных (в частности, их имя) .

Вместо того, чтобы делать "min" или "max" в моей агрегированной группе, я хотел сделать "first". Но, по-видимому, в TSQL нет «первой» агрегатной функции.

Есть предложения о том, как получить этот запрос? Очевидно, что получить декартово произведение Пользователя x легко:

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

Но как мне получить только первого пользователя из продукта, основываясь на порядке его User.ID?

Ответы [ 12 ]

25 голосов
/ 21 апреля 2009

Вместо того, чтобы группировать, сделайте так ...

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
9 голосов
/ 11 апреля 2012

Я знаю, что мой ответ немного запоздал, но это может помочь другим. Есть способ добиться First () и Last () в SQL Server, и вот он:

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

Используйте Min () для First () и Max () для Last (). DATE_FIELD должна быть датой, определяющей, является ли это первой или последней записью. DESIRED_FIELD - это поле, в котором вы хотите указать первое или последнее значение. Что он делает:

  1. Добавить дату в формате ISO в начале строки (длиной 23 символа)
  2. Добавить DESIRED_FIELD к этой строке
  3. Получить значение MIN / MAX для этого поля (поскольку оно начинается с даты, вы получите первую или последнюю запись)
  4. Материал, который объединяет строку для удаления первых 23 символов (часть даты)

Вот, пожалуйста!

РЕДАКТИРОВАТЬ: у меня возникли проблемы с первой формулой: когда DATE_FIELD имеет .000 в виде миллисекунд, SQL Server возвращает дату в виде строки без НО миллисекунд вообще, таким образом удаляя первые 4 символа из DESIRED_FIELD. Я просто изменил формат на «20» (без миллисекунд), и все отлично работает. Единственным недостатком является то, что если у вас есть два поля, которые были созданы в одно и то же время, сортировка может быть грязной ... в этом случае вы можете вернуться к "126" для формата.

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

РЕДАКТИРОВАТЬ 2: Моим первоначальным намерением было вернуть последнюю (или первую) строку NON NULL. Меня спросили, как вернуть последний или первый ряд, будет ли он нулевым или нет. Просто добавьте ISNULL в DESIRED_FIELD. Когда вы объединяете две строки с оператором +, когда одна из них имеет значение NULL, результат равен NULL. Поэтому используйте следующее:

Stuff(Max(Convert(Varchar, DATE_FIELD, 20) + IsNull(Convert(Varchar, DESIRED_FIELD), '')), 1, 19, '')
7 голосов
/ 10 февраля 2012
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

Это можно упростить с помощью предложения Partition By. В приведенном выше примере, если у учетной записи есть три пользователя, то подзапрос нумерует их 1,2 и 3, а для другого AccountKey он сбрасывает нумерацию. Это означает, что для каждого уникального AccountKey всегда будет 1, и, возможно, 2,3,4 и т. Д.

Таким образом, вы фильтруете по Рейтингу = 1, чтобы получить первое из каждой группы.

Это даст вам одну строку для каждой учетной записи, и если для этой учетной записи будет хотя бы один пользователь, то это даст вам пользователя с самым низким ключом (поскольку я использую левое соединение, вы всегда получите список учетной записи даже если нет пользователя). Замените Order By u.UserKey другим полем, если вы предпочитаете, чтобы первый пользователь был выбран в алфавитном порядке или по некоторым другим критериям.

3 голосов
/ 05 июля 2012

Реакция Доминика Гуле на ПЕРСОНАЛ очень приятна. Но если DATE_FIELD имеет значение SMALLDATETIME (вместо DATETIME), то длина ISO 8601 будет равна 19 вместо 23 (поскольку SMALLDATETIME не имеет миллисекунд), поэтому настройте параметр STUFF соответствующим образом, иначе возвращаемое значение из функции STUFF будет неправильным ( пропущены первые четыре символа).

2 голосов
/ 29 октября 2015

Я протестировал все методы, самый простой и быстрый метод для достижения этой цели - использование external / cross apply

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

CROSS APPLY работает так же, как INNER JOIN, и извлекает строки, с которыми связаны обе таблицы, тогда как OUTER APPLY работает как LEFT OUTER JOIN и выбирает все строки из левой таблицы (здесь Account)

2 голосов
/ 29 сентября 2015

Вы можете использовать OUTER APPLY, см. документацию .

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 голосов
/ 02 декабря 2011

First и Last не существуют в Sql Server 2005 или 2008, но в Sql Server 2012 есть функция First_Value, Last_Value. Я попытался внедрить агрегат First и Last для Sql Server 2005 и столкнулся с препятствием, заключающимся в том, что сервер sql гарантирует вычисление агрегата в определенном порядке. (См. Атрибут SqlUserDefinedAggregateAttribute.IsInvariantToOrder Свойство, которое не реализовано.) Это может быть связано с тем, что анализатор запросов пытается выполнить вычисление агрегата в нескольких потоках и объединяет результаты, что ускоряет выполнение, но не гарантирует порядок в какие элементы агрегированы.

1 голос
/ 21 апреля 2009
SELECT (SELECT TOP 1 Name 
        FROM User 
        WHERE Account_ID = a.AccountID 
        ORDER BY UserID) [Name],
       a.*
FROM Account a
0 голосов
/ 21 апреля 2017

Создайте и присоединитесь к подпункту «FirstUser», который возвращает первого пользователя для каждой учетной записи

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 голосов
/ 10 января 2017

(немного не по теме, но) Я часто запускаю агрегированные запросы, чтобы вывести список исключений, а затем хочу узнать, ПОЧЕМУ клиент находится в результатах, поэтому используйте MIN и MAX, чтобы получить 2 полуслучайных выборки, которые я могу посмотрите подробнее

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
...