Chciałbym zoptymalizować moje zapytania, więc przyjrzę się mysql-slow.log
.
Większość moich powolnych zapytań zawiera ORDER BY RAND()
. Nie mogę znaleźć prawdziwego rozwiązania tego problemu. Jest możliwe rozwiązanie na MySQLPerformanceBlog, ale myślę, że to nie wystarczy. W przypadku źle zoptymalizowanych (lub często aktualizowanych, zarządzanych przez użytkowników) tabel nie działa lub muszę uruchomić co najmniej dwa zapytania, zanim będę mógł wybrać PHP
wygenerowany przeze mnie losowy wiersz.
Czy jest jakieś rozwiązanie tego problemu?
Fikcyjny przykład:
SELECT accomodation.ac_id,
accomodation.ac_status,
accomodation.ac_name,
accomodation.ac_status,
accomodation.ac_images
FROM accomodation, accomodation_category
WHERE accomodation.ac_status != 'draft'
AND accomodation.ac_category = accomodation_category.acat_id
AND accomodation_category.acat_slug != 'vendeglatohely'
AND ac_images != 'b:0;'
ORDER BY
RAND()
LIMIT 1
mysql
random
performance
fabrik
źródło
źródło
Odpowiedzi:
Spróbuj tego:
SELECT * FROM ( SELECT @cnt := COUNT(*) + 1, @lim := 10 FROM t_random ) vars STRAIGHT_JOIN ( SELECT r.*, @lim := @lim - 1 FROM t_random r WHERE (@cnt := @cnt - 1) AND RAND(20090301) < @lim / @cnt ) i
Jest to szczególnie wydajne na
MyISAM
(ponieważCOUNT(*)
jest natychmiastowe), ale nawetInnoDB
wtedy jest10
bardziej wydajne niżORDER BY RAND()
.Główną ideą jest to, że nie sortujemy, ale zamiast tego przechowujemy dwie zmienne i obliczamy
running probability
wiersz, który ma zostać wybrany w bieżącym kroku.Zobacz ten artykuł na moim blogu, aby uzyskać więcej szczegółów:
Aktualizacja:
Jeśli chcesz wybrać tylko jeden losowy rekord, spróbuj tego:
SELECT aco.* FROM ( SELECT minid + FLOOR((maxid - minid) * RAND()) AS randid FROM ( SELECT MAX(ac_id) AS maxid, MIN(ac_id) AS minid FROM accomodation ) q ) q2 JOIN accomodation aco ON aco.ac_id = COALESCE ( ( SELECT accomodation.ac_id FROM accomodation WHERE ac_id > randid AND ac_status != 'draft' AND ac_images != 'b:0;' AND NOT EXISTS ( SELECT NULL FROM accomodation_category WHERE acat_id = ac_category AND acat_slug = 'vendeglatohely' ) ORDER BY ac_id LIMIT 1 ), ( SELECT accomodation.ac_id FROM accomodation WHERE ac_status != 'draft' AND ac_images != 'b:0;' AND NOT EXISTS ( SELECT NULL FROM accomodation_category WHERE acat_id = ac_category AND acat_slug = 'vendeglatohely' ) ORDER BY ac_id LIMIT 1 ) )
Zakłada się, że Twoje
ac_id
są rozmieszczone mniej więcej równomiernie.źródło
@fabrik
: Spróbuj teraz. Byłoby naprawdę pomocne, gdybyś opublikował skrypty tabeli, abym mógł je sprawdzić przed wysłaniem.To zależy od tego, jak losowy musisz być. Połączone rozwiązanie działa całkiem dobrze w IMO. O ile nie masz dużych luk w polu ID, nadal jest to dość przypadkowe.
Jednak powinieneś móc to zrobić w jednym zapytaniu, używając tego (do wyboru pojedynczej wartości):
SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1
Inne rozwiązania:
random
do tabeli i wypełnij je liczbami losowymi. Następnie możesz wygenerować liczbę losową w PHP i zrobić"SELECT ... WHERE rnd > $random"
źródło
SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1
ale wydaje się, że nie działa to poprawnie, ponieważ nigdy nie zwraca ostatniego rekorduSELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1
Wygląda na to, że mi się to udajeOto jak bym to zrobił:
SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*) FROM accomodation a JOIN accomodation_category c ON (a.ac_category = c.acat_id) WHERE a.ac_status != 'draft' AND c.acat_slug != 'vendeglatohely' AND a.ac_images != 'b:0;'; SET @sql := CONCAT(' SELECT a.ac_id, a.ac_status, a.ac_name, a.ac_status, a.ac_images FROM accomodation a JOIN accomodation_category c ON (a.ac_category = c.acat_id) WHERE a.ac_status != ''draft'' AND c.acat_slug != ''vendeglatohely'' AND a.ac_images != ''b:0;'' LIMIT ', @r, ', 1'); PREPARE stmt1 FROM @sql; EXECUTE stmt1;
źródło
OFFSET
(do czego@r
służy) nie zapobiega skanowaniu - aż do pełnego skanowania tabeli.(Tak, zostanę obrzydzony za brak tu mięsa, ale czy nie możesz być weganinem przez jeden dzień?)
Przypadek: Konsekutywne AUTO_INCREMENT bez przerw, zwrócone 1 wiersz
Przypadek: Kolejne AUTO_INCREMENT bez przerw, 10 wierszy
Przypadek: AUTO_INCREMENT z przerwami, zwrócony 1 wiersz
Przypadek: Dodatkowa kolumna typu FLOAT do losowania
Przypadek: kolumna UUID lub MD5
Te 5 skrzynek może być bardzo wydajnych w przypadku dużych stołów. Zobacz mój blog po szczegóły.
źródło
To da ci jedno zapytanie podrzędne, które użyje indeksu do uzyskania losowego identyfikatora, a drugie zapytanie uruchomi twoją połączoną tabelę.
SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, accomodation_category WHERE accomodation.ac_status != 'draft' AND accomodation.ac_category = accomodation_category.acat_id AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' AND accomodation.ac_id IS IN ( SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1 )
źródło
Rozwiązanie dla twojego przykładu fałszywego byłoby:
SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, JOIN accomodation_category ON accomodation.ac_category = accomodation_category.acat_id JOIN ( SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id ) AS Choices USING (ac_id) WHERE accomodation.ac_id >= Choices.ac_id AND accomodation.ac_status != 'draft' AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' LIMIT 1
Aby dowiedzieć się więcej o alternatywach
ORDER BY RAND()
, przeczytaj ten artykuł .źródło
Optymalizuję wiele istniejących zapytań w moim projekcie. Rozwiązanie Quassnoi bardzo pomogło mi przyspieszyć zapytania! Jednak trudno jest włączyć to rozwiązanie do wszystkich zapytań, szczególnie w przypadku skomplikowanych zapytań obejmujących wiele podzapytań na wielu dużych tabelach.
Dlatego używam mniej zoptymalizowanego rozwiązania. Zasadniczo działa tak samo, jak rozwiązanie Quassnoi.
SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, accomodation_category WHERE accomodation.ac_status != 'draft' AND accomodation.ac_category = accomodation_category.acat_id AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' AND rand() <= $size * $factor / [accomodation_table_row_count] LIMIT $size
$size * $factor / [accomodation_table_row_count]
oblicza prawdopodobieństwo wybrania losowego wiersza. Rand () wygeneruje liczbę losową. Wiersz zostanie wybrany, jeśli rand () jest mniejszy lub równy prawdopodobieństwu. To skutecznie dokonuje losowego wyboru w celu ograniczenia rozmiaru tabeli. Ponieważ istnieje szansa, że zwróci mniej niż zdefiniowany limit liczby, musimy zwiększyć prawdopodobieństwo, aby upewnić się, że wybieramy wystarczającą liczbę wierszy. Dlatego mnożymy rozmiar $ przez współczynnik $ (zwykle ustawiam współczynnik $ = 2, działa w większości przypadków). Wreszcie robimylimit $size
Teraz problemem jest ustalenie accomodation_table_row_count . Jeśli znamy rozmiar tabeli, MOŻEMY na stałe zakodować rozmiar tabeli. To działałoby najszybciej, ale oczywiście nie jest to idealne rozwiązanie. Jeśli używasz Myisam, uzyskiwanie liczby stołów jest bardzo wydajne. Ponieważ używam innodb, po prostu robię proste liczenie + wybór. W twoim przypadku wyglądałoby to tak:
SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, accomodation_category WHERE accomodation.ac_status != 'draft' AND accomodation.ac_category = accomodation_category.acat_id AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' AND rand() <= $size * $factor / (select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`)) LIMIT $size
Najtrudniejsze jest ustalenie odpowiedniego prawdopodobieństwa. Jak widać, poniższy kod w rzeczywistości oblicza tylko przybliżony rozmiar tabeli temp (w rzeczywistości jest zbyt przybliżony!):
(select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))
Ale możesz udoskonalić tę logikę, aby uzyskać bliższe przybliżenie rozmiaru tabeli. Zwróć uwagę, że lepiej jest zaznaczyć OVER niż niedostatecznie zaznaczyć wiersze. tzn. jeśli prawdopodobieństwo jest zbyt niskie, ryzykujesz, że nie wybierzesz wystarczającej liczby wierszy.To rozwiązanie działa wolniej niż rozwiązanie Quassnoi, ponieważ musimy ponownie obliczyć rozmiar tabeli. Jednak uważam, że to kodowanie jest o wiele łatwiejsze w zarządzaniu. Jest to kompromis między dokładnością i wydajnością a złożonością kodowania . Powiedziawszy to, na dużych stołach jest to nadal znacznie szybsze niż Order by Rand ().
Uwaga: Jeśli pozwala na to logika zapytań, należy przeprowadzić losowy wybór możliwie jak najwcześniej przed wykonaniem jakichkolwiek operacji łączenia.
źródło
function getRandomRow(){ $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT); $res = getRowById($id); if(!empty($res)) return $res; return getRandomRow(); } //rowid is a key on table function getRowById($rowid=false){ return db select from table where rowid = $rowid; }
źródło