If you want to see the impact of a single query on I/O, you can also use EXPLAIN (ANALYZE, BUFFERS). Since disk reads are heavily dependent on what is already cached, results are obviously going to vary across runs.
Example on a table without index, where you can see 32 blocks were cached and 65562 blocks were read:
explain (analyze, verbose, buffers) SELECT ipaddr, status, bytes, upstream_time, timestamp, path from accesslog where timestamp > '2016-02-05 09:35:00' and timestamp < '2016-02-05 10:00:00';
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Seq Scan on public.accesslog (cost=0.00..94247.03 rows=9191 width=74) (actual time=483.209..558.841 rows=8189 loops=1)
Output: ipaddr, status, bytes, upstream_time, "timestamp", path
Filter: ((accesslog."timestamp" > '2016-02-05 09:35:00+00'::timestamp with time zone) AND (accesslog."timestamp" < '2016-02-05 10:00:00+00'::timestamp with time zone))
Rows Removed by Filter: 1900894
Buffers: shared hit=32 read=65562
Planning time: 0.112 ms
Execution time: 559.331 ms
(7 rows)
Same query, after I create a B-tree index on timestamp. Here you can see 6 blocks were cached and 311 were read:
explain (analyze, verbose, buffers) SELECT ipaddr, status, bytes, upstream_time, timestamp, path from accesslog where timestamp > '2016-02-05 09:35:00' and timestamp < '2016-02-05 10:00:00';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using accesslog_timestamp_idx on public.accesslog (cost=0.43..607.15 rows=9186 width=74) (actual time=0.083..7.658 rows=8189 loops=1)
Output: ipaddr, status, bytes, upstream_time, "timestamp", path
Index Cond: ((accesslog."timestamp" > '2016-02-05 09:35:00+00'::timestamp with time zone) AND (accesslog."timestamp" < '2016-02-05 10:00:00+00'::timestamp with time zone))
Buffers: shared hit=6 read=311
Planning time: 0.640 ms
Execution time: 8.348 ms
(6 rows)
And a repeat of same query shows that the second time all 317 blocks were cached:
explain (analyze, verbose, buffers) SELECT ipaddr, status, bytes, upstream_time, timestamp, path from accesslog where timestamp > '2016-02-05 09:35:00' and timestamp < '2016-02-05 10:00:00';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using accesslog_timestamp_idx on public.accesslog (cost=0.43..607.15 rows=9186 width=74) (actual time=0.051..8.436 rows=8189 loops=1)
Output: ipaddr, status, bytes, upstream_time, "timestamp", path
Index Cond: ((accesslog."timestamp" > '2016-02-05 09:35:00+00'::timestamp with time zone) AND (accesslog."timestamp" < '2016-02-05 10:00:00+00'::timestamp with time zone))
Buffers: shared hit=317
Planning time: 0.154 ms
Execution time: 9.454 ms
(6 rows)