【问题标题】:Select records with highest values for each subset为每个子集选择具有最高值的记录
【发布时间】:2013-02-14 04:19:56
【问题描述】:

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

也就是说,给定这些记录:

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 完成此任务?理想情况下,我想要一个充当范围的解决方案。

【问题讨论】:

  • 如果不使用 ActiveRecord,您将在 5 分钟内获得 3 个 SQL 工作答案。
  • 同意,现在是回归真正 SQL 的好时机。
  • 拥有 AR 示波器解决方案有多重要? ActiveRecord 对数据库和 SQL 的理解相当原始和有限,因此它在处理比select * from t where ... 更复杂的任何事情时都会遇到问题,您需要比这更高级的东西才能获得可靠的结果。你的数据有什么限制吗?如果数据受到限制,我们可能会想出一些类似于范围的东西。
  • @muistooshort 目前我只是加载所有记录并拒绝 Ruby 中价值较低的记录。这冒犯了我的效率意识。我不反对 SQL 解决方案,我只是希望能够将其视为可链接范围(User.things.highest_path_values.where() 等),结合使用 AR 预加载,等等。我不确定哪种数据约束有用 - 值是浮点数,路径是字符串(或 null),并且每条记录都分类了与过滤过程无关的其他数据。
  • “因为我们不懂 SQL,所以全部用 Ruby 做”是 Rails 的典型特征,这也是数据库人对 ActiveRecord 如此嗤之以鼻的原因之一;你是对的 (IMO) 被这种方法的代价冒犯了。当你可以按自己的方式做事时,Rails 很好,但当你不能时,它很快就会崩溃。直接的解决方案涉及一个窗​​口函数、一个派生表和一个 UNION,而 ActiveRecord 不太喜欢这些东西。通过约束,我正在考虑唯一性约束。让我再考虑一下。

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


【解决方案1】:

您可以通过使用 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 on。如果查询运行缓慢,请在路径和值上建立索引。

你只得到 1 个查询,它是一个可链接的范围。

【讨论】:

  • 我不得不将两个子查询都用括号括起来,并进行了编辑以反映这一点,但除此之外,这就像一个魅力。非常感谢。
  • 太棒了!我很高兴它对你有用。实际上我从来没有想过这样做,但现在我这样做了,如果我遇到这种情况,我可能会自己使用它。
【解决方案2】:

将您的描述直接音译为 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_numberwindow function 允许我们按path 对行进行分组,按value 对每个组进行排序,然后对每个组中的行进行编号。在psql 里面玩一下SQL,你会看到它是如何工作的,other window functions available 可以让你做各种美妙的事情。
  2. 您将 NULL path 值与非 NULL paths 分开处理,因此内部查询中的 path is not null
  3. 我们可以通过从派生表中选择行号为 1 的行(即 where r = 1)来剥离每个 path 组中的第一行。
  4. path is null 行的处理很容易通过部分查询来处理。
  5. UNION 用于将查询的结果集连接在一起。

我想不出任何方法来使用 ActiveRecord 构造这样的查询,也想不出任何方法将这样的查询与 ActiveRecord 的作用域机制集成。如果您可以轻松地仅访问 ActiveRecord::Relation 的 WHERE 组件,那么您可以使用范围链的 WHERE 组件来扩充该查询的 where path is not nullwhere path is null 组件。不过我不知道该怎么做。

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

【讨论】:

  • +1:虽然严格来说,这并不能解决我的问题,但它很有趣,我已将窗口函数放在我的“需要了解的事情”列表中现在很快'。
【解决方案3】:

我没有使用 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-10-20
    • 1970-01-01
    • 2022-11-07
    • 2017-10-03
    • 2012-05-18
    • 2022-12-04
    • 2019-04-13
    • 1970-01-01
    相关资源
    最近更新 更多