Internally, there are **two separate forms** of `IN`, as well as for the `ANY` construct.

One or each, taking a ***set***, is equivalent to the other and also leads to the same query plan that can use the plain index. Details:

- [IN vs ANY operator in PostgreSQL][1]

Consequently, these 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):

    EXPLAIN ANALYZE
    SELECT *
    FROM t
    WHERE (a,b) = ANY(VALUES (1,1),(1,2));

Or:

    ...
    WHERE (a,b) IN (VALUES (1,1),(1,2));

Identical for both:

>                                                             QUERY PLAN
>     
>     --------------------------------------------------------------------------------------------------------------------------
>      Nested Loop  (cost=0.33..16.71 rows=1 width=8) (actual time=0.101..0.101 rows=0 loops=1)
>        ->  Unique  (cost=0.04..0.05 rows=2 width=8) (actual time=0.068..0.070 rows=2 loops=1)
>              ->  Sort  (cost=0.04..0.04 rows=2 width=8) (actual time=0.067..0.068 rows=2 loops=1)
>                    Sort Key: "*VALUES*".column1, "*VALUES*".column2
>                    Sort Method: quicksort  Memory: 25kB
>                    ->  Values Scan on "*VALUES*"  (cost=0.00..0.03 rows=2 width=8) (actual time=0.005..0.005 rows=2 loops=1)
>        ->  Index Only Scan using t_plain_idx on t  (cost=0.29..8.32 rows=1 width=8) (actual time=0.009..0.009 rows=0 loops=2)
>              Index Cond: ((a = "*VALUES*".column1) AND (b = "*VALUES*".column2))
>              Heap Fetches: 0
>      Planning time: 4.080 ms
>      Execution time: 0.202 ms

The second form of each is different: ANY takes an actual ***array***, while IN takes a comma separated ***list of values*** (elements)

This has different consequences for **typing** the input. As we can see in the `EXPLAIN` output of the question, this form

    WHERE (a,b) = ANY(ARRAY[(1,1),(1,2)]);

is seen as shorthand for:

    ROW(a, b) = ANY (ARRAY[ROW(1, 1), ROW(1, 2)])

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 here.

On the other hand, when using `IN` with a *list* of composite values

    WHERE (a,b) IN ((1,1),(1,2));

.. values are taken as *elements* and Postgres is able to compare individual integer values as we can see in the `EXPLAIN` output once more:

>     Filter: ((b = 1) OR (b = 2))

Hence the simple index  can be used.

  [1]: http://stackoverflow.com/a/34627688/939860