1) On get_rank() do a count(*) where score < users_score;
2) Store row_number() over() [...] order by score in a lookup table and refresh every so often. Use this for get_rank() lookups.
The problem I have is that you either have to calculate the rank on read using a linear-time count() or on write which means updating n/2 rows on average with their new position.
edited formatting
I am concerned about the complexity of this approach though and I don't think the concessions we're doing away with are that terrible. How meaningful is it to be rank 5623 vs. rank 5249, or even rank 2519? I don't think people can make much sense of ranks beyond 100, but maybe I'm wrong. I would rather break rankings into smaller groups. Which is more interesting, being ranked 23,528 worldwide, or being the town champion? I think the latter is much more meaningful to a person.
As for complexity. The complexity of learning is a noop as far as I'm concerned. Duplicate data? Not concerned. Managing 2 stores...slightly concerning. Agreed. Maintenance? The entire change is largely encapsulated in a new ~30 line Rank class. Sure, I'd rather not have that extra code.
I hadn't considered rank-groups (thanks for the idea though!). I'll definitely give that more thought. Nonetheless, I think I rather have the raw data available and accessible (in addition to any additional abstraction). I don't like making decision for the developers. Maybe 100 ranks isn't enough for them (I've seen them do some pretty creative stuff with the bit of data we're collecting already).