Table t
has two indexes:
create table t (a int, b int);
create type int_pair as (a int, b int);
create index t_row_idx on t (((a,b)::int_pair));
create index t_a_b_idx on t (a,b);
insert into t (a,b)
select i, i
from generate_series(1, 100000) g(i)
;
No index is used with the any
operator:
explain analyze
select *
from t
where (a,b) = any(array[(1,1),(1,2)])
;
QUERY PLAN
---------------------------------------------------------------------------------------------------
Seq Scan on t (cost=0.00..1693.00 rows=1000 width=8) (actual time=0.042..126.789 rows=1 loops=1)
Filter: (ROW(a, b) = ANY (ARRAY[ROW(1, 1), ROW(1, 2)]))
Rows Removed by Filter: 99999
Planning time: 0.122 ms
Execution time: 126.836 ms
But one of them is used with the in
operator:
explain analyze
select *
from t
where (a,b) in ((1,1),(1,2))
;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Index Only Scan using t_a_b_idx on t (cost=0.29..8.32 rows=1 width=8) (actual time=0.028..0.029 rows=1 loops=1)
Index Cond: (a = 1)
Filter: ((b = 1) OR (b = 2))
Heap Fetches: 1
Planning time: 0.161 ms
Execution time: 0.066 ms
It uses the record index if the record is cast to the correct type:
explain analyze
select *
from t
where (a,b)::int_pair = any(array[row(1,1),row(1,2)])
;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------
Index Scan using t_row_idx on t (cost=0.42..12.87 rows=2 width=8) (actual time=0.106..0.126 rows=1 loops=1)
Index Cond: (ROW(a, b)::int_pair = ANY (ARRAY[ROW(1, 1), ROW(1, 2)]))
Planning time: 0.208 ms
Execution time: 0.203 ms
Why doesn't the planner use the non record index for the any
operator as it uses it for the in
operator?
Best Answer
Internally, there are two separate forms of
IN
, as well as for theANY
construct.One of each, taking a set, is equivalent to the other and
expr IN (<set>)
also leads to the same query plan asexpr = ANY(<set>)
that can use a plain index. Details:Consequently, the following two queries are equivalent and both can use the plain index
t_a_b_idx
(which can also be the solution if you are trying to get your query to use the index):Or:
Identical for both:
However, this cannot easily be passed to a function, since there are no "table variables" in Postgres. Which leads to the problem that started this topic:
There are various workarounds for that problem. One being the alternative answer I added there. Some others:
The second form of each is different:
ANY
takes an actual array, whileIN
takes a comma separated list of values.This has different consequences for typing the input. As we can see in the
EXPLAIN
output of the question, this form:is seen as shorthand for:
And actual ROW values are compared. Postgres is not currently smart enough to see that the index on the composite type
t_row_idx
is applicable. Nor does it realize that the simple indext_a_b_idx
should be applicable as well.An explicit cast helps to overcome this lack of smarts:
Casting the right operand (
::int_pair[]
) is optional (though preferable for performance and to avoid ambiguities). Once the left operand has a well-known type, the right operand is coerced from "anonymous record" to a matching type. Only then, the operator is defined unambiguously. And Postgres picks applicable indexes based on the operator and the left operand. For many operators that define aCOMMUTATOR
, the query planner can flip operands to bring the indexed expression to the left. But that's not possible with theANY
construct.Related:
Is there a way to usefully index a text column containing regex patterns?
.. values are taken as elements and Postgres is able to compare individual integer values as we can see in the
EXPLAIN
output once more:Hence Postgres finds that the simple index
t_a_b_idx
can be used.Consequently, there would be another solution for the particular case in the example: since the custom composite type
int_pair
in the example happens to be equivalent to the row type of the tablet
itself, we could simplify:Then this query would use the index without any more explicit casting:
But typical use cases won't be able to utilize the implicitly existing type of the table row.