MySQL เข้าร่วมแถวล่าสุดเท่านั้นหรือไม่


106

ฉันมีลูกค้าโต๊ะที่เก็บ customer_id อีเมลและข้อมูลอ้างอิง มีตาราง customer_data เพิ่มเติมที่จัดเก็บบันทึกประวัติของการเปลี่ยนแปลงที่เกิดขึ้นกับลูกค้ากล่าวคือเมื่อมีการเปลี่ยนแปลงเกิดขึ้นจะมีการแทรกแถวใหม่

ในการแสดงข้อมูลลูกค้าในตารางจำเป็นต้องเชื่อมตารางทั้งสองเข้าด้วยกันอย่างไรก็ตามควรรวมแถวล่าสุดจาก customer_data เข้ากับตารางลูกค้าเท่านั้น

มีความซับซ้อนขึ้นเล็กน้อยเมื่อแบบสอบถามมีการแบ่งหน้าดังนั้นจึงมีขีด จำกัด และออฟเซ็ต

ฉันจะทำสิ่งนี้กับ MySQL ได้อย่างไร? ฉันคิดว่าฉันอยากจะใส่ DISTINCT ไว้ตรงนั้นสักแห่ง ...

แบบสอบถามในนาทีเป็นเช่นนี้ -

SELECT *, CONCAT(title,' ',forename,' ',surname) AS name
FROM customer c
INNER JOIN customer_data d on c.customer_id=d.customer_id
WHERE name LIKE '%Smith%' LIMIT 10, 20

นอกจากนี้ฉันคิดถูกไหมที่คิดว่าจะใช้ CONCAT กับ LIKE ในลักษณะนี้ได้

(ฉันขอขอบคุณที่ INNER JOIN อาจจะใช้ JOIN ผิดประเภทจริงๆฉันไม่รู้ว่าความแตกต่างระหว่าง JOIN ที่แตกต่างกันคืออะไรฉันจะตรวจสอบในตอนนี้!)


ตารางประวัติลูกค้ามีลักษณะอย่างไร? แถวล่าสุดถูกกำหนดอย่างไร? มีฟิลด์เวลาประทับหรือไม่
Daniel Vassallo

ล่าสุดเป็นเพียงแถวสุดท้ายที่แทรก - ดังนั้นคีย์หลักจึงเป็นตัวเลขสูงสุด
bcmcfc

ทำไมไม่ทริกเกอร์? ดูคำตอบนี้: stackoverflow.com/questions/26661314/…
Rodrigo Polo

คำตอบส่วนใหญ่ / ทั้งหมดใช้เวลานานเกินไปโดยมีหลายล้านแถว มีโซลูชันบางอย่างที่ มีประสิทธิภาพที่ดีขึ้น
Halil Özgür

คำตอบ:


150

คุณอาจต้องการลองทำสิ่งต่อไปนี้:

SELECT    CONCAT(title, ' ', forename, ' ', surname) AS name
FROM      customer c
JOIN      (
              SELECT    MAX(id) max_id, customer_id 
              FROM      customer_data 
              GROUP BY  customer_id
          ) c_max ON (c_max.customer_id = c.customer_id)
JOIN      customer_data cd ON (cd.id = c_max.max_id)
WHERE     CONCAT(title, ' ', forename, ' ', surname) LIKE '%Smith%' 
LIMIT     10, 20;

โปรดทราบว่า a JOINเป็นเพียงคำพ้องความหมายของINNER JOIN.

กรณีทดสอบ:

CREATE TABLE customer (customer_id int);
CREATE TABLE customer_data (
   id int, 
   customer_id int, 
   title varchar(10),
   forename varchar(10),
   surname varchar(10)
);

INSERT INTO customer VALUES (1);
INSERT INTO customer VALUES (2);
INSERT INTO customer VALUES (3);

INSERT INTO customer_data VALUES (1, 1, 'Mr', 'Bobby', 'Smith');
INSERT INTO customer_data VALUES (2, 1, 'Mr', 'Bob', 'Smith');
INSERT INTO customer_data VALUES (3, 2, 'Mr', 'Jane', 'Green');
INSERT INTO customer_data VALUES (4, 2, 'Miss', 'Jane', 'Green');
INSERT INTO customer_data VALUES (5, 3, 'Dr', 'Jack', 'Black');

ผลลัพธ์ (สอบถามโดยไม่มีLIMITและWHERE):

SELECT    CONCAT(title, ' ', forename, ' ', surname) AS name
FROM      customer c
JOIN      (
              SELECT    MAX(id) max_id, customer_id 
              FROM      customer_data 
              GROUP BY  customer_id
          ) c_max ON (c_max.customer_id = c.customer_id)
JOIN      customer_data cd ON (cd.id = c_max.max_id);

+-----------------+
| name            |
+-----------------+
| Mr Bob Smith    |
| Miss Jane Green |
| Dr Jack Black   |
+-----------------+
3 rows in set (0.00 sec)

3
ขอบคุณสำหรับระดับรายละเอียดที่คุณได้เข้าไปที่นั่น ฉันหวังว่ามันจะช่วยคนอื่นเช่นเดียวกับฉัน!
bcmcfc

21
ในระยะยาวแนวทางนี้อาจสร้างปัญหาด้านประสิทธิภาพเนื่องจากจำเป็นต้องสร้างตารางชั่วคราว ดังนั้นวิธีแก้ปัญหาอื่น (ถ้าเป็นไปได้) คือการเพิ่มฟิลด์บูลีนใหม่ (is_last) ใน customer_data ซึ่งคุณจะต้องอัปเดตทุกครั้งที่มีการเพิ่มรายการใหม่ รายการสุดท้ายจะมี is_last = 1 อื่น ๆ ทั้งหมดสำหรับลูกค้ารายนี้ - is_last = 0
cephuo

5
ผู้คนควร (โปรด) อ่านคำตอบต่อไปนี้ด้วย (จาก Danny Coulombe) เนื่องจากคำตอบนี้ (ขออภัยดาเนียล) ช้ามากพร้อมข้อความค้นหาที่ยาวขึ้น / ข้อมูลเพิ่มเติม ทำให้หน้าของฉัน "รอ" โหลด 12 วินาที; ดังนั้นโปรดตรวจสอบstackoverflow.com/a/35965649/2776747ด้วย ฉันไม่สังเกตเห็นมันจนกระทั่งหลังจากมีการเปลี่ยนแปลงอื่น ๆ มากมายดังนั้นฉันจึงใช้เวลานานมากในการค้นหา
ศิลปะ

คุณไม่รู้ว่าสิ่งนี้ช่วยฉันได้มากแค่ไหน :) ขอบคุณอาจารย์
node_man

109

หากคุณกำลังทำงานกับข้อความค้นหาจำนวนมากคุณควรย้ายคำขอสำหรับแถวล่าสุดในส่วนคำสั่ง where เร็วกว่ามากและดูสะอาดกว่า

SELECT c.*,
FROM client AS c
LEFT JOIN client_calling_history AS cch ON cch.client_id = c.client_id
WHERE
   cch.cchid = (
      SELECT MAX(cchid)
      FROM client_calling_history
      WHERE client_id = c.client_id AND cal_event_id = c.cal_event_id
   )

4
ว้าวฉันเกือบจะไม่เชื่อในความแตกต่างของประสิทธิภาพนี้ ไม่แน่ใจว่าทำไมมันถึงรุนแรงขนาดนี้ แต่จนถึงตอนนี้มันเร็วกว่ามากจนรู้สึกว่าฉันทำผิดที่อื่น ...
Brian Leishman

2
ฉันหวังว่าจะได้ +1 สิ่งนี้มากกว่าหนึ่งครั้งเพื่อให้มีคนเห็นมากขึ้น ฉันได้ทดสอบสิ่งนี้ไปพอสมควรและทำให้การค้นหาของฉันแทบจะเป็นไปในทันที (WorkBench พูดว่า 0.000 วินาทีแม้จะมีsql_no_cache set) ในขณะที่การค้นหาในการเข้าร่วมนั้นใช้เวลาหลายวินาทีในการดำเนินการ ยังคงงุนงง แต่ฉันหมายความว่าคุณไม่สามารถโต้แย้งกับผลลัพธ์แบบนั้นได้
Brian Leishman

1
คุณจะเข้าร่วม 2 ตารางโดยตรงก่อนแล้วจึงกรองด้วย WHERE ฉันคิดว่ามันเป็นปัญหาด้านประสิทธิภาพอย่างมากหากคุณมีลูกค้าเป็นล้านคนและมีประวัติการโทรหลายสิบล้านครั้ง เนื่องจาก SQL จะพยายามรวม 2 ตารางก่อนแล้วกรองลงไปที่ไคลเอนต์เดียว ฉันอยากจะกรองไคลเอนต์และประวัติการโทรที่เกี่ยวข้องจากตารางก่อนในแบบสอบถามย่อยจากนั้นจึงเข้าร่วมตาราง
Tarik

1
ฉันคิดว่า "ca.client_id" และ "ca.cal_event_id" ต้องเป็น "c" สำหรับทั้งคู่
Herbert Van-Vliet

1
ฉันเห็นด้วยกับ @NickCoons ค่า NULL จะไม่ถูกส่งกลับเนื่องจากค่าเหล่านี้ไม่รวมอยู่ใน where clause คุณจะรวมค่า NULL และยังคงรักษาประสิทธิภาพที่ยอดเยี่ยมของแบบสอบถามนี้ไว้ได้อย่างไร
aanders77

10

สมมติว่าcustomer_dataมีการตั้งชื่อคอลัมน์การสร้างอัตโนมัติIdคุณสามารถทำได้:

SELECT CONCAT(title,' ',forename,' ',surname) AS name *
FROM customer c
    INNER JOIN customer_data d 
        ON c.customer_id=d.customer_id
WHERE name LIKE '%Smith%'
    AND d.ID = (
                Select Max(D2.Id)
                From customer_data As D2
                Where D2.customer_id = D.customer_id
                )
LIMIT 10, 20

9

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

SELECT MAX(d.id), d2.*, CONCAT(title,' ',forename,' ',surname) AS name
FROM customer AS c 
LEFT JOIN customer_data as d ON c.customer_id=d.customer_id 
LEFT JOIN customer_data as d2 ON d.id=d2.id
WHERE CONCAT(title, ' ', forename, ' ', surname) LIKE '%Smith%'
GROUP BY c.customer_id LIMIT 10, 20;

โดยพื้นฐานแล้วนี่คือการค้นหารหัสสูงสุดของตารางข้อมูลของคุณที่เชื่อมต่อกับลูกค้าจากนั้นเข้าร่วมตารางข้อมูลกับรหัสสูงสุดที่พบ สาเหตุที่เป็นเช่นนี้เนื่องจากการเลือกจำนวนสูงสุดของกลุ่มไม่ได้รับประกันว่าข้อมูลที่เหลือจะตรงกับรหัสเว้นแต่คุณจะรวมกลับเข้ามาเอง

ฉันไม่ได้ทดสอบสิ่งนี้กับ MySQL เวอร์ชันใหม่กว่า แต่ใช้งานได้บน 4.0.30


นี่คือความประณีตในความเรียบง่าย เหตุใดจึงเป็นครั้งแรกที่ฉันได้เห็นแนวทางนี้ โปรดสังเกตว่าEXPLAINสิ่งนี้ใช้ตารางและไฟล์ชั่วคราว การเพิ่มORDER BY NULLในตอนท้ายจะกำจัดไฟล์ออกไป
Timo

สำหรับความเสียใจของฉันวิธีแก้ปัญหาที่ไม่สวยงามของฉันเองนั้นเร็วกว่าข้อมูลของฉันถึง 3.5 เท่า ฉันใช้คิวรีย่อยเพื่อเลือกตารางหลักบวกกับ ID ล่าสุดของตารางที่เข้าร่วมจากนั้นแบบสอบถามภายนอกที่เลือกคิวรีย่อยและอ่านข้อมูลจริงจากตารางที่รวม ฉันกำลังเข้าร่วม 5 ตารางในตารางหลักและทดสอบด้วยเงื่อนไขที่เลือก 1,000 ระเบียน ดัชนีเหมาะสมที่สุด
Timo

ฉันใช้วิธีแก้ปัญหาของคุณกับSELECT *, MAX(firstData.id), MAX(secondData.id) [...]. ตามหลักเหตุผลการเปลี่ยนเป็นSELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]ฉันสามารถทำให้เร็วขึ้นอย่างมาก สิ่งนี้ช่วยให้การรวมแรกอ่านจากดัชนีเท่านั้นแทนที่จะต้องอ่านข้อมูลทั้งหมดจากดัชนีหลัก ตอนนี้โซลูชันที่สวยงามใช้เวลาเพียง 1.9 เท่าตราบเท่าที่โซลูชันที่ใช้แบบสอบถามย่อย
Timo

ไม่ทำงานอีกต่อไปใน MySQL 5.7 ตอนนี้ d2. * จะส่งคืนข้อมูลสำหรับแถวแรกในกลุ่มไม่ใช่แถวสุดท้าย SELECT MAX (R1.id), R2. * FROM ใบแจ้งหนี้ I LEFT JOIN ตอบกลับ R1 ON I.id = R1.invoice_id LEFT JOIN การตอบกลับ R2 บน R1.id = R2.id GROUP BY I.id LIMIT 0,10
Marco Marsala

6

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

หากคุณไม่สามารถแก้ไขสคีมาฐานข้อมูลเดิมของคุณได้แสดงว่ามีคำตอบที่ดีมากมายและแก้ปัญหาได้ดี

อย่างไรก็ตามหากคุณสามารถแก้ไขสคีมาของคุณได้ฉันขอแนะนำให้เพิ่มฟิลด์ในcustomerตารางของคุณที่เก็บบันทึกidล่าสุดcustomer_dataสำหรับลูกค้ารายนี้:

CREATE TABLE customer (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  current_data_id INT UNSIGNED NULL DEFAULT NULL
);

CREATE TABLE customer_data (
   id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
   customer_id INT UNSIGNED NOT NULL, 
   title VARCHAR(10) NOT NULL,
   forename VARCHAR(10) NOT NULL,
   surname VARCHAR(10) NOT NULL
);

การสอบถามลูกค้า

การสืบค้นนั้นง่ายและรวดเร็วที่สุดเท่าที่จะทำได้:

SELECT c.*, d.title, d.forename, d.surname
FROM customer c
INNER JOIN customer_data d on d.id = c.current_data_id
WHERE ...;

ข้อเสียเปรียบคือความซับซ้อนเป็นพิเศษเมื่อสร้างหรืออัปเดตลูกค้า

การอัปเดตลูกค้า

เมื่อใดก็ตามที่คุณต้องการอัปเดตลูกค้าคุณต้องใส่ระเบียนใหม่ในcustomer_dataตารางและอัปเดตcustomerระเบียน

INSERT INTO customer_data (customer_id, title, forename, surname) VALUES(2, 'Mr', 'John', 'Smith');
UPDATE customer SET current_data_id = LAST_INSERT_ID() WHERE id = 2;

การสร้างลูกค้า

การสร้างลูกค้าเป็นเพียงเรื่องของการแทรกcustomerรายการจากนั้นเรียกใช้คำสั่งเดียวกัน:

INSERT INTO customer () VALUES ();

SET @customer_id = LAST_INSERT_ID();
INSERT INTO customer_data (customer_id, title, forename, surname) VALUES(@customer_id, 'Mr', 'John', 'Smith');
UPDATE customer SET current_data_id = LAST_INSERT_ID() WHERE id = @customer_id;

ห่อ

ความซับซ้อนพิเศษในการสร้าง / อัปเดตลูกค้าอาจเป็นเรื่องที่น่ากลัว แต่ก็สามารถทำให้เกิดโดยอัตโนมัติได้อย่างง่ายดายด้วยทริกเกอร์

สุดท้ายหากคุณใช้ ORM สิ่งนี้สามารถจัดการได้ง่ายมาก ORM สามารถดูแลการแทรกค่าอัปเดตรหัสและรวมสองตารางให้คุณโดยอัตโนมัติ

Customerรูปแบบที่เปลี่ยนแปลงได้ของคุณจะมีลักษณะดังนี้:

class Customer
{
    private int id;
    private CustomerData currentData;

    public Customer(String title, String forename, String surname)
    {
        this.update(title, forename, surname);
    }

    public void update(String title, String forename, String surname)
    {
        this.currentData = new CustomerData(this, title, forename, surname);
    }

    public String getTitle()
    {
        return this.currentData.getTitle();
    }

    public String getForename()
    {
        return this.currentData.getForename();
    }

    public String getSurname()
    {
        return this.currentData.getSurname();
    }
}

และCustomerDataแบบจำลองที่ไม่เปลี่ยนรูปของคุณซึ่งมีเฉพาะ getters:

class CustomerData
{
    private int id;
    private Customer customer;
    private String title;
    private String forename;
    private String surname;

    public CustomerData(Customer customer, String title, String forename, String surname)
    {
        this.customer = customer;
        this.title    = title;
        this.forename = forename;
        this.surname  = surname;
    }

    public String getTitle()
    {
        return this.title;
    }

    public String getForename()
    {
        return this.forename;
    }

    public String getSurname()
    {
        return this.surname;
    }
}

ฉันรวมแนวทางนี้เข้ากับโซลูชันของ @ payne8 (ด้านบน) เพื่อให้ได้ผลลัพธ์ที่ต้องการโดยไม่ต้องมีแบบสอบถามย่อย
Ginger and Lavender

2
SELECT CONCAT(title,' ',forename,' ',surname) AS name * FROM customer c 
INNER JOIN customer_data d on c.id=d.customer_id WHERE name LIKE '%Smith%' 

ฉันคิดว่าคุณต้องเปลี่ยน c.customer_id เป็น c.id

อื่น ๆ ปรับปรุงโครงสร้างตาราง


ฉันลดลงเพราะฉันอ่านคำตอบของคุณผิดและตอนแรกฉันคิดว่ามันผิด Haste เป็นที่ปรึกษาที่ไม่ดี :-)
Wirone

1

คุณยังสามารถทำได้

SELECT    CONCAT(title, ' ', forename, ' ', surname) AS name
FROM      customer c
LEFT JOIN  (
              SELECT * FROM  customer_data ORDER BY id DESC
          ) customer_data ON (customer_data.customer_id = c.customer_id)
GROUP BY  c.customer_id          
WHERE     CONCAT(title, ' ', forename, ' ', surname) LIKE '%Smith%' 
LIMIT     10, 20;

0

เป็นความคิดที่ดีที่จะบันทึกข้อมูลจริงลงในตาราง " customer_data " ด้วยข้อมูลนี้คุณสามารถเลือกข้อมูลทั้งหมดจากตาราง "customer_data" ได้ตามที่คุณต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.