diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index 38078763f57e8bdbcb1141bb819f8389a4a5963d..e72ebc8c3a8f74006fce657ccb2eddc0413725a5 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -30,6 +30,7 @@
 #include "executor/nodeIndexonlyscan.h"
 #include "executor/nodeIndexscan.h"
 #include "storage/bufmgr.h"
+#include "storage/predicate.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
@@ -52,7 +53,6 @@ IndexOnlyNext(IndexOnlyScanState *node)
 	ExprContext *econtext;
 	ScanDirection direction;
 	IndexScanDesc scandesc;
-	HeapTuple	tuple;
 	TupleTableSlot *slot;
 	ItemPointer tid;
 
@@ -78,6 +78,8 @@ IndexOnlyNext(IndexOnlyScanState *node)
 	 */
 	while ((tid = index_getnext_tid(scandesc, direction)) != NULL)
 	{
+		HeapTuple	tuple = NULL;
+
 		/*
 		 * We can skip the heap fetch if the TID references a heap page on
 		 * which all tuples are known visible to everybody.  In any case,
@@ -147,6 +149,18 @@ IndexOnlyNext(IndexOnlyScanState *node)
 			}
 		}
 
+		/*
+		 * Predicate locks for index-only scans must be acquired at the page
+		 * level when the heap is not accessed, since tuple-level predicate
+		 * locks need the tuple's xmin value.  If we had to visit the tuple
+		 * anyway, then we already have the tuple-level lock and can skip the
+		 * page lock.
+		 */
+		if (tuple == NULL)
+			PredicateLockPage(scandesc->heapRelation,
+							  ItemPointerGetBlockNumber(tid),
+							  estate->es_snapshot);
+
 		return slot;
 	}
 
diff --git a/src/test/isolation/expected/index-only-scan.out b/src/test/isolation/expected/index-only-scan.out
new file mode 100644
index 0000000000000000000000000000000000000000..47983ebd5881cfb0ad682d85dd95d2585d9583ed
--- /dev/null
+++ b/src/test/isolation/expected/index-only-scan.out
@@ -0,0 +1,41 @@
+Parsed test spec with 2 sessions
+
+starting permutation: rxwy1 c1 rywx2 c2
+step rxwy1: DELETE FROM taby WHERE id = (SELECT min(id) FROM tabx);
+step c1: COMMIT;
+step rywx2: DELETE FROM tabx WHERE id = (SELECT min(id) FROM taby);
+step c2: COMMIT;
+
+starting permutation: rxwy1 rywx2 c1 c2
+step rxwy1: DELETE FROM taby WHERE id = (SELECT min(id) FROM tabx);
+step rywx2: DELETE FROM tabx WHERE id = (SELECT min(id) FROM taby);
+step c1: COMMIT;
+step c2: COMMIT;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rxwy1 rywx2 c2 c1
+step rxwy1: DELETE FROM taby WHERE id = (SELECT min(id) FROM tabx);
+step rywx2: DELETE FROM tabx WHERE id = (SELECT min(id) FROM taby);
+step c2: COMMIT;
+step c1: COMMIT;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rywx2 rxwy1 c1 c2
+step rywx2: DELETE FROM tabx WHERE id = (SELECT min(id) FROM taby);
+step rxwy1: DELETE FROM taby WHERE id = (SELECT min(id) FROM tabx);
+step c1: COMMIT;
+step c2: COMMIT;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rywx2 rxwy1 c2 c1
+step rywx2: DELETE FROM tabx WHERE id = (SELECT min(id) FROM taby);
+step rxwy1: DELETE FROM taby WHERE id = (SELECT min(id) FROM tabx);
+step c2: COMMIT;
+step c1: COMMIT;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rywx2 c2 rxwy1 c1
+step rywx2: DELETE FROM tabx WHERE id = (SELECT min(id) FROM taby);
+step c2: COMMIT;
+step rxwy1: DELETE FROM taby WHERE id = (SELECT min(id) FROM tabx);
+step c1: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 2184975dcb12e3a20e3e37f724985798a4df58eb..75e33bc99fc30a1116d5676bbb0fe72c0da3f495 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -9,6 +9,7 @@ test: ri-trigger
 test: partial-index
 test: two-ids
 test: multiple-row-versions
+test: index-only-scan
 test: fk-contention
 test: fk-deadlock
 test: fk-deadlock2
diff --git a/src/test/isolation/specs/index-only-scan.spec b/src/test/isolation/specs/index-only-scan.spec
new file mode 100644
index 0000000000000000000000000000000000000000..417bb02102e9d6009dc28ebc4865f72b6847a0d4
--- /dev/null
+++ b/src/test/isolation/specs/index-only-scan.spec
@@ -0,0 +1,46 @@
+# index-only scan test
+#
+# This test tries to expose problems with the interaction between index-only
+# scans and SSI.
+#
+# Any overlap between the transactions must cause a serialization failure.
+
+setup
+{
+  CREATE TABLE tabx (id int NOT NULL);
+  INSERT INTO tabx SELECT generate_series(1,10000);
+  ALTER TABLE tabx ADD PRIMARY KEY (id);
+  CREATE TABLE taby (id int NOT NULL);
+  INSERT INTO taby SELECT generate_series(1,10000);
+  ALTER TABLE taby ADD PRIMARY KEY (id);
+}
+setup { VACUUM FREEZE ANALYZE tabx; }
+setup { VACUUM FREEZE ANALYZE taby; }
+
+teardown
+{
+  DROP TABLE tabx;
+  DROP TABLE taby;
+}
+
+session "s1"
+setup
+{
+  BEGIN ISOLATION LEVEL SERIALIZABLE;
+  SET LOCAL seq_page_cost = 0.1;
+  SET LOCAL random_page_cost = 0.1;
+  SET LOCAL cpu_tuple_cost = 0.03;
+}
+step "rxwy1" { DELETE FROM taby WHERE id = (SELECT min(id) FROM tabx); }
+step "c1" { COMMIT; }
+
+session "s2"
+setup
+{
+  BEGIN ISOLATION LEVEL SERIALIZABLE;
+  SET LOCAL seq_page_cost = 0.1;
+  SET LOCAL random_page_cost = 0.1;
+  SET LOCAL cpu_tuple_cost = 0.03;
+}
+step "rywx2" { DELETE FROM tabx WHERE id = (SELECT min(id) FROM taby); }
+step "c2" { COMMIT; }