Pobieranie ostatniego rekordu w każdej grupie-MySQL

Istnieje tabela messages, która zawiera dane, jak pokazano poniżej:

Id   Name   Other_Columns
-------------------------
1    A       A_data_1
2    A       A_data_2
3    A       A_data_3
4    B       B_data_1
5    B       B_data_2
6    C       C_data_1

Jeśli uruchomię zapytanie select * from messages group by name, otrzymam wynik jako:

1    A       A_data_1
4    B       B_data_1
6    C       C_data_1

Jakie zapytanie zwróci następujący wynik?

3    A       A_data_3
5    B       B_data_2
6    C       C_data_1

Oznacza to, że należy zwrócić ostatni rekord w każdej grupie.

Obecnie jest to zapytanie, którego używam:

SELECT
  *
FROM (SELECT
  *
FROM messages
ORDER BY id DESC) AS x
GROUP BY name
Ale to wygląda na wysoce nieefektywne. Jakieś inne sposoby na osiągnięcie tego samego wyniku?
Author: DineshDB, 2009-08-21

21 answers

MySQL 8.0 obsługuje teraz funkcje windowing, jak prawie wszystkie popularne implementacje SQL. Z tą standardową składnią możemy pisać zapytania greatest-n-per-group:

WITH ranked_messages AS (
  SELECT m.*, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id DESC) AS rn
  FROM messages AS m
)
SELECT * FROM ranked_messages WHERE rn = 1;

Poniżej znajduje się oryginalna odpowiedź, którą napisałem na to pytanie w 2009 roku:


Piszę rozwiązanie w ten sposób:

SELECT m1.*
FROM messages m1 LEFT JOIN messages m2
 ON (m1.name = m2.name AND m1.id < m2.id)
WHERE m2.id IS NULL;

Jeśli chodzi o wydajność, jedno lub drugie rozwiązanie może być lepsze, w zależności od charakteru danych. Więc należy przetestować oba zapytania i użyć tego, który jest lepszy w wydajność biorąc pod uwagę bazę danych.

Na przykład, mam kopię zrzutu sierpniowego Stoskoverflow . Użyję tego do analizy porównawczej. W tabeli Posts znajduje się 1 114 357 wierszy. To działa na MySQL 5.0.75 na moim MacBooku Pro 2.40 GHz.

Napiszę zapytanie, aby znaleźć najnowszy post dla danego identyfikatora użytkownika (mojego).

Pierwsze użycie techniki pokazanej przez @Eric z GROUP BY w subquery:

SELECT p1.postid
FROM Posts p1
INNER JOIN (SELECT pi.owneruserid, MAX(pi.postid) AS maxpostid
            FROM Posts pi GROUP BY pi.owneruserid) p2
  ON (p1.postid = p2.maxpostid)
WHERE p1.owneruserid = 20860;

1 row in set (1 min 17.89 sec)

Nawet na EXPLAIN analiza trwa ponad 16 sekund:

+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
| id | select_type | table      | type   | possible_keys              | key         | key_len | ref          | rows    | Extra       |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL                       | NULL        | NULL    | NULL         |   76756 |             | 
|  1 | PRIMARY     | p1         | eq_ref | PRIMARY,PostId,OwnerUserId | PRIMARY     | 8       | p2.maxpostid |       1 | Using where | 
|  2 | DERIVED     | pi         | index  | NULL                       | OwnerUserId | 8       | NULL         | 1151268 | Using index | 
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
3 rows in set (16.09 sec)

Teraz Wygeneruj ten sam wynik zapytania używając moja technika z LEFT JOIN:

SELECT p1.postid
FROM Posts p1 LEFT JOIN posts p2
  ON (p1.owneruserid = p2.owneruserid AND p1.postid < p2.postid)
WHERE p2.postid IS NULL AND p1.owneruserid = 20860;

1 row in set (0.28 sec)

Analiza EXPLAIN pokazuje, że obie tabele są w stanie wykorzystać swoje indeksy:

+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
| id | select_type | table | type | possible_keys              | key         | key_len | ref   | rows | Extra                                |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
|  1 | SIMPLE      | p1    | ref  | OwnerUserId                | OwnerUserId | 8       | const | 1384 | Using index                          | 
|  1 | SIMPLE      | p2    | ref  | PRIMARY,PostId,OwnerUserId | OwnerUserId | 8       | const | 1384 | Using where; Using index; Not exists | 
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
2 rows in set (0.00 sec)

Oto DDL dla mojej Posts tabeli:

CREATE TABLE `posts` (
  `PostId` bigint(20) unsigned NOT NULL auto_increment,
  `PostTypeId` bigint(20) unsigned NOT NULL,
  `AcceptedAnswerId` bigint(20) unsigned default NULL,
  `ParentId` bigint(20) unsigned default NULL,
  `CreationDate` datetime NOT NULL,
  `Score` int(11) NOT NULL default '0',
  `ViewCount` int(11) NOT NULL default '0',
  `Body` text NOT NULL,
  `OwnerUserId` bigint(20) unsigned NOT NULL,
  `OwnerDisplayName` varchar(40) default NULL,
  `LastEditorUserId` bigint(20) unsigned default NULL,
  `LastEditDate` datetime default NULL,
  `LastActivityDate` datetime default NULL,
  `Title` varchar(250) NOT NULL default '',
  `Tags` varchar(150) NOT NULL default '',
  `AnswerCount` int(11) NOT NULL default '0',
  `CommentCount` int(11) NOT NULL default '0',
  `FavoriteCount` int(11) NOT NULL default '0',
  `ClosedDate` datetime default NULL,
  PRIMARY KEY  (`PostId`),
  UNIQUE KEY `PostId` (`PostId`),
  KEY `PostTypeId` (`PostTypeId`),
  KEY `AcceptedAnswerId` (`AcceptedAnswerId`),
  KEY `OwnerUserId` (`OwnerUserId`),
  KEY `LastEditorUserId` (`LastEditorUserId`),
  KEY `ParentId` (`ParentId`),
  CONSTRAINT `posts_ibfk_1` FOREIGN KEY (`PostTypeId`) REFERENCES `posttypes` (`PostTypeId`)
) ENGINE=InnoDB;
 732
Author: Bill Karwin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-26 20:38:20

upd: 2017-03-31, Wersja 5.7.5 MySQL sprawił, że przełącznik ONLY_FULL_GROUP_BY był domyślnie włączony (stąd nie-deterministyczna Grupa zapytań została wyłączona). Co więcej, zaktualizowali grupę według wdrożenia, a rozwiązanie może nie działać zgodnie z oczekiwaniami nawet przy wyłączonym przełączniku. Trzeba to sprawdzić.

Powyższe rozwiązanie Billa Karwina działa dobrze, gdy liczba elementów w grupach jest raczej mała, ale wydajność zapytania staje się zła, gdy grupy są dość duże, ponieważ rozwiązanie wymaga około n*n/2 + n/2 Tylko IS NULL porównań.

Wykonałem testy na tablicy InnoDB 18684446 wierszy z 1182 grupami. Tabela zawiera wyniki testów dla testów funkcjonalnych i ma (test_id, request_id) jako klucz podstawowy. Tak więc test_id jest grupą i szukałem ostatniej request_id dla każdej test_id.

Rozwiązanie Bill ' a działa już od kilku godzin na moim dell e4310 i nie wiem kiedy się skończy mimo że działa na indeksie pokrycia (stąd using index w wyjaśnieniu). Mam kilka innych rozwiązań, które są oparte na tych samych pomysłach:]}
  • jeśli indeks bazowy jest indeksem BTREE (co zwykle ma miejsce), największa para (group_id, item_value) jest ostatnią wartością w każdym group_id, to jest pierwsza dla każdego group_id jeśli przechodzimy przez indeks w porządku malejącym;
  • jeśli odczytamy wartości, które są objęte indeksem, wartości są odczytywane w kolejności indeksu;
  • każdy indeks domyślnie zawiera kolumny klucza podstawowego dołączone do tego (czyli klucz podstawowy znajduje się w indeksie pokrycia). W poniższych rozwiązaniach operuję bezpośrednio na kluczu podstawowym, w Twoim przypadku wystarczy dodać kolumny klucza podstawowego w wyniku.
  • w wielu przypadkach znacznie taniej jest zebrać wymagane identyfikatory wierszy w wymaganej kolejności w zapytaniu podrzędnym i dołączyć wynik zapytania podrzędnego do id. Ponieważ dla każdego wiersza w wyniku zapytania podrzędnego MySQL będzie potrzebował jednego pobierania opartego na kluczu podstawowym, podquery będą umieszczane jako pierwsze w join, a wiersze będą wyświetlane w kolejności ID w podquery (jeśli pominiemy jawną kolejność przez dla join)

3 ways MySQL uses indexes to świetny artykuł, aby zrozumieć niektóre szczegóły.

Rozwiązanie 1

[[15]} ten jest niesamowicie szybki, zajmuje około 0,8 SEK na moich 18m + rzędach:
SELECT test_id, MAX(request_id), request_id
FROM testresults
GROUP BY test_id DESC;

Jeśli chcesz zmienić kolejność na ASC, umieść ją w zapytaniu podrzędnym, zwróć tylko identyfikatory i użyj tego jako podquery do dołączenia do reszty kolumn:

SELECT test_id, request_id
FROM (
    SELECT test_id, MAX(request_id), request_id
    FROM testresults
    GROUP BY test_id DESC) as ids
ORDER BY test_id;

Ten zajmuje około 1,2 sekundy na moich danych.

Rozwiązanie 2

Oto inne rozwiązanie, które zajmuje około 19 sekund dla mojego stołu:

SELECT test_id, request_id
FROM testresults, (SELECT @group:=NULL) as init
WHERE IF(IFNULL(@group, -1)=@group:=test_id, 0, 1)
ORDER BY test_id DESC, request_id DESC

Zwraca również testy w kolejności malejącej. Jest znacznie wolniejszy, ponieważ wykonuje pełne skanowanie indeksów, ale jest tutaj, aby dać ci pomysł, jak wypisać N maksymalnych wierszy dla każdej grupy.

Wadą zapytania jest to, że jego wynik nie może być buforowany przez bufor zapytań.

 120
Author: newtover,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-31 15:08:26

Użyj subquery , aby zwrócić poprawną grupę, ponieważ jesteś w połowie drogi.

Spróbuj tego:

select
    a.*
from
    messages a
    inner join 
        (select name, max(id) as maxid from messages group by name) as b on
        a.id = b.maxid

Jeśli nie id chcesz max:

select
    a.*
from
    messages a
    inner join 
        (select name, max(other_col) as other_col 
         from messages group by name) as b on
        a.name = b.name
        and a.other_col = b.other_col

W ten sposób unikasz skorelowanych zapytań podrzędnych i / lub porządkowania w swoich zapytaniach podrzędnych, które wydają się być bardzo powolne / nieefektywne.

 80
Author: Eric,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-21 17:14:13

Doszedłem do innego rozwiązania, które polega na pobraniu identyfikatorów dla ostatniego posta w każdej grupie, a następnie wybraniu z tabeli wiadomości, używając wyniku z pierwszego zapytania jako argumentu dlaWHERE x IN konstruktu:

SELECT id, name, other_columns
FROM messages
WHERE id IN (
    SELECT MAX(id)
    FROM messages
    GROUP BY name
);

Nie wiem, jak to działa w porównaniu z innymi rozwiązaniami, ale działało spektakularnie dla mojej tabeli z 3 + milionami wierszy. (4 sekunda wykonania z 1200 + wyników)

powinno to działać zarówno na MySQL jak i SQL Server.

 34
Author: JYelton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-20 21:46:38

Rozwiązanie przez sub query fiddle Link

select * from messages where id in
(select max(id) from messages group by Name)

Rozwiązanie przez warunek join fiddle link

select m1.* from messages m1 
left outer join messages m2 
on ( m1.id<m2.id and m1.name=m2.name )
where m2.id is null

Powodem tego postu jest podanie tylko linku. Ten sam SQL jest już dostępny w innych odpowiedziach.

 24
Author: Vipin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-25 08:36:42

Jeszcze nie testowałem z dużym DB, ale myślę, że może to być szybsze niż łączenie tabel:

SELECT *, Max(Id) FROM messages GROUP BY Name
 6
Author: Shai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-14 07:07:11

Oto dwie propozycje. Po pierwsze, jeśli mysql obsługuje ROW_NUMBER (), jest to bardzo proste:

WITH Ranked AS (
  SELECT Id, Name, OtherColumns,
    ROW_NUMBER() OVER (
      PARTITION BY Name
      ORDER BY Id DESC
    ) AS rk
  FROM messages
)
  SELECT Id, Name, OtherColumns
  FROM messages
  WHERE rk = 1;
Zakładam, że przez "ostatni" masz na myśli ostatni w kolejności identyfikacyjnej. Jeśli nie, należy odpowiednio zmienić klauzulę ORDER BY okna ROW_NUMBER (). Jeśli funkcja ROW_NUMBER () nie jest dostępna, jest to inne rozwiązanie:

Po drugie, jeśli nie, to często jest to dobry sposób postępowania:

SELECT
  Id, Name, OtherColumns
FROM messages
WHERE NOT EXISTS (
  SELECT * FROM messages as M2
  WHERE M2.Name = messages.Name
  AND M2.Id > messages.Id
)

Innymi słowy, wybierz Wiadomości, w których nie ma później-ID wiadomości o tej samej nazwie.

 4
Author: Steve Kass,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-21 17:26:12

Oto moje rozwiązanie:

SELECT 
  DISTINCT NAME,
  MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES 
FROM MESSAGE;
 4
Author: Abhishek Yadav,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-08 19:03:49

Oto inny sposób na uzyskanie ostatniego powiązanego rekordu za pomocą {[1] } z order by i SUBSTRING_INDEX, aby wybrać jeden z rekordów z listy

SELECT 
  `Id`,
  `Name`,
  SUBSTRING_INDEX(
    GROUP_CONCAT(
      `Other_Columns` 
      ORDER BY `Id` DESC 
      SEPARATOR '||'
    ),
    '||',
    1
  ) Other_Columns 
FROM
  messages 
GROUP BY `Name` 

Powyższe zapytanie grupuje wszystkie Other_Columns, które są w tej samej grupie Name i używając ORDER BY id DESC połączy wszystkie Other_Columns w określonej grupie w porządku malejącym z podanym separatorem w moim przypadku użyłem ||, używając SUBSTRING_INDEX na tej liście wybierze pierwszą

Demo Fiddle

 3
Author: M Khalid Junaid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-30 06:01:52
SELECT 
  column1,
  column2 
FROM
  table_name 
WHERE id IN 
  (SELECT 
    MAX(id) 
  FROM
    table_name 
  GROUP BY column1) 
ORDER BY column1 ;
 3
Author: jeet singh parmar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-04 11:38:30

Spróbuj tego:

SELECT jos_categories.title AS name,
       joined .catid,
       joined .title,
       joined .introtext
FROM   jos_categories
       INNER JOIN (SELECT *
                   FROM   (SELECT `title`,
                                  catid,
                                  `created`,
                                  introtext
                           FROM   `jos_content`
                           WHERE  `sectionid` = 6
                           ORDER  BY `id` DESC) AS yes
                   GROUP  BY `yes`.`catid` DESC
                   ORDER  BY `yes`.`created` DESC) AS joined
         ON( joined.catid = jos_categories.id )  
 2
Author: Pro Web Design,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-15 13:47:27

Stąd też można podziwiać widoki.

Http://sqlfiddle.com/#! 9 / ef42b/9

PIERWSZE ROZWIĄZANIE

SELECT d1.ID,Name,City FROM Demo_User d1
INNER JOIN
(SELECT MAX(ID) AS ID FROM Demo_User GROUP By NAME) AS P ON (d1.ID=P.ID);

DRUGIE ROZWIĄZANIE

SELECT * FROM (SELECT * FROM Demo_User ORDER BY ID DESC) AS T GROUP BY NAME ;
 2
Author: Shrikant Gupta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-28 09:07:12

Czy możemy użyć tej metody do usunięcia duplikatów w tabeli? Zestaw wyników jest w zasadzie zbiorem unikalnych rekordów, więc gdybyśmy mogli usunąć wszystkie rekordy Nie w zestawie wyników, nie mielibyśmy żadnych duplikatów? Próbowałem tego, ale mySQL dał błąd 1093.

DELETE FROM messages WHERE id NOT IN
 (SELECT m1.id  
 FROM messages m1 LEFT JOIN messages m2  
 ON (m1.name = m2.name AND m1.id < m2.id)  
 WHERE m2.id IS NULL)

Czy istnieje sposób, aby zapisać wyjście do zmiennej temp, a następnie usunąć z NOT in (zmienna temp)? @ Bill dzięki za bardzo przydatne rozwiązanie.

EDIT: chyba znalazłem rozwiązanie:

DROP TABLE IF EXISTS UniqueIDs; 
CREATE Temporary table UniqueIDs (id Int(11)); 

INSERT INTO UniqueIDs 
    (SELECT T1.ID FROM Table T1 LEFT JOIN Table T2 ON 
    (T1.Field1 = T2.Field1 AND T1.Field2 = T2.Field2 #Comparison Fields  
    AND T1.ID < T2.ID) 
    WHERE T2.ID IS NULL); 

DELETE FROM Table WHERE id NOT IN (SELECT ID FROM UniqueIDs);
 1
Author: Simon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-08 01:57:49

Poniższe zapytanie będzie działać dobrze, jak na twoje pytanie.

SELECT M1.* 
FROM MESSAGES M1,
(
 SELECT SUBSTR(Others_data,1,2),MAX(Others_data) AS Max_Others_data
 FROM MESSAGES
 GROUP BY 1
) M2
WHERE M1.Others_data = M2.Max_Others_data
ORDER BY Others_data;
 1
Author: Teja,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-18 20:21:00

Hi @ Vijay Dev jeśli twoja tabela messages zawiera Id, który jest automatycznym przyrostem klucza podstawowego, to aby pobrać najnowszy rekord na podstawie klucza podstawowego, Twoje zapytanie powinno wyglądać następująco:

SELECT m1.* FROM messages m1 INNER JOIN (SELECT max(Id) as lastmsgId FROM messages GROUP BY Name) m2 ON m1.Id=m2.lastmsgId
 1
Author: bikashphp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-21 14:08:16

Jeśli chcesz, aby ostatni wiersz dla każdego Name, możesz nadać numer wiersza każdej grupie wierszy przez Name i kolejność przez Id w porządku malejącym.

Zapytanie

SELECT t1.Id, 
       t1.Name, 
       t1.Other_Columns
FROM 
(
     SELECT Id, 
            Name, 
            Other_Columns,
    (
        CASE Name WHEN @curA 
        THEN @curRow := @curRow + 1 
        ELSE @curRow := 1 AND @curA := Name END 
    ) + 1 AS rn 
    FROM messages t, 
    (SELECT @curRow := 0, @curA := '') r 
    ORDER BY Name,Id DESC 
)t1
WHERE t1.rn = 1
ORDER BY t1.Id;

SQL

 1
Author: Wanderer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-19 04:36:11

Podejście ze znaczną prędkością jest następujące.

SELECT * 
FROM messages a
WHERE Id = (SELECT MAX(Id) FROM messages WHERE a.Name = Name)

Wynik

Id  Name    Other_Columns
3   A   A_data_3
5   B   B_data_2
6   C   C_data_1
 1
Author: Song Zhengyi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-10 20:33:11

A może tak:

SELECT DISTINCT ON (name) *
FROM messages
ORDER BY name, id DESC;

Miałem podobny problem (na PostgreSQL tough) i na tabeli rekordów 1M. To rozwiązanie trwa 1.7 s vs 44s produkowane przez ten z lewym złączem. W moim przypadku musiałem filtrować corrispondant twojego pola name względem wartości NULL, co skutkowało jeszcze lepszymi wynikami o 0.2 sek

 0
Author: Azathoth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-30 10:50:40

Oczywiście istnieje wiele różnych sposobów uzyskania tych samych wyników, twoje pytanie wydaje się być skutecznym sposobem uzyskania ostatnich wyników w każdej grupie w MySQL. Jeśli pracujesz z ogromną ilością danych i zakładając, że używasz InnoDB nawet z najnowszymi wersjami MySQL (takimi jak 5.7.21 i 8.0.4-RC), może to nie być skuteczny sposób na zrobienie tego.

Czasami musimy to zrobić z tabelami zawierającymi nawet ponad 60 milionów wierszy.

Dla tych przykłady użyję danych z tylko około 1,5 miliona wierszy, w których zapytania będą musiały znaleźć wyniki dla wszystkich grup w danych. W naszych rzeczywistych przypadkach często musielibyśmy zwrócić dane z około 2000 grup (które hipotetycznie nie wymagałyby zbadania bardzo dużo danych).

Użyję następujących tabel:

CREATE TABLE temperature(
  id INT UNSIGNED NOT NULL AUTO_INCREMENT, 
  groupID INT UNSIGNED NOT NULL, 
  recordedTimestamp TIMESTAMP NOT NULL, 
  recordedValue INT NOT NULL,
  INDEX groupIndex(groupID, recordedTimestamp), 
  PRIMARY KEY (id)
);

CREATE TEMPORARY TABLE selected_group(id INT UNSIGNED NOT NULL, PRIMARY KEY(id)); 

Tabela temperatury jest wypełniona około 1,5 miliona przypadkowych rekordów i 100 różnych grup. Selected_group jest wypełniona tych 100 grup (w naszych przypadkach byłoby to zwykle mniej niż 20% dla wszystkich grup).

Ponieważ te dane są losowe, oznacza to, że wiele wierszy może mieć te same rekordy czasu. Chcemy uzyskać listę wszystkich wybranych grup w kolejności groupID z ostatnim zarejestrowanym znacznikiem czasu dla każdej grupy, a jeśli ta sama grupa ma więcej niż jeden pasujący wiersz, taki jak ten, to ostatni pasujący identyfikator tych wierszy.

Jeśli hipotetycznie MySQL miał funkcję last (), która zwracała wartości z ostatniego wiersza w klauzuli special ORDER BY możemy po prostu zrobić:

SELECT 
  last(t1.id) AS id, 
  t1.groupID, 
  last(t1.recordedTimestamp) AS recordedTimestamp, 
  last(t1.recordedValue) AS recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.groupID = g.id
ORDER BY t1.recordedTimestamp, t1.id
GROUP BY t1.groupID;

Który musiałby zbadać tylko kilka 100 wierszy w tym przypadku, ponieważ nie używa żadnej z normalnych funkcji GROUP BY. Wykonałoby się to w ciągu 0 sekund, a tym samym byłoby wysoce wydajne. Zauważ, że normalnie w MySQL widzimy klauzulę ORDER BY następującą po klauzuli GROUP BY, jednak ta klauzula ORDER BY jest używana do określenia kolejności dla funkcji last (), jeśli była po grupie by wtedy to zamawiaj grupy. Jeśli nie ma klauzuli GROUP BY, to ostatnie wartości będą takie same we wszystkich zwracanych wierszach.

Jednak MySQL tego nie ma, więc spójrzmy na różne pomysły na to, co ma i udowodnijmy, że żadna z nich nie jest wydajna.

Przykład 1

SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.id = (
  SELECT t2.id
  FROM temperature t2 
  WHERE t2.groupID = g.id
  ORDER BY t2.recordedTimestamp DESC, t2.id DESC
  LIMIT 1
);

To zbadało 3,009,254 wierszy i zajęło ~0,859 sekundy na 5.7.21 i nieco dłużej na 8.0.4-rc

Przykład 2

SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue 
FROM temperature t1
INNER JOIN ( 
  SELECT max(t2.id) AS id   
  FROM temperature t2
  INNER JOIN (
    SELECT t3.groupID, max(t3.recordedTimestamp) AS recordedTimestamp
    FROM selected_group g
    INNER JOIN temperature t3 ON t3.groupID = g.id
    GROUP BY t3.groupID
  ) t4 ON t4.groupID = t2.groupID AND t4.recordedTimestamp = t2.recordedTimestamp
  GROUP BY t2.groupID
) t5 ON t5.id = t1.id;

To zbadało 1,505,331 wierszy i trwało ~1.25 sekund na 5.7.21 i nieco dłużej na 8.0.4-rc

Przykład 3

SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue 
FROM temperature t1
WHERE t1.id IN ( 
  SELECT max(t2.id) AS id   
  FROM temperature t2
  INNER JOIN (
    SELECT t3.groupID, max(t3.recordedTimestamp) AS recordedTimestamp
    FROM selected_group g
    INNER JOIN temperature t3 ON t3.groupID = g.id
    GROUP BY t3.groupID
  ) t4 ON t4.groupID = t2.groupID AND t4.recordedTimestamp = t2.recordedTimestamp
  GROUP BY t2.groupID
)
ORDER BY t1.groupID;

To zbadało 3,009,685 wierszy i zajęło ~1,95 sekundy na 5.7.21 i nieco dłużej na 8.0.4-rc

Przykład 4

SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.id = (
  SELECT max(t2.id)
  FROM temperature t2 
  WHERE t2.groupID = g.id AND t2.recordedTimestamp = (
      SELECT max(t3.recordedTimestamp)
      FROM temperature t3 
      WHERE t3.groupID = g.id
    )
);

To zbadało 6,137,810 wierszy i zajęło ~2,2 sekundy na 5.7.21 i nieco dłużej na 8.0.4-rc

Przykład 5

SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM (
  SELECT 
    t2.id, 
    t2.groupID, 
    t2.recordedTimestamp, 
    t2.recordedValue, 
    row_number() OVER (
      PARTITION BY t2.groupID ORDER BY t2.recordedTimestamp DESC, t2.id DESC
    ) AS rowNumber
  FROM selected_group g 
  INNER JOIN temperature t2 ON t2.groupID = g.id
) t1 WHERE t1.rowNumber = 1;

To zbadało 6,017,808 wierszy i zajęło ~4,2 sekundy na 8.0.4-rc

Przykład 6

SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue 
FROM (
  SELECT 
    last_value(t2.id) OVER w AS id, 
    t2.groupID, 
    last_value(t2.recordedTimestamp) OVER w AS recordedTimestamp, 
    last_value(t2.recordedValue) OVER w AS recordedValue
  FROM selected_group g
  INNER JOIN temperature t2 ON t2.groupID = g.id
  WINDOW w AS (
    PARTITION BY t2.groupID 
    ORDER BY t2.recordedTimestamp, t2.id 
    RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  )
) t1
GROUP BY t1.groupID;

To zbadało 6,017,908 wierszy i zajęło ~17.5 sekund na 8.0.4-rc

Przykład 7

SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue 
FROM selected_group g
INNER JOIN temperature t1 ON t1.groupID = g.id
LEFT JOIN temperature t2 
  ON t2.groupID = g.id 
  AND (
    t2.recordedTimestamp > t1.recordedTimestamp 
    OR (t2.recordedTimestamp = t1.recordedTimestamp AND t2.id > t1.id)
  )
WHERE t2.id IS NULL
ORDER BY t1.groupID;
To trwało wieczność, więc musiałem go zabić.
 0
Author: Yoseph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-30 06:20:59

Jeśli wydajność jest naprawdę Twoim zmartwieniem, możesz wprowadzić nową kolumnę w tabeli o nazwie IsLastInGroup Typu BIT.

Ustaw na true w kolumnach, które są ostatnie i utrzymuj je z każdym wierszem insert/update / delete. Pisanie będzie wolniejsze, ale skorzystasz na czytaniu. To zależy od Twojego przypadku użycia i polecam go tylko wtedy, gdy jesteś skoncentrowany na czytaniu.

Więc twoje zapytanie będzie wyglądało następująco:

SELECT * FROM Messages WHERE IsLastInGroup = 1
 0
Author: jabko87,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-02 15:05:59
select * from messages group by name desc
 -2
Author: huuang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-18 14:21:07