【问题标题】:Elegant PostgreSQL Group by for Ruby on Rails / ActiveRecord优雅的 PostgreSQL Group by for Ruby on Rails / ActiveRecord
【发布时间】:2011-03-31 13:11:06
【问题描述】:

尝试使用 PostgreSQL 检索按日期分组的 ActiveRecord 对象数组。

更具体地说,我正在尝试翻译以下 MySQL 查询:

@posts = Post.all(:group => "date(date)", 
   :conditions => ["location_id = ? and published = ?", @location.id, true], 
   :order => "created_at DESC")

我知道 PostgreSQL 对 SQL 标准的解释比 MySQL 更严格,因此这种类型的查询将无法工作......并且已经阅读了 StackOverflow 和其他地方关于该主题的许多帖子 - 但没有一个似乎是关于这个问题的明确答案

我已经尝试了各种查询与 group by 和 distinct 子句的组合,但没有太多乐趣 - 目前我有一个相当不雅的 hack,虽然有效,但当我看到它时让我脸红。

使用 Rails 和 PostgreSQL 进行此类查询的正确方法是什么? (忽略这个肯定应该在 ActiveRecord 级别抽象出来的事实)

【问题讨论】:

  • 一个“数组...按日期分组”——这没有任何意义。你想达到什么目的?您可以按日期(日期)订购吗?
  • 除 MySQL 以外的任何数据库都会拒绝非法 SQL。数据库不会猜测您今天想要什么结果,数据库应该只在所有情况下得到所有正确的结果。在 MySQL 中使用 ONLY_FULL_GROUP_BY,上述查询也会被 MySQL 拒绝。
  • 嗨 Dan - 我正在尝试获取 Post 对象数组,但我只想检索任何给定日期的一个帖子(当天的最新帖子)。
  • 并不是说它更严格,而是 MySQL 愿意对一些常见的行为(如自动增量等)做出假设。如果您考虑数据库必须做什么才能检索有序组中的第一行,它基本上是: 1. 检索按组列排序的整个集合 2. 按顺序对组列的每个子集进行排序第 3 条。将每个子集的第一行作为一个全新的集合,然后按顺序对其进行排序 PG 只是不做假设,因此您可以对子查询显式地做同样的事情,这在 PG 中非常有效。跨度>

标签: ruby-on-rails postgresql activerecord


【解决方案1】:

您要在此处使用的 PostgreSQL 功能是 DISTINCT ON。通过 ActiveRecord 进行此查询有两种基本方法。

第一种方法是只指定:select:order 选项。当您有一个没有:joins:include 的相当简单的查询时,这非常有用。

Post.all(
  :select => 'DISTINCT ON (date::date) *',
  :order => 'date::date DESC, created_at DESC'
)

如果您有一个更复杂的查询,ActiveRecord 会生成自己的SELECT 子句,您可以使用子查询来选择目标记录。

Post.all(
  :joins => 'INNER JOIN (SELECT DISTINCT ON (date::date) id FROM posts ORDER BY date::date DESC, created_at DESC) x ON x.id = posts.id'
)

请注意,这可能比第一种方法慢一些,具体取决于您的数据。如果需要,我只会使用这种方法。请务必使用类似生产的数据进行基准测试。

【讨论】:

    【解决方案2】:

    我的解决方案:

    def self.columns_list
       column_names.collect { |c| "#{table_name}.#{c}" }.join(",")
     end
    
     scope :selling, joins(:products).group(columns_list)
    

    简单且可重复。

    【讨论】:

      【解决方案3】:

      虽然 SQL 在回答诸如“每天最近的帖子是什么时候?”之类的问题时非常简单。当你问“每天最新的帖子是什么?”时,这不是很直接。

      如果不使用子 SELECT(或多个 SQL 语句),您将无法检索每天的最新帖子。这可能对你有用(使用 Post.find_by_sql 或类似的):

      SELECT P.*, M.just_day, M.max_created_at
      FROM posts P
      JOIN (
        SELECT date(P2.date) AS just_day, MAX(P2.created_at) AS max_created_at
        FROM posts P2
        P.location_id='12345' AND P.published=true
        GROUP BY date(P2.date)
      ) AS M  
         ON AND M.max_created_at = P.created_at
      WHERE P.location_id='12345' AND P.published=true
      

      上面的SQL语句应该足够了如果你可以确定两个帖子在created_at列中不会有相同的值。如果您不能保证创建的 at 列的唯一性,那么您要么需要过滤掉 Ruby 中的重复项(这不应该太低效,因为大概您将在列表中循环)或者您需要做 N +1 SQL 语句。 (实际上你可以进行每行选择,但 AFAIK 与 N+1 SQL 语句一样低效。)

      以下是循环时删除重复项的方法:

      last_post = nil
      posts.each do |post|
        unless post.just_day == last_past.try(:just_day)
          # Do stuff
          last_post = post
        end
      end
      

      也就是说,如果你有足够的天数来保证每天的 SELECT 还不错的话,你可以只用 Ruby/ActiveRecord 很好地编写它:

      days = Post.group("date(date)")
      posts = days.each { |day| Post.order('created DESC').where("date(day) = ?", day) }
      

      如果您使用分页(比如每页 10 个项目),那么每页需要 11 个 SQL 语句。不是想法,但简单可能值得低效。

      老实说,如果您希望此查询经常运行并且具有相当大的数据集,那么我建议您添加一个名为 most_recent 的布尔列。过去几天的最后一个帖子不会改变。你只需要担心今天的帖子。只需设置一个 cron 作业在一天结束后几分钟运行,以更新最后一天的值。如果你想要更新的东西,你可以让 cron 作业每 5 分钟运行一次。或者,如果您需要实时,则添加一个 after_save 回调,为今天所有非当前帖子的帖子设置 most_recent 为 false。

      这个问题类似:MySQL: Getting highest score for a user

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-02-11
        • 1970-01-01
        • 2023-03-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-01-21
        • 1970-01-01
        相关资源
        最近更新 更多