Dlaczego wyższe przesunięcie LIMIT w MYSQL spowalnia zapytanie?

173

Scenariusz w skrócie: tabela z ponad 16 milionami rekordów [rozmiar 2 GB]. Im większe przesunięcie LIMIT z SELECT, tym wolniejsze staje się zapytanie, gdy używa się ORDER BY * primary_key *

Więc

SELECT * FROM large ORDER BY `id`  LIMIT 0, 30 

zajmuje znacznie mniej niż

SELECT * FROM large ORDER BY `id` LIMIT 10000, 30 

To zamawia tylko 30 płyt i tak samo. Więc to nie jest narzut z ORDER BY.
Teraz pobieranie ostatnich 30 wierszy zajmuje około 180 sekund. Jak mogę zoptymalizować to proste zapytanie?

Rahman
źródło
UWAGA: jestem autorem. MySQL nie odwołuje się do indeksu (PRIMARY) w powyższych przypadkach. zobacz poniższy link użytkownika „Quassnoi”, aby uzyskać wyjaśnienie.
Rahman

Odpowiedzi:

197

To normalne, że wyższe przesunięcia spowalniają zapytanie, ponieważ zapytanie musi odliczać pierwsze OFFSET + LIMITrekordy (i pobierać tylko LIMITz nich). Im wyższa jest ta wartość, tym dłużej trwa zapytanie.

Zapytanie nie może przejść od razu, OFFSETponieważ po pierwsze rekordy mogą mieć różną długość, a po drugie, mogą istnieć luki w usuniętych rekordach. Musi sprawdzić i policzyć każdy rekord w drodze.

Zakładając, że idjest PRIMARY KEYz MyISAMtabeli, można ją przyspieszyć stosując ten trick:

SELECT  t.*
FROM    (
        SELECT  id
        FROM    mytable
        ORDER BY
                id
        LIMIT 10000, 30
        ) q
JOIN    mytable t
ON      t.id = q.id

Zobacz ten artykuł:

Quassnoi
źródło
7
Zachowanie MySQL "wczesne wyszukiwanie wierszy" było odpowiedzią na pytanie, dlaczego mówi tak długo. Dzięki sztuczce, którą podałeś, tylko dopasowane identyfikatory (bezpośrednio przez indeks) są wiązane, oszczędzając niepotrzebne wyszukiwania wierszy zbyt wielu rekordów. To załatwiło sprawę, hura!
Rahman
4
@harald: co dokładnie masz na myśli, mówiąc „nie działa”? To czysta poprawa wydajności. Jeśli nie ma indeksu do użytku przez ORDER BYlub indeks obejmuje wszystkie potrzebne pola, nie potrzebujesz tego obejścia.
Quassnoi
6
@ f055: odpowiedź mówi „przyspiesz”, a nie „natychmiast”. Czy przeczytałeś pierwsze zdanie odpowiedzi?
Quassnoi
3
Czy można uruchomić coś takiego dla InnoDB?
NeverEndingQueue
3
@Lanti: opublikuj to jako osobne pytanie i nie zapomnij oznaczyć go tagiem postgresql. To jest odpowiedź specyficzna dla MySQL.
Quassnoi,
220

Sam miałem ten sam problem. Biorąc pod uwagę fakt, że chcesz zebrać dużą ilość tych danych, a nie konkretny zestaw 30, prawdopodobnie uruchomisz pętlę i zwiększysz przesunięcie o 30.

Zamiast tego możesz zrobić:

  1. Trzymaj ostatni identyfikator zestawu danych (30) (np. LastId = 530)
  2. Dodaj warunek WHERE id > lastId limit 0,30

Więc zawsze możesz mieć przesunięcie ZERO. Będziesz zaskoczony poprawą wydajności.

Nikos Kyr
źródło
Czy to działa, jeśli są luki? Co się stanie, jeśli nie masz jednego unikalnego klucza (na przykład klucza złożonego)?
xaisoft
8
Może nie być oczywiste dla wszystkich, że to działa tylko wtedy, gdy zestaw wyników jest posortowany według tego klucza, w porządku rosnącym (w przypadku porządku malejącego ten sam pomysł działa, ale zmień> lastid na <lastid.) Nie ma znaczenia, czy jest to klucz podstawowy lub inne pole (lub grupa pól).
Eloff
Dobra robota, ten człowiek! Bardzo proste rozwiązanie, które rozwiązało mój problem :-)
oodavid
30
Tylko uwaga, że ​​limit / offset jest często używany w wynikach podzielonych na strony, a trzymanie lastId jest po prostu niemożliwe, ponieważ użytkownik może przeskoczyć do dowolnej strony, a nie zawsze do następnej. Innymi słowy, przesunięcie często musi być obliczane dynamicznie na podstawie strony i limitu, zamiast podążać za ciągłym wzorem.
Tom
3
Szerzej
Rick James
17

MySQL nie może przejść bezpośrednio do 10000. rekordu (lub 80000 bajtu, jak sugerujesz), ponieważ nie może założyć, że jest tak spakowany / uporządkowany (lub że ma ciągłe wartości od 1 do 10000). Chociaż w rzeczywistości może tak być, MySQL nie może zakładać, że nie ma dziur / luk / usuniętych identyfikatorów.

Tak więc, jak zauważył bobs, MySQL będzie musiał pobrać 10000 wierszy (lub przejść przez 10000-te pozycje indeksu id) przed znalezieniem 30 do zwrócenia.

EDYCJA : Aby zilustrować mój punkt widzenia

Zauważ, że chociaż

SELECT * FROM large ORDER BY id LIMIT 10000, 30 

byłby wolny (er) ,

SELECT * FROM large WHERE id >  10000 ORDER BY id LIMIT 30 

byłby szybszy (er) i zwróciłby te same wyniki pod warunkiem, że nie ma brakujących ids (tj. luk).

Riedsio
źródło
2
To jest poprawne. Ale skoro jest ograniczony przez „id”, dlaczego trwa to tak długo, kiedy ten identyfikator znajduje się w indeksie (klucz podstawowy)? Optymalizator powinien bezpośrednio odwołać się do tego indeksu, a następnie pobrać wiersze z dopasowanymi identyfikatorami (które pochodzą z tego indeksu)
Rahman,
1
Jeśli użyłeś klauzuli WHERE w id, może to przejść bezpośrednio do tego znaku. Jeśli jednak nałożysz na to ograniczenie, uporządkowane według id, jest to tylko względna przeciwwaga do początku, więc musi być poprzecznie na całej długości.
Riedsio
Bardzo dobry artykuł eversql.com/…
Pažout
Pracował dla mnie @Riedsio Thanks.
mahesh kajale
8

Znalazłem ciekawy przykład optymalizacji zapytań SELECT ORDER BY id LIMIT X, Y. Mam 35 milionów wierszy, więc znalezienie ich zajęło 2 minuty.

Oto sztuczka:

select id, name, address, phone
FROM customers
WHERE id > 990
ORDER BY id LIMIT 1000;

Po prostu wpisz GDZIE z ostatnim identyfikatorem, który dostałeś, aby znacznie zwiększyć wydajność. U mnie było to od 2 minut do 1 sekundy :)

Inne ciekawe sztuczki tutaj: http://www.iheavy.com/2013/06/19/3-ways-to-optimize-for-paging-in-mysql/

Działa również ze sznurkami

sym
źródło
1
działa to tylko w przypadku tabel, w których żadne dane nie są usuwane
miro Kwietnia
1
@miro Jest to prawdą tylko wtedy, gdy pracujesz przy założeniu, że twoje zapytanie może wyszukiwać losowe strony, czego nie sądzę, że zakłada ten plakat. Chociaż nie podoba mi się ta metoda w większości przypadków w świecie rzeczywistym, będzie działać z lukami, o ile zawsze opierasz ją na ostatnim uzyskanym identyfikatorze.
Gremio
5

Czasochłonną częścią tych dwóch zapytań jest pobieranie wierszy z tabeli. Logicznie rzecz biorąc, w tej LIMIT 0, 30wersji trzeba pobrać tylko 30 wierszy. W tej LIMIT 10000, 30wersji ocenianych jest 10000 wierszy i zwracanych jest 30 wierszy. Proces odczytu danych może zostać zoptymalizowany, ale weź pod uwagę następujące kwestie:

A co by było, gdybyś miał w zapytaniach klauzulę WHERE? Silnik musi zwrócić wszystkie wiersze, które się kwalifikują, a następnie posortować dane i ostatecznie pobrać 30 wierszy.

Weź również pod uwagę przypadek, w którym wiersze nie są przetwarzane w sekwencji ORDER BY. Wszystkie kwalifikujące się wiersze muszą zostać posortowane, aby określić, które wiersze mają zostać zwrócone.

bobs
źródło
1
zastanawiam się tylko, dlaczego pobranie tych 10000 wierszy zajmuje czas. Indeks użyty w tym polu (id, który jest kluczem podstawowym) powinien sprawić, że pobieranie tych wierszy będzie tak szybkie, jak wyszukiwanie tego indeksu PK dla rekordu nr. 10000, co z kolei ma być szybkie jak szukanie pliku do tego przesunięcia pomnożonego przez długość rekordu indeksu (tj. Szukanie 10000 * 8 = bajt nr 80000 - biorąc pod uwagę, że 8 to długość rekordu indeksu)
Rahman
@Rahman - Jedynym sposobem, aby policzyć ponad 10000 wierszy, jest przechodzenie nad nimi jeden po drugim. Może to po prostu obejmować indeks, ale nadal indeksowanie wierszy wymaga czasu. Nie ma struktury MyISAM ani InnoDB, która mogłaby poprawnie (we wszystkich przypadkach) „szukać” w celu nagrania 10000. Sugestia 10000 * 8 zakłada (1) MyISAM, (2) rekord o stałej długości i (3) nigdy nie usuwa z tabeli . W każdym razie indeksy MyISAM są BTrees, więc nie zadziała.
Rick James
Jak stwierdzono w tej odpowiedzi, uważam, że naprawdę powolną częścią jest wyszukiwanie wierszy, a nie przechodzenie przez indeksy (co oczywiście również się sumuje, ale nie jest tak blisko, jak wyszukiwanie wierszy na dysku). Opierając się na zapytaniach obejściowych przedstawionych dla tego problemu, uważam, że wyszukiwania wierszy mają zwykle miejsce, jeśli wybierasz kolumny poza indeksem - nawet jeśli nie są one częścią klauzuli order by lub where. Nie znalazłem powodu, dla którego jest to konieczne, ale wydaje się, że niektóre z obejść pomagają.
Gremio
1

Dla zainteresowanych porównaniem i liczbami :)

Eksperyment 1: zbiór danych zawiera około 100 milionów wierszy. Każdy wiersz zawiera kilka BIGINT, TINYINT, a także dwa pola TEXT (celowo) zawierające około 1k znaków.

  • Niebieski: = SELECT * FROM post ORDER BY id LIMIT {offset}, 5
  • Pomarańczowy: = metoda @ Quassnoi. SELECT t.* FROM (SELECT id FROM post ORDER BY id LIMIT {offset}, 5) AS q JOIN post t ON t.id = q.id
  • Oczywiście trzecia metoda ... WHERE id>xxx LIMIT 0,5nie pojawia się tutaj, ponieważ powinna to być stała czasowa.

Eksperyment 2: Podobna rzecz, z tą różnicą, że jeden wiersz ma tylko 3 BIGINT.

  • zielony: = niebieski przed
  • czerwony: = pomarańczowy wcześniej

wprowadź opis obrazu tutaj

ch271828n
źródło