使用Ruby on Rails进行Postgres公用表表达式查询

时间:2014-12-15 19:46:58

标签: ruby-on-rails postgresql ruby-on-rails-4 common-table-expression

我正在尝试在Rails应用程序中找到使用Common Table Expressions进行Postgres查询的最佳方法,因为他们知道显然ActiveRecord不支持CTE。

我有一个名为user_activity_transitions的表,其中包含一系列正在启动和停止的用户活动记录(每行指的是状态更改:例如启动或停止)。

一个user_activity_id可能会有很多情侣开始停止,这两对情节分为2个不同的行。 如果活动当前正在进行并且尚未停止,那么也可能只有“已启动”。 sort_key从第一个状态开始为0,每个状态变化以10为增量。

id      to_state     sort_key     user_activity_id    created_at
1       started      0            18                  2014-11-15 16:56:00
2       stopped      10           18                  2014-11-15 16:57:00
3       started      20           18                  2014-11-15 16:58:00
4       stopped      30           18                  2014-11-15 16:59:00
5       started      40           18                  2014-11-15 17:00:00

我想要的是以下输出,将启动停止的夫妇分组在一起以便能够计算持续时间等。

user_activity_id     started_created_at      stopped_created_at
18                   2014-11-15 16:56:00     2014-11-15 16:57:00
18                   2014-11-15 16:58:00     2014-11-15 16:59:00
18                   2014-11-15 17:00:00     null

表的实现方式使得运行该查询变得更加困难,但对于未来的更改(例如新的中间状态)更加灵活,因此不会对其进行修改。

我的Postgres查询(以及Rails中的相关代码):

query = <<-SQL
    with started as (
    select 
        id,
        sort_key,
        user_activity_id,
        created_at as started_created_at
    from
        user_activity_transitions
    where  
        sort_key % 4 = 0
    ), stopped as (
    select 
        id,
        sort_key-10 as sort_key2,
        user_activity_id,
        created_at as stopped_created_at
    from
    user_activity_transitions
    where
        sort_key % 4 = 2
    )
    select
        started.user_activity_id AS user_activity_id,
        started.started_created_at AS started_created_at,
        stopped.stopped_created_at AS stopped_created_at
    FROM
        started
    left join stopped on stopped.sort_key2 = started.sort_key
    and stopped.user_activity_id = started.user_activity_id
SQL

results = ActiveRecord::Base.connection.execute(query)

它的作用是“欺骗”SQL根据排序键的模数检查连接2个连续的行。

查询工作正常。但是使用这个原始AR调用会让我很烦,特别是因为connection.execute返回的内容非常混乱。我基本上需要遍历结果并将其放入正确的哈希值。

2个问题:

  1. 有没有办法摆脱CTE并运行相同的查询 Rails魔术?
  2. 如果没有,是否有更好的方法可以在漂亮的哈希中获得我想要的结果?
  3. 请记住,我对Rails很新,而不是查询专家,因此可能会有明显的改进......

    非常感谢!

2 个答案:

答案 0 :(得分:3)

  

我正在尝试在Rails应用程序中找到使用Common Table Expressions进行Postgres查询的最佳方法,因为我们知道ActiveRecord显然支持CTE。

据我所知,ActiveRecord不支持CTE。引擎盖下使用的Arel支持它们,但它们不会暴露在AR的界面中。

  

有没有办法摆脱CTE并使用Rails魔法运行相同的查询?

不是真的。您可以在AR的API中编写它,但您只需将相同的SQL拆分写入一些方法调用。

  

如果没有,是否有更好的方法可以在漂亮的哈希中获得我想要的结果?

我试图运行查询,我得到以下内容对我来说似乎很好。你得到了不同的结果吗?

[
  {"user_activity_id"=>"18", "started_created_at"=>"2014-11-15 16:56:00", "stopped_created_at"=>"2014-11-15 16:57:00"},
  {"user_activity_id"=>"18", "started_created_at"=>"2014-11-15 16:58:00", "stopped_created_at"=>"2014-11-15 16:59:00"},
  {"user_activity_id"=>"18", "started_created_at"=>"2014-11-15 17:00:00", "stopped_created_at"=>nil}
]

我假设你有一个名为UserActivityTransition的模型用于操纵数据。您也可以使用该模型来获得结果。

results = UserActivityTransition.find_by_sql(query)
results.size # => 3
results.first.started_created_at # => 2014-11-15 16:56:00 UTC

请注意,在检查结果时,这些“虚拟”属性将不可见,但它们就在那里。

答案 1 :(得分:1)

尽管Rails不直接支持CTE,但是您可以模拟单个CTE并仍然利用ActiveRecord。代替CTE,使用from子查询。

Thing
  .from(
    # Using a subquery in place of a single CTE
    Thing
      .select(
        '*',
        %{row_number() over(
            partition by
              this, that
            order by
              created_at desc
          ) as rank
        }
      )
    :things
  )
  .where(rank: 1)

这与...并不完全相同,但等同于...

with ranked_things as (
  select
    *,
    row_number() over(
      partition by
        this, that
      order by
        created_at desc
    ) as rank
)
select *
from ranked_things
where rank = 1