【问题标题】:How do I select unique records by column with ActiveRecord and Postgresql如何使用 ActiveRecord 和 Postgresql 按列选择唯一记录
【发布时间】:2011-10-14 06:10:43
【问题描述】:

给定以下记录(第一行是列名):

name              platform           other_columns     date
Eric              Ruby               something         somedate
Eric              Objective-C        something         somedate
Joe               Ruby               something         somedate

如何检索包含所有列的单一记录,以便名称列在结果集中始终是唯一的?我希望此示例中的查询返回第一个 Eric(带 Ruby)记录。

我认为我得到的最接近的是使用“select distinct on (name) *...”,但这需要我先按名称排序,而我实际上想按日期列对记录进行排序。

  • 按日期排序记录
  • 如果有多个同名记录,选择一个(没关系)
  • 选择所有列

如何在 Rails on PostgreSQL 中实现这一点?

【问题讨论】:

    标签: sql ruby-on-rails ruby-on-rails-3 postgresql


    【解决方案1】:

    您不能做一个简单的.group(:name),因为当您选择未分组和未聚合的列时,这会在您的 SQL 中产生 GROUP BY name,这会导致选择哪一行和 PostgreSQL (rightly IMHO) complains 的歧义:

    当 GROUP BY 存在时,除非在聚合函数中,否则 SELECT 列表表达式引用未分组的列是无效的,因为对于未分组的列将返回多个可能的值。

    如果您开始使用以下方式向分组中添加更多列:

    T.group(T.columns.collect(&:name))
    

    然后你会按照你不想要的东西进行分组,最终你会拉出整个桌子,这不是你想要的。如果您尝试聚合以避免分组问题,您最终会混合不同的行(即,一列将来自一行,而另一列将来自另一行),这也不是您想要的。

    ActiveRecord 确实不是为这类事情而构建的,但您可以根据自己的意愿对其进行一些努力。

    您正在使用 AR,因此您可能有一个 id 列。如果您有 PostgreSQL 8.4 或更高版本,那么您可以使用 window functions 作为一种本地化的 GROUP BY;您需要窗口两次:一次找出 name/thedate 对,然后再选择一个 id(以防万一您有多行具有相同的 namethedate 匹配最早的thedate),因此得到一个唯一的行:

    select your_table.*
    from your_table
    where id in (
        -- You don't need DISTINCT here as the IN will take care of collapsing duplicates.
        select min(yt.id) over (partition by yt.name)
        from (
            select distinct name, min(thedate) over (partition by name) as thedate
            from your_table
        ) as dt
        join your_table as yt
          on yt.name = dt.name and yt.thedate = dt.thedate
    )
    

    然后将其包装在 find_by_sql 中,您就拥有了您的对象。

    如果您将 Heroku 与共享数据库(或其他没有 8.4 或更高版本的环境)一起使用,那么您将被PostgreSQL 8.3 卡住,并且您将没有窗口函数。在这种情况下,您可能希望过滤掉 Ruby-land 中的重复项:

    with_dups = YourTable.find_by_sql(%Q{
        select yt.*
        from your_table yt
        join (select name, min(thedate) as thedate from your_table group by name) as dt
          on yt.name = dt.name and yt.thedate = dt.thedate
    });
    
    # Clear out the duplicates, sorting by id ensures consistent results
    unique_matches = with_dups.sort_by(&:id).group_by(&:name).map { |x| x.last.first }
    

    如果您非常确定不会出现重复的 name/min(thedate) 对,那么兼容 8.3 的解决方案可能是您的最佳选择;但是,如果会有很多重复,那么您希望数据库做尽可能多的工作以避免创建数千个您将要丢弃的 AR 对象。

    也许其他比我更强大的 PostgreSQL-Fu 的人会出现并提供更好的东西。

    【讨论】:

    • @apneadiving:在你的“挑战”之后,我不得不接受 :)
    • 这个答案终于帮助我理解了 PostgreSQL 内部发生的这种查询。感谢您的详细回答。
    • @johnny.rodgers:酷。理解是好的,这就是我们都在这里的原因:)
    【解决方案2】:

    我不关心当有多个名称时检索哪一行(这将适用于所有列)并且表具有该结构,您可以简单地执行类似的查询

    SELECT * FROM table_name GROUP BY `name` ORDER BY `date`
    

    或在 Rails 中

    TableClass.group(:name).order(:date)
    

    【讨论】:

    • 当我执行该方法时,我收到以下错误:列“games.id”必须出现在 GROUP BY 子句中或用于聚合函数中
    • 不要投票,而是更好地解释您的结构,从您的问题来看,您似乎只有一个表,由于该错误消息,这是不正确的。而是发布您的完整结构。
    • 目前整个数据库只有一个表。我仍然得到那个错误。
    • 您应该发布完整的结构和完整的查询。您是否正在使用带有 SUM、AVERAGE、MAX 或类似功能的 games.id 列?如果是这样,那就是错误的原因。
    【解决方案3】:

    获取名称和最短日期列表,并将其连接回原始表以获取您要查找的行集。

    select
        b.*
    from
        (select name, min(date) as mindate from table group by name) a
        inner join table b
            on  a.name = b.name and a.mindate = b.date
    

    【讨论】:

    • 如果name, min(date) 对在表中出现两次,则存在唯一性问题。
    【解决方案4】:

    我知道这个问题已经 8 岁了。当前的 ruby​​ 版本是2.5.32.6.1 已发布。 Rails 稳定版是5.2.26.0.0 beta2 已发布。

    让我们将您的表命名为Person

    Person.all.order(:date).group_by(&:name).map{|p| p.last.last}
    
    Person.all.order(:date).group_by(&:name).collect {|key, value| value.last}
    

    说明:首先获取person表中的所有记录。然后按日期(降序或升序)排序,然后按名称分组(重复名称的记录将被分组)。

    Person.all.order(:date).group_by(&:name)
    

    这会返回哈希。

    {"Eric" => [#<Person id: 1, name: "Eric", other_fields: "">, #<Person id: 2, name: "Eric", other_fields: "">], "Joe" => [#<Person id: 3, name: "Joe", other_fields: "">]}
    

    解决方案 1: .map 方法。

    Person.all.order(:date).group_by(&:name).map{|p| p.last.last}
    

    我们得到了哈希。我们将其作为数组循环。 p.last会给

    [[#<Person id: 1, name: "Eric", other_fields: "">, #<Person id: 2, name: "Eric", other_fields: "">],[#<Person id: 3, name: "Joe", other_fields: "">]]
    

    使用p.last.firstp.last.last 获取嵌套数组的第一条或最后一条记录。

    解决方案 2: .collect.each 方法。

    Person.all.order(:date).group_by(&:name).collect {|key, value| value.last}
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-18
      • 2010-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多