连接集必须包含所有值但可能包含更多值的SQL

我有三个表格, sportsoffers_sportsoffers_sports

 class Offer < ActiveRecord::Base has_and_belongs_to_many :sports end class Sport < ActiveRecord::Base has_and_belongs_to_many :offers end 

我想选择包含一系列运动名称的优惠。 他们必须包含所有的sports可能有更多。

让我们说我有这三个优惠:

 light: - "Yoga" - "Bodyboarding" medium: - "Yoga" - "Bodyboarding" - "Surfing" all: - "Yoga" - "Bodyboarding" - "Surfing" - "Parasailing" - "Skydiving" 

鉴于arrays["Bodyboarding", "Surfing"]我想要得到medium而不是light

我尝试过这个答案,但结果是零行:

 Offer.joins(:sports) .where(sports: { name: ["Bodyboarding", "Surfing"] }) .group("sports.name") .having("COUNT(distinct sports.name) = 2") 

转换为SQL:

 SELECT "offers".* FROM "offers" INNER JOIN "offers_sports" ON "offers_sports"."offer_id" = "offers"."id" INNER JOIN "sports" ON "sports"."id" = "offers_sports"."sport_id" WHERE "sports"."name" IN ('Bodyboarding', 'Surfing') GROUP BY sports.name HAVING COUNT(distinct sports.name) = 2; 

一个ActiveRecord答案会很好,但我会满足于SQL,最好是兼容Postgres。

数据:

 offers ====================== id | name ---------------------- 1 | light 2 | medium 3 | all 4 | extreme sports ====================== id | name ---------------------- 1 | "Yoga" 2 | "Bodyboarding" 3 | "Surfing" 4 | "Parasailing" 5 | "Skydiving" offers_sports ====================== offer_id | sport_id ---------------------- 1 | 1 1 | 2 2 | 1 2 | 2 2 | 3 3 | 1 3 | 2 3 | 3 3 | 4 3 | 5 4 | 3 4 | 4 4 | 5 

offer.id ,而不是sports.name (或sports.name ):

 SELECT o.* FROM sports s JOIN offers_sports os ON os.sport_id = s.id JOIN offers o ON os.offer_id = o.id WHERE s.name IN ('Bodyboarding', 'Surfing') GROUP BY o.id -- !! HAVING count(*) = 2; 

假设典型的实现:

  • offer.idoffer.id被定义为主键。
  • sports.name被定义为唯一的。
  • (sport_id, offer_id)被定义为唯一(或PK)。

您在计数中不需要DISTINCT 。 而count(*)甚至更便宜了。

相关答案与可能的技术武器:

  • 如何以多次通过关系过滤SQL结果

由@max(OP)添加 – 这是上面的查询转入ActiveRecord:

 class Offer < ActiveRecord::Base has_and_belongs_to_many :sports def self.includes_sports(*sport_names) joins(:sports) .where(sports: { name: sport_names }) .group('offers.id') .having("count(*) = ?", sport_names.size) end end 

一种方法是使用数组和array_agg聚合函数。

 SELECT "offers".*, array_agg("sports"."name") as spnames FROM "offers" INNER JOIN "offers_sports" ON "offers_sports"."offer_id" = "offers"."id" INNER JOIN "sports" ON "sports"."id" = "offers_sports"."sport_id" GROUP BY "offers"."id" HAVING array_agg("sports"."name")::text[] @> ARRAY['Bodyboarding','Surfing']::text[]; 

收益:

  id | name | spnames ----+--------+--------------------------------------------------- 2 | medium | {Yoga,Bodyboarding,Surfing} 3 | all | {Yoga,Bodyboarding,Surfing,Parasailing,Skydiving} (2 rows) 

@>运算符意味着左侧的数组必须包含右侧的所有元素,但可能包含更多元素。 spnames列仅用于show,但您可以安全地删除它。

有两件事你必须非常注意这一点。

  1. 即使使用Postgres 9.4(我还没有尝试9.5),比较数组的类型转换是草率的,经常出错,告诉你它无法找到将它们转换为可比值的方法,因此你可以在示例中看到我使用::text[]手动施放两边。

  2. 我不知道对数组参数的支持级别是Ruby,还是RoR框架,所以你可能最终必须手动转义字符串(如果用户输入)并使用ARRAY[]语法形成数组。