PostgreSQL & JDBC: How to select rows matching pairs of values

Given a sequence of pairs of (id, version) in code, how do you efficiently select all records in your DB that match? You can construct a long string with SELECT …​ WHERE (id = 'a' AND version = 1) OR (id = 'b' AND version = 20) OR …​ but that’s not very efficient. If you only had a single value then, in PostgreSQL, you could use id=ANY(?) and pass in a char array. But what if you have multiple conditions/columns that must match? Unnest to the rescue!

Here is the complete solution:

(let [id+ver-pairs [["a",1], ["b",20], ["c",1]]
      ids (map first id+ver-pairs)
      versions (map last id+ver-pairs)]
   (next.jdbc/execute! conn
      ["select * from thing t join unnest(?,?) as x(id,version)
                              on ( = and t.version = x.version)"
        (into-array ids) (into-array versions)]))

Bonus: Producing the SQL with HoneySQL:

  (-> (hh/select :*)
      (hh/from [:thing :t])
      (hh/join [[:unnest
                 (into-array String ["a" "b" "c"]) ; (1)
                 (into-array [1 20 1])]
                [[:'x :id :version]]] ; (2)
               [:and [:=]
                [:= :t.version :x.version]]))
  {:quoted true})
1Notice that you may need to provide explicit type argument to into-array so that you don’t end up with an Object array. The Postgres JDBC driver can auto-convert some Java arrays to binary representations that work with PreparedStatement.setObject, but an object array is not one of them.
2The quote in :'x ensures that the name stays as-is, i.e. x(id,version) - otherwise it would be upper-cased (even though it doesn’t really matter, since in effect the name would be unquoted and thus case-insensitive, as identifiers are by default). See Usage > Functions: “If a keyword begins with ', the function name is formatted as a SQL entity rather than being converted to uppercase and having hyphens - converted to spaces”.
Isak mentioned that he has been using json_to_recordset with an array of objects, as in join json_to_recordset(?::json) as x(id int, version int) …​, though it is possible my way is more efficient.

Bonus: Leverage composites for tuple matches with a subquery

A tip from my awesome colleague Fredrik Røtnes. In my case, the ids and versions come from the client app. But what if they come elsewhere from the DB? Here we can leverage composites:

SELECT * FROM thing WHERE ((id, version)) IN (SELECT DISTINCT other_id, other_version FROM other_table)

and in Honey:

    (-> (hh/select :*)
        (hh/from :thing)
           (hh/columns :id :version)
           (-> (hh/select-distinct :other-id :other-version)
               (hh/from :other-table))])))

Bonus 2: Use a multi-dimensional input with IN …​ with unnest and composites

We can combine the previous tip with unnest for multi-dimensional array input:

;; Return Honey where clause, to include in `(hh/where ...)`:
(let [id+ver-pairs [["a",1], ["b",20], ["c",1]]]
   (hh/columns :id :version)
   (-> (hh/select :input.version)
       (hh/from [[:unnest
                  (into-array String (map first id+ver-pairs))
                  (long-array (map second id+ver-pairs))]
                 [[:'input :id :version]]]))])

Tags: database performance

Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob