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

时间:2016-03-21 13:03:46

标签: sql ruby-on-rails postgresql rails-activerecord relational-division

我有三个表offerssports和联接表offers_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"

鉴于数组["Bodyboarding", "Surfing"],我希望得到mediumall,而不是light

我尝试了this answer的内容,但结果中的行数为零:

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

2 个答案:

答案 0 :(得分:2)

<div style="white-space: nowrap;"> Texttttttttttttttttttttttttttttttttttttttttttttttttttttttttt Float text </div> 分组,而不是offer.id(或sports.name)分组:

sports.id

假设典型的实施:

  • 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.id被定义为主键。
  • sports.id定义为唯一。
  • sports.name中的
  • (sport_id, offer_id)被定义为唯一(或PK)。

您在计算中不需要offers_sportsDISTINCT甚至更便宜了。

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

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

count(*)

答案 1 :(得分:1)

一种方法是使用数组和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列仅供展示,但您可以安全地将其删除。

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

  1. 即使使用Postgres 9.4(我还没有试过9.5),比较数组的类型转换很草率而且经常出错,告诉你它无法找到将它们转换为可比值的方法,正如您在示例中所见,我使用::text[]手动投射双方。

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