Получение последних записей в запросе

У меня есть следующие таблицы:

tblPerson:

PersonID | Name
---------------------
   1     | John Smith
   2     | Jane Doe
   3     | David Hoshi

tblLocation:

LocationID | Timestamp | PersonID | X | Y | Z | More Columns...
---------------------------------------------------------------
    40     | Jan. 1st  |     3    | 0 | 0 | 0 | More Info...
    41     | Jan. 2nd  |     1    | 1 | 1 | 0 | More Info...
    42     | Jan. 2nd  |     3    | 2 | 2 | 2 | More Info...
    43     | Jan. 3rd  |     3    | 4 | 4 | 4 | More Info...
    44     | Jan. 5th  |     2    | 0 | 0 | 0 | More Info...

Я могу создать SQL-запрос, который получает записи местоположения для каждого Лица, например:

SELECT LocationID, Timestamp, Name, X, Y, Z 
FROM tblLocation 
JOIN tblPerson 
ON tblLocation.PersonID = tblPerson.PersonID;

чтобы создать следующее:

LocationID | Timestamp |    Name     | X | Y | Z |
--------------------------------------------------
    40     | Jan. 1st  | David Hoshi | 0 | 0 | 0 |
    41     | Jan. 2nd  | John Smith  | 1 | 1 | 0 |
    42     | Jan. 2nd  | David Hoshi | 2 | 2 | 2 |
    43     | Jan. 3rd  | David Hoshi | 4 | 4 | 4 |
    44     | Jan. 5th  | Jane Doe    | 0 | 0 | 0 |

Моя проблема в том, что нас беспокоит только самая последняя запись местоположения. Таким образом, нас действительно интересуют только следующие строки: LocationID 41, 43 и 44.

Вопрос: как мы можем запросить эти таблицы, чтобы предоставить нам самые последние данные для каждого человека? Какая специальная группировка должна произойти для получения желаемого результата?

Ответ 1

MySQL не имеет функций ранжирования/аналитики/окон.

SELECT tl.locationid, tl.timestamp, tp.name, X, Y, Z
  FROM tblPerson tp
  JOIN tblLocation tl ON tl.personid = tp.personid
  JOIN (SELECT t.personid,
               MAX(t.timestamp) AS max_date
          FROM tblLocation t
      GROUP BY t.personid) x ON x.personid = tl.personid
                            AND x.max_date = tl.timestamp

SQL Server 2005+ и Oracle 9i + поддерживают аналитику, поэтому вы можете использовать:

SELECT x.locationid, x.timestamp, x.name, x.X, x.Y, x.Z
  FROM (SELECT tl.locationid, tl.timestamp, tp.name, X, Y, Z,
               ROW_NUMBER() OVER (PARTITION BY tp.name ORDER BY tl.timestamp DESC) AS rank
          FROM tblPerson tp
          JOIN tblLocation tl ON tl.personid = tp.personid) x
WHERE x.rank = 1

Использование переменной для получения того же, что и функция ROW_NUMBER в MySQL:

SELECT x.locationid, x.timestamp, x.name, x.X, x.Y, x.Z
  FROM (SELECT tl.locationid, tl.timestamp, tp.name, X, Y, Z,
               CASE
                 WHEN @name != t.name THEN
                   @rownum := 1
                 ELSE @rownum := @rownum + 1
               END AS rank,
               @name := tp.name
          FROM tblLocation tl
          JOIN tblPerson tp ON tp.personid = tl.personid
          JOIN (SELECT @rownum := NULL, @name := '') r
      ORDER BY tp.name, tl.timestamp DESC) x
WHERE x.rank = 1

Ответ 2

Это классический вопрос "максимум на группу", который появляется на переполнении стека почти каждый день. Существует множество способов решить эту проблему, и вы можете найти примеры решений поиск. Вот один из способов сделать это в MySQL:

SELECT
    location.LocationId,
    location.Timestamp,
    person.Name,
    location.X,
    location.Y,
    location.Z
FROM (
    SELECT
        LocationID,
        @rn := CASE WHEN @prev_PersonID = PersonID
                    THEN @rn + 1
                    ELSE 1
               END AS rn,
        @prev_PersonID := PersonID
    FROM (SELECT @prev_PersonID := NULL) vars, tblLocation
    ORDER BY PersonID, Timestamp DESC
) T1
JOIN tblLocation location ON location.LocationID = T1.LocationId
JOIN tblPerson person ON person.PersonID = location.PersonID
WHERE rn = 1

Ответ 3

Как отмечает @Mark Byers, эта проблема часто возникает при переполнении стека.

Здесь решение, которое я чаще всего рекомендую, учитывая ваши таблицы:

SELECT p.*, l1.*
FROM tblPerson p
JOIN tblLocation l1 ON p.PersonID = l1.PersonID
LEFT OUTER JOIN tblLocation l2 ON p.PersonID = l2.PersonID AND 
  (l1.timestamp < l2.timestamp OR l1.timestamp = l2.timestamp AND l1.LocationId < l2.LocationId)
WHERE l2.LocationID IS NULL;

Чтобы увидеть другие примеры, следуйте тегу greatest-n-per-group, который я добавил к вашему вопросу.