Gogolowałem, samokształciłem się i szukałem rozwiązania przez wiele godzin, ale bez powodzenia. Znalazłem tutaj kilka podobnych pytań, ale nie tę sprawę.
Moje stoły:
- osoby (~ 10 mln rzędów)
- atrybuty (lokalizacja, wiek, ...)
- linki (M: M) między osobami i atrybutami (~ 40 mln wierszy)
Sytuacja:
staram się wybrać wszystkie identyfikatory osób ( person_id
) z niektórych lokalizacji ( location.attribute_value BETWEEN 3000 AND 7000
), będąc płcią ( gender.attribute_value = 1
), urodzonymi za kilka lat ( bornyear.attribute_value BETWEEN 1980 AND 2000
) i mającymi kolor oczu ( eyecolor.attribute_value IN (2,3)
).
To jest moje zapytanie, które tooks 3 ~ 4 min. i chciałbym zoptymalizować:
SELECT person_id
FROM person
LEFT JOIN attribute location ON location.attribute_type_id = 1 AND location.person_id = person.person_id
LEFT JOIN attribute gender ON gender.attribute_type_id = 2 AND gender.person_id = person.person_id
LEFT JOIN attribute bornyear ON bornyear.attribute_type_id = 3 AND bornyear.person_id = person.person_id
LEFT JOIN attribute eyecolor ON eyecolor.attribute_type_id = 4 AND eyecolor.person_id = person.person_id
WHERE 1
AND location.attribute_value BETWEEN 3000 AND 7000
AND gender.attribute_value = 1
AND bornyear.attribute_value BETWEEN 1980 AND 2000
AND eyecolor.attribute_value IN (2,3)
LIMIT 100000;
Wynik:
+-----------+
| person_id |
+-----------+
| 233 |
| 605 |
| ... |
| 8702599 |
| 8703617 |
+-----------+
100000 rows in set (3 min 42.77 sec)
Wyjaśnij przedłużenie:
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| 1 | SIMPLE | bornyear | range | attribute_type_id,attribute_value,person_id | attribute_value | 5 | NULL | 1265229 | 100.00 | Using where |
| 1 | SIMPLE | location | ref | attribute_type_id,attribute_value,person_id | person_id | 5 | test1.bornyear.person_id | 4 | 100.00 | Using where |
| 1 | SIMPLE | eyecolor | ref | attribute_type_id,attribute_value,person_id | person_id | 5 | test1.bornyear.person_id | 4 | 100.00 | Using where |
| 1 | SIMPLE | gender | ref | attribute_type_id,attribute_value,person_id | person_id | 5 | test1.eyecolor.person_id | 4 | 100.00 | Using where |
| 1 | SIMPLE | person | eq_ref | PRIMARY | PRIMARY | 4 | test1.location.person_id | 1 | 100.00 | Using where; Using index |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
5 rows in set, 1 warning (0.02 sec)
Profilowy:
+------------------------------+-----------+
| Status | Duration |
+------------------------------+-----------+
| Sending data | 3.069452 |
| Waiting for query cache lock | 0.000017 |
| Sending data | 2.968915 |
| Waiting for query cache lock | 0.000019 |
| Sending data | 3.042468 |
| Waiting for query cache lock | 0.000043 |
| Sending data | 3.264984 |
| Waiting for query cache lock | 0.000017 |
| Sending data | 2.823919 |
| Waiting for query cache lock | 0.000038 |
| Sending data | 2.863903 |
| Waiting for query cache lock | 0.000014 |
| Sending data | 2.971079 |
| Waiting for query cache lock | 0.000020 |
| Sending data | 3.053197 |
| Waiting for query cache lock | 0.000087 |
| Sending data | 3.099053 |
| Waiting for query cache lock | 0.000035 |
| Sending data | 3.064186 |
| Waiting for query cache lock | 0.000017 |
| Sending data | 2.939404 |
| Waiting for query cache lock | 0.000018 |
| Sending data | 3.440288 |
| Waiting for query cache lock | 0.000086 |
| Sending data | 3.115798 |
| Waiting for query cache lock | 0.000068 |
| Sending data | 3.075427 |
| Waiting for query cache lock | 0.000072 |
| Sending data | 3.658319 |
| Waiting for query cache lock | 0.000061 |
| Sending data | 3.335427 |
| Waiting for query cache lock | 0.000049 |
| Sending data | 3.319430 |
| Waiting for query cache lock | 0.000061 |
| Sending data | 3.496563 |
| Waiting for query cache lock | 0.000029 |
| Sending data | 3.017041 |
| Waiting for query cache lock | 0.000032 |
| Sending data | 3.132841 |
| Waiting for query cache lock | 0.000050 |
| Sending data | 2.901310 |
| Waiting for query cache lock | 0.000016 |
| Sending data | 3.107269 |
| Waiting for query cache lock | 0.000062 |
| Sending data | 2.937373 |
| Waiting for query cache lock | 0.000016 |
| Sending data | 3.097082 |
| Waiting for query cache lock | 0.000261 |
| Sending data | 3.026108 |
| Waiting for query cache lock | 0.000026 |
| Sending data | 3.089760 |
| Waiting for query cache lock | 0.000041 |
| Sending data | 3.012763 |
| Waiting for query cache lock | 0.000021 |
| Sending data | 3.069694 |
| Waiting for query cache lock | 0.000046 |
| Sending data | 3.591908 |
| Waiting for query cache lock | 0.000060 |
| Sending data | 3.526693 |
| Waiting for query cache lock | 0.000076 |
| Sending data | 3.772659 |
| Waiting for query cache lock | 0.000069 |
| Sending data | 3.346089 |
| Waiting for query cache lock | 0.000245 |
| Sending data | 3.300460 |
| Waiting for query cache lock | 0.000019 |
| Sending data | 3.135361 |
| Waiting for query cache lock | 0.000021 |
| Sending data | 2.909447 |
| Waiting for query cache lock | 0.000039 |
| Sending data | 3.337561 |
| Waiting for query cache lock | 0.000140 |
| Sending data | 3.138180 |
| Waiting for query cache lock | 0.000090 |
| Sending data | 3.060687 |
| Waiting for query cache lock | 0.000085 |
| Sending data | 2.938677 |
| Waiting for query cache lock | 0.000041 |
| Sending data | 2.977974 |
| Waiting for query cache lock | 0.000872 |
| Sending data | 2.918640 |
| Waiting for query cache lock | 0.000036 |
| Sending data | 2.975842 |
| Waiting for query cache lock | 0.000051 |
| Sending data | 2.918988 |
| Waiting for query cache lock | 0.000021 |
| Sending data | 2.943810 |
| Waiting for query cache lock | 0.000061 |
| Sending data | 3.330211 |
| Waiting for query cache lock | 0.000025 |
| Sending data | 3.411236 |
| Waiting for query cache lock | 0.000023 |
| Sending data | 23.339035 |
| end | 0.000807 |
| query end | 0.000023 |
| closing tables | 0.000325 |
| freeing items | 0.001217 |
| logging slow query | 0.000007 |
| logging slow query | 0.000011 |
| cleaning up | 0.000104 |
+------------------------------+-----------+
100 rows in set (0.00 sec)
Struktury tabel:
CREATE TABLE `attribute` (
`attribute_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`attribute_type_id` int(11) unsigned DEFAULT NULL,
`attribute_value` int(6) DEFAULT NULL,
`person_id` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`attribute_id`),
KEY `attribute_type_id` (`attribute_type_id`),
KEY `attribute_value` (`attribute_value`),
KEY `person_id` (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=40000001 DEFAULT CHARSET=utf8;
CREATE TABLE `person` (
`person_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`person_name` text CHARACTER SET latin1,
PRIMARY KEY (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=20000001 DEFAULT CHARSET=utf8;
Zapytanie zostało wykonane na serwerze wirtualnym DigitalOcean z dyskiem SSD i 1 GB pamięci RAM.
Zakładam, że może występować problem z projektowaniem bazy danych. Czy masz jakieś sugestie, aby lepiej zaprojektować tę sytuację? A może po prostu dostosować powyższy wybór?
źródło
attribute (person_id, attribute_type_id, attribute_value)
(attribute_type_id, attribute_value, person_id)
i(attribute_type_id, person_id, attribute_value)
Odpowiedzi:
Wybierz kilka atrybutów, które chcesz uwzględnić
person
. Indeksuj je w kilku kombinacjach - używaj indeksów złożonych, a nie indeksów jednokolumnowych.To jest w zasadzie jedyne wyjście z EAV-sucks-at-performance, czyli tam, gdzie jesteś.
Oto więcej dyskusji: http://mysql.rjweb.org/doc.php/eav, w tym sugestia użycia JSON zamiast tabeli klucz-wartość.
źródło
Dodaj indeksy do
attribute
:(person_id, attribute_type_id, attribute_value)
i(attribute_type_id, attribute_value, person_id)
Wyjaśnienie
Przy obecnym projekcie
EXPLAIN
oczekuje, że zapytanie obejrzy1,265,229 * 4 * 4 * 4 = 80,974,656
wierszeattribute
. Można zmniejszyć tę liczbę, dodając Composite Index naattribute
za(person_id, attribute_type_id)
. Korzystanie z tego indeksu zapytanie zbada tylko 1 zamiast 4 wiersze dla każdegolocation
,eyecolor
igender
.Można przedłużyć ten indeks obejmuje
attribute_type_value
również:(person_id, attribute_type_id, attribute_value)
. Spowodowałoby to przekształcenie tego indeksu w indeks pokrywający dla tego zapytania, co również powinno poprawić wydajność.Ponadto dodanie indeksu do
(attribute_type_id, attribute_value, person_id)
(ponownie indeksu obejmującego przez włączenieperson_id
) powinno poprawić wydajność w porównaniu z samym użyciem indeksu, wattribute_value
którym należy zbadać więcej wierszy. W takim przypadku nastąpi przyspieszenie pierwszego kroku twojego wyjaśnienia: wybranie zakresu odbornyear
.Korzystanie z tych dwóch indeków skróciło czas wykonywania zapytania w moim systemie z ~ 2,0 s do ~ 0,2 s, a wynik wyjaśniania wygląda następująco:
źródło
SELECT person.person_id
ponieważ w przeciwnym razie nie zadziałałoby, oczywiście. Czy zrobiłeś toANALYZE TABLE attribute
po dodaniu indków? Możesz również dodać swoje noweEXPLAIN
wyniki (po dodaniu indków) do swojego pytania.Używasz tak zwanego projektu Entity-Attribute-Value, który często źle się sprawdza, z założenia.
Klasycznym relacyjnym sposobem zaprojektowania tego byłoby utworzenie osobnej tabeli dla każdego atrybutu. Ogólnie rzecz biorąc, można mieć te oddzielne tabele:
location
,gender
,bornyear
,eyecolor
.Zależy to od tego, czy określone atrybuty są zawsze zdefiniowane dla danej osoby, czy nie. I czy dana osoba może mieć tylko jedną wartość atrybutu. Na przykład osoba zwykle ma tylko jedną płeć. W obecnym projekcie nic nie stoi na przeszkodzie, aby dodać trzy wiersze dla tej samej osoby z różnymi wartościami dla płci. Możesz także ustawić wartość płci nie na 1 lub 2, ale na pewną liczbę, która nie ma sensu, na przykład 987 i nie ma żadnych ograniczeń w bazie danych, które by to uniemożliwiły. Ale to kolejna osobna kwestia utrzymania integralności danych przy projektowaniu EAV.
Jeśli zawsze znasz płeć danej osoby, nie ma sensu umieszczać jej w osobnej tabeli, a lepiej jest mieć
GenderID
wperson
tabeli niepustą kolumnę , która byłaby kluczem obcym do tabeli odnośników z listą wszystkie możliwe płcie i ich imiona. Jeśli znasz płeć danej osoby przez większość czasu, ale nie zawsze, możesz ustawić tę kolumnę na wartość zerową i ustawić ją,NULL
gdy informacje nie będą dostępne. Jeśli przez większość czasu płeć danej osoby nie jest znana, lepiej mieć oddzielną tabelę,gender
która prowadzi doperson
1: 1 i zawiera wiersze tylko dla osób o znanej płci.Podobne uwagi dotyczą
eyecolor
ibornyear
- jest mało prawdopodobne, aby dana osoba miała dwie wartości dlaeyecolor
lubbornyear
.Jeśli dana osoba może mieć kilka wartości atrybutu, zdecydowanie umieściłbyś go w osobnej tabeli. Na przykład często zdarza się, że osoba ma kilka adresów (dom, praca, poczta, wakacje itp.), Więc umieściłbyś je wszystkie w tabeli
location
. Tabeleperson
ilocation
zostaną połączone 1: M.Jeśli używasz projektu EAV, to przynajmniej wykonałbym następujące czynności.
attribute_type_id
,attribute_value
,person_id
abyNOT NULL
.attribute.person_id
zperson.person_id
.(attribute_type_id, attribute_value, person_id)
. Ważna jest tutaj kolejność kolumn.Napiszę takie zapytanie. Użyj
INNER
zamiastLEFT
łączyć i jawnie zapisuj podzapytanie dla każdego atrybutu, aby dać optymalizatorowi wszystkie szanse na użycie indeksu.Ponadto, może być warte podzielenia się
attribute
stolikattribute_type_id
.źródło
JOIN ( SELECT ... )
nie optymalizuje się dobrze.JOINing
bezpośrednio do stołu działa lepiej (ale nadal jest problematyczne).Mam nadzieję, że znalazłem wystarczające rozwiązanie. Inspiruje go ten artykuł .
Krótka odpowiedź:
ft_min_word_len=1
(dla MyISAM) w[mysqld]
sekcji iinnodb_ft_min_token_size=1
(dla InnoDb) wmy.cnf
pliku, zrestartuj usługę mysql.SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000
gdzie123
, są identyfikatory, które są związane osoby powinny w . To zapytanie zajęło mniej niż 1 sekundę.456
789
attribute_1
Szczegółowa odpowiedź:
Krok 1. Tworzenie tabeli z indeksami pełnotekstowymi. InnoDb obsługuje indeksy pełnotekstowe z MySQL 5.7, więc jeśli używasz wersji 5.5 lub 5.6, powinieneś użyć MyISAM. Czasami wyszukiwanie FT jest jeszcze szybsze niż InnoDb.
Krok 2. Wstaw dane z tabeli EAV (encja-atrybut-wartość). Na przykład podany w pytaniu można to zrobić za pomocą 1 prostego SQL:
Wynik powinien być mniej więcej taki:
Krok 3. Wybierz z tabeli z zapytaniem takim jak to:
Zapytanie wybiera wszystkie wiersze:
attr_1
:3000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
1
doattr_2
(ta kolumna reprezentuje płeć, więc jeśli to rozwiązanie zostało dostosowane, powinno byćsmallint(1)
z prostym indeksem itp.)1980, 1981, 1982, 1983 or 1984
zattr_3
2
lub3
wattr_4
Wniosek:
Wiem, że to rozwiązanie nie jest idealne i idealne w wielu sytuacjach, ale może być stosowane jako dobra alternatywa dla projektu stołu EAV.
Mam nadzieję, że to komuś pomoże.
źródło
Spróbuj użyć wskazówek indeksu zapytań, które wyglądają odpowiednio
Wskazówki dotyczące indeksu MySQL
źródło