MySQL DOŁĄCZYĆ tylko do ostatniego wiersza?

103

Mam klienta tabeli, który przechowuje identyfikator_klienta, adres e-mail i dane referencyjne. Istnieje dodatkowa tabela customer_data, która przechowuje historyczny zapis zmian dokonanych w kliencie, tj. Kiedy następuje zmiana, wstawiany jest nowy wiersz.

Aby wyświetlić informacje o kliencie w tabeli, dwie tabele muszą zostać połączone, jednak tylko najnowszy wiersz z customer_data powinien zostać dołączony do tabeli customer.

To staje się trochę bardziej skomplikowane, ponieważ zapytanie jest podzielone na strony, więc ma limit i przesunięcie.

Jak mogę to zrobić z MySQL? Myślę, że chcę gdzieś umieścić WYRÓŻNIENIE ...

Pytanie w tej chwili wygląda następująco:

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

Dodatkowo, czy mam rację sądząc, że mogę w ten sposób używać funkcji CONCAT z LIKE?

(Rozumiem, że INNER JOIN może być niewłaściwym typem JOIN do użycia. Właściwie nie mam pojęcia, jaka jest różnica między różnymi JOINami. Zamierzam się temu teraz przyjrzeć!)

bcmcfc
źródło
Jak wygląda tabela historii klientów? Jak jest określany ostatni wiersz? Czy istnieje pole sygnatury czasowej?
Daniel Vassallo
Najnowszy to po prostu ostatni wstawiony wiersz - więc jego klucz podstawowy to najwyższa liczba.
bcmcfc
Dlaczego nie wyzwalacz? spójrz na tę odpowiedź: stackoverflow.com/questions/26661314/…
Rodrigo Polo
Większość / wszystkie odpowiedzi trwały zbyt długo przy milionach wierszy. Istnieje kilka rozwiązań o lepszej wydajności.
Halil Özgür

Odpowiedzi:

146

Możesz spróbować następujących rzeczy:

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;

Zwróć uwagę, że a JOINjest tylko synonimem INNER JOIN.

Przypadek testowy:

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

Wynik (zapytanie bez LIMITi 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)
Daniel Vassallo
źródło
3
Dziękuję za poziom szczegółowości, który tam wprowadziłeś. Mam nadzieję, że pomoże to nie tylko mnie, ale i innym!
bcmcfc
21
W dłuższej perspektywie takie podejście może powodować problemy z wydajnością, ponieważ wymagałoby utworzenia tabeli tymczasowej. Więc innym rozwiązaniem (jeśli to możliwe) jest dodanie nowego pola boolowskiego (is_last) w customer_data, które musiałbyś aktualizować za każdym razem, gdy dodawany jest nowy wpis. Ostatni wpis będzie miał is_last = 1, wszystkie inne dla tego klienta - is_last = 0.
cephuo
5
Ludzie powinni (proszę) przeczytać również następującą odpowiedź (od Danny'ego Coulombe), ponieważ ta odpowiedź (przepraszam Danielu) jest strasznie powolna z dłuższymi zapytaniami / większą ilością danych. Moja strona „czekała” na załadowanie przez 12 sekund; Dlatego sprawdź również stackoverflow.com/a/35965649/2776747 . Zauważyłem to dopiero po wielu innych zmianach, więc zajęło mi to bardzo dużo czasu, zanim się dowiedziałem.
Art.
Nie masz pojęcia, jak bardzo mi to pomogło :) Dziękuję mistrzu
node_man
105

Jeśli pracujesz z ciężkimi zapytaniami, lepiej przenieś żądanie dla najnowszego wiersza w klauzuli where. Jest dużo szybszy i wygląda czysto.

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
   )
Danny Coulombe
źródło
4
Wow, prawie nie wierzę, jak duża jest to różnica w wydajności. Nie jestem pewien, dlaczego to było jeszcze tak drastyczne, ale do tej pory było o wiele szybciej, że wydaje mi się, że zawaliłem gdzie indziej ...
Brian Leishman
2
Naprawdę chciałbym móc dać +1 więcej niż raz, żeby było to częściej widoczne. Testowałem to całkiem sporo i w jakiś sposób sprawia, że ​​moje zapytania są praktycznie natychmiastowe (WorkBench dosłownie mówi 0.000 sekund, nawet z sql_no_cache set), podczas gdy wyszukiwanie w złączeniu trwało kilka sekund. Wciąż zdumiony, ale mam na myśli, że nie można dyskutować z takimi wynikami.
Brian Leishman
1
Najpierw bezpośrednio dołączasz do 2 tabel, a następnie filtrujesz za pomocą WHERE. Myślę, że jest to ogromny problem z wydajnością, jeśli masz milion klientów i dziesiątki milionów historii połączeń. Ponieważ SQL spróbuje najpierw połączyć 2 tabele, a następnie przefiltrować do pojedynczego klienta. Wolałbym raczej odfiltrować klientów i powiązane historie połączeń z tabel najpierw w zapytaniu podrzędnym, a następnie dołączyć do tabel.
Tarik
1
Przypuszczam, że „ca.client_id” i „ca.cal_event_id” muszą mieć wartość „c” dla obu.
Herbert Van-Vliet
1
Zgadzam się z @NickCoons. Wartości NULL nie zostaną zwrócone, ponieważ są wykluczone przez klauzulę where. W jaki sposób możesz dołączyć wartości NULL i zachować doskonałą wydajność tego zapytania?
aanders77
10

Zakładając, że kolumna z autoinkrementacją customer_datama nazwę Id, możesz wykonać:

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
Tomasz
źródło
9

Dla każdego, kto musi pracować ze starszą wersją MySQL (starsza niż 5.0 ish), nie możesz wykonywać pod-zapytań dla tego typu zapytań. Oto rozwiązanie, które udało mi się zrobić i wydawało się, że działa świetnie.

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;

Zasadniczo jest to znalezienie maksymalnego identyfikatora tabeli danych, dołączenie jej do klienta, a następnie połączenie tabeli danych z maksymalnym znalezionym identyfikatorem. Powodem tego jest to, że wybranie maksimum grupy nie gwarantuje, że reszta danych będzie zgodna z id, chyba że połączysz ją z powrotem.

Nie testowałem tego na nowszych wersjach MySQL, ale działa na 4.0.30.

payne8
źródło
To jest wyjątkowe w swojej prostocie. Dlaczego jest to pierwszy raz, kiedy widziałem takie podejście? Zauważ, że EXPLAINoznacza to, że używa tymczasowej tabeli i sortowania plików. Dodanie ORDER BY NULLna końcu usuwa sortowanie plików.
Timo
Żałuję, że moje własne, nie tak piękne rozwiązanie jest 3,5 razy szybsze dla moich danych. Użyłem podzapytania do wybrania głównej tabeli oraz najnowszych identyfikatorów połączonych tabel, a następnie zapytania zewnętrznego, które wybiera podzapytanie i odczytuje rzeczywiste dane z połączonych tabel. Dołączam 5 tabel do głównej tabeli i testuję z warunkiem Where, który wybiera 1000 rekordów. Indeksy są optymalne.
Timo
Używałem twojego rozwiązania z SELECT *, MAX(firstData.id), MAX(secondData.id) [...]. Logicznie rzecz biorąc, zmiana na SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]I była w stanie zrobić to znacznie szybciej. Dzięki temu pierwsze sprzężenia mogą odczytywać tylko z indeksu, zamiast odczytywać wszystkie dane z indeksu podstawowego. Teraz ładne rozwiązanie zajmuje tylko 1,9 razy dłużej niż rozwiązanie oparte na podzapytaniach.
Timo
To już nie działa w MySQL 5.7. Teraz d2. * Zwróci dane dla pierwszego wiersza w grupie, a nie ostatniego. SELECT MAX (R1.id), R2. * Z faktur 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
5

Wiem, że to pytanie jest stare, ale przez lata poświęcano mu dużo uwagi i myślę, że brakuje w nim koncepcji, która może pomóc komuś w podobnej sprawie. Dodam to tutaj dla kompletności.

Jeśli nie możesz zmodyfikować oryginalnego schematu bazy danych, oznacza to, że podano wiele dobrych odpowiedzi i rozwiązano problem.

Jeśli jednak możesz zmodyfikować swój schemat, radziłbym dodać do customertabeli pole idzawierające najnowszy customer_datarekord dla tego klienta:

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

Zapytania klientów

Zapytania są tak proste i szybkie, jak to tylko możliwe:

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

Wadą jest dodatkowa złożoność podczas tworzenia lub aktualizowania klienta.

Aktualizacja klienta

Jeśli chcesz zaktualizować klienta, wstaw nowy rekord do customer_datatabeli i zaktualizuj customerrekord.

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;

Tworzenie klienta

Utworzenie klienta to tylko kwestia wstawienia customerwpisu, a następnie uruchomienia tych samych instrukcji:

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;

Podsumowując

Dodatkowa złożoność tworzenia / aktualizowania klienta może być przerażająca, ale można ją łatwo zautomatyzować za pomocą wyzwalaczy.

Wreszcie, jeśli używasz ORM, może to być naprawdę łatwe w zarządzaniu. ORM może zająć się wstawieniem wartości, aktualizacją identyfikatorów i automatycznym połączeniem dwóch tabel.

Oto jak wyglądałby Twój zmienny Customermodel:

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

I Twój niezmienny CustomerDatamodel, który zawiera tylko metody pobierające:

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;
    }
}
Benzoes
źródło
Połączyłem to podejście z rozwiązaniem @ payne8 (powyżej), aby uzyskać pożądany wynik bez żadnych podzapytań.
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%' 

myślę, że musisz zmienić c.customer_id na c.id

w przeciwnym razie zaktualizuj strukturę tabeli

Pramendra Gupta
źródło
Głosowałem negatywnie, ponieważ źle odczytałem twoją odpowiedź i początkowo pomyślałem, że jest błędna. Pośpiech to zły doradca :-)
Wirone
1

Ty też możesz to zrobić

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;
Ajay Kumar
źródło
0

Dobrze jest logować rzeczywiste dane do tabeli „ customer_data ”. Dzięki tym danym możesz dowolnie wybierać wszystkie dane z tabeli „customer_data”.

Burçin
źródło