Kirjautuminen

Haku

Tehtävät

Kilpailu

Algoritmikisa
Putka Open 2020 -kisan
2. kierros päättyy klo 23:00!

Keskustelu: Nettisivujen teko: Tietokannan optimointi

Sivu 1 / 1

Sivun loppuun

AkeMake [14.09.2020 15:53:58]

Lainaa #

Toisessa ketjussa jo hiukan sivuttiin tätä tietokannan optimointia ja se olikin sen hetkisillä kyselyillä riittävän nopea minulle. Nyt teen kuitenkin hiukan laajennusta koodiini ja jostain syystä SQL-kysely vie paljon aikaa, vaikka minusta indeksit näyttäisivät olevan kohdillaan.
Kyseessä on siis sama 25 miljoonaa riviä sisältävä user_logs taulu, josta haen tietoa seuraavalla kyselyllä.

SELECT COUNT(*) AS hits, DATE_FORMAT(FROM_UNIXTIME(timecreated), '%Y-%m-%d') AS timestamp
FROM user_logs
WHERE userid = ? AND action NOT IN ('loggedinas', 'loggedout') AND realuserid IS NULL
GROUP BY timestamp

Tietokannalla kestää yli kymmenen sekuntia suorittaa tämä kysely käyttäjien kohdalla, jotka ovat olleet hyvin aktiivisia sivuilla siitä huolimatta, että tietokannassa on indeksi

CREATE INDEX user_actions
ON user_logs(userid,action,realuserid);

Missä kohtaa indeksini tai kyselyni menee pieleen?

Lebe80 [14.09.2020 16:00:02]

Lainaa #

Onkos tuo COUNT(*) sellainen, jossa ei indexejä pysty hyödyntämään?

AkeMake [14.09.2020 16:04:48]

Lainaa #

En ole vielä kovin sinut näiden indeksien kanssa. Tuo COUNT(*):ko sen tekee, ettei kysely voi käyttää tätä määrittelemääni indeksiä? Miten indeksit toimivat tuon COUNT(*) kanssa?

Lebe80 [14.09.2020 16:37:41]

Lainaa #

Ok, ei johtune siitä:

So overall is always better to leave it as COUNT(*) and let the optimizer choose.

Metabolix [14.09.2020 18:27:12]

Lainaa #

Mitähän EXPLAIN sanoo tuosta kyselystä?

Olisi järkevää hankkiutua eroon DATE_FORMAT-kutsusta ja merkkijonoista, nimittäin DATE(x) näyttää olevan todella paljon nopeampi kuin DATE_FORMAT(x).

Kuten toisessa keskustelussa mainitsin, jos käytetään paljon juuri päivämäärää, voisi olla fiksua laittaa tauluun suoraan päivämäärä, niin ei tarvitsisi tehdä muunnosta. Tällöin myös päivämäärän ottaminen mukaan indeksiin saattaisi auttaa.

AkeMake [14.09.2020 21:36:59]

Lainaa #

Niin tosiaan tuo DATE_FORMAT -> DATE muutos unohtui tehdä. Tein sen nyt ja kyllä se hiukan nopeutti hakua, mutta ei mitenkään merkittävästi (10s -> 9s).

Muutin hakua enemmän tarkoituksiini sopivaksi ja muutin indeksin sen mukaan, mutta mikään ei muuttunut kyselyn keston suhteen. Kysely näyttää nyt tältä:

SELECT COUNT(*) AS hits, DATE(FROM_UNIXTIME(timecreated, '%Y-%m-%d')) AS timestamp
FROM user_logs
WHERE action NOT IN ('loggedinas', 'loggedout') AND realuserid IS NULL AND target NOT IN ('user_login', 'webservice_login') AND userid = ?
GROUP BY timestamp

ja EXPLAIN sanoo:

id	select_type	table		partitions	type	possible_keys			key			key_len	ref		rows	filtered	Extra
1	SIMPLE		user_logs	NULL		range	user_actions,loggedin	loggedin	410		NULL	2138871	8.00		Using index condition; Using where; Using temporary; Using filesort

Nyt user_actions ja loggedin ovat:

CREATE INDEX user_actions
ON user_logs(action,realuserid,target,userid);
CREATE INDEX loggedin
ON user_logs(userid,action);

EDIT1: Jotenkin jäi huomaamatta tuo ohje jättää merkkijonot pois. Pudotin '%Y-%m-%d' pois ja kysely nopeutui, mutta jälleen vain marginaalisen vähän. Kaikkein aktiivisimman käyttäjän kohdalla kysely nopeutui 29s -> 28s.

EDIT2: Jos laitan tauluun suoraan päivämäärän, niin millä kyselyllä saan lisättyä sen päivämäärän näille valmiiksi taulussa oleville 25 miljoonalle riville? Veikkaan, että

UPDATE user_logs SET datecreated = DATE(timecreated)

Metabolix [14.09.2020 22:18:16]

Lainaa #

EXPLAIN kertoo, että kysely käyttää loggedin-indeksiä, eli jostain syystä ensimmäinen indeksi ei tule käyttöön.

Mitähän action ja target ovat tyypiltään? Toivottavasti eivät merkkijonoja! Tekstin käsittely on käytännössä aina hitaampaa kuin lukujen, ja toistuva teksti myös vie turhaa tilaa tietokannassa. Jos nuo ovat merkkijonoja, kannattaa siirtyä joko ENUMin käyttöön tai järjestelyyn, jossa vaihtoehdot on listattu toisessa taulussa ja muualle tallennetaan vain id. Eli esimerkiksi actions-taulussa olisi lista näistä (1=loggedinas, 2=loggedout) ja user_log-taulussa olisi vain viittaus action_id. ENUM-tyypillä voi välttää ylimääräisen taulun, mutta ENUM tuo omat rajoituksensa: vaihtoehtojen muuttaminen tai lisääminen jälkikäteen on vaikeampaa, ja samoja vaihtoehtoja ei voi suoraan käyttää useassa eri taulussa (ts. ne pitää määritellä aina erikseen).

Päivämäärien lisäämisessä muista edelleen se FROM_UNIXTIME, koska DATE(luku) tuottaa vain arvon NULL, paitsi jos luku sattuu olemaan muotoa YYYYMMDD.

AkeMake [15.09.2020 07:45:55]

Lainaa #

timestamp bigint(10), action varchar(100), realuserid bigint(10), target varchar(100), userid bigint(10)

Eli kyllä tässä nimenomaan tutkitaan kahden merkkijonon arvoja. Sanotko, että tämä 28 sekuntia tulee siitä, että haetaan noista kahdesta merkkijonosta? Pitänee sitten työstää, että saa ne erillisiin tauluihin.

The Alchemist [15.09.2020 08:33:33]

Lainaa #

Ei mitään erillisiä tauluja vaan opettelet käyttämään enumeja. Tästäkin on jo aiemmin sanottu.

Tuo 28 sekuntia tulee ensisijaisesti siitä, ettei kysely käytä sellaista indeksiä, minkä pitäisi auttaa nopeuttamaan kyselyä. Silloin kun kanta ei osaa tai halua käyttää indeksejä, niin kyselyt ovat aina hitaita, olipa sarakkeiden tietotyyppi mitä tahansa.

NOT IN -kyselyistä joku on sanonut, että MySQL ei käytä indeksiä sen vuoksi, että sen pitää kuitenkin selvittää, mitä muita arvoja kyseisellä sarakkeella voi olla.

Jos tuo selitys pitää paikkansa, niin enumien käyttö luultavasti korjaisi ongelman automaattisesti, sillä silloin MySQL tietää suoraan enum-listan perusteella, mitä arvoja sarakkeessa voi olla. Kun käytetään avoimia varchareja, niin kanta ei voi tietää arvojoukkoa lukematta kaikkia rivejä läpi.

(Omaan mutukorvaani kuulostaa siltä, että jos lisäät erilliset indeksit action- ja target-sarakkeille, niin sekin voi helpottaa tilannetta, mutta en todellakaan tiedä varmaksi. Eikä se silti välttämättä ole yhtä nopea kuin jos käyttäisit enumeja. Indeksien laatiminen on oma taitolajinsa, eikä niitä kannata roiskia sinne tänne ns. varmuuden vuoksi.)

Toki yleensä ottaen on järkevämpi olla käyttämättä NOT IN -kyselyitä, koska todennäköisesti haluat kuitenkin lukea vain ne rivit, joilla on jokin sallittu arvo, kuin minkä tahansa rivin, jolla ei ole noita kiellettyjä arvoja. (Ehkä myöhemmin lisäät kantaan uuden kielletyn arvon ja sitten on hankala päivitellä noita NOT IN -kyselyitä.)


Sivun alkuun

Vastaus

Muista lukea kirjoitusohjeet.
Tietoa sivustosta