Postgresql small index for row contains query
Since seems like each value of id has many occurences in the table.  Given that, you can use a GIN index with its compressed posting lists to save space.  Usually GIN indexes are used with array types or their equivalents, but you can use the btree_gin extension to get access to GIN indexes with a scalar type.

However, GIN indexes have several limitations.  They don't support ORDER BY, which doesn't seem to matter to you.  And they don't support index-only-scans, which may be important to you.  Even if the index shrinks to fit in RAM, each tuple pointer found in the index still has to go to the table for resolution.  If your Btree index doesn't fit in RAM, surely your table doesn't either.  So you will still have lots of random seeking into the table.  

Indeed, that was almost certainly the problem in the first place--not random seeks against the btree index, but rather against the table.  When you are using the BRIN index to drive table access, you naturally are going to visit the table with high locality of reference.  It is not that the BRIN index is effective, it is that the same condition that allows the BRIN to be used also makes the table access for the particular set of rows being returned also be efficient.  If you were to replace the BRIN indexes with equivalent Btree indexes, you would probably find that while the storage requirement went up, the performance stays mostly the same.