查找关联计数大于零的所有记录


98

我正在尝试做一些我认为很简单但似乎并非如此的事情。

我的项目模型有很多职位空缺。

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

我要获得所有至少有1个空缺的项目。我尝试过这样的事情:

Project.joins(:vacancies).where('count(vacancies) > 0')

但它说

SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0)

Answers:


65

joins默认情况下使用内部Project.joins(:vacancies)联接,因此使用实际上只会返回具有关联空缺的项目。

更新:

正如@mackskatz在评论中指出的那样,如果没有group子句,则上面的代码将为空缺多个的项目返回重复的项目。要删除重复项,请使用

Project.joins(:vacancies).group('projects.id')

更新:

正如@Tolsee指出的那样,您也可以使用distinct

Project.joins(:vacancies).distinct

举个例子

[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""

1
但是,如果不应用group by子句,这将为具有多个空缺的项目返回多个Project对象。
mackshkatz

1
但是,不会生成有效的SQL语句。
大卫·奥尔德里奇

好吧,这就是Rails。如果您可以提供sql答案(并解释为什么这样做效率不高),则可能会更有帮助。
jvnill

您如何看待Project.joins(:vacancies).distinct
Tolsee '19

1
这是@Tolsee btw:D
Tolsee '19

167

1)获得至少1个空缺的项目:

Project.joins(:vacancies).group('projects.id')

2)要获得空缺超过1个的项目:

Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')

3)或者,如果Vacancy模型设置了计数器缓存:

belongs_to :project, counter_cache: true

那么这也将起作用:

Project.where('vacancies_count > ?', 1)

vacancy可能需要手动指定变形规则?


2
应该不是Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')吗?查询空缺数量而不是项目ID
Keith Mattix '18

不,@KeithMattix,它应该不会。它可以是,但是,如果它读取更好的给你; 这是一个偏好问题。可以使用连接表中保证在每一行中都有一个值的任何字段来完成计数。最有意义的候选人projects.idproject_idvacancies.id。我选择计数project_id是因为它是进行联接的字段;如果愿意的话,加入的脊椎。这也提醒我这是一个联接表。
阿尔塔

36

是的,vacancies不是加入的领域。我相信你想要:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")

16
# None
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')

5

对has_many表执行内部联接,将其与group或组合在一起uniq可能会非常低效,而在SQL中,将其更好地实现为EXISTS与相关子查询一起使用的半联接。

这使查询优化器可以探测空位表,以检查是否存在具有正确的project_id的行。无论是一行还是一百万个都具有该project_id,这无关紧要。

在Rails中并不是那么简单,但是可以通过以下方式实现:

Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)

同样,找到所有没有空缺的项目:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)

编辑:在最新的Rails版本中,您会收到不赞成使用的警告,告诉您不要依赖exists委托给arel。使用以下方法解决此问题:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)

编辑:如果您对原始SQL不满意,请尝试:

Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)

您可以通过添加类方法来隐藏的使用arel_table,以减少混乱,例如:

class Project
  def self.id_column
    arel_table[:id]
  end
end

...所以...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)

这两个建议似乎不起作用...子查询Vacancy.where("vacancies.project_id = projects.id").exists?产生truefalseProject.where(true)是一个ArgumentError
Les Nightingill

Vacancy.where("vacancies.project_id = projects.id").exists?不会执行-它将引发错误,因为该projects关系在查询中将不存在(并且上面的示例代码中也没有问号)。因此,将其分解为两个表达式是无效的并且不起作用。在最近的Rails中Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)提出了弃用警告...我将更新问题。
大卫·奥尔德里奇

4

在Rails 4+中,您还可以使用includeeager_load获得相同的答案:

Project.includes(:vacancies).references(:vacancies).
        where.not(vacancies: {id: nil})

Project.eager_load(:vacancies).where.not(vacancies: {id: nil})


3

没有太多的Rails魔术,您可以执行以下操作:

Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')

这种类型的条件将在所有Rails版本中都适用,因为许多工作直接在数据库端完成。另外,链接.count方法也可以很好地工作。我已经像Project.joins(:vacancies)以前一样被查询所困扰。当然,由于它不是数据库无关的,因此有其优缺点。


1
这比join和group方法要慢得多,因为“ select count(*)..”子查询将针对每个项目执行。
YasirAzgar'3

@YasirAzgar join和group方法比“ exists”方法慢,因为即使有上百万个子行,它仍将访问所有子行。
David Aldridge

0

您还可以使用EXISTSwith SELECT 1而不是从vacancies表中选择所有列:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")

-6

该错误告诉您,职位空缺基本上不是项目中的一列。

这应该工作

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')

7
aggregate functions are not allowed in WHERE
卡米尔·莱洛尼克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.