MySQL THAM GIA chỉ vào hàng gần đây nhất?


103

Tôi có một khách hàng bảng lưu trữ customer_id, email và tài liệu tham khảo. Có thêm một bảng customer_data lưu trữ hồ sơ lịch sử về những thay đổi được thực hiện cho khách hàng, tức là khi có một thay đổi được thực hiện, một hàng mới sẽ được chèn vào.

Để hiển thị thông tin khách hàng trong một bảng, hai bảng cần được nối với nhau, tuy nhiên, chỉ hàng gần đây nhất từ ​​customer_data mới được kết hợp với bảng khách hàng.

Nó phức tạp hơn một chút ở chỗ truy vấn được phân trang, do đó có một giới hạn và một phần bù.

Làm cách nào để thực hiện việc này với MySQL? Tôi nghĩ tôi muốn đặt một DISTINCT ở đó ở đâu đó ...

Truy vấn vào lúc này là như thế này-

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

Ngoài ra, tôi có đúng khi nghĩ rằng tôi có thể sử dụng CONCAT với LIKE theo cách này không?

(Tôi đánh giá cao rằng INNER JOIN có thể là loại JOIN sai để sử dụng. Tôi thực sự không biết sự khác biệt giữa các JOIN khác nhau là gì. Tôi sẽ xem xét điều đó ngay bây giờ!)


Bảng lịch sử khách hàng như thế nào? Làm thế nào để xác định hàng gần đây nhất? Có trường dấu thời gian không?
Daniel Vassallo

Gần đây nhất chỉ đơn giản là hàng cuối cùng được chèn - vì vậy khóa chính của nó là số cao nhất.
bcmcfc

Tại sao không phải là một kích hoạt? hãy nhìn vào câu trả lời này: stackoverflow.com/questions/26661314/...
Rodrigo Polo

Hầu hết / tất cả các câu trả lời mất quá nhiều thời gian với hàng triệu hàng. Có một số giải pháp có hiệu suất tốt hơn.
Halil Özgür

Câu trả lời:


142

Bạn có thể muốn thử những cách sau:

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;

Lưu ý rằng a JOINchỉ là một từ đồng nghĩa với INNER JOIN.

Trường hợp thử nghiệm:

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

Kết quả (truy vấn không có LIMITWHERE):

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)

2
Cảm ơn mức độ chi tiết mà bạn đã xem xét ở đó. Tôi hy vọng nó sẽ giúp những người khác cũng như tôi!
bcmcfc

21
Về lâu dài, cách tiếp cận này có thể tạo ra các vấn đề về hiệu suất vì nó cần phải tạo một bảng tạm thời. Vì vậy, một giải pháp khác (nếu có thể) là thêm một trường boolean mới (is_last) trong customer_data mà bạn sẽ phải cập nhật mỗi khi một mục nhập mới được thêm vào. Mục nhập cuối cùng sẽ có is_last = 1, tất cả các mục khác cho khách hàng này - is_last = 0.
cephuo

5
Mọi người cũng nên (vui lòng) đọc câu trả lời sau (từ Danny Coulombe), vì câu trả lời này (xin lỗi Daniel) rất chậm với các truy vấn dài hơn / nhiều dữ liệu hơn. Làm cho trang của tôi "chờ" trong 12 giây để tải; Vì vậy, vui lòng kiểm tra stackoverflow.com/a/35965649/2776747 . Tôi đã không nhận thấy nó cho đến khi có rất nhiều thay đổi khác nên tôi đã mất rất nhiều thời gian để tìm ra.
nghệ

Bạn không có ý tưởng này đã giúp tôi bao nhiêu :) Cảm ơn bạn chủ
node_man

103

Nếu bạn đang làm việc với các truy vấn nặng, bạn nên chuyển yêu cầu cho hàng mới nhất trong mệnh đề where. Nó nhanh hơn rất nhiều và trông sạch sẽ hơn.

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
Chà, tôi gần như không tin vào mức độ chênh lệch hiệu suất này. Bạn không chắc chắn lý do tại sao mà rất quyết liệt, nhưng cho đến nay nó đã quá nhanh nó nhiều đến nỗi nó cảm thấy như tôi sai lầm ở một nơi khác ...
Brian Leishman

2
Tôi thực sự ước mình có thể +1 mục này nhiều hơn một lần để nó được xem nhiều hơn. Tôi đã thử nghiệm điều này khá nhiều và bằng cách nào đó, nó làm cho các truy vấn của tôi gần như ngay lập tức (WorkBench nói theo nghĩa đen là 0,000 giây, ngay cả với sql_no_cache set), trong khi thực hiện tìm kiếm trong liên kết mất nhiều giây để hoàn thành. Vẫn còn bối rối, nhưng tôi có nghĩa là bạn không thể tranh cãi với kết quả như vậy.
Brian Leishman

1
Bạn đang tham gia trực tiếp 2 bảng trước và sau đó lọc với WHERE. Tôi nghĩ rằng đó là một vấn đề lớn về hiệu suất nếu bạn có một triệu khách hàng và hàng chục triệu lịch sử cuộc gọi. Vì SQL sẽ cố gắng nối 2 bảng trước rồi lọc xuống máy khách duy nhất. Tôi muốn lọc các máy khách và lịch sử gọi liên quan từ các bảng trước trong một truy vấn phụ và sau đó nối các bảng.
Tarik

1
Tôi cho rằng "ca.client_id" và "ca.cal_event_id" phải là "c" cho cả hai.
Herbert Van-Vliet

1
Tôi đồng ý với @NickCoons. Giá trị NULL sẽ không được trả về vì chúng bị loại trừ bởi mệnh đề where. Làm thế nào bạn sẽ bao gồm các giá trị NULL và vẫn giữ hiệu suất tuyệt vời của truy vấn này?
aanders77

10

Giả sử cột autoincrement trong customer_datađược đặt tên Id, bạn có thể làm:

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

Đối với bất kỳ ai phải làm việc với phiên bản MySQL cũ hơn (trước 5.0 ish), bạn không thể thực hiện các truy vấn phụ cho loại truy vấn này. Đây là giải pháp tôi đã có thể làm và nó có vẻ hoạt động tuyệt vời.

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;

Về cơ bản, đây là việc tìm id tối đa của bảng dữ liệu của bạn để kết hợp nó với khách hàng, sau đó kết hợp bảng dữ liệu với id tối đa được tìm thấy. Lý do cho điều này là vì việc chọn giá trị tối đa của một nhóm không đảm bảo rằng phần còn lại của dữ liệu khớp với id trừ khi bạn kết hợp nó trở lại chính nó.

Tôi chưa thử nghiệm điều này trên các phiên bản MySQL mới hơn nhưng nó hoạt động trên 4.0.30.


Điều này là tinh tế trong sự đơn giản của nó. Tại sao đây là lần đầu tiên tôi thấy cách tiếp cận này? Lưu ý rằng EXPLAINđiều này sử dụng bảng tạm thời và sắp xếp tệp. Thêm ORDER BY NULLvào cuối sẽ loại bỏ tập tin.
Timo

Tôi rất tiếc, giải pháp không đẹp đẽ của riêng tôi nhanh gấp 3,5 lần cho dữ liệu của tôi. Tôi đã sử dụng một truy vấn phụ để chọn bảng chính cộng với các ID gần đây nhất của các bảng đã kết hợp, sau đó truy vấn bên ngoài chọn truy vấn con và đọc dữ liệu thực tế từ các bảng đã kết hợp. Tôi đang nối 5 bảng vào bảng chính và thử nghiệm với điều kiện nơi chọn 1000 bản ghi. Chỉ số là tối ưu.
Timo

Tôi đã sử dụng giải pháp của bạn với SELECT *, MAX(firstData.id), MAX(secondData.id) [...]. Về mặt logic, bằng cách thay đổi thành SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]tôi đã có thể làm cho nó nhanh hơn đáng kể. Điều này cho phép các phép nối đầu tiên chỉ đọc từ chỉ mục, thay vì phải đọc tất cả dữ liệu từ chỉ mục chính. Giờ đây, giải pháp đẹp chỉ mất 1,9 lần so với giải pháp dựa trên truy vấn con.
Timo

Nó không hoạt động nữa trong MySQL 5.7. Bây giờ d2. * Sẽ trả về dữ liệu cho hàng đầu tiên trong nhóm, không phải hàng cuối cùng. CHỌN MAX (R1.id), R2. * TỪ các hóa đơn I LEFT JOIN phản hồi R1 ON I.id = R1.invoice_id LEFT JOIN phản hồi R2 ON R1.id = R2.id GROUP BY I.id LIMIT 0,10
Marco Marsala

5

Tôi biết câu hỏi này đã cũ, nhưng nó đã được chú ý rất nhiều trong những năm qua và tôi nghĩ rằng nó đang thiếu một khái niệm có thể giúp ích cho ai đó trong trường hợp tương tự. Tôi thêm nó ở đây vì lợi ích hoàn chỉnh.

Nếu bạn không thể sửa đổi lược đồ cơ sở dữ liệu ban đầu của mình, thì rất nhiều câu trả lời hay đã được cung cấp và giải quyết vấn đề tốt.

Tuy nhiên, nếu bạn có thể sửa đổi lược đồ của mình, tôi khuyên bạn nên thêm một trường trong customerbảng của bạn để lưu giữ idbản customer_dataghi mới nhất cho khách hàng này:

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

Truy vấn khách hàng

Truy vấn dễ dàng và nhanh chóng nhất có thể:

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

Hạn chế là sự phức tạp thêm khi tạo hoặc cập nhật khách hàng.

Cập nhật khách hàng

Bất cứ khi nào bạn muốn cập nhật khách hàng, bạn chèn một bản ghi mới vào customer_databảng và cập nhật customerbản ghi.

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;

Tạo khách hàng

Việc tạo khách hàng chỉ là việc chèn customermục nhập, sau đó chạy các câu lệnh tương tự:

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;

Kết thúc

Sự phức tạp thêm cho việc tạo / cập nhật khách hàng có thể đáng sợ, nhưng nó có thể dễ dàng được tự động hóa với các trình kích hoạt.

Cuối cùng, nếu bạn đang sử dụng ORM, điều này có thể thực sự dễ quản lý. ORM có thể đảm nhận việc chèn các giá trị, cập nhật id và tự động nối hai bảng cho bạn.

Đây là cách Customermô hình có thể thay đổi của bạn trông như thế nào:

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

CustomerDatamô hình bất biến của bạn , chỉ chứa các 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;
    }
}

Tôi đã kết hợp phương pháp này với giải pháp của @ payne8 (ở trên) để có được kết quả mong muốn mà không cần bất kỳ truy vấn phụ nào.
Gừng và Oải hương

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%' 

tôi nghĩ bạn cần thay đổi c.customer_id thành c.id

khác cập nhật cấu trúc bảng


Tôi đã phản đối vì tôi đã đọc sai câu trả lời của bạn và ban đầu tôi nghĩ nó sai. Haste là một cố vấn tồi :-)
Wirone

1

Bạn cũng có thể làm điều này

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

Bạn nên ghi dữ liệu thực tế vào bảng " customer_data ". Với dữ liệu này, bạn có thể chọn tất cả dữ liệu từ bảng "customer_data" như bạn muốn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.