onsdag 30 januari 2008

Slumpad sortering i Ruby on Rails


Jag arbetar just nu med en webbplats där jag ska visa en slumpvis utvald medlem på varje sida. Enkelt, tänkte jag använde mig av följande kod:

@member = Member.find(:first, :order => "RAND()")

Jag fick direkt felet SQLite3::SQLException: no such function: RAND: SELECT * FROM members ORDER BY RAND() LIMIT 1. Visst ja. Nu när Rails 2 har kommit kör vi ju SQLite3 istället för MySQL på utvecklingsburken. En Google-sökning talar om att SQLite använder RANDOM() istället för RAND(). Lätt fixat med andra ord:

@member = Member.find(:first, :order => "RANDOM()")

Sedan började jag fundera. Varför finns det inte en inbyggd Rails-funktion för det här? Member.find(:random) hade ju suttit fint. Jag hittade en ticket på RailsTrac som förklarade att ORDER BY RAND() är ett dåligt sätt att hitta en slumpvis utvald rad på. Det hade jag ingen aning om. Jag måste säga att jag gillar när Rails gör mig till en bättre programmerare på det här sättet.

Anledningen till att det är dumt att köra ORDER BY RAND() är att databasen måste gå igenom varenda medlem i tabellen, generera ett slumptal för varje rad och sedan sortera efter detta slumptal. Det är helt enkelt krävande. Har du tio medlemmar i databasen kommer det inte spela någon större roll, men har du tiotusen blir det lite segare.

Dessutom vill jag ju ha en lösning som fungerar oavsett om jag använder MySQL eller SQLite3 eftersom jag ibland växlar mellan de två i utvecklingsläge och produktionsläge. Lösningen blev följande:

@member = Member.find(:first, :offset => (Member.count * rand ).to_i)

Visst, koden genererar två databasfrågor, men slutresultatet kommer antagligen att bli snabbare. Dessutom fungerar det på båda databaserna. Vill man ha det snyggt i sin controller kan man ju dessutom lägga en del av koden i modellen.

# member.rb
class Member < ActiveRecord::Base
def self.random
Member.find(:first,
:offset => ( Member.count * rand ).to_i)
end
end

# members_controller.rb
def random_member
@random_member = Member.random
end


Den nya koden blir betydligt snabbare. Jag tog en databas med omkring 34 000 användare, och RAND()-varianten tar cirka 0,55 sekunder medan de två frågorna som genereras av min nya kod sammanlagt tar cirka 0,04 sekunder.

Är det någon som har en smartare lösning?

1 kommentar: