Where Any

Helpers for using the PostgreSQL ANY() and ALL() expressions in ActiveRecord queries. This provides the functionality of WHERE IN, but in a more prepared statement friendly way.

Tested and validated only for PostgreSQL.

Installation

Add this line to your application's Gemfile:

gem 'where_any'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install where_any

Then in any of your models:

class User < ApplicationRecord
  extend WhereAny

  # ...
end

Or, to install these helpers for your entire application:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  extend WhereAny

  # ...
end

Usage

To make a where ANY() query, use the where_any method:

User.where_any(:id, [1, 2, 3, 4, 5])

The first argument of where_any refers to the column being tested, and the second argument is a list of values to include in the condition.

Which would produce the following SQL:

SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1)  [["id", "{1,2,3,4,5}"]

It's also possible to construct a negated where ANY() query, like so:

User.where_none(:id, [1, 2, 3, 4, 5])

Which would produce the following SQL:

SELECT "users".* FROM "users" WHERE "users"."id" != ALL($1)  [["id", "{1,2,3,4,5}"]]

Where ANY vs. Where IN

The advantage of using where where_any over ActiveRecord's built-in WHERE IN support is that it produces the same SQL statement regardless of the number of elements supplied. This is advantageous when using prepared statements, as the same statement can be reused regardless of the number of inputs supplied.

Consider for example:

User.where(id: [1, 2, 3])
User.where(id: [1, 2, 3, 4])
User.where(id: [1, 2, 3, 4, 5])

# Versus

User.where_any(:id, [1, 2, 3])
User.where_any(:id, [1, 2, 3, 4])
User.where_any(:id, [1, 2, 3, 4, 5])

These sets of queries produce the following sets of SQL respectively:

SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]
SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3, $4)  [["id", 1], ["id", 2], ["id", 3], ["id", 4]]
SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3, $4, $5)  [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5]]

-- Versus

SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1)  [["id", "{1,2,3}"]]
SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1)  [["id", "{1,2,3,4}"]]
SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1)  [["id", "{1,2,3,4,5}"]]

Using the ANY() notation allows us to reuse the same query with the same number of parameter binds regardless of what number of inputs are supplied.

Performance

Using modern version of Postgres, there is no disadvantage to using ANY() from a query plan perspective.

Here is an example query plan when using where_any():

EXPLAIN for: SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1) [["id", "{1,2,3,4,5}"]]
                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Scan using users_pkey on users
   Index Cond: (id = ANY ('{1,2,3,4,5}'::integer[]))
(2 rows)

And here is the query plan when using ActiveRecord's WHERE IN:

EXPLAIN for: SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3, $4, $5) [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5]]
                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Scan using users_pkey on users
   Index Cond: (id = ANY ('{1,2,3,4,5}'::integer[]))
(2 rows)

Note how these two queries produced the exact same query plan. According to the PostgreSQL manual, these operations are equivalent: https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-ANY-SOME

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/thriver/where_any.