选择每个子集具有最高值的记录

时间:2013-02-28 04:44:55

标签: ruby-on-rails-3 postgresql rails-activerecord arel groupwise-maximum

我有一组记录,其中一些(但不是全部)有一个“路径”字段,并且都有一个“值”字段。我希望只选择那些没有路径,或者具有特定路径的所有记录的最大值。

也就是说,鉴于这些记录:

Name:  Path:   Value:
A      foo     5
B      foo     6
C      NULL    2
D      bar     2
E      NULL    4

我想返回B,C,D和E,但不返回A(因为A有一个路径,它的路径与B相同,但A的值较低)。

如何使用ActiveRecord,ARel和Postgres实现这一目标?理想情况下,我想要一个作为范围的解决方案。

3 个答案:

答案 0 :(得分:2)

您可以通过使用2个子查询来使用这样的东西(只会执行一个具有子查询的SQL查询)。没有测试,但应该让你朝着正确的方向。这是为Postgres。

scope :null_ids, -> { where(path: nil).select('id') }
scope :non_null_ids, -> { where('path IS NOT NULL').select('DISTINCT ON (path) id').order('path, value desc, id') }
scope :stuff, -> {
  subquery = [null_ids, non_null_ids].map{|q| "(#{q.to_sql})"}.join(' UNION ')
  where("#{table_name}.id IN (#{subquery})")
}

如果您使用的是其他数据库,则可能需要对non_nulls范围使用group / order而不是distinct。如果查询运行缓慢,请在路径和值上添加索引。

您只获得1个查询,并且它是可链接的范围。

答案 1 :(得分:1)

将您的描述直接转换为SQL,如下所示:

select name, path, value
from (
    select name, path, value,
           row_number() over (partition by path order by value desc) as r
    from your_table
    where path is not null
) as dt
where r = 1
union all
select name, path, value
from your_table
where path is null

您可以将其包裹在find_by_sql中,并将对象从另一侧取出。

该查询的工作原理如下:

  1. row_number window function允许我们按path对行进行分组,按value对每个组进行排序,然后对每个组中的行进行编号。在psql内稍微使用SQL,你会看到它是如何工作的,有other window functions available可以让你做各种奇妙的事情。
  2. 您将NULL path值与非NULL path分开处理,因此内部查询中的path is not null
  3. 我们可以通过从派生表中选择行号为1的那些行(即path)来剥离每个where r = 1组中的第一行。
  4. 部分查询可以轻松处理path is null行。
  5. UNION用于将查询的结果集连接在一起。
  6. 我想不出使用ActiveRecord构建这样一个查询的方法,我也不会想到任何方法将这样的查询与ActiveRecord的范围机制集成。如果您可以轻松访问ActiveRecord::Relation的WHERE组件,那么您可以使用范围链的WHERE组件来扩充该查询的where path is not nullwhere path is null组件。我不知道怎么做。

    事实上,我倾向于放弃ActiveRecord。我发现ActiveRecord对于我做的大多数复杂事情来说都相当麻烦,并且不像SQL那样具有表现力。这适用于我曾经使用的每个ORM,因此问题不是特定于ActiveRecord。

答案 2 :(得分:0)

我没有使用ActiveRecord的经验,但是这里有一个SQLAlchemy的例子,用于沉默刚才使用的SQL群;)

q1 = Session.query(Record).filter(Record.path != None)
q1 = q1.distinct(Record.path).order_by(Record.path, Record.value.desc())

q2 = Session.query(Record).filter(Record.path == None)

query = q1.from_self().union(q2)
# Further chaining, e.g. query = query.filter(Record.value > 3) to return B, E

for record in query:
    print record.name