12

Say I have two relations that hold records in the same model, such as:

@companies1 = Company.where(...)
@companies2 = Company.where(...)

How can I find the intersection of these two relations, i.e. only those companies that exist within both?

sscirrus
  • 55,407
  • 41
  • 135
  • 228
  • Seems you don't want sql here. So you could use the array intersection method: http://www.ruby-doc.org/core/classes/Array.html#M000274 – apneadiving Jun 22 '11 at 06:17

5 Answers5

14

By default connecting those where together creates AND which is what you want.

So many be:

class Company < ActiveRecord::Base
  def self.where_1
    where(...)
  end
  def self.where_2
    where(...)
  end
end

@companies = Company.where_1.where_2

====== UPDATED ======

There are two cases:

# case 1: the fields selecting are different
Company.where(:id => [1, 2, 3, 4]) & Company.where(:other_field => true)
# a-rel supports &, |, +, -, but please notice case 2

# case 2
Company.where(:id => [1, 2, 3]) & Company.where(:id => [1, 2, 4, 5])

# the result would be the same as
Company.where(:id => [1, 2, 4, 5])
# because it is &-ing the :id key, instead of the content inside :id key

So if you are in case 2, you will need to do like what @apneadiving commented.

Company.where(...).all & Company.where(...).all

Of course, doing this sends out two queries and most likely queried more results than you needed.

PeterWong
  • 15,951
  • 9
  • 59
  • 68
9

I solve similar problem this way

Company.connection.unprepared_statement do
  Company.find_by_sql "#{@companies1.to_sql} INTERSECT #{@companies2.to_sql}"
end

We need unprepared_statement block here because latest Rails versions use prepared statements to speed up arel queries, but we need pure SQL in place.

mikdiet
  • 9,859
  • 8
  • 59
  • 68
  • This is so nice. It could be implemented with `string::join` to intersect a collection of sql statements. – jgomo3 Mar 15 '20 at 12:43
7

Use sql keyword INTERSECT.

params1 = [1,2,4]
params2 = [1,3,4]
query = "
SELECT companies.* FROM companies
WHERE id in (?,?,?)
INTERSECT
SELECT companies.* FROM companies
WHERE id in (?,?,?)
"
Company.find_by_sql([query, *params1, *params2])

it will be faster than previous solution.

Michael
  • 548
  • 8
  • 30
  • Thanks for your answer! What happens if `params1` and `params2` are both arrays of indetermine length? – sscirrus Jul 03 '12 at 04:25
  • 1. You can change where condition, if you finding not by ids [about](http://www.w3schools.com/sql/sql_where.asp). 2. Something like: "...WHERE id IN (#{params1.map{'?'}.join(',')})..." – Michael Jul 04 '12 at 08:19
  • 1
    I found that `#{Array.new(params1.size, '?').join(',')}` is faster. But it is not so important in this case. – Michael Jul 06 '12 at 09:53
1

You could use ActiveRecord::SpawnMethods#merge

Example:

Company.where(condition: 'value').merge(Company.where(other_condition: 'value'))
KARASZI István
  • 30,900
  • 8
  • 101
  • 128
0

For anyone who is stuck with Rails4 and cant use Rails5 .or syntax:

I had a dynamically number of big queries, which had similar conditions ( and therefore also similar results). My rake server would have problems when all of them at once would get instantiated, converted to arrays and then merged.

I needed a ActiveRecord::Relation (not fired yet) to use with find_each.

Looked something like this:

Class Conditions
  def initialize
    self.where_arr = []
    self.joins_arr = []
  end

  def my_condition1
    where_arr << 'customers.status = "active"'
    joins_arr << :customer
  end

  def my_condition2
    where_arr << 'companies.id = 1'
  end
end

conditions = []    
joins = []
# probably call them in a .each block with .send
conditions1 = Conditions.new
conditions1.my_condition1
conditions1.my_condition2
conditions << "(#{conditions1.where_arr.join(' AND ')})"
joins << conditions1.joins_arr

conditions2 = Conditions.new
conditions2.my_condition1
joins << conditions2.joins_arr

Company.joins(joins).where(conditions.join(' OR '))

=> SELECT companies.* FROM companies 
INNER JOIN customers ON companies.customer_id = customers.id 
WHERE (customers.status = 'active' AND companies.id = 1) OR
(customers.status = 'active')

Its kind of hacky but it works, until you can hopefully migrate to Rails 5