ดึงข้อมูลระเบียนสุดท้ายในแต่ละกลุ่ม - MySQL


954

มีตารางmessagesที่มีข้อมูลดังแสดงด้านล่าง:

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

ถ้าฉันเรียกใช้คิวselect * from messages group by nameรีฉันจะได้ผลลัพธ์ดังนี้:

1    A       A_data_1
4    B       B_data_1
6    C       C_data_1

แบบสอบถามใดจะส่งคืนผลลัพธ์ต่อไปนี้

3    A       A_data_3
5    B       B_data_2
6    C       C_data_1

นั่นคือบันทึกสุดท้ายในแต่ละกลุ่มควรส่งคืน

ในปัจจุบันนี่คือแบบสอบถามที่ฉันใช้:

SELECT
  *
FROM (SELECT
  *
FROM messages
ORDER BY id DESC) AS x
GROUP BY name

แต่มันดูไร้ประสิทธิภาพมาก วิธีอื่นใดเพื่อให้ได้ผลลัพธ์เดียวกัน


2
ดูคำตอบที่ยอมรับได้ในstackoverflow.com/questions/1379565/…เพื่อการแก้ปัญหาที่มีประสิทธิภาพมากขึ้น
eyaler

ทำซ้ำของstackoverflow.com/q/121387/684229
TMS

7
เหตุใดคุณจึงไม่สามารถเพิ่ม DESC เช่นเลือก * จากกลุ่มข้อความโดยใช้ชื่อ DESC
Kim Prince


2
@KimPrince ดูเหมือนว่าคำตอบที่คุณแนะนำไม่ได้ทำตามที่คาดไว้! ฉันลองใช้วิธีของคุณและใช้แถวแรกสำหรับแต่ละกลุ่มและสั่ง DESC ไม่ใช้แถวสุดท้ายของแต่ละกลุ่ม
Ayrat

คำตอบ:


967

ขณะนี้ MySQL 8.0 รองรับฟังก์ชั่นหน้าต่างเช่นเดียวกับการใช้งาน SQL ที่เป็นที่นิยมเกือบทั้งหมด ด้วยไวยากรณ์มาตรฐานนี้เราสามารถเขียนเคียวรีที่ยิ่งใหญ่ที่สุด -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;

ด้านล่างนี้เป็นคำตอบเดิมที่ฉันเขียนสำหรับคำถามนี้ในปี 2009:


ฉันเขียนวิธีนี้:

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

เกี่ยวกับประสิทธิภาพหนึ่งวิธีหรืออื่น ๆ อาจดีกว่าขึ้นอยู่กับลักษณะของข้อมูลของคุณ ดังนั้นคุณควรทดสอบทั้งแบบสอบถามและใช้ที่ดีกว่าประสิทธิภาพให้ฐานข้อมูลของคุณ

ตัวอย่างเช่นผมมีสำเนาของการถ่ายโอนข้อมูล StackOverflow สิงหาคม ฉันจะใช้เพื่อการเปรียบเทียบ มี 1,114,357 แถวในPostsตาราง สิ่งนี้ทำงานบนMySQL 5.0.75 บน Macbook Pro 2.40GHz ของฉัน

ฉันจะเขียนแบบสอบถามเพื่อค้นหาโพสต์ล่าสุดสำหรับ ID ผู้ใช้ที่ระบุ (ของฉัน)

ก่อนอื่นให้ใช้เทคนิคที่แสดงโดย @Eric พร้อมกับGROUP BYในแบบสอบถามย่อย:

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)

แม้แต่การEXPLAINวิเคราะห์ใช้เวลากว่า 16 วินาที:

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

ตอนนี้สร้างผลลัพธ์แบบสอบถามเดียวกันโดยใช้เทคนิคของฉันด้วย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)

การEXPLAINวิเคราะห์แสดงให้เห็นว่าทั้งสองตารางสามารถใช้ดัชนีได้:

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

นี่คือ DDL สำหรับPostsตารางของฉัน:

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;

8
จริงๆ? จะเกิดอะไรขึ้นถ้าคุณมีผลงานมากมาย ตัวอย่างเช่นหากคุณกำลังทำงานโดยมีการควบคุมเวอร์ชันในองค์กรให้พูดและคุณมีไฟล์หลายรุ่นต่อหนึ่งไฟล์ผลการเข้าร่วมนั้นจะมีขนาดใหญ่มาก คุณเคยเปรียบเทียบวิธีการสืบค้นย่อยด้วยวิธีนี้หรือไม่? ฉันอยากรู้อยากเห็นที่จะชนะ แต่ไม่อยากรู้อยากเห็นพอที่จะไม่ถามคุณก่อน
Eric

2
ทำการทดสอบบางอย่าง บนโต๊ะเล็ก ๆ (ประมาณ 300,000 ระเบียน, กลุ่ม ~ 190k, ดังนั้นจึงไม่ใช่กลุ่มใหญ่หรืออะไรก็ตาม) การสืบค้นจะเชื่อมโยงกัน (8 วินาทีต่อครั้ง)
Eric

1
@BillKarwin: ดูmeta.stackexchange.com/questions/123017โดยเฉพาะความคิดเห็นด้านล่างคำตอบของ Adam Rackis แจ้งให้เราทราบหากคุณต้องการเรียกคืนคำตอบสำหรับคำถามใหม่
Robert Harvey

3
@Tim ไม่<=ไม่ช่วยถ้าคุณมีคอลัมน์ที่ไม่ซ้ำกัน คุณต้องใช้คอลัมน์ที่ไม่ซ้ำกันเป็น tiebreaker
Bill Karwin

2
ประสิทธิภาพลดลงแบบทวีคูณเมื่อจำนวนแถวเพิ่มขึ้นหรือเมื่อกลุ่มใหญ่ขึ้น ตัวอย่างเช่นกลุ่มที่ประกอบด้วย 5 วันที่จะให้ผล 4 + 3 + 2 + 1 + 1 = 11 แถวทางซ้ายเข้าร่วมซึ่งหนึ่งแถวจะถูกกรองในท้ายที่สุด ประสิทธิภาพของการเข้าร่วมกับผลลัพธ์ที่จัดกลุ่มเป็นแบบเชิงเส้นเกือบ การทดสอบของคุณดูมีข้อบกพร่อง
Salman

147

UPD: 2017-03-31, รุ่น5.7.5ของ MySQL ได้ทำการเปิดใช้งานสวิตช์ ONLY_FULL_GROUP_BY ตามค่าเริ่มต้น (ดังนั้นกลุ่มที่ไม่ใช่การกำหนดกลุ่มตามแบบสอบถามจะปิดใช้งาน) นอกจากนี้พวกเขายังอัปเดตการใช้งาน GROUP BY และการแก้ปัญหาอาจไม่ทำงานตามที่คาดไว้อีกต่อไปแม้จะปิดสวิตช์ไว้ก็ตาม หนึ่งต้องตรวจสอบ

วิธีแก้ปัญหาของ Bill Karwin ทำงานได้ดีเมื่อนับจำนวนรายการภายในกลุ่มมีขนาดค่อนข้างเล็ก แต่ประสิทธิภาพของการค้นหาจะไม่ดีเมื่อกลุ่มมีขนาดใหญ่เนื่องจากโซลูชันต้องการn*n/2 + n/2เพียงIS NULLการเปรียบเทียบ

ฉันทำแบบทดสอบของฉันบนตารางของ18684446แถวกับ1182กลุ่มInnoDB ตารางประกอบด้วย testresults สำหรับการทดสอบการทำงานและมี(test_id, request_id)คีย์หลัก ดังนั้นtest_idเป็นกลุ่มและฉันค้นหาล่าสุดrequest_idสำหรับแต่ละtest_idสำหรับแต่ละ

โซลูชันของ Bill ได้ทำงานมาแล้วเป็นเวลาหลายชั่วโมงใน dell e4310 ของฉันและฉันไม่รู้ว่าจะเสร็จสิ้นแม้ว่าจะทำงานกับดัชนีครอบคลุม (ดังนั้น using indexใน EXPLAIN)

ฉันมีวิธีแก้ไขปัญหาอื่นสองสามข้อที่ใช้แนวคิดเดียวกัน:

  • หากดัชนีอ้างอิงคือดัชนี BTREE (ซึ่งโดยปกติจะเป็นกรณีนี้), (group_id, item_value)คู่ที่ใหญ่ที่สุดคือค่าสุดท้ายภายในแต่ละค่าgroup_id, ซึ่งเป็นครั้งแรกสำหรับแต่ละgroup_idถ้าเราเดินผ่านดัชนีตามลำดับจากมากไปน้อย;
  • หากเราอ่านค่าที่ครอบคลุมโดยดัชนีค่าจะอ่านตามลำดับของดัชนี
  • ดัชนีแต่ละรายการจะมีคอลัมน์คีย์หลักต่อท้ายนั้น (นั่นคือคีย์หลักอยู่ในดัชนีครอบคลุม) ในโซลูชันด้านล่างฉันทำงานโดยตรงกับคีย์หลักในกรณีที่คุณคุณจะต้องเพิ่มคอลัมน์คีย์หลักในผลลัพธ์
  • ในหลายกรณีมีราคาถูกกว่ามากในการรวบรวม id แถวที่ต้องการในลำดับที่ต้องการในแบบสอบถามย่อยและเข้าร่วมผลลัพธ์ของแบบสอบถามย่อยบน id เนื่องจากสำหรับแต่ละแถวในผลลัพธ์แบบสอบถามย่อย MySQL จะต้องดึงข้อมูลครั้งเดียวตามคีย์หลักแบบสอบถามย่อยจะถูกใส่ไว้ก่อนในการเข้าร่วมและแถวจะถูกส่งออกตามลำดับของรหัสในแบบสอบถามย่อย (ถ้าเราละเว้นคำสั่งชัดเจนโดย สำหรับการเข้าร่วม)

3 วิธีที่ MySQL ใช้ดัชนีเป็นบทความที่ดีในการทำความเข้าใจรายละเอียดบางอย่าง

โซลูชันที่ 1

อันนี้เร็วอย่างไม่น่าเชื่อใช้เวลาประมาณ 0,8 วินาทีในแถว 18M + ของฉัน:

SELECT test_id, MAX(request_id) AS request_id
FROM testresults
GROUP BY test_id DESC;

หากคุณต้องการเปลี่ยนคำสั่งเป็น ASC ให้วางไว้ในแบบสอบถามย่อยส่งคืนรหัสเท่านั้นและใช้เป็นแบบสอบถามย่อยเพื่อเข้าร่วมคอลัมน์ที่เหลือ:

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

สิ่งนี้ใช้เวลาประมาณ 1,2 วินาทีในข้อมูลของฉัน

โซลูชันที่ 2

นี่คือวิธีแก้ปัญหาอื่นที่ใช้เวลาประมาณ 19 วินาทีสำหรับตารางของฉัน:

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

ส่งคืนการทดสอบตามลำดับจากมากไปน้อยเช่นกัน มันช้ากว่ามากเพราะใช้การสแกนดัชนีแบบเต็ม แต่อยู่ที่นี่เพื่อให้คุณทราบถึงวิธีการส่งออกแถว N สูงสุดสำหรับแต่ละกลุ่ม

ข้อเสียของแบบสอบถามคือไม่สามารถแคชผลลัพธ์ของแบบสอบถามได้


โปรดเชื่อมโยงไปยังดัมพ์ของตารางของคุณเพื่อให้ผู้คนสามารถทดสอบบนแพลตฟอร์มของพวกเขาได้
Pacerier

3
โซลูชันที่ 1 ไม่สามารถทำงานได้คุณไม่สามารถเลือก request_id ได้โดยไม่ต้องเป็นกลุ่มตามข้อ
ก.ย.

2
@ giòนี่คือคำตอบคือ 5 ปี จนกระทั่ง MySQL 5.7.5 ONLY_FULL_GROUP_BY ถูกปิดใช้งานโดยค่าเริ่มต้นและการแก้ปัญหานี้ทำงานออกจากกล่องdev.mysql.com/doc/relnotes/mysql/5.7/en/... ตอนนี้ฉันไม่แน่ใจว่าวิธีการแก้ปัญหายังคงทำงานเมื่อคุณปิดการใช้งานโหมดเพราะการใช้งาน GROUP BY มีการเปลี่ยนแปลง
newtover

หากคุณต้องการ ASC ในโซลูชันแรกมันจะทำงานได้ไหมถ้าคุณเปลี่ยน MAX เป็น MIN?
จิน

@JinIzzraeel คุณมี MIN โดยค่าเริ่มต้นที่ด้านบนของแต่ละกลุ่ม (เป็นคำสั่งของดัชนีครอบคลุม): SELECT test_id, request_id FROM testresults GROUP BY test_id;จะส่งคืน request_id ขั้นต่ำสำหรับแต่ละ test_id
newtover

101

ใช้แบบสอบถามย่อยของคุณเพื่อส่งกลับการจัดกลุ่มที่ถูกต้องเพราะคุณอยู่ที่นั่นครึ่งหนึ่ง

ลองสิ่งนี้:

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

หากไม่ใช่idคุณต้องการจำนวนสูงสุด:

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

ด้วยวิธีนี้คุณหลีกเลี่ยงแบบสอบถามย่อยที่มีความสัมพันธ์และ / หรือการสั่งซื้อในแบบสอบถามย่อยของคุณซึ่งมีแนวโน้มที่จะช้า / ไม่มีประสิทธิภาพ


1
หมายเหตุข้อสังเกตสำหรับการแก้ปัญหาด้วยother_col: หากคอลัมน์นั้นไม่ซ้ำกันคุณอาจได้รับหลายระเบียนกลับมาเหมือนกันnameหากพวกเขาผูกmax(other_col)ไว้ ผมพบว่าโพสต์นี้nameที่อธิบายถึงวิธีการแก้ปัญหาสำหรับความต้องการของฉันที่ฉันต้องตรงหนึ่งบันทึกต่อ
Eric Simonton

ในบางสถานการณ์คุณสามารถใช้โซลูชันนี้ได้ แต่จะได้รับการยอมรับ
tom10271

จากประสบการณ์ของฉันมันคือการจัดกลุ่มตารางข้อความแช่งทั้งหมดที่มีแนวโน้มที่จะช้า / ไม่มีประสิทธิภาพ! ในคำอื่น ๆ โปรดทราบว่าแบบสอบถามย่อยต้องใช้การสแกนแบบเต็มตารางและทำการจัดกลุ่มเพื่อให้บูต ... เว้นแต่ว่าเครื่องมือเพิ่มประสิทธิภาพของคุณกำลังทำบางสิ่งที่ไม่ได้เป็นของฉัน ดังนั้นวิธีนี้ขึ้นอยู่กับการถือครองตารางทั้งหมดไว้ในหน่วยความจำอย่างมาก
Timo

ผู้ที่จะได้ประโยชน์จากINDEX(name, id)และINDEX(name, other_col)
Rick James

55

ฉันมาถึงวิธีการแก้ปัญหาที่แตกต่างกันซึ่งจะได้รับ ID สำหรับการโพสต์ล่าสุดภายในแต่ละกลุ่มจากนั้นเลือกจากตารางข้อความโดยใช้ผลลัพธ์จากการสืบค้นแรกเป็นอาร์กิวเมนต์สำหรับการWHERE x INสร้าง:

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

ฉันไม่ทราบว่าวิธีการนี้มีประสิทธิภาพอย่างไรเมื่อเปรียบเทียบกับโซลูชันอื่น ๆ (การประมวลผล 4 วินาทีพร้อมผลลัพธ์มากกว่า 1200 รายการ)

สิ่งนี้จะทำงานได้ทั้งบน MySQL และ SQL Server


เพียงตรวจสอบให้แน่ใจว่าคุณมีดัชนี (ชื่อ, id)
ซามูเอลÅslund

1
ดีกว่ามากที่ตัวเองเข้าร่วม
anwerj

ฉันได้เรียนรู้บางอย่างจากคุณว่าเป็นงานที่ดีและการสืบค้นนี้เร็วขึ้น
ฮัมฟรีย์

33

แก้ไขโดยลิงค์ย่อยซอเชื่อมโยง

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

การแก้ปัญหาโดยเข้าร่วมลิงค์ซอเงื่อนไข

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

เหตุผลสำหรับการโพสต์นี้คือการให้ลิงค์ซอเท่านั้น SQL เดียวกันนั้นมีให้ในคำตอบอื่นแล้ว


1
@AlexanderSuraphel mysql5.5 ไม่สามารถใช้งานได้ในซอตอนนี้การเชื่อมโยงซอถูกสร้างขึ้นโดยใช้ที่ ตอนนี้ซอวันสนับสนุน mysql5.6 ฉันเปลี่ยนฐานข้อมูลเพื่อ mysql 5.6 และฉันสามารถสร้างสคีมาและเรียกใช้ sql
Vipin

8

วิธีการที่มีความเร็วพอสมควรมีดังต่อไปนี้

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

ผลลัพธ์

Id  Name    Other_Columns
3   A   A_data_3
5   B   B_data_2
6   C   C_data_1

สมมติฐานidนี้ได้รับคำสั่งในแบบที่คุณต้องการ ในกรณีทั่วไปจำเป็นต้องมีคอลัมน์อื่น
Rick James

6

นี่คือคำแนะนำสองข้อ ก่อนอื่นหาก mysql รองรับ ROW_NUMBER () มันง่ายมาก:

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;

ฉันสมมติว่า "สุดท้าย" คุณหมายถึงลำดับสุดท้ายในรหัส หากไม่มีให้เปลี่ยนส่วนคำสั่ง ORDER BY ของหน้าต่าง ROW_NUMBER () หาก ROW_NUMBER () ไม่พร้อมใช้งานนี่เป็นอีกวิธีการหนึ่ง:

ประการที่สองหากไม่เป็นเช่นนี้มักจะเป็นวิธีที่ดีในการดำเนินการ:

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

กล่าวอีกนัยหนึ่งให้เลือกข้อความที่ไม่มีข้อความ ID ในภายหลังที่มีชื่อเดียวกัน


8
MySQL ไม่รองรับ ROW_NUMBER () หรือ CTE
Bill Karwin

1
MySQL 8.0 (และ MariaDB 10.2) ตอนนี้รองรับROW_NUMBER()และ CTE
Rick James

6

ฉันยังไม่ได้ทดสอบกับฐานข้อมูลขนาดใหญ่ แต่ฉันคิดว่านี่อาจเร็วกว่าการเข้าร่วมตาราง:

SELECT *, Max(Id) FROM messages GROUP BY Name

14
ผลตอบแทนนี้ข้อมูลโดยพลการ กล่าวคือมีคอลัมน์ที่ส่งคืนอาจไม่ได้มาจากระเบียนที่มี MAX (Id)
อันตราย

มีประโยชน์ในการเลือก max Id จากชุดของเร็กคอร์ดที่มีเงื่อนไข WHERE: "SELECT Max (Id) จาก Prod WHERE Pn = '" + Pn + "'" มันจะส่งคืน Id สูงสุดจากชุดของเร็กคอร์ดที่มี Pn.In เดียวกัน # c ใช้ reader.GetString (0) เพื่อรับผล
นิโคลา

5

นี่เป็นอีกวิธีหนึ่งในการรับระเบียนที่เกี่ยวข้องล่าสุดโดยใช้GROUP_CONCATคำสั่งซื้อโดยและSUBSTRING_INDEXเพื่อเลือกหนึ่งในระเบียนจากรายการ

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

แบบสอบถามข้างต้นจะจัดกลุ่มทั้งหมดOther_Columnsที่อยู่ในNameกลุ่มเดียวกันและการใช้ORDER BY id DESCจะเข้าร่วมทั้งหมดOther_Columnsในกลุ่มที่ระบุในลำดับถัดลงมาพร้อมกับตัวคั่นที่ให้ไว้ในกรณีของฉันฉันได้ใช้การ||ใช้SUBSTRING_INDEXมากกว่ารายการนี้จะเลือกคนแรก

ซอสาธิต


ระวังว่าgroup_concat_max_lenจะ จำกัด จำนวนแถวที่คุณสามารถจัดการได้
Rick James

5

เห็นได้ชัดว่ามีหลายวิธีในการรับผลลัพธ์ที่เหมือนกันคำถามของคุณน่าจะเป็นวิธีที่มีประสิทธิภาพในการรับผลลัพธ์สุดท้ายในแต่ละกลุ่มใน MySQL หากคุณกำลังทำงานกับข้อมูลจำนวนมากและสมมติว่าคุณกำลังใช้ InnoDB กับ MySQL เวอร์ชันล่าสุด (เช่น 5.7.21 และ 8.0.4-rc) ดังนั้นอาจไม่มีวิธีที่มีประสิทธิภาพในการทำเช่นนี้

บางครั้งเราต้องทำสิ่งนี้กับตารางที่มีมากกว่า 60 ล้านแถว

สำหรับตัวอย่างเหล่านี้ฉันจะใช้ข้อมูลที่มีเพียงประมาณ 1.5 ล้านแถวที่แบบสอบถามจะต้องค้นหาผลลัพธ์สำหรับทุกกลุ่มในข้อมูล ในกรณีที่เกิดขึ้นจริงของเราเรามักจะต้องส่งคืนข้อมูลจากกลุ่มประมาณ 2,000 กลุ่ม (ซึ่งสมมุติว่าไม่ต้องการตรวจสอบข้อมูลมากนัก)

ฉันจะใช้ตารางต่อไปนี้:

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)); 

ตารางอุณหภูมิบรรจุด้วยการสุ่มประมาณ 1.5 ล้านบันทึกและมี 100 กลุ่มที่แตกต่างกัน Selected_group มีประชากร 100 กลุ่ม (ในกรณีของเราซึ่งปกติจะน้อยกว่า 20% สำหรับทุกกลุ่ม)

เนื่องจากข้อมูลนี้มีการสุ่มหมายความว่าหลายแถวสามารถมีบันทึกไว้เหมือนกัน สิ่งที่เราต้องการคือการรับรายชื่อของกลุ่มที่เลือกทั้งหมดตามลำดับของ groupID ที่บันทึกไว้ครั้งสุดท้ายสำหรับแต่ละกลุ่มและหากกลุ่มเดียวกันมีมากกว่าหนึ่งแถวที่ตรงกันเช่นนั้นรหัสการจับคู่ล่าสุดของแถวเหล่านั้น

ถ้าสมมุติว่า MySQL มีฟังก์ชั่นสุดท้าย () ซึ่งคืนค่าจากแถวสุดท้ายในประโยค ORDER BY พิเศษแล้วเราก็ทำได้:

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;

ซึ่งจะต้องตรวจสอบเพียง 100 แถวในกรณีนี้เนื่องจากไม่ได้ใช้ฟังก์ชัน GROUP BY ปกติใด ๆ สิ่งนี้จะดำเนินการใน 0 วินาทีและด้วยเหตุนี้จึงมีประสิทธิภาพสูง โปรดทราบว่าโดยปกติใน MySQL เราจะเห็น ORDER BY clause ตาม GROUP BY clause แต่ประโยค ORDER BY นี้จะถูกใช้เพื่อกำหนด ORDER สำหรับฟังก์ชั่นสุดท้าย () ถ้ามันเป็นหลังจาก GROUP BY แล้วมันจะทำการสั่งกลุ่ม หากไม่มี GROUP GROUP clause อยู่ค่าสุดท้ายจะเหมือนกันในแถวที่ส่งคืนทั้งหมด

อย่างไรก็ตาม MySQL ไม่มีสิ่งนี้ดังนั้นเรามาดูแนวคิดที่แตกต่างกันของสิ่งที่มันมีและพิสูจน์ว่าไม่มีสิ่งใดที่มีประสิทธิภาพ

ตัวอย่างที่ 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
);

สิ่งนี้ตรวจสอบ 3,009,254 แถวและใช้เวลาประมาณ 0.859 วินาทีใน 5.7.21 และอีกต่อไปเล็กน้อยใน 8.0.4-rc

ตัวอย่างที่ 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;

สิ่งนี้ตรวจสอบ 1,505,331 แถวและใช้เวลา ~ 1.25 วินาทีใน 5.7.21 และอีกต่อไปเล็กน้อยใน 8.0.4-rc

ตัวอย่างที่ 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;

สิ่งนี้ตรวจสอบ 3,009,685 แถวและใช้เวลาประมาณ 1.95 วินาทีใน 5.7.21 และยาวกว่าบน 8.0.4-rc เล็กน้อย

ตัวอย่างที่ 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
    )
);

สิ่งนี้ทำการตรวจสอบ 6,137,810 แถวและใช้เวลาประมาณ 2.2 วินาทีใน 5.7.21 และอีกต่อไปเล็กน้อยบน 8.0.4-rc

ตัวอย่างที่ 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;

สิ่งนี้ตรวจสอบ 6,017,808 แถวและใช้เวลาประมาณ 4.2 วินาทีใน 8.0.4-rc

ตัวอย่างที่ 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;

สิ่งนี้ตรวจสอบ 6,017,908 แถวและใช้เวลาประมาณ 17.5 วินาทีใน 8.0.4-rc

ตัวอย่างที่ 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;

อันนี้พากันตลอดไปดังนั้นฉันต้องฆ่ามัน


นี่เป็นปัญหาที่แตกต่าง และการแก้ปัญหาคือการค้นหาทั้งหมดของ UNION
Paul Spiegel

@ PaulSpiegel ฉันคิดว่าคุณล้อเล่นเกี่ยวกับยูเนี่ยนใหญ่ทั้งหมด นอกจากความจริงที่ว่าเราจำเป็นต้องรู้ทุกกลุ่มที่เลือกไว้ล่วงหน้าและด้วย 2,000 กลุ่มที่เลือกซึ่งจะเป็นการสืบค้นที่ใหญ่มากอย่างไม่น่าเชื่อ สารละลาย.
Yoseph

ฉันจริงจังจริงๆ ฉันเคยทดสอบสิ่งนั้นในอดีตด้วยสองสามร้อยกลุ่ม เมื่อคุณต้องการจัดการความสัมพันธ์ในกลุ่มใหญ่ ๆ UNION ALL เป็นวิธีเดียวใน MySQL ที่จะบังคับใช้แผนการดำเนินการที่เหมาะสมที่สุด SELECT DISTINCT(groupID)รวดเร็วและจะให้ข้อมูลทั้งหมดที่คุณต้องการในการสร้างแบบสอบถาม คุณควรจะปรับขนาดแบบสอบถามได้ตราบใดที่ไม่เกินmax_allowed_packetซึ่งเริ่มต้นที่ 4MB ใน MySQL 5.7
Paul Spiegel

5

เราจะดูว่าคุณสามารถใช้ MySQL เพื่อรับบันทึกล่าสุดในกลุ่มตามบันทึกได้อย่างไร ตัวอย่างเช่นถ้าคุณมีชุดโพสต์ผลลัพธ์นี้

id category_id post_title

1 1 Title 1

2 1 Title 2

3 1 Title 3

4 2 Title 4

5 2 Title 5

6 3 Title 6

ฉันต้องการที่จะได้รับการโพสต์ล่าสุดในแต่ละหมวดหมู่ซึ่งเป็นชื่อ 3, ชื่อ 5 และ 6 ชื่อเพื่อให้ได้โพสต์ตามหมวดหมู่คุณจะใช้ MySQL กลุ่มโดยแป้นพิมพ์

select * from posts group by category_id

แต่ผลลัพธ์ที่เราได้รับกลับมาจากแบบสอบถามนี้คือ

id category_id post_title

1 1 Title 1

4 2 Title 4

6 3 Title 6

กลุ่มโดยจะส่งคืนระเบียนแรกในกลุ่มในชุดผลลัพธ์เสมอ

SELECT id, category_id, post_title FROM posts WHERE id IN ( SELECT MAX(id) FROM posts GROUP BY category_id );

นี่จะส่งคืนโพสต์ที่มี ID สูงสุดในแต่ละกลุ่ม

id category_id post_title

3 1 Title 3

5 2 Title 5

6 3 Title 6

อ้างอิงคลิกที่นี่


4
SELECT 
  column1,
  column2 
FROM
  table_name 
WHERE id IN 
  (SELECT 
    MAX(id) 
  FROM
    table_name 
  GROUP BY column1) 
ORDER BY column1 ;

คุณช่วยอธิบายคำตอบของคุณหน่อยได้ไหม? เหตุใดข้อความค้นหาของคุณจึงนิยมใช้แบบสอบถามต้นฉบับของ Vijays
janfoeh

4

นี่คือทางออกของฉัน:

SELECT 
  DISTINCT NAME,
  MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES 
FROM MESSAGE;

สิ่งนี้จะไม่ส่งคืนข้อความล่าสุดต่อชื่อ และเป็นเพียงรุ่น overcomplicated SELECT NAME, MAX(MESSAGES) MESSAGES FROM MESSAGE GROUP BY NAMEของ
Paul Spiegel

นอกจากนี้สูตรนี้ไม่มีประสิทธิภาพอย่างไม่มีการลด
Rick James

3

ลองสิ่งนี้:

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 )  

3

สวัสดี @Vijay Dev หากข้อความในตารางของคุณมีรหัสซึ่งเป็นคีย์หลักที่เพิ่มขึ้นอัตโนมัติแล้วเพื่อดึงข้อมูลพื้นฐานการบันทึกล่าสุดในคีย์หลักแบบสอบถามของคุณควรอ่านดังต่อไปนี้:

SELECT m1.* FROM messages m1 INNER JOIN (SELECT max(Id) as lastmsgId FROM messages GROUP BY Name) m2 ON m1.Id=m2.lastmsgId

อันนี้เร็วที่สุดที่ฉันพบ
CORSAIR

3

คุณสามารถดูได้จากที่นี่เช่นกัน

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

โซลูชั่นแรก

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);

ทางออกที่สอง

SELECT * FROM (SELECT * FROM Demo_User ORDER BY ID DESC) AS T GROUP BY NAME ;



2

มีวิธีใดบ้างที่เราสามารถใช้วิธีนี้ในการลบรายการที่ซ้ำกันในตาราง? ชุดผลลัพธ์นั้นเป็นชุดของระเบียนที่ไม่ซ้ำกันดังนั้นหากเราสามารถลบระเบียนทั้งหมดที่ไม่อยู่ในชุดผลลัพธ์เราจะไม่มีการทำซ้ำอย่างมีประสิทธิภาพหรือไม่ ฉันลองสิ่งนี้ แต่ mySQL ให้ข้อผิดพลาด 1,093 ครั้ง

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)

มีวิธีที่จะบันทึกผลลัพธ์ไปยังตัวแปร temp แล้วลบจาก NOT IN (ตัวแปร temp) หรือไม่ @ การเรียกเก็บเงินขอบคุณสำหรับโซลูชั่นที่มีประโยชน์มาก

แก้ไข: คิดว่าฉันพบทางออก:

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);

2

แบบสอบถามด้านล่างจะทำงานได้ดีตามคำถามของคุณ

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;

2

หากคุณต้องการแถวสุดท้ายสำหรับแต่ละแถวNameคุณสามารถกำหนดหมายเลขแถวให้กับแต่ละกลุ่มแถวโดยNameและเรียงตามลำดับจากIdมากไปน้อย

QUERY

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;

ซอ Fiddle


2

เกี่ยวกับสิ่งนี้:

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

ฉันมีปัญหาที่คล้ายกัน (ใน postgresql แกร่ง) และในตารางบันทึก 1M โซลูชันนี้ใช้เวลา 1.7 วินาทีเทียบกับ 44 วินาทีที่ผลิตโดยหนึ่งเดียวที่มี LEFT JOIN ในกรณีของฉันฉันต้องกรอง corrispondant ของเขตข้อมูลชื่อของคุณกับค่า NULL ทำให้การแสดงดีขึ้น 0.2 วินาที


1

หากประสิทธิภาพเป็นเรื่องที่คุณกังวลคุณสามารถแนะนำคอลัมน์ใหม่บนตารางที่เรียกว่า IsLastInGroup type BIT

ตั้งค่าเป็นจริงในคอลัมน์ที่มีอยู่และคงไว้ด้วยการแทรก / อัพเดต / ลบทุกแถว การเขียนจะช้าลง แต่คุณจะได้รับประโยชน์จากการอ่าน มันขึ้นอยู่กับกรณีการใช้งานของคุณและฉันขอแนะนำเฉพาะเมื่อคุณเน้นการอ่าน

ดังนั้นข้อความค้นหาของคุณจะมีลักษณะดังนี้:

SELECT * FROM Messages WHERE IsLastInGroup = 1

บางตารางใน Moodle มีคอลัมน์ตั้งค่าสถานะเช่นนี้
ลอเรนซ์


0

คุณสามารถจัดกลุ่มตามการนับและรับรายการสุดท้ายของกลุ่มเช่น:

SELECT 
    user,
    COUNT(user) AS count,
    MAX(id) as last
FROM request 
GROUP BY user

0

หวังว่าแบบสอบถาม Oracle ด้านล่างสามารถช่วย:

WITH Temp_table AS
(
    Select id, name, othercolumns, ROW_NUMBER() over (PARTITION BY name ORDER BY ID 
    desc)as rank from messages
)
Select id, name,othercolumns from Temp_table where rank=1

0

วิธีอื่น:

ค้นหาสิ่งที่เหมาะสมด้วยค่าสูงสุด m2_price ภายในแต่ละโปรแกรม (คุณสมบัติ n รายการใน 1 โปรแกรม):

select * from properties p
join (
    select max(m2_price) as max_price 
    from properties 
    group by program_id
) p2 on (p.program_id = p2.program_id)
having p.m2_price = max_price
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.