3

I have Articles that have_many Metrics. I wish to order the Articles by a specific Metric.value when Metric.name = "score". (Metric records various article stats as 'name' and 'value' pairs. An Article can have multiple metrics, and even multiple 'scores', although I'm only interested in ordering by the most recent.)

class Article
  has_many :metrics

class Metric
  #  name       :string(255)
  #  value      :decimal(, )
  belongs_to :article

I'm struggling to write a scope to do this - any ideas? Something like this?

scope :highest_score, joins(:metrics).order('metrics.value DESC')
                      .where('metrics.name = "score"')

UPDATE:

  • An article may have many "scores" stored in the metrics table (as they are calculated weekly/monthly/yearly etc.) but I'm only interested in using the first-found (most recent) "score" for any one article. The Metric model has a default_scope that ensures DESCending ordering.

  • Fixed typo on quote location for 'metrics.value DESC'.

  • Talking to my phone-a-friend uber rails hacker, it looks likely I need a raw SQL query for this. Now I'm in way over my head... (I'm using Postgres if that helps.)

Thanks!

UPDATE 2:

Thanks to Erwin's great SQL query suggestion I have a raw SQL query that works:

SELECT a.*
FROM   articles a
LEFT   JOIN (
   SELECT DISTINCT ON (article_id)
          article_id, value
   FROM   metrics m
   WHERE  name = 'score'
   ORDER  BY article_id, date_created DESC
   ) m ON m.article_id = a.id
ORDER  BY m.value DESC;

article_list_by_desc_score = ActiveRecord::Base.connection.execute(sql)

Which gives an array of hashes representing article data (but not article objects??).

Follow-up question:

Any way of translating this back into an activerecord query for Rails? (so I can then use it in a scope)

SOLUTION UPDATE:

In case anyone is looking for the final ActiveRecord query - many thanks to Mattherick who helped me in this question. The final working query is:

scope :highest_score, joins(:metrics).where("metrics.name"
      => "score").order("metrics.value desc").group("metrics.article_id", 
      "articles.id", "metrics.value", "metrics.date_created")
      .order("metrics.date_created desc")

Thanks everyone!

Community
  • 1
  • 1
djoll
  • 1,139
  • 1
  • 12
  • 31
  • order('metrics.value' DESC) . Shouldn't the desc be within the quotes? like order('metrics.value DESC'). Anyways what error are you getting? – manoj May 05 '13 at 08:40
  • Thanks manoj - the DESC/quote thing was a typo. The error I get is "ActiveRecord::StatementInvalid: PGError: ERROR: column "score" does not exist" so a syntax error on my part as "score" is the value of the "name" column of the "metrics" table. – djoll May 05 '13 at 09:50
  • What do you mean by "poor mans noSQL"? Are you using AR's key/value store? Postgres hstore? – cpuguy83 May 05 '13 at 11:59
  • Thanks for asking cpuguy83. Ahh, I should move that noSQL reference as it confuses the question. Really, I'm just using the Metrics table in a standard Postgres database with 'names' and 'values'. The 'name' column is conceptually like a key, but that's it's only similarity with noSQL. – djoll May 05 '13 at 21:56
  • 1
    If you want to do this in SQL you'll probably need to post table definitions with sample data to http://sqlfiddle.com/ (PostgreSQL) and link to the result here. – Craig Ringer May 06 '13 at 00:17
  • Thanks Craig, I'll have a look at that. – djoll May 06 '13 at 05:59

1 Answers1

3

The query could work like this:

SELECT a.*
FROM   article a
LEFT   JOIN (
   SELECT DISTINCT ON (article_id)
          article_id, value
   FROM   metrics m
   WHERE  name = 'score'
   ORDER  BY article_id, date_created DESC
   ) m ON m.metrics_id = a.metrics_id
ORDER  BY m.value DESC;

First, retrieve the "most recent" value for name = 'score' per article in the subquery m. More explanation for the used technique in this related answer:

You seem to fall victim to a very basic misconception though:

but I'm only interested in using the first-found (most recent) "score" for any one article. The Metric model has a default_scope that ensures DESCending ordering.

There is no "natural order" in a table. In a SELECT, you need to ORDER BY well defined criteria. For the purpose of this query I am assuming a column metrics.date_created. If you have nothing of the sort, you have no way to define "most recent" and are forced to fall back to an arbitrary pick from multiple qualifying rows:

   ORDER  BY article_id

This is not reliable. Postgres will pick a row as it choses. May change with any update to the table or any change in the query plan.

Next, LEFT JOIN to the the table article and ORDER BY value. NULL sorts last, so articles without qualifying value go last.

Note: some not-so-smart ORMs (and I am afraid Ruby's ActiveRecord is one of them) use the non-descriptive and non-distinctive id as name for the primary key. You'll have to adapt to your actual column names, which you didn't provide.

Performance

Should be decent. This is a "simple" query as far as Postgres is concerned. A partial multicolumn index on table metrics would make it faster:

CREATE INDEX metrics_some_name_idx ON metrics(article_id, date_created)
WHERE name = 'score';

Columns in this order. In PostgreSQL 9.2+ you could add the column value to make index-only scans possible:

CREATE INDEX metrics_some_name_idx ON metrics(article_id, date_created, value)
WHERE name = 'score';
Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Thanks Erwin, great info. My Metrics table is by default ordered by descending date_created (it has all the usual timestamps). I'll give the raw sql a try - thanks! – djoll May 06 '13 at 06:00
  • Erwin, thanks again. I've had a bit of a play with the SQL you gave, and after a bit of massaging have the query working. Though I can't work out how to use this in a scope in model — see the update to my question. – djoll May 06 '13 at 10:56
  • @dj.: I am not an expert with Ruby. But I added some more info as far as Postgres is concerned. – Erwin Brandstetter May 06 '13 at 13:43
  • Thanks Erwin. I had one of those index's already setup, and will add the other. Thanks again. You get the gong! – djoll May 07 '13 at 08:41