diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 8b488cfd8f6..eda0445550b 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1224,19 +1224,23 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	}
 
 	/*
-	 * For seqscan and sample scans in a serializable transaction, acquire a
-	 * predicate lock on the entire relation. This is required not only to
-	 * lock all the matching tuples, but also to conflict with new insertions
-	 * into the table. In an indexscan, we take page locks on the index pages
-	 * covering the range specified in the scan qual, but in a heap scan there
-	 * is nothing more fine-grained to lock. A bitmap scan is a different
-	 * story, there we have already scanned the index and locked the index
-	 * pages covering the predicate. But in that case we still have to lock
-	 * any matching heap tuples. For sample scan we could optimize the locking
-	 * to be at least page-level granularity, but we'd need to add per-tuple
-	 * locking for that.
-	 */
-	if (scan->rs_base.rs_flags & (SO_TYPE_SEQSCAN | SO_TYPE_SAMPLESCAN))
+	 * For seqscan, sample and TID range scans in a serializable transaction,
+	 * acquire a predicate lock on the entire relation. This is required not
+	 * only to lock all the matching tuples, but also to conflict with new
+	 * insertions into the table. In an indexscan, we take page locks on the
+	 * index pages covering the range specified in the scan qual, but in a
+	 * heap scan there is nothing more fine-grained to lock. A bitmap scan is
+	 * a different story, there we have already scanned the index and locked
+	 * the index pages covering the predicate. But in that case we still have
+	 * to lock any matching heap tuples. For sample scan we could optimize the
+	 * locking to be at least page-level granularity, but we'd need to add
+	 * per-tuple locking for that.  A TID range scan is like a seqscan in this
+	 * respect: it reads heap blocks directly with no index involved, so there
+	 * is nothing finer to lock, and heap_insert() only checks for conflicts
+	 * against relation-level predicate locks anyway.
+	 */
+	if (scan->rs_base.rs_flags & (SO_TYPE_SEQSCAN | SO_TYPE_SAMPLESCAN |
+								  SO_TYPE_TIDRANGESCAN))
 	{
 		/*
 		 * Ensure a missing snapshot is noticed reliably, even if the
diff --git a/src/test/isolation/expected/predicate-tidrangescan.out b/src/test/isolation/expected/predicate-tidrangescan.out
new file mode 100644
index 00000000000..57200e51bdd
--- /dev/null
+++ b/src/test/isolation/expected/predicate-tidrangescan.out
@@ -0,0 +1,106 @@
+Parsed test spec with 2 sessions
+
+starting permutation: rxy1 rxy2 wx1 wy2 c1 c2
+step rxy1: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step rxy2: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step wx1: update tidrange_tbl set p = 1 where ctid = '(0,1)';
+step wy2: update tidrange_tbl set p = 1 where ctid = '(0,2)';
+step c1: commit;
+step c2: commit;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rxy1 rxy2 wy2 wx1 c1 c2
+step rxy1: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step rxy2: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step wy2: update tidrange_tbl set p = 1 where ctid = '(0,2)';
+step wx1: update tidrange_tbl set p = 1 where ctid = '(0,1)';
+step c1: commit;
+step c2: commit;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rxy2 rxy1 wx1 wy2 c2 c1
+step rxy2: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step rxy1: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step wx1: update tidrange_tbl set p = 1 where ctid = '(0,1)';
+step wy2: update tidrange_tbl set p = 1 where ctid = '(0,2)';
+step c2: commit;
+step c1: commit;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rxy1 rxy2 wi1 wi2 c1 c2
+step rxy1: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step rxy2: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step wi1: insert into tidrange_tbl values (3, 10);
+step wi2: insert into tidrange_tbl values (4, 20);
+step c1: commit;
+step c2: commit;
+ERROR:  could not serialize access due to read/write dependencies among transactions
+
+starting permutation: rxy2 rxy1 wi2 wi1 c2 c1
+step rxy2: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step rxy1: select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)';
+sum
+---
+  0
+(1 row)
+
+step wi2: insert into tidrange_tbl values (4, 20);
+step wi1: insert into tidrange_tbl values (3, 10);
+step c2: commit;
+step c1: commit;
+ERROR:  could not serialize access due to read/write dependencies among transactions
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..9f3b85d2a06 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -105,6 +105,7 @@ test: vacuum-conflict
 test: vacuum-skip-locked
 test: stats
 test: horizons
+test: predicate-tidrangescan
 test: predicate-hash
 test: predicate-gist
 test: predicate-gin
diff --git a/src/test/isolation/specs/predicate-tidrangescan.spec b/src/test/isolation/specs/predicate-tidrangescan.spec
new file mode 100644
index 00000000000..e7944dd3ba8
--- /dev/null
+++ b/src/test/isolation/specs/predicate-tidrangescan.spec
@@ -0,0 +1,56 @@
+# Test for relation level predicate locking in TID range scans
+#
+# A TID range scan reads a range of heap blocks directly, with no index
+# involved, so like a sequential scan it has nothing finer to lock than the
+# whole relation.  Verify that the relation level SIREAD lock is acquired, by
+# checking that write skew and phantom rows seen through a TID range scan are
+# detected.
+
+setup
+{
+ create table tidrange_tbl (id int, p int);
+ insert into tidrange_tbl values (1, 0), (2, 0);
+}
+
+teardown
+{
+ drop table tidrange_tbl;
+}
+
+session s1
+setup
+{
+ begin isolation level serializable;
+ set enable_seqscan = off;
+}
+step rxy1	{ select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)'; }
+step wx1	{ update tidrange_tbl set p = 1 where ctid = '(0,1)'; }
+step wi1	{ insert into tidrange_tbl values (3, 10); }
+step c1		{ commit; }
+
+session s2
+setup
+{
+ begin isolation level serializable;
+ set enable_seqscan = off;
+}
+step rxy2	{ select sum(p) from tidrange_tbl
+			  where ctid >= '(0,0)' and ctid < '(1,0)'; }
+step wy2	{ update tidrange_tbl set p = 1 where ctid = '(0,2)'; }
+step wi2	{ insert into tidrange_tbl values (4, 20); }
+step c2		{ commit; }
+
+# Both transactions read the whole TID range, then each updates a row that the
+# other one read.  No serial order produces sum(p) = 2, so one of them has to
+# be aborted.
+
+permutation rxy1 rxy2 wx1 wy2 c1 c2
+permutation rxy1 rxy2 wy2 wx1 c1 c2
+permutation rxy2 rxy1 wx1 wy2 c2 c1
+
+# Both transactions read the whole TID range, then each inserts a row that
+# falls inside the range the other one read.
+
+permutation rxy1 rxy2 wi1 wi2 c1 c2
+permutation rxy2 rxy1 wi2 wi1 c2 c1
