diff --git a/src/box/memtx_hash.c b/src/box/memtx_hash.c
index 48c892c4249eaa0ae52c9f3b21a82719d9c02786..63b67d0458fde19d353f431b4f35f6b70478887d 100644
--- a/src/box/memtx_hash.c
+++ b/src/box/memtx_hash.c
@@ -316,16 +316,18 @@ memtx_hash_index_get(struct index *base, const char *key,
 	(void) part_count;
 
 	struct space *space = space_by_id(base->def->space_id);
+	struct txn *txn = in_txn();
+	uint32_t iid = base->def->iid;
 	*result = NULL;
 	uint32_t h = key_hash(key, base->def->key_def);
 	uint32_t k = light_index_find_key(&index->hash_table, h, key);
 	if (k != light_index_end) {
 		struct tuple *tuple = light_index_get(&index->hash_table, k);
-		uint32_t iid = base->def->iid;
-		struct txn *txn = in_txn();
 		bool is_rw = txn != NULL;
 		*result = memtx_tx_tuple_clarify(txn, space, tuple, iid,
 						 0, is_rw);
+	} else {
+		memtx_tx_track_point(txn, space, iid, key);
 	}
 	return 0;
 }
@@ -437,6 +439,10 @@ memtx_hash_index_create_iterator(struct index *base, enum iterator_type type,
 		light_index_iterator_key(&index->hash_table, &it->iterator,
 				key_hash(key, base->def->key_def), key);
 		it->base.next = hash_iterator_eq;
+		if (it->iterator.slotpos == light_index_end)
+			memtx_tx_track_point(in_txn(),
+					     space_by_id(it->base.space_id),
+					     index->base.def->iid, key);
 		break;
 	default:
 		diag_set(UnsupportedIndexFeature, base->def,
diff --git a/src/box/memtx_tree.cc b/src/box/memtx_tree.cc
index 26148443bbacd17e75ea54cfcb887ed4e9492b2b..0fb5c3ef355a70fa6d00c8bf51aa3bd45be96db6 100644
--- a/src/box/memtx_tree.cc
+++ b/src/box/memtx_tree.cc
@@ -490,6 +490,10 @@ tree_iterator_start(struct iterator *iterator, struct tuple **ret)
 	it->base.next = tree_iterator_dummie;
 	memtx_tree_t<USE_HINT> *tree = &index->tree;
 	enum iterator_type type = it->type;
+	struct txn *txn = in_txn();
+	struct space *space = space_by_id(iterator->space_id);
+	uint32_t iid = iterator->index->def->iid;
+	struct key_def *cmp_def = index->base.def->cmp_def;
 	bool exact = false;
 	assert(it->current.tuple == NULL);
 	if (it->key_data.key == 0) {
@@ -503,14 +507,24 @@ tree_iterator_start(struct iterator *iterator, struct tuple **ret)
 			it->tree_iterator =
 				memtx_tree_lower_bound(tree, &it->key_data,
 						       &exact);
-			if (type == ITER_EQ && !exact)
+			if (type == ITER_EQ && !exact) {
+				if (it->key_data.part_count ==
+				    cmp_def->part_count)
+					memtx_tx_track_point(txn, space, iid,
+							     it->key_data.key);
 				return 0;
+			}
 		} else { // ITER_GT, ITER_REQ, ITER_LE
 			it->tree_iterator =
 				memtx_tree_upper_bound(tree, &it->key_data,
 						       &exact);
-			if (type == ITER_REQ && !exact)
+			if (type == ITER_REQ && !exact) {
+				if (it->key_data.part_count ==
+				    cmp_def->part_count)
+					memtx_tx_track_point(txn, space, iid,
+							     it->key_data.key);
 				return 0;
+			}
 		}
 		if (iterator_type_is_reverse(type)) {
 			/*
@@ -537,10 +551,7 @@ tree_iterator_start(struct iterator *iterator, struct tuple **ret)
 	it->current = *res;
 	tree_iterator_set_next_method(it);
 
-	uint32_t iid = iterator->index->def->iid;
 	bool is_multikey = iterator->index->def->key_def->is_multikey;
-	struct txn *txn = in_txn();
-	struct space *space = space_by_id(iterator->space_id);
 	bool is_rw = txn != NULL;
 	uint32_t mk_index = is_multikey ? (uint32_t)res->hint : 0;
 	*ret = memtx_tx_tuple_clarify(txn, space, *ret, iid, mk_index, is_rw);
@@ -720,6 +731,8 @@ memtx_tree_index_get(struct index *base, const char *key,
 	struct memtx_tree_index<USE_HINT> *index =
 		(struct memtx_tree_index<USE_HINT> *)base;
 	struct key_def *cmp_def = memtx_tree_cmp_def(&index->tree);
+	struct txn *txn = in_txn();
+	struct space *space = space_by_id(base->def->space_id);
 	struct memtx_tree_key_data<USE_HINT> key_data;
 	key_data.key = key;
 	key_data.part_count = part_count;
@@ -729,10 +742,10 @@ memtx_tree_index_get(struct index *base, const char *key,
 		memtx_tree_find(&index->tree, &key_data);
 	if (res == NULL) {
 		*result = NULL;
+		if (part_count == cmp_def->part_count)
+			memtx_tx_track_point(txn, space, base->def->iid, key);
 		return 0;
 	}
-	struct txn *txn = in_txn();
-	struct space *space = space_by_id(base->def->space_id);
 	bool is_rw = txn != NULL;
 	bool is_multikey = base->def->key_def->is_multikey;
 	uint32_t mk_index = is_multikey ? (uint32_t)res->hint : 0;
diff --git a/src/box/memtx_tx.c b/src/box/memtx_tx.c
index 8603fc746c2650862f2293c55651e7b410dcce66..3421d8d74c9722831978c5899be921a061f4b71a 100644
--- a/src/box/memtx_tx.c
+++ b/src/box/memtx_tx.c
@@ -59,6 +59,95 @@ memtx_tx_story_key_hash(const struct tuple *a)
 #define MH_SOURCE
 #include "salad/mhash.h"
 
+/**
+ * An element that stores the fact that some transaction have read
+ * a full key and found nothing.
+ */
+struct point_hole_item {
+	/** A link of headless list of items with the same index and key. */
+	struct rlist ring;
+	/** Link in txn->point_holes_list. */
+	struct rlist in_point_holes_list;
+	/** Saved index->unique_id. */
+	uint32_t index_unique_id;
+	/** Precalculated hash for storing in hash table.. */
+	uint32_t hash;
+	/** Saved txn. */
+	struct txn *txn;
+	/** Saved key. Points to @a short_key or allocated in txn's region. */
+	const char *key;
+	/** Saved key len. */
+	size_t key_len;
+	/** Storage for short key. @key may point here. */
+	char short_key[16];
+	/** Flag that the hash tables stores pointer to this item. */
+	bool is_head;
+};
+
+/**
+ * Helper structure for searching for point_hole_item in the hash table,
+ * @sa point_hole_item_pool.
+ */
+struct point_hole_key {
+	struct index *index;
+	struct tuple *tuple;
+};
+
+/** Hash calculatore for the key. */
+static uint32_t
+point_hole_storage_key_hash(struct point_hole_key *key)
+{
+	struct key_def *def = key->index->def->key_def;
+	return key->index->unique_id ^ def->tuple_hash(key->tuple, def);
+}
+
+/** point_hole_item comparator. */
+static int
+point_hole_storage_equal(const struct point_hole_item *obj1,
+			 const struct point_hole_item *obj2)
+{
+	/* Canonical msgpack is comparable by memcmp. */
+	if (obj1->index_unique_id != obj2->index_unique_id ||
+	    obj1->key_len != obj2->key_len)
+		return 1;
+	return memcmp(obj1->key, obj2->key, obj1->key_len) != 0;
+}
+
+/** point_hole_item comparator with key. */
+static int
+point_hole_storage_key_equal(const struct point_hole_key *key,
+			     const struct point_hole_item *object)
+{
+	if (key->index->unique_id != object->index_unique_id)
+		return 1;
+	assert(key->index != NULL);
+	assert(key->tuple != NULL);
+	struct key_def *def = key->index->def->key_def;
+	hint_t oh = def->key_hint(object->key, def->part_count, def);
+	hint_t kh = def->tuple_hint(key->tuple, def);
+	return def->tuple_compare_with_key(key->tuple, kh, object->key,
+					   def->part_count, oh, def);
+}
+
+/**
+ * Hash table definition for hole read storage.
+ * The key is constructed by unique index ID and search key.
+ * Actually it stores pointers to point_hole_item structures.
+ * If more than one point_hole_item is added to the hash table,
+ * it is simply added to the headless list in existing point_hole_item.
+ */
+
+#define mh_name _point_holes
+#define mh_key_t struct point_hole_key *
+#define mh_node_t struct point_hole_item *
+#define mh_arg_t int
+#define mh_hash(a, arg) ((*(a))->hash)
+#define mh_hash_key(a, arg) ( point_hole_storage_key_hash(a) )
+#define mh_cmp(a, b, arg) point_hole_storage_equal(*(a), *(b))
+#define mh_cmp_key(a, b, arg) point_hole_storage_key_equal((a), *(b))
+#define MH_SOURCE
+#include "salad/mhash.h"
+
 struct tx_manager
 {
 	/**
@@ -71,6 +160,12 @@ struct tx_manager
 	struct mempool memtx_tx_story_pool[BOX_INDEX_MAX];
 	/** Hash table tuple -> memtx_story of that tuple. */
 	struct mh_history_t *history;
+	/** Mempool for point_hole_item objects. */
+	struct mempool point_hole_item_pool;
+	/** Hash table that hold point selects with empty result. */
+	struct mh_point_holes_t *point_holes;
+	/** Count of elements in point_holes table. */
+	size_t point_holes_size;
 	/** List of all memtx_story objects. */
 	struct rlist all_stories;
 	/** Iterator that sequentially traverses all memtx_story objects. */
@@ -103,6 +198,14 @@ memtx_tx_manager_init()
 			       cord_slab_cache(), item_size);
 	}
 	txm.history = mh_history_new();
+	if (txm.history == NULL)
+		panic("mh_history_new()");
+	mempool_create(&txm.point_hole_item_pool,
+		       cord_slab_cache(), sizeof(struct point_hole_item));
+	txm.point_holes = mh_point_holes_new();
+	if (txm.point_holes == NULL)
+		panic("mh_history_new()");
+	txm.point_holes_size = 0;
 	rlist_create(&txm.all_stories);
 	txm.traverse_all_stories = &txm.all_stories;
 }
@@ -112,8 +215,12 @@ memtx_tx_manager_free()
 {
 	for (size_t i = 0; i < BOX_INDEX_MAX; i++)
 		mempool_destroy(&txm.memtx_tx_story_pool[i]);
+	mh_history_delete(txm.history);
+	mempool_destroy(&txm.point_hole_item_pool);
+	mh_point_holes_delete(txm.point_holes);
 }
 
+
 int
 memtx_tx_cause_conflict(struct txn *breaker, struct txn *victim)
 {
@@ -539,6 +646,8 @@ struct memtx_tx_conflict
 {
 	/* The transaction that will conflict us upon commit. */
 	struct txn *breaker;
+	/* The transaction that will conflicted by upon commit. */
+	struct txn *victim;
 	/* Link in single-linked list. */
 	struct memtx_tx_conflict *next;
 };
@@ -549,7 +658,7 @@ struct memtx_tx_conflict
  * @return 0 on success, -1 on memory error.
  */
 static int
-memtx_tx_save_conflict(struct txn *breaker,
+memtx_tx_save_conflict(struct txn *breaker, struct txn *victim,
 		       struct memtx_tx_conflict **conflicts_head,
 		       struct region *region)
 {
@@ -562,6 +671,7 @@ memtx_tx_save_conflict(struct txn *breaker,
 		return -1;
 	}
 	next_conflict->breaker = breaker;
+	next_conflict->victim = victim;
 	next_conflict->next = *conflicts_head;
 	*conflicts_head = next_conflict;
 	return 0;
@@ -617,6 +727,7 @@ memtx_tx_story_find_visible_tuple(struct memtx_story *story,
 		}
 		if (cross_conflict) {
 			if (memtx_tx_save_conflict(story->add_stmt->txn,
+						   stmt->txn,
 						   collected_conflicts,
 						   region) != 0)
 				return -1;
@@ -655,6 +766,50 @@ memtx_tx_check_dup(struct tuple *new_tuple, struct tuple *old_tuple,
 	return 0;
 }
 
+static struct point_hole_item *
+point_hole_storage_find(struct index *index, struct tuple *tuple)
+{
+	struct point_hole_key key;
+	key.index = index;
+	key.tuple = tuple;
+	mh_int_t pos = mh_point_holes_find(txm.point_holes, &key, 0);
+	if (pos == mh_end(txm.point_holes))
+		return NULL;
+	return *mh_point_holes_node(txm.point_holes, pos);
+}
+
+/**
+ * Check for possible conflicts during inserting @a new tuple, and given
+ * that it was real insertion, not the replacement of existion tuple.
+ * It's the moment where we can search for stored point hole trackers
+ * and detect conflicts.
+ * Since the insertions in not completed succesfully, we better store
+ * conflicts to the special temporary storage @a collected_conflicts in
+ * other to become real conflint only when insertion success is inevitable.
+ */
+static int
+check_hole(struct space *space, uint32_t index,
+	   struct tuple *new_tuple, struct txn *inserter,
+	   struct memtx_tx_conflict **collected_conflicts,
+	   struct region *region)
+{
+	struct point_hole_item *list =
+		point_hole_storage_find(space->index[index], new_tuple);
+	if (list == NULL)
+		return 0;
+
+	struct point_hole_item *item = list;
+	do {
+		if (memtx_tx_save_conflict(inserter, item->txn,
+					   collected_conflicts, region) != 0)
+			return -1;
+		item = rlist_entry(item->ring.next,
+				   struct point_hole_item, ring);
+
+	} while (item != list);
+	return 0;
+}
+
 /**
  * Check that replaced tuples in space's indexes does not violate common
  * replace rules. See memtx_space_replace_all_keys comment.
@@ -676,6 +831,10 @@ check_dup_clean(struct txn_stmt *stmt, struct tuple *new_tuple,
 			       mode, space->index[0], space) != 0)
 		return -1;
 
+	if (replaced[0] == NULL)
+		check_hole(space, 0, new_tuple, stmt->txn,
+			   collected_conflicts, region);
+
 	for (uint32_t i = 1; i < space->index_count; i++) {
 		/*
 		 * Check that visible tuple is NULL or the same as in the
@@ -683,6 +842,8 @@ check_dup_clean(struct txn_stmt *stmt, struct tuple *new_tuple,
 		 */
 		if (replaced[i] == NULL) {
 			/* NULL is OK. */
+			check_hole(space, i, new_tuple, stmt->txn,
+				   collected_conflicts, region);
 			continue;
 		}
 		if (!replaced[i]->is_dirty) {
@@ -711,6 +872,10 @@ check_dup_clean(struct txn_stmt *stmt, struct tuple *new_tuple,
 		if (memtx_tx_check_dup(new_tuple, replaced[0], check_visible,
 				       DUP_INSERT, space->index[i], space) != 0)
 			return -1;
+
+		if (check_visible == NULL)
+			check_hole(space, i, new_tuple, stmt->txn,
+				   collected_conflicts, region);
 	}
 
 	*old_tuple = replaced[0];
@@ -745,6 +910,10 @@ check_dup_dirty(struct txn_stmt *stmt, struct tuple *new_tuple,
 			       mode, space->index[0], space) != 0)
 		return -1;
 
+	if (visible_replaced == NULL)
+		check_hole(space, 0, new_tuple, stmt->txn,
+			   collected_conflicts, region);
+
 	for (uint32_t i = 1; i < space->index_count; i++) {
 		/*
 		 * Check that visible tuple is NULL or the same as in the
@@ -752,6 +921,8 @@ check_dup_dirty(struct txn_stmt *stmt, struct tuple *new_tuple,
 		 */
 		if (replaced[i] == NULL) {
 			/* NULL is OK. */
+			check_hole(space, i, new_tuple, stmt->txn,
+				   collected_conflicts, region);
 			continue;
 		}
 		if (!replaced[i]->is_dirty) {
@@ -778,6 +949,10 @@ check_dup_dirty(struct txn_stmt *stmt, struct tuple *new_tuple,
 				       check_visible, DUP_INSERT,
 				       space->index[i], space) != 0)
 			return -1;
+
+		if (check_visible == NULL)
+			check_hole(space, i, new_tuple, stmt->txn,
+				   collected_conflicts, region);
 	}
 
 	*old_tuple = visible_replaced;
@@ -861,6 +1036,8 @@ memtx_tx_history_add_stmt(struct txn_stmt *stmt, struct tuple *old_tuple,
 				memtx_tx_story_get(directly_replaced[i]);
 			memtx_tx_story_link_story(add_story, next, i);
 		}
+
+
 	} else {
 		if (old_tuple->is_dirty) {
 			del_story = memtx_tx_story_get(old_tuple);
@@ -882,11 +1059,12 @@ memtx_tx_history_add_stmt(struct txn_stmt *stmt, struct tuple *old_tuple,
 	/* Purge found conflicts. */
 	while (collected_conflicts != NULL) {
 		if (memtx_tx_cause_conflict(collected_conflicts->breaker,
-					    stmt->txn) != 0)
+					    collected_conflicts->victim) != 0)
 			goto fail;
 		collected_conflicts = collected_conflicts->next;
 	}
 
+
 	if (new_tuple != NULL) {
 		/*
 		 * A space holds references to all his tuples.
@@ -1292,6 +1470,158 @@ memtx_tx_track_read(struct txn *txn, struct space *space, struct tuple *tuple)
 	return 0;
 }
 
+/**
+ * Create new point_hole_item by given argumnets and put it to hash table.
+ */
+static int
+point_hole_storage_new(struct index *index, const char *key,
+		       size_t key_len, struct txn *txn)
+{
+	struct mempool *pool = &txm.point_hole_item_pool;
+	struct point_hole_item *object =
+		(struct point_hole_item *) mempool_alloc(pool);
+	if (object == NULL) {
+		diag_set(OutOfMemory, sizeof(*object),
+			 "mempool_alloc", "point_hole_item");
+		return -1;
+	}
+
+	rlist_create(&object->ring);
+	rlist_create(&object->in_point_holes_list);
+	object->txn = txn;
+	object->index_unique_id = index->unique_id;
+	if (key_len <= sizeof(object->short_key)) {
+		object->key = object->short_key;
+	} else {
+		object->key = (char *)region_alloc(&txn->region, key_len);
+		if (object->key == NULL) {
+			mempool_free(pool, object);
+			diag_set(OutOfMemory, key_len, "tx region",
+				 "point key");
+			return -1;
+		}
+	}
+	memcpy((char *)object->key, key, key_len);
+	object->key_len = key_len;
+	object->is_head = true;
+
+	struct key_def *def = index->def->key_def;
+	object->hash = object->index_unique_id ^ def->key_hash(key, def);
+
+	const struct point_hole_item **put =
+		(const struct point_hole_item **) &object;
+	struct point_hole_item *replaced = NULL;
+	struct point_hole_item **preplaced = &replaced;
+	mh_int_t pos = mh_point_holes_put(txm.point_holes, put,
+					    &preplaced, 0);
+	if (pos == mh_end(txm.point_holes)) {
+		mempool_free(pool, object);
+		diag_set(OutOfMemory, pos + 1, "mh_holes_storage_put",
+			 "mh_holes_storage_node");
+		return -1;
+	}
+	if (preplaced != NULL) {
+		/*
+		 * The item in hash table was overwitten. It's OK, but
+		 * we need replaced item to the item list.
+		 * */
+		rlist_add(&replaced->ring, &object->ring);
+		assert(replaced->is_head);
+		replaced->is_head = false;
+	} else {
+		txm.point_holes_size++;
+	}
+	rlist_add(&txn->point_holes_list, &object->in_point_holes_list);
+	return 0;
+}
+
+static void
+point_hole_storage_delete(struct point_hole_item *object)
+{
+	if (!object->is_head) {
+		/*
+		 * The deleting item is linked list, and the hash table
+		 * doesn't point directly to this item. Delete from the
+		 * list and that's enough.
+		 */
+		assert(!rlist_empty(&object->ring));
+		rlist_del(&object->ring);
+	} else if (!rlist_empty(&object->ring)) {
+		/*
+		 * Hash table point to this item, but there are more
+		 * items in the list. Relink the hash table with any other
+		 * item in the list, and delele this item from the list.
+		 */
+		struct point_hole_item *another =
+			rlist_entry(&object->ring, struct point_hole_item,
+				    ring);
+
+		const struct point_hole_item **put =
+			(const struct point_hole_item **) &another;
+		struct point_hole_item *replaced = NULL;
+		struct point_hole_item **preplaced = &replaced;
+		mh_int_t pos = mh_point_holes_put(txm.point_holes, put,
+						    &preplaced, 0);
+		assert(pos != mh_end(txm.point_holes)); (void)pos;
+		assert(replaced == object);
+		rlist_del(&object->ring);
+	} else {
+		/*
+		 * Hash table point to this item, and it's the last in the
+		 * list. We have to remove the item from the hash table.
+		 */
+		int exist = 0;
+		const struct point_hole_item **put =
+			(const struct point_hole_item **) &object;
+		mh_int_t pos = mh_point_holes_put_slot(txm.point_holes, put,
+						       &exist, 0);
+		assert(exist);
+		assert(pos != mh_end(txm.point_holes));
+		mh_point_holes_del(txm.point_holes, pos, 0);
+		txm.point_holes_size--;
+	}
+	rlist_del(&object->in_point_holes_list);
+	struct mempool *pool = &txm.point_hole_item_pool;
+	mempool_free(pool, object);
+}
+
+/**
+ * Record in TX manager that a transaction @a txn have read a nothing
+ * from @a space and @ a index with @ key.
+ * The key is expected to be full, that is has part count equal to part
+ * count in unique cmp_key of the index.
+ * @return 0 on success, -1 on memory error.
+ */
+int
+memtx_tx_track_point_slow(struct txn *txn, struct space *space,
+			  uint32_t index, const char *key)
+{
+	if (txn->status != TXN_INPROGRESS)
+		return 0;
+
+	struct key_def *def = space->index[index]->def->key_def;
+	const char *tmp = key;
+	for (uint32_t i = 0; i < def->part_count; i++)
+		mp_next(&tmp);
+	size_t key_len = tmp - key;
+	return point_hole_storage_new(space->index[index], key, key_len, txn);
+}
+
+/**
+ * Clean memtx_tx part of @a txm.
+ */
+void
+memtx_tx_clean_txn(struct txn *txn)
+{
+	while (!rlist_empty(&txn->point_holes_list)) {
+		struct point_hole_item *object =
+			rlist_first_entry(&txn->point_holes_list,
+					  struct point_hole_item,
+					  in_point_holes_list);
+		point_hole_storage_delete(object);
+	}
+}
+
 static uint32_t
 memtx_tx_snapshot_cleaner_hash(const struct tuple *a)
 {
diff --git a/src/box/memtx_tx.h b/src/box/memtx_tx.h
index 9922b5f5751350affdc1a76eafe10c48f312f073..6de6e04487433ffa7cf401b1cd5226d31673183c 100644
--- a/src/box/memtx_tx.h
+++ b/src/box/memtx_tx.h
@@ -268,6 +268,30 @@ memtx_tx_tuple_clarify_slow(struct txn *txn, struct space *space,
 int
 memtx_tx_track_read(struct txn *txn, struct space *space, struct tuple *tuple);
 
+
+/** Helper of memtx_tx_track_point */
+int
+memtx_tx_track_point_slow(struct txn *txn, struct space *space, uint32_t index,
+			  const char *key);
+
+/**
+ * Record in TX manager that a transaction @a txn have read a nothing
+ * from @a space and @ a index with @ key.
+ * The key is expected to be full, that is has part count equal to part
+ * count in unique cmp_key of the index.
+ * @return 0 on success, -1 on memory error.
+ */
+static inline int
+memtx_tx_track_point(struct txn *txn, struct space *space, uint32_t index,
+		     const char *key)
+{
+	if (!memtx_tx_manager_use_mvcc_engine)
+		return 0;
+	if (txn == NULL)
+		return 0;
+	return memtx_tx_track_point_slow(txn, space, index, key);
+}
+
 /**
  * Clean a tuple if it's dirty - finds a visible tuple in history.
  * @param txn - current transactions.
@@ -293,6 +317,12 @@ memtx_tx_tuple_clarify(struct txn *txn, struct space *space,
 					   is_prepared_ok);
 }
 
+/**
+ * Clean memtx_tx part of @a txm.
+ */
+void
+memtx_tx_clean_txn(struct txn *txn);
+
 /**
  * Notify manager the a space is deleted.
  * It's necessary because there is a chance that garbage collector hasn't
diff --git a/src/box/txn.c b/src/box/txn.c
index 959a3c3ee9b1216198956cf68e418e1c02a38f38..52c18af4f3bf1e9b26799c095c7fe27db8274b6a 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -200,6 +200,7 @@ txn_new(void)
 	assert(region_used(&region) == sizeof(*txn));
 	txn->region = region;
 	rlist_create(&txn->read_set);
+	rlist_create(&txn->point_holes_list);
 	rlist_create(&txn->conflict_list);
 	rlist_create(&txn->conflicted_by_list);
 	rlist_create(&txn->in_read_view_txs);
@@ -212,6 +213,7 @@ txn_new(void)
 inline static void
 txn_free(struct txn *txn)
 {
+	memtx_tx_clean_txn(txn);
 	struct tx_read_tracker *tracker, *tmp;
 	rlist_foreach_entry_safe(tracker, &txn->read_set,
 				 in_read_set, tmp) {
diff --git a/src/box/txn.h b/src/box/txn.h
index 8794335cd623919f37c821a5a3b65edc6ba94176..52e38562fd848e745f9f6bea06d417e112d24c3f 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -395,6 +395,8 @@ struct txn {
 	struct rlist in_read_view_txs;
 	/** List of tx_read_trackers with stories that the TX have read. */
 	struct rlist read_set;
+	/** List of point hole reads. @sa struct point_hole_item. */
+	struct rlist point_holes_list;
 };
 
 static inline bool
diff --git a/test/box/tx_man.result b/test/box/tx_man.result
index 4d07474a65d5e582a3639fe436d96c367253e567..dcf73357317ccb644a644891053a0a8f4b6362b1 100644
--- a/test/box/tx_man.result
+++ b/test/box/tx_man.result
@@ -1006,6 +1006,577 @@ s:drop()
  | ---
  | ...
 
+-- Point holes
+-- HASH
+-- One select
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='hash'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx2('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx1('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- One hash get
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='hash'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx2('s:get{1}')
+ | ---
+ | - 
+ | ...
+tx2('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx1('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- Same value get and select
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='hash'})
+ | ---
+ | ...
+i2 = s:create_index('sk', {type='hash'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx3:begin()
+ | ---
+ | - 
+ | ...
+tx2('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx3('s:get{1}')
+ | ---
+ | - 
+ | ...
+tx3('s:replace{3, 3, 3}')
+ | ---
+ | - - [3, 3, 3]
+ | ...
+tx1('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+tx3:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- Different value get and select
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='hash'})
+ | ---
+ | ...
+i2 = s:create_index('sk', {type='hash'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx3:begin()
+ | ---
+ | - 
+ | ...
+tx1('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:get{2}')
+ | ---
+ | - 
+ | ...
+tx1('s:replace{3, 3, 3}')
+ | ---
+ | - - [3, 3, 3]
+ | ...
+tx2('s:replace{4, 4, 4}')
+ | ---
+ | - - [4, 4, 4]
+ | ...
+tx3('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx3('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx3:commit()
+ | ---
+ | - 
+ | ...
+tx1:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ |   - [2, 2, 2]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- Different value get and select but in coorrect orders
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='hash'})
+ | ---
+ | ...
+i2 = s:create_index('sk', {type='hash'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx3:begin()
+ | ---
+ | - 
+ | ...
+tx1('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:get{2}')
+ | ---
+ | - 
+ | ...
+tx1('s:replace{3, 3, 3}')
+ | ---
+ | - - [3, 3, 3]
+ | ...
+tx2('s:replace{4, 4, 4}')
+ | ---
+ | - - [4, 4, 4]
+ | ...
+tx3('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx3('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - 
+ | ...
+tx3:commit()
+ | ---
+ | - 
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ |   - [2, 2, 2]
+ |   - [3, 3, 3]
+ |   - [4, 4, 4]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+--TREE
+-- One select
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='tree'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx2('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx1('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- One get
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='tree'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx2('s:get{1}')
+ | ---
+ | - 
+ | ...
+tx2('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx1('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- Same value get and select
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='tree'})
+ | ---
+ | ...
+i2 = s:create_index('sk', {type='tree'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx3:begin()
+ | ---
+ | - 
+ | ...
+tx2('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx3('s:get{1}')
+ | ---
+ | - 
+ | ...
+tx3('s:replace{3, 3, 3}')
+ | ---
+ | - - [3, 3, 3]
+ | ...
+tx1('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+tx3:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- Different value get and select
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='tree'})
+ | ---
+ | ...
+i2 = s:create_index('sk', {type='tree'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx3:begin()
+ | ---
+ | - 
+ | ...
+tx1('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:get{2}')
+ | ---
+ | - 
+ | ...
+tx1('s:replace{3, 3, 3}')
+ | ---
+ | - - [3, 3, 3]
+ | ...
+tx2('s:replace{4, 4, 4}')
+ | ---
+ | - - [4, 4, 4]
+ | ...
+tx3('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx3('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx3:commit()
+ | ---
+ | - 
+ | ...
+tx1:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+tx2:commit()
+ | ---
+ | - - {'error': 'Transaction has been aborted by conflict'}
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ |   - [2, 2, 2]
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- Different value get and select but in coorrect orders
+s = box.schema.space.create('test')
+ | ---
+ | ...
+i1 = s:create_index('pk', {type='tree'})
+ | ---
+ | ...
+i2 = s:create_index('sk', {type='tree'})
+ | ---
+ | ...
+tx1:begin()
+ | ---
+ | - 
+ | ...
+tx2:begin()
+ | ---
+ | - 
+ | ...
+tx3:begin()
+ | ---
+ | - 
+ | ...
+tx1('s:select{1}')
+ | ---
+ | - - []
+ | ...
+tx2('s:get{2}')
+ | ---
+ | - 
+ | ...
+tx1('s:replace{3, 3, 3}')
+ | ---
+ | - - [3, 3, 3]
+ | ...
+tx2('s:replace{4, 4, 4}')
+ | ---
+ | - - [4, 4, 4]
+ | ...
+tx3('s:replace{1, 1, 1}')
+ | ---
+ | - - [1, 1, 1]
+ | ...
+tx3('s:replace{2, 2, 2}')
+ | ---
+ | - - [2, 2, 2]
+ | ...
+tx1:commit()
+ | ---
+ | - 
+ | ...
+tx2:commit()
+ | ---
+ | - 
+ | ...
+tx3:commit()
+ | ---
+ | - 
+ | ...
+s:select{}
+ | ---
+ | - - [1, 1, 1]
+ |   - [2, 2, 2]
+ |   - [3, 3, 3]
+ |   - [4, 4, 4]
+ | ...
+s:drop()
+ | ---
+ | ...
+
 test_run:cmd("switch default")
  | ---
  | - true
diff --git a/test/box/tx_man.test.lua b/test/box/tx_man.test.lua
index 32d384435a7439a5eca2837a333d1363e8a16cb4..90c445e9440cdca5cedf8366c6baf5c0514882f8 100644
--- a/test/box/tx_man.test.lua
+++ b/test/box/tx_man.test.lua
@@ -297,6 +297,173 @@ s:replace{1, 1, 2 }
 s:select{}
 s:drop()
 
+-- Point holes
+-- HASH
+-- One select
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='hash'})
+tx1:begin()
+tx2:begin()
+tx2('s:select{1}')
+tx2('s:replace{2, 2, 2}')
+tx1('s:replace{1, 1, 1}')
+tx1:commit()
+tx2:commit()
+s:select{}
+s:drop()
+
+-- One hash get
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='hash'})
+tx1:begin()
+tx2:begin()
+tx2('s:get{1}')
+tx2('s:replace{2, 2, 2}')
+tx1('s:replace{1, 1, 1}')
+tx1:commit()
+tx2:commit()
+s:select{}
+s:drop()
+
+-- Same value get and select
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='hash'})
+i2 = s:create_index('sk', {type='hash'})
+tx1:begin()
+tx2:begin()
+tx3:begin()
+tx2('s:select{1}')
+tx2('s:replace{2, 2, 2}')
+tx3('s:get{1}')
+tx3('s:replace{3, 3, 3}')
+tx1('s:replace{1, 1, 1}')
+tx1:commit()
+tx2:commit()
+tx3:commit()
+s:select{}
+s:drop()
+
+-- Different value get and select
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='hash'})
+i2 = s:create_index('sk', {type='hash'})
+tx1:begin()
+tx2:begin()
+tx3:begin()
+tx1('s:select{1}')
+tx2('s:get{2}')
+tx1('s:replace{3, 3, 3}')
+tx2('s:replace{4, 4, 4}')
+tx3('s:replace{1, 1, 1}')
+tx3('s:replace{2, 2, 2}')
+tx3:commit()
+tx1:commit()
+tx2:commit()
+s:select{}
+s:drop()
+
+-- Different value get and select but in coorrect orders
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='hash'})
+i2 = s:create_index('sk', {type='hash'})
+tx1:begin()
+tx2:begin()
+tx3:begin()
+tx1('s:select{1}')
+tx2('s:get{2}')
+tx1('s:replace{3, 3, 3}')
+tx2('s:replace{4, 4, 4}')
+tx3('s:replace{1, 1, 1}')
+tx3('s:replace{2, 2, 2}')
+tx1:commit()
+tx2:commit()
+tx3:commit()
+s:select{}
+s:drop()
+
+--TREE
+-- One select
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='tree'})
+tx1:begin()
+tx2:begin()
+tx2('s:select{1}')
+tx2('s:replace{2, 2, 2}')
+tx1('s:replace{1, 1, 1}')
+tx1:commit()
+tx2:commit()
+s:select{}
+s:drop()
+
+-- One get
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='tree'})
+tx1:begin()
+tx2:begin()
+tx2('s:get{1}')
+tx2('s:replace{2, 2, 2}')
+tx1('s:replace{1, 1, 1}')
+tx1:commit()
+tx2:commit()
+s:select{}
+s:drop()
+
+-- Same value get and select
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='tree'})
+i2 = s:create_index('sk', {type='tree'})
+tx1:begin()
+tx2:begin()
+tx3:begin()
+tx2('s:select{1}')
+tx2('s:replace{2, 2, 2}')
+tx3('s:get{1}')
+tx3('s:replace{3, 3, 3}')
+tx1('s:replace{1, 1, 1}')
+tx1:commit()
+tx2:commit()
+tx3:commit()
+s:select{}
+s:drop()
+
+-- Different value get and select
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='tree'})
+i2 = s:create_index('sk', {type='tree'})
+tx1:begin()
+tx2:begin()
+tx3:begin()
+tx1('s:select{1}')
+tx2('s:get{2}')
+tx1('s:replace{3, 3, 3}')
+tx2('s:replace{4, 4, 4}')
+tx3('s:replace{1, 1, 1}')
+tx3('s:replace{2, 2, 2}')
+tx3:commit()
+tx1:commit()
+tx2:commit()
+s:select{}
+s:drop()
+
+-- Different value get and select but in coorrect orders
+s = box.schema.space.create('test')
+i1 = s:create_index('pk', {type='tree'})
+i2 = s:create_index('sk', {type='tree'})
+tx1:begin()
+tx2:begin()
+tx3:begin()
+tx1('s:select{1}')
+tx2('s:get{2}')
+tx1('s:replace{3, 3, 3}')
+tx2('s:replace{4, 4, 4}')
+tx3('s:replace{1, 1, 1}')
+tx3('s:replace{2, 2, 2}')
+tx1:commit()
+tx2:commit()
+tx3:commit()
+s:select{}
+s:drop()
+
 test_run:cmd("switch default")
 test_run:cmd("stop server tx_man")
 test_run:cmd("cleanup server tx_man")