MySQL JOIN仅返回最新的一行?

152

我有一个名为customer的表格,其中存储了customer_id、email和reference。另外还有一个名为customer_data的表格,它存储客户变更的历史记录,即当有变更时,新行会被插入。

为了在表格中显示客户信息,需要将这两个表格连接起来,但只需将customer_data的最近一行与customer表格连接。

这个查询有一定的复杂性,因为它是分页查询,所以具有限制和偏移量。

我该如何在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 之间有什么区别。我现在要去研究一下!)


客户历史记录表长什么样?如何确定最近的行?是否有时间戳字段? - Daniel Vassallo
最近的记录就是最后插入的一行 - 因此它的主键是最高的数字。 - bcmcfc
为什么不使用触发器?看看这个答案: http://stackoverflow.com/questions/26661314/best-and-optimal-way-to-join-max-value-from-other-table/26664982#26664982 - Rodrigo Polo
大多数/全部的答案都需要处理数百万行数据,耗时较长。有一些解决方案 [(https://dev.mysql.com/doc/refman/5.5/en/example-maximum-column-group-row.html)]可以提高性能。 - Halil Özgür
这个回答解决了你的问题吗?获取某一列最大值所在的行 - philipxy
10个回答

201
如果您在处理大量查询时,最好将请求最新行的条件移至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
   )

6
哇,我几乎不敢相信这有多大的性能差异。目前还不确定为什么会有这么大的变化,但到目前为止运行速度快了很多,感觉好像在其他地方弄错了什么... - Brian Leishman
3
我真的希望我能够点赞超过一次,这样它就可以得到更多关注。经过我的测试,不知何故,使用这种方法可以使我的查询几乎瞬间完成(即使使用 sql_no_cache set 命令,WorkBench 也显示为0.000秒),而在联接中进行搜索则需要多秒才能完成。仍然感到困惑,但是这种结果是无可争议的。 - Brian Leishman
2
你现在是直接将两个表连接起来,然后再用WHERE进行筛选。如果你有一百万个客户和数千万条通话记录,我认为这会是一个巨大的性能问题。因为SQL会先尝试将两个表连接起来,然后再过滤出单个客户。我宁愿先从子查询中过滤出客户和相关的通话历史记录,然后再连接这些表。 - Tarik
3
我同意@NickCoons的观点。NULL值不会被返回,因为它们被where子句排除了。你如何在保持查询性能优异的同时包含NULL值? - aanders77
6
我认为您可以这样获取NULL记录:<code> SELECT c.*, FROM client AS c LEFT JOIN client_calling_history AS cch ON cch.client_id = c.client_id WHERE ( SELECT MAX(cchid) FROM client_calling_history WHERE client_id = c.client_id AND cal_event_id = c.cal_event_id ) IS NULL OR cch.cchid = ( SELECT MAX(cchid) FROM client_calling_history WHERE client_id = c.client_id AND cal_event_id = c.cal_event_id ) </code> - Kevin
显示剩余8条评论

193

您可以尝试以下方法:

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;

请注意,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');

查询结果(不包含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)

27
从长远来看,这种方法可能会创建性能问题,因为它需要创建一个临时表。因此,另一种解决方案(如果可能的话)是在customer_data中添加一个新的布尔字段(is_last),每当添加新条目时都需要更新该字段。最后一个条目将具有is_last = 1,该客户的所有其他条目均为is_last = 0。 - cephuo
12
请大家也阅读以下答案(来自Danny Coulombe),因为这个答案(抱歉Daniel)在处理更长的查询/更多数据时非常缓慢。我的页面等待了12秒才加载;所以请您也查看 https://dev59.com/4XA65IYBdhLWcg3wzyH8#35965649。我之前并没有注意到它,直到做了很多其他更改后才发现。 - Carrot
这个解决方案虽然可爱,但是非常慢。执行计划显示,连接的“Group by”会创建临时表并进行全表扫描。我曾经遇到过生产问题,使用这种方法查询25个ID的数据会占用15GB RDS服务器内存,并完全卡住应用程序。 - Jeka Developer

12

假设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之前),则无法对此类查询进行子查询。以下是我找到的解决方案,它似乎非常有效。

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;

本质上,这是查找数据表的最大ID,将其连接到客户端,然后将数据表连接到找到的最大ID。之所以这样做,是因为选择组的最大值并不能保证其余数据与ID匹配,除非您将其重新连接到自身。

我尚未在MySQL的新版本上测试过此方法,但它适用于4.0.30版本。


这种方法的简单之处令人惊叹。为什么我第一次看到这样的方法?请注意EXPLAIN说明使用了临时表和文件排序。在结尾加上 ORDER BY NULL 可以清除文件排序。 - Timo
令我遗憾的是,我的解决方案并不完美,但对于我的数据来说速度快了3.5倍。我使用了子查询来选择主表和连接表中最近的ID,然后使用外部查询来选择子查询并从连接表中读取实际数据。我将5个表连接到主表,并使用where条件选择1000条记录进行测试。索引是最优的。 - Timo
我使用了你的解决方案,其中包括 SELECT *, MAX(firstData.id), MAX(secondData.id) [...]。逻辑上,通过改为 SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...],我能够使它显著加快。这使得第一个连接只需从索引中读取,而不必从主索引中读取所有数据。现在,漂亮的解决方案只需要比基于子查询的解决方案多花费1.9倍的时间。 - Timo
2
在MySQL 5.7中,它不再起作用。现在d2.*将返回组中第一行的数据,而不是最后一行。SELECT MAX(R1.id), R2.* FROM invoices I LEFT JOIN responses R1 ON I.id=R1.invoice_id LEFT JOIN responses R2 ON R1.id=R2.id GROUP BY I.id LIMIT 0,10 - Marco Marsala

7
我知道这个问题已经很久了,但是它在这些年里得到了很多关注,我认为它缺少一个概念,这个概念可能会帮助到类似情况的人们。出于完整性考虑,在此处添加它。
如果您无法修改原始数据库架构,则已提供很多好的答案,并且可以很好地解决问题。
然而,如果您可以修改架构,我建议在您的“customer”表中添加一个字段,用于保存此客户的最新“customer_data”记录的“id”。
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可以负责插入值、更新ID并自动为您连接两个表。

以下是可变的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模型,该模型仅包含getter:

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。
否则,更新表结构。

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;

1

这个问题现在已经比较老了,但它仍然似乎受到关注。最近的MySQL版本(8.0及以上)使用窗口函数有更好的替代方案:

SELECT *
FROM 
(
    SELECT *, CONCAT(title,' ',forename,' ',surname) AS name,
        row_number() over (PARTITION BY d.customer_id ORDER BY d.id) rn
    FROM customer c
    INNER JOIN customer_data d on c.customer_id=d.customer_id
    WHERE name LIKE '%Smith%' 
)
WHERE rn = 1
LIMIT 10, 20

我还为这个问题添加了mysql-5.7标签,因为通过谷歌找到这个问题的人不太可能读过前几篇高票答案以发现这个更新/更好的技术。该标签使得它更清晰,这些回答适用于早期版本的MySQL。 - Joel Coehoorn
也适用于 MariaDB 10.1 及以上版本。 - Moshe L

0

将实际数据记录到“customer_data”表中是一个好主意。有了这些数据,您可以根据需要选择“customer_data”表中的所有数据。


这个答案与问题无关。 - Muhammad Zakaria

-1

左连接最近的第一行或最后一行的简单解决方案是使用ON短语上的select语句

SELECT *
FROM A
LEFT JOIN B
ON A.id = (SELECT MAX(id) FROM B WHERE id = A.id)

其中 A.id 是自增的主键。


MAX(id)?为什么要将表B的最大主键ID与表A的主键ID连接起来?在您的连接查询中,外键没有任何用处。 - Anuj Shrestha
所请求的问题正在寻找“仅最近一行”,并考虑id作为主键,使用MAX(id)获取表B具有两个优点:一是它仅返回一行,二是它返回最近的一行!这是我在大多数情况下使用的方法,效果非常好。 - Saghachi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接