【问题标题】:MySQL JOIN the most recent row only?MySQL 只加入最近的行?
【发布时间】:2011-04-06 20:24:21
【问题描述】:

我有一个表 customer,其中存储了 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 之间的区别是什么。我现在要研究一下!)

【问题讨论】:

  • 客户历史记录表是什么样子的?最近的行是如何确定的?有时间戳字段吗?
  • 最近的只是插入的最后一行 - 所以它的主键是最大的数字。
  • 为什么不用触发器?看看这个答案:stackoverflow.com/questions/26661314/…
  • 大多数/所有答案都花费了数百万行的时间。还有性能更好的somesolutions

标签: mysql sql join


【解决方案1】:

您可能想尝试以下方法:

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)

【讨论】:

  • 从长远来看,这种方法可能会产生性能问题,因为它需要创建一个临时表。因此,另一个解决方案(如果可能)是在 customer_data 中添加一个新的布尔字段 (is_last),每次添加新条目时都必须更新该字段。最后一个条目将具有 is_last=1,此客户的所有其他条目 - is_last=0。
  • 人们应该(请)也阅读以下答案(来自 Danny Coulombe),因为这个答案(对不起,丹尼尔)对于更长的查询/更多的数据非常慢。让我的页面“等待”12 秒才能加载;所以也请检查stackoverflow.com/a/35965649/2776747。直到做了很多其他更改后我才注意到它,所以我花了很长时间才发现。
【解决方案2】:

如果您正在处理繁重的查询,最好将请求移至 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
   )

【讨论】:

  • 哇,我几乎不敢相信这是多么大的性能差异。不知道为什么那会如此激烈,但到目前为止它是如此之快以至于感觉就像我在其他地方搞砸了......
  • 我真的希望我能不止一次 +1,这样它就会被更多人看到。我已经对此进行了相当多的测试,不知何故它使我的查询几乎是瞬时的(WorkBench 字面意思是 0.000 秒,即使使用sql_no_cache set),而在连接中进行搜索需要几秒钟才能完成。仍然感到困惑,但我的意思是你不能与这样的结果争论。
  • 您是直接加入 2 个表,然后使用 WHERE 进行过滤。我认为,如果您拥有一百万个客户和数千万条通话记录,那将是一个巨大的性能问题。因为 SQL 会先尝试加入 2 个表,然后再过滤到单个客户端。我宁愿先在子查询中从表中过滤客户端和相关调用历史记录,然后再加入表。
  • 我想“ca.client_id”和“ca.cal_event_id”都必须是“c”。
  • 我同意@NickCoons。不会返回 NULL 值,因为它们被 where 子句排除。您将如何在包含 NULL 值的同时保持该查询的出色性能?
【解决方案3】:

假设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

【讨论】:

    【解决方案4】:

    对于必须使用旧版本 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 会清除文件排序。
    • 遗憾的是,我自己的不那么漂亮的解决方案对我的数据来说是 3.5 倍。我使用了一个子查询来选择主表以及连接表的最新 ID,然后使用一个外部查询选择子查询并从连接表中读取实际数据。我将 5 个表加入到主表中,并使用选择 1000 条记录的 where 条件进行测试。索引是最优的。
    • 我将您的解决方案与SELECT *, MAX(firstData.id), MAX(secondData.id) [...] 一起使用。从逻辑上讲,通过更改为SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...],我能够显着加快速度。这允许第一个连接仅从索引中读取,而不必从主索引中读取所有数据。现在,漂亮的解决方案只需要基于子查询的解决方案的 1.9 倍。
    • 它在 MySQL 5.7 中不再起作用。现在 d2.* 将返回组中第一行的数据,而不是最后一行。 SELECT MAX(R1.id), R2.* FROM invoices I LEFT JOIN 响应 R1 ON I.id=R1.invoice_id LEFT JOIN 响应 R2 ON R1.id=R2.id GROUP BY I.id LIMIT 0,10
    【解决方案5】:

    我知道这个问题很老了,但多年来它受到了很多关注,我认为它缺少一个可以帮助类似情况的人的概念。为了完整起见,我在这里添加它。

    如果您无法修改原始数据库架构,那么已经提供了很多很好的答案并且可以很好地解决问题。

    但是,如果您可以修改您的架构,我建议在您的 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 的解决方案(上图)相结合,在没有任何子查询的情况下获得了我想要的结果。
    【解决方案6】:
    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

    否则更新表结构

    【讨论】:

      【解决方案7】:

      你也可以这样做

      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;
      

      【讨论】:

        【解决方案8】:

        将实际数据记录到“customer_data”表中是个好主意。使用此数据,您可以根据需要从“customer_data”表中选择所有数据。

        【讨论】:

        • 这个答案与问题无关。
        【解决方案9】:

        左加入最近/最近的 1 行的简单解决方案是使用 select over ON 短语

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

        其中 A.id 是自增主键。

        【讨论】:

          猜你喜欢
          • 2018-04-23
          • 1970-01-01
          • 1970-01-01
          • 2021-11-17
          • 1970-01-01
          • 2017-10-06
          • 1970-01-01
          • 2017-11-01
          • 1970-01-01
          相关资源
          最近更新 更多