1

这是我在使用 PostgreSQL 9.1 和 Rails 3.0.7 时遇到的最令人费解的问题。我在同一个 Ubuntu 10.04 vbox 客户机上运行 Postgres 和 Rails 3:

"PostgreSQL 9.1.3 on i686-pc-linux-gnu, compiled by gcc-4.4.real (Ubuntu 4.4.3-4ubuntu5) 4.4.3, 32-bit"

可能应用程序需要运行一个基于 created_at 时间戳的简单查询,该时间戳是基于服务器的时区的一天。就我而言,东部时间。我尝试了“with time zone”和“at time zone”的不同变体,并让它在 pgAdmin 中正常工作,但在 Rails 网站中却没有。

这是带有 ARel 查询的类方法。

def self.transactions2(business_id, begin_date, end_date )
trans = LogVouchers.select( %{
      created_at
  } ).
where( %{
     created_at >= ( TIMESTAMP WITH TIME ZONE ? at time zone 'utc')::timestamp
    and created_at < (TIMESTAMP WITH TIME ZONE ? at time zone 'utc')::timestamp
  },
     begin_date,
    end_date  )

end

这是控制器代码:

  def voucher_transactions
    default_date = Time.zone.now.beginning_of_day

    @end_date = params[:end_date].blank? ? \
      Date.new(default_date.year, default_date.month,  default_date.day )  : \
        Date.new(params[:end_date][:year].to_i, params[:end_date][:month].to_i, \
        params[:end_date][:day].to_i)

    @begin_date = params[:begin_date].blank? ? \
      Date.new(default_date.year, default_date.month,  default_date.day )   : \
        @begin_date = Date.new(params[:begin_date][:year].to_i, \
        params[:begin_date][:month].to_i, params[:begin_date][:day].to_i)

    @data2  = LogVouchers.transactions2(business_id, @begin_date, @end_date + 1.day)
    ...
    end

这是 rails 日志文件中的实际 SQL 代码和调试检查:

      LogVouchers Load (0.7ms)  SELECT 
 created_at
 FROM "log_vouchers" WHERE (
 created_at >= ( TIMESTAMP WITH TIME ZONE '2012-04-28' at time zone 'utc')::timestamp
 and created_at < (TIMESTAMP WITH TIME ZONE '2012-04-29' at time zone 'utc')::timestamp
 )

  ### 2012-05-01 21:18:04 -0400  voucher_transactions() @data2 =[
  #<LogVouchers created_at: "2012-04-28 03:14:29">
, #<LogVouchers created_at: "2012-04-28 03:15:24">
, #<LogVouchers created_at: "2012-04-28 03:18:19">
, #<LogVouchers created_at: "2012-04-28 03:38:35">
, #<LogVouchers created_at: "2012-04-28 03:58:08">
, #<LogVouchers created_at: "2012-04-28 15:44:46">
, #<LogVouchers created_at: "2012-04-28 17:12:20">
, #<LogVouchers created_at: "2012-04-28 18:46:45">
, #<LogVouchers created_at: "2012-04-28 18:55:47">
, #<LogVouchers created_at: "2012-04-28 18:57:52">
, #<LogVouchers created_at: "2012-04-28 19:02:15">
, #<LogVouchers created_at: "2012-04-28 19:02:50">
, #<LogVouchers created_at: "2012-04-28 19:46:36">
, #<LogVouchers created_at: "2012-04-28 19:47:17">
, #<LogVouchers created_at: "2012-04-28 19:50:22">
, #<LogVouchers created_at: "2012-04-28 19:50:56">]

但是,如果我直接在 pgAdmin 的查询窗口上运行 SQL 代码,我会得到正确的结果:

"2012-04-28 15:44:46.641305"
"2012-04-28 17:12:20.615641"
"2012-04-28 18:46:45.277561"
"2012-04-28 18:55:47.40109"
"2012-04-28 18:57:52.616501"
"2012-04-28 19:02:15.542964"
"2012-04-28 19:02:50.888847"
"2012-04-28 19:46:36.16556"
"2012-04-28 19:47:17.084047"
"2012-04-28 19:50:22.672805"
"2012-04-28 19:50:56.376571"

请注意,UTC 时间“2012-04-28 03:14:29”附近的 created_at 的 5 条记录不在正确的结果集中。

现在最令人费解的问题是:
Rails 如何将相同的 SQL 语句发送到 Postgres 数据库并得到不同的结果?

我显然在这里遗漏了一些东西。有大神可以帮忙吗?

附加说明 2012-5-2

背景:默认情况下,rails 3.0 在所有 db 表中生成 created_at 列作为 Postgres 中的“没有时区的时间戳”。我需要根据我的服务器时区“美国/纽约”运行每日报告。由于涉及的表数量,我使用 ARel select 方法来连接多个表并传入 begin_date 和 end_date 时间戳。我很难得到正确的结果集返回。它应该返回带有 UTC 时间戳的记录:

2012-04-28 04:00:00    to   2012-04-29 04:00:00    

但它返回了带有 UTC 时间戳的记录:

2012-04-28 00:00:00    to   2012-04-29 00:00:00    

我能想到的是改变类方法:

def self.transactions2(business_id, begin_date, end_date )

st_tz_offset      =   Time.zone.now.formatted_offset(true)
st_begin_date_tz  =   "#{begin_date} 00:00:00#{st_tz_offset}"
st_end_date_tz    =   "#{end_date} 00:00:00#{st_tz_offset}"

trans = LogVouchers.select( %{
      created_at
  } ).
where( %{
     created_at >= ( TIMESTAMP WITH TIME ZONE ? at time zone 'utc')
    and created_at < (TIMESTAMP WITH TIME ZONE ? at time zone 'utc')
  },
     st_begin_date_tz  ,
    st_end_date_tz  )

end

实际上,它告诉 Postgres 在比较之前将其转换timestamp with time zonetimestamp without time zone。这对我有用。我知道这太复杂了。我非常感谢有人指出实现这一目标的更好方法。

附加说明 2

  • 如果可能,我会避免更改 Ubuntu/Linux 服务器、Postgres 服务器中的默认设置,因为它往往会导致其他副作用。
  • 将来,这些每日截止时间戳将基于服务器的TimeZone设置current_user而不是TimeZone服务器的设置。因为同一个 Web 应用程序的用户可以跨越多个时区,并且每日报告必须对各自的登录用户有意义。
4

1 回答 1

3

您的表达取决于当地时间设置。这:

SET  timezone = 'EST';
SELECT TIMESTAMP WITH TIME ZONE '2012-04-29' AT TIME ZONE 'UTC';

返回与此不同的时间戳:

SET  timezone = '-2';
SELECT TIMESTAMP WITH TIME ZONE '2012-04-29' AT TIME ZONE 'UTC';

显然,您在来自 pgAdmin 的连接中的时间设置与在来自 ARel 的连接中的时间设置不同。通过以下方式了解:

SHOW timezone;

timezone在 中定义postgresql.conf,但可以随时设置为不同的值。您可以使用以下命令将其重置为默认值:

RESET timezone;

我在最近的回答中更详细地解释了 PostgreSQL 对时间戳和时区的处理。手册中有关时区的
更多信息。


如果您想要“UTC 午夜”,请改用以下表达式:

SELECT TIMESTAMP '2012-04-29' AT TIME ZONE 'UTC';

或者简单地说:

SELECT '2012-04-29 0:0 +0'::timestamptz;

无论设置如何,这都会返回相同的内部值timezone。不过,它将根据当地时区显示。因此,使用 EST,您将看到:

2012-04-28 19:00:00-05

无时区

根据您的附加信息,您的表包含类型的数据timestamp without time zone- 或者简单地说timestamp,默认为without time zone. 这些时间戳隐含地基于EST.

如果您想检索一天的数据,从午夜开始,在午夜结束,与您当前所在的时区无关,请不要使用数据类型timestamp with time zone(或timestamptz)。只需使用timestamp,一切都很时髦。服务器应该得到:

WHERE created_at >= timestamp '2012-04-28 0:0'
AND   created_at <  timestamp '2012-04-29 0:0'

或者:

WHERE created_at >= '2012-04-28 0:0'::timestamp
AND   created_at <  '2012-04-29 0:0'::timestamp

甚至只是:

WHERE created_at >= '2012-04-28 0:0'
AND   created_at <  '2012-04-29 0:0'

如果您想要根据当前时区的不同数据片段,那么服务器应该得到这个:

WHERE created_at >= '2012-04-28 0:0'::timestamptz AT TIME ZONE 'EST'
AND   created_at <  '2012-04-29 0:0'::timestamptz AT TIME ZONE 'EST'

该术语'2012-04-28 0:0'::timestamptz AT TIME ZONE 'EST'在当地午夜时计算纽约的时间(没有时区)。

或者,您可以相应地计算应用程序中的时间戳,并使用上述不带时区的查询。

于 2012-05-02T02:13:55.870 回答