Home > Back-end >  How to match elements in an array of composite type?
How to match elements in an array of composite type?

Time:10-16

Let's say we have two tables:

CREATE TABLE element (
    pk1 BIGINT NOT NULL,
    pk2 BIGINT NOT NULL,
    pk3 BIGINT NOT NULL,
    -- other columns ...
    PRIMARY KEY (pk1, pk2, pk3)
);

CREATE TYPE element_pk_t AS (
    pk1 BIGINT,
    pk2 BIGINT,
    pk3 BIGINT
);

CREATE TABLE collection (
    id BIGINT,
    elements element_pk_t[] NOT NULL,
);

The element has a composite PK. The custom type element_pk_t registers a matching composite type. The collection table contains array of element_pk_t.

I want to query all rows from table element where the PK matches an element in a chosen collection.elements, in a single query.

What I've tried:

SELECT * 
FROM element 
WHERE (pk1, pk2, pk3) IN (SELECT unnest(elements) 
                          FROM collection 
                          WHERE id = 1);

I get an error in the IN clause:

ERROR: subquery has too few columns

However, this works:

SELECT * 
FROM element 
WHERE (pk1, pk2, pk3) IN ((1, 2, 3), (4, 5, 6));

So it seems that the problem is how to expand the customized type element_pk_t to 3 columns that can match (pk1, pk2, pk3).

CodePudding user response:

You need to do an extra level of unwrapping in order to get this to work using the IN clause. There needs to be a regular set of fields returned from the subquery to compare to the outer row.

SELECT *
FROM element
WHERE row(pk1, pk2, pk3) IN (
  SELECT (x.el).pk1, (x.el).pk2, (x.el).pk3
  FROM collection
  CROSS JOIN LATERAL (SELECT unnest(elements) el FROM collection) x
  WHERE id = 1
)

Cleaner would be to join by checking if the element is in the array within a join clause.

SELECT e.*
FROM element e
INNER JOIN collection c ON row(e.pk1, e.pk2, e.pk3) = ANY(c.elements)
   AND c.id = 1

CodePudding user response:

This works:

SELECT *
FROM   element 
WHERE  (pk1, pk2, pk3) IN (SELECT (unnest(elements)).*
                           FROM   collection
                           WHERE  id = 1);

Or more verbose, but preferable:

SELECT *
FROM   element 
WHERE  (pk1, pk2, pk3) IN (SELECT (e).*
                           FROM   collection c, unnest(c.elements) e
                           WHERE  c.id = 1);

More robust and avoids evaluating unnest() multiple times. See:

This works, too:

SELECT *
FROM   element 
WHERE  ROW((pk1, pk2, pk3)) IN (SELECT unnest(elements)
                                FROM   collection
                                WHERE  id = 1);

The core of the problem is that IN taking a subquery knows two separate forms. Quoting the manual:

expression IN (subquery)

row_constructor IN (subquery)

Your failing query resolves to the second form, while you (understandably) expect the first. But the second form does this:

The left-hand side of this form of IN is a row constructor, as described in Section 4.2.13. The right-hand side is a parenthesized subquery, which must return exactly as many columns as there are expressions in the left-hand row. The left-hand expressions are evaluated and compared row-wise to each row of the subquery result. [...]

My first and second query make it work by decomposing the row type to the right of the operator. So Postgres has three bigint values left and right and is satisfied.

My third query makes it work by nesting the row type to the left in another row constructor. Postgres only decomposes the first level and ends up with a single composite type - matching the single composite type to the right.

Note that the keyword ROW is required for the single field we are wrapping. The manual:

The key word ROW is optional when there is more than one expression in the list.


Your working query is subtly different as it provides a list of values to the right instead of a subquery (set). That's a different implementation taking a different code path. It even gets a separate chapter in the manual. This variant has no special treatment for a ROW constructor to the left. So it just works as expected (by you).

More equivalent (working) syntax variants with = ANY:

SELECT * FROM element 
WHERE (pk1, pk2, pk3) = ANY ('{"(1,2,3)","(2,3,4)"}'::element_pk_t[]);

SELECT * FROM element 
WHERE (pk1, pk2, pk3) = ANY (ARRAY[(1,2,3)::element_pk_t,(2,3,4)::element_pk_t]);

SELECT * FROM element 
WHERE (pk1, pk2, pk3) = ANY (ARRAY[(1,2,3),(2,3,4)]::element[]);

Also valid with (pk1, pk2, pk3)::element_pk_t or ROW(pk1, pk2, pk3)::element_pk_t

See:

Since your source is an array, Daniel's second query with (e.pk1, e.pk2, e.pk3) = ANY(c.elements) lends itself naturally.
But for a bet on the fastest query, my money is on my second variant, because I expect it to use the PK index optimally.

Just as proof of concept. Like a_horse commented: a normalized DB design will probably scale best.

  • Related