MySQL JOIN только к самой последней строке?


104

У меня есть таблица customer, в которой хранятся customer_id, адрес электронной почты и ссылка. Существует дополнительная таблица customer_data, в которой хранится историческая запись изменений, внесенных в клиента, то есть при внесении изменений вставляется новая строка.

Чтобы отобразить информацию о клиенте в таблице, необходимо объединить две таблицы, однако только самая последняя строка из customer_data должна быть присоединена к таблице клиентов.

Это немного усложняется тем, что запрос разбивается на страницы, поэтому имеет ограничение и смещение.

Как я могу это сделать с MySQL? Я думаю, что хочу где-нибудь поставить ОТЛИЧИТЕЛЬНЫЙ ...

В данный момент запрос такой:

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. Я собираюсь изучить это сейчас!)


Как выглядит таблица истории покупателя? Как определяется самая последняя строка? Есть ли поле с отметкой времени?
Даниэль Вассалло

Самая последняя - это просто последняя вставленная строка, поэтому ее первичный ключ - это наибольшее число.
bcmcfc 01

Почему не триггер? взгляните на этот ответ: stackoverflow.com/questions/26661314/…
Родриго Поло

Большинство / все ответы занимали слишком много времени с миллионами строк. Есть некоторые решения с более высокой производительностью.
Халил Озгюр

Ответы:


146

Вы можете попробовать следующее:

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 01

21
В долгосрочной перспективе этот подход может создать проблемы с производительностью, так как потребуется создать временную таблицу. Таким образом, другое решение (если возможно) - добавить новое логическое поле (is_last) в customer_data, которое вам придется обновлять каждый раз, когда добавляется новая запись. Последняя запись будет иметь is_last = 1, все остальные для этого клиента - is_last = 0.
cephuo

5
Люди должны (пожалуйста) также прочитать следующий ответ (от Дэнни Куломба), потому что этот ответ (извините, Даниэль) ужасно медленный с более длинными запросами / большим количеством данных. Моя страница "ждала" 12 секунд загрузки; Поэтому, пожалуйста, также проверьте stackoverflow.com/a/35965649/2776747 . Я заметил это только после множества других изменений, так что мне потребовалось очень много времени, чтобы узнать.
Art

Вы не представляете, насколько это мне помогло :) Спасибо, мастер
node_man 04

106

Если вы работаете с тяжелыми запросами, вам лучше переместить запрос последней строки в предложение 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
Ничего себе, я почти не верю, насколько это разница в производительности. Пока не уверен, почему это было так резко, но пока это было настолько быстрее, что мне кажется, что я напортачил где-то еще ...
Брайан Лейшман

2
Я действительно хотел бы добавить +1 больше одного раза, чтобы это было видно больше. Я довольно много это тестировал, и почему-то мои запросы выполняются практически мгновенно (WorkBench буквально говорит 0,000 секунд, даже с sql_no_cache set), тогда как выполнение поиска в объединении заняло несколько секунд. Все еще сбиты с толку, но я имею в виду, что с такими результатами не поспоришь.
Брайан Лейшман

1
Сначала вы напрямую объединяете 2 таблицы, а затем выполняете фильтрацию с помощью WHERE. Я думаю, что это серьезная проблема с производительностью, если у вас миллион клиентов и десятки миллионов истории звонков. Потому что SQL сначала попытается объединить 2 таблицы, а затем отфильтровать до одного клиента. Я бы предпочел сначала отфильтровать клиентов и связанные истории вызовов из таблиц в подзапросе, а затем присоединиться к таблицам.
Тарик

1
Я полагаю, что «ca.client_id» и «ca.cal_event_id» должны быть «c» для обоих.
Герберт Ван-Влит

1
Я согласен с @NickCoons. Значения NULL не будут возвращены, потому что они исключены предложением where. Как вы собираетесь включить значения NULL и при этом сохранить отличную производительность этого запроса?
aanders77 08

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 раза быстрее. Я использовал подзапрос, чтобы выбрать основную таблицу плюс самые последние идентификаторы объединенных таблиц, а затем внешний запрос, который выбирает подзапрос и считывает фактические данные из объединенных таблиц. Я объединяю 5 таблиц в основную таблицу и тестирую с условием where, которое выбирает 1000 записей. Индексы оптимальные.
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 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таблицу поле , содержащее 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модель, содержащая только геттеры:

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

иначе обновить структуру таблицы


Я проголосовал против, потому что неправильно прочитал ваш ответ и сначала подумал, что это неправильно. Спешка - плохой советчик :-)
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» по своему желанию.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.