Rails расширяет поля с помощью scope, PG не нравится - PullRequest
8 голосов
/ 25 апреля 2011

У меня есть модель виджетов.Виджеты принадлежат модели Магазина, которая принадлежит модели Области, которая принадлежит Компании.В модели компании мне нужно найти все связанные виджеты.Легко:

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: ОШИБКА: должен появиться столбец "widgets.id"в предложении GROUP BY или для использования в агрегатной функции LINE 1: SELECT "widgets". "id" AS t0_r0, "widgets". "user_id ...

Как переписать виджетОбласть .in_company, чтобы AR не расширял запрос на выборку, чтобы включить каждое поле модели виджета?

Ответы [ 5 ]

10 голосов
/ 24 мая 2011

Как объяснил Фрэнк, PostgreSQL отклонит любой запрос, который не возвращает воспроизводимый набор строк.

Предположим, у вас есть запрос типа:

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

PostgreSQL отклонит егопотому что b не указано в операторе group by.Запустите это в MySQL, напротив, и оно будет принято.В последнем случае, однако, запускаются несколько вставок, обновлений и удалений, и порядок строк на страницах диска оказывается другим.

Если память служит, детали реализации таковы, что MySQL фактически будет сортировать поа, б и вернуть первый б в наборе.Но что касается стандарта SQL, его поведение не определено - и, несомненно, PostgreSQL не всегда сортирует перед запуском агрегатных функций.

Потенциально это может привести к различным значениямb в наборе результатов в PostgreSQL.И, таким образом, PostgreSQL выдает ошибку, если вы не будете более конкретны:

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

Что выделил Фрэнк, так это то, что в PostgreSQL 9.1, если a является первичным ключом, вы можете оставить b неуказанным- планировщика научили игнорировать последующую группу по полям, когда применимые первичные ключи подразумевают уникальную строку.

В частности, для вашей проблемы вам нужно указать свою группу, как вы в настоящее время делаете, плюс каждое поле, на котором вы основываете агрегат, то есть "widgets"."id", "widgets"."user_id", [snip], но не такие вещи, как sum(amount), которые являются вызовами агрегатных функций.

Как примечание, не относящееся к теме, я неВы уверены, что ваша ORM / модель работает, но генерируемый SQL не оптимален.Многие из тех левых внешних объединений кажутся внутренними соединениями.Это позволит планировщику выбрать соответствующий порядок соединения, где это применимо.

3 голосов
/ 18 мая 2011

PostgreSQL версии 9.1 ( бета на данный момент ) может решить вашу проблему, но только при наличии функциональной зависимости от первичного ключа.

Из примечаний к выпуску:

Разрешить не-GROUP BY столбцы в списке целей запроса, если первичный ключ указан в предложении GROUP BY (Питер Эйзентраут)

Некоторые другие системы баз данных уже допускали такое поведение, и из-запервичный ключ, результат однозначный.

Вы можете запустить тест и посмотреть, решит ли он вашу проблему.Если вы можете дождаться выпуска продукта, это может решить проблему без изменения кода.

2 голосов
/ 23 мая 2011

Во-первых, упростите свою жизнь, сохраняя все даты в стандартном часовом поясе.Изменение даты с часовыми поясами действительно должно быть сделано в представлении для удобства пользователя.Одно это должно избавить вас от многих трудностей.

Если вы уже в работе, напишите миграцию, чтобы создать столбец normalised_date, где бы это ни было полезно.

nr Я предлагаю другимпроблема здесь заключается в использовании сырого SQL, который вам не подойдет.Чтобы избежать этого, попробуйте использовать гем под названием Squeel (aka Metawhere 2) http://metautonomo.us/projects/squeel/

Если вы используете это, вы сможете удалить жестко запрограммированный SQL и позволить рельсам вернуться к своей магии.

Например:

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

становится (как только вы уберете необходимость нормализации даты):

.select{sum(amount).as(total_amount)}
0 голосов
/ 05 ноября 2014

сортировка в 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]
0 голосов
/ 27 мая 2011

Извините, что отвечаю на мой собственный вопрос, но я понял это.

Во-первых, позвольте мне извиниться перед теми, кто думал, что у меня может быть проблема с SQL или Postgres, это не так.Проблема связана с ActiveRecord и генерируемым им SQL.

Ответ таков: используйте .joins вместо .includes .Поэтому я просто изменил строку в верхнем коде, и она работает, как и ожидалось.

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

Я предполагаю, что при использовании .include ActiveRecord пытается быть умным и использовать JOINS в SQL, но этонедостаточно умен для этого конкретного случая и генерировал этот уродливый SQL для выбора всех связанных столбцов.

Однако все ответы научили меня немного о Postgres, которого я не знал, так что большое спасибо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...