2011-04-25 45 views
8

我有一個Widgets模型。小部件屬於Store模型,屬於Area模型,屬於公司。在公司模型中,我需要找到所有關聯的小部件。易:Rails使用示波器擴展字段,PG不喜歡它

class Widget < ActiveRecord::Base 
    def self.in_company(company) 
    includes(:store => {:area => :company}).where(:companies => {:id => company.id}) 
    end 
end 

將產生這個美麗的查詢:

> Widget.in_company(Company.first).count 

SQL (50.5ms) SELECT COUNT(DISTINCT "widgets"."id") FROM "widgets" LEFT OUTER JOIN "stores" ON "stores"."id" = "widgets"."store_id" LEFT OUTER JOIN "areas" ON "areas"."id" = "stores"."area_id" LEFT OUTER JOIN "companies" ON "companies"."id" = "areas"."company_id" WHERE "companies"."id" = 1 
=> 15088 

不過,我以後需要在更復雜的範圍內使用這個範圍。問題在於AR通過選擇單個字段來擴展查詢,這些字段在PG中失敗,因爲所選字段必須位於GROUP BY子句或聚合函數中。

這是更復雜的範圍。

def self.sum_amount_chart_series(company, start_time) 
    orders_by_day = Widget.in_company(company).archived.not_void. 
        where(:print_datetime => start_time.beginning_of_day..Time.zone.now.end_of_day). 
        group(pg_print_date_group). 
        select("#{pg_print_date_group} as print_date, sum(amount) as total_amount") 

end 

def self.pg_print_date_group 
    "CAST((print_datetime + interval '#{tz_offset_hours} hours') AS date)" 
end 

,這是它是扔在PG選擇:

> Widget.sum_amount_chart_series(Company.first, 1.day.ago) 

SELECT "widgets"."id" AS t0_r0, "widgets"."user_id" AS t0_r1,<...BIG SNIP, YOU GET THE IDEA...> FROM "widgets" LEFT OUTER JOIN "stores" ON "stores"."id" = "widgets"."store_id" LEFT OUTER JOIN "areas" ON "areas"."id" = "stores"."area_id" LEFT OUTER JOIN "companies" ON "companies"."id" = "areas"."company_id" WHERE "companies"."id" = 1 AND "widgets"."archived" = 't' AND "widgets"."voided" = 'f' AND ("widgets"."print_datetime" BETWEEN '2011-04-24 00:00:00.000000' AND '2011-04-25 23:59:59.999999') GROUP BY CAST((print_datetime + interval '-7 hours') AS date) 

生成該錯誤:

PGError: ERROR: column "widgets.id" must appear in the GROUP BY clause or be used in an aggregate function LINE 1: SELECT "widgets"."id" AS t0_r0, "widgets"."user_id...

如何重寫Widget.in_company範圍,使AR做不擴展選擇查詢以包含每個Widget模型字段?

+0

Rails 3的可能?我曾在Rails3中/ Heroku的一個簡單的情況下,我沒有選擇特定的列 - 所以在做一個選擇*,然後得到這個錯誤 - 通過將特定的列選擇固定的 - 但你正在做的.... :( – 2011-05-20 07:28:44

回答

3

PostgreSQL版本9.1(beta at this moment)可能會解決您的問題,但前提是隻有在主鍵上存在功能依賴關係。

從發行說明:

Allow non-GROUP BY columns in the query target list when the primary key is specified in the GROUP BY clause (Peter Eisentraut)

Some other database system already allowed this behavior, and because of the primary key, the result is unambiguous.

您可以運行一個測試,看看它是否解決您的問題。如果您可以等待產品發佈,則可以在不更改代碼的情況下解決問題。

+0

雖然這確實說明了這個問題,對於許多Rails開發人員來說,升級到更高版本的PostgreSQL並不是一個可行的選擇,就像Heroku在PostgreSQL 8.3上運行一樣。我會獎勵我的賞金給那些能夠拿出一個簡單例子的人給出了同樣的錯誤以及另一種編寫查詢的方式以獲得相同的預期信息。如果有多人交付,賞金將由誰來提供對錯誤的更清晰解釋以及如何避免它在未來 – 2011-05-18 19:40:46

+0

@Eric:現在更清楚嗎? – 2011-05-24 07:21:27

2

首先通過將所有日期存儲在標準時區來簡化您的生活。爲了方便用戶,更改日期和時區應該在視圖中完成。這本身應該可以爲你節省很多痛苦。

如果您已在生產中,請編寫一個遷移程序以創建normalised_date列,只要它有幫助。

nrI建議這裏的另一個問題是使用原始SQL,哪些rails不會爲你動手。爲了避免這種嘗試,使用名爲Squeel(又名Metawhere 2)的寶石http://metautonomo.us/projects/squeel/

如果你使用這個,你應該能夠刪除硬編碼的SQL,並讓rails重拾神奇。

例如:

.select("#{pg_print_date_group} as print_date, sum(amount) as total_amount") 

變(一旦你刪除需要正常化日期):

.select{sum(amount).as(total_amount)} 
10

弗蘭克解釋說,PostgreSQL將拒絕不返回任何重複性查詢一組行。

假設你喜歡的查詢:

select a, b, agg(c) 
from tbl 
group by a 

PostgreSQL將拒絕它,因爲bgroup by聲明中未指定。相反,在MySQL中運行它,它將被接受。然而,在後一種情況下,引發一些插入,更新和刪除操作,並且磁盤頁面上的行的順序變得不同。

如果提供內存服務,實現細節是這樣的,MySQL將實際按a,b排序並返回集合中的第一個b。但就SQL標準而言,這種行爲沒有具體說明,並且確實如此,PostgreSQL在運行聚合函數之前總是排序而不是

這可能會導致PostgreSQL中結果集中的b的值不同。 - 規劃者已經

select a, b, agg(c) 
from tbl 
group by a, b 

什麼弗蘭克強調的是,在PostgreSQL的9.1,如果a是主鍵,比你可以離開b不明:就這樣,PostgreSQL的,除非你是更具體的會產生錯誤當適用的主鍵意味着唯一的行時,教導忽略隨後的字段組。

特別是對於你的問題,你需要爲你做目前指定的組,加上,你的基礎上彙總,即"widgets"."id", "widgets"."user_id", [snip]但不是這樣的東西sum(amount),這是聚合函數調用各個領域。

作爲偏離主題的一面,我不確定你的ORM /模型是如何工作的,但它生成的SQL並不是最優的。許多這些離開外部連接似乎應該是內部連接。這將導致計劃者在適用的情況下選擇適當的連接次序。

+0

我要求的一切都寫得很清楚,謝謝! – 2011-05-24 08:28:14

0

對不起回答我自己的問題,但我想通了。

首先,讓我向那些認爲我可能有SQL或Postgres問題的人道歉,事實並非如此。問題在於ActiveRecord和它正在生成的SQL。

答案是...使用。加入而不是。包括。所以我只是改變了最上面的代碼行,它按預期工作。

class Widget < ActiveRecord::Base 
    def self.in_company(company) 
    joins(:store => {:area => :company}).where(:companies => {:id => company.id}) 
    end 
end 

我猜使用.includes時,ActiveRecord的是試圖將智能和使用的SQL連接,但它並不適合這種特殊情況下足夠聰明,並生成難看SQL來選擇所有相關的列。

但是,所有的答覆都教會了我很多關於Postgres的知識,所以我非常感謝。

0

排序在mysql中:

> ids = [11,31,29] 
=> [11, 31, 29] 
> Page.where(id: ids).order("field(id, #{ids.join(',')})") 
Postgres裏

def self.order_by_ids(ids) 
    order_by = ["case"] 
    ids.each_with_index.map do |id, index| 
    order_by << "WHEN id='#{id}' THEN #{index}" 
    end 
    order_by << "end" 
    order(order_by.join(" ")) 
end 

User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id) 
#=> [3,2,1]