diff --git a/changelogs/unreleased/gh-10870-fix-vinyl-tx-stmt-overwrite-multikey.md b/changelogs/unreleased/gh-10870-fix-vinyl-tx-stmt-overwrite-multikey.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bee6e7deff8fbc851d0f539cacea648ed6a8be6
--- /dev/null
+++ b/changelogs/unreleased/gh-10870-fix-vinyl-tx-stmt-overwrite-multikey.md
@@ -0,0 +1,6 @@
+## bugfix/vinyl
+
+* Fixed a bug when a tuple could disappear from a multikey index in case it
+  replaced a tuple with duplicate multikey array entries created in the same
+  transaction. With the enabled `defer_deletes` space option, the bug could
+  also trigger a crash (gh-10869, gh-10870).
diff --git a/src/box/vy_tx.c b/src/box/vy_tx.c
index a0d85a67973d34cdd427b1ca79d6b52ab8d4b8e6..8f9acb94e2c38552898cbe27de2274aa98268b74 100644
--- a/src/box/vy_tx.c
+++ b/src/box/vy_tx.c
@@ -1071,6 +1071,23 @@ static int
 vy_tx_set_entry(struct vy_tx *tx, struct vy_lsm *lsm, struct vy_entry entry)
 {
 	assert(vy_stmt_type(entry.stmt) != 0);
+
+	struct txv *old = write_set_search_key(&tx->write_set, lsm, entry);
+	if (old != NULL && old->entry.stmt == entry.stmt) {
+		/*
+		 * The inserted statement is already indexed in the write set.
+		 * This may happen only if this is a multikey index and the
+		 * indexed array has duplicate entries. Inserting a duplicate
+		 * into the write set is pointless. Moreover, it may break
+		 * assumptions taken by the optimizations applied below, like
+		 * REPLACE + DELETE = NOP. Let's skip it.
+		 */
+		assert(lsm->cmp_def->is_multikey);
+		assert(!old->is_overwritten);
+		assert(old->entry.hint != entry.hint);
+		return 0;
+	}
+
 	/**
 	 * A statement in write set must have and unique lsn
 	 * in order to differ it from cachable statements in mem and run.
@@ -1078,7 +1095,6 @@ vy_tx_set_entry(struct vy_tx *tx, struct vy_lsm *lsm, struct vy_entry entry)
 	vy_stmt_set_lsn(entry.stmt, INT64_MAX);
 	struct vy_entry applied = vy_entry_none();
 
-	struct txv *old = write_set_search_key(&tx->write_set, lsm, entry);
 	/* Found a match of the previous action of this transaction */
 	if (old != NULL && vy_stmt_type(entry.stmt) == IPROTO_UPSERT) {
 		assert(lsm->index_id == 0);
diff --git a/test/vinyl-luatest/gh_10820_tx_stmt_overwrite_test.lua b/test/vinyl-luatest/tx_stmt_overwrite_test.lua
similarity index 75%
rename from test/vinyl-luatest/gh_10820_tx_stmt_overwrite_test.lua
rename to test/vinyl-luatest/tx_stmt_overwrite_test.lua
index 47118417f9c60e2ce6008055ddd26b6019c025d9..ae96d664b44d636ebdb2247d558838ab9e37ef8b 100644
--- a/test/vinyl-luatest/gh_10820_tx_stmt_overwrite_test.lua
+++ b/test/vinyl-luatest/tx_stmt_overwrite_test.lua
@@ -39,6 +39,15 @@ g.before_each(function(cg)
             unique = params.unique,
             parts = {{3, 'unsigned'}},
         })
+        box.schema.space.create('test3', {
+            engine = 'vinyl',
+            defer_deletes = params.defer_deletes,
+        })
+        box.space.test3:create_index('i1')
+        box.space.test3:create_index('i2', {
+            unique = params.unique,
+            parts = {{'[2][*]', 'unsigned'}},
+        })
     end, {cg.params})
 end)
 
@@ -50,9 +59,13 @@ g.after_each(function(cg)
         if box.space.test2 ~= nil then
             box.space.test2:drop()
         end
+        if box.space.test3 ~= nil then
+            box.space.test3:drop()
+        end
     end)
 end)
 
+-- gh-10820
 g.test_case_1 = function(cg)
     cg.server:exec(function()
         local s = box.space.test1
@@ -71,6 +84,7 @@ g.test_case_1 = function(cg)
     end)
 end
 
+-- gh-10822
 g.test_case_2 = function(cg)
     cg.server:exec(function()
         local s = box.space.test1
@@ -164,3 +178,43 @@ g.test_case_6 = function(cg)
         t.assert_covers(s.index.i3:stat(), stat)
     end)
 end
+
+-- gh-10869
+g.test_case_7 = function(cg)
+    cg.server:exec(function()
+        local s = box.space.test3
+        s:insert({1, {10}})
+        box.begin()
+        s:replace({1, {1, 1}})
+        s:update({1}, {{'=', 2, {2, 3, 4}}})
+        box.commit()
+        box.snapshot()
+        local tuple = {1, {2, 3, 4}}
+        t.assert_equals(s.index.i1:select({}, {fullscan = true}),
+                        {tuple})
+        t.assert_equals(s.index.i2:select({}, {fullscan = true}),
+                        {tuple, tuple, tuple})
+        t.assert_covers(s.index.i1:stat(),
+                        {memory = {rows = 0}, disk = {rows = 1}})
+        t.assert_covers(s.index.i2:stat(),
+                        {memory = {rows = 0}, disk = {rows = 3}})
+    end)
+end
+
+-- gh-10870
+g.test_case_8 = function(cg)
+    cg.server:exec(function()
+        local s = box.space.test3
+        box.begin()
+        s:replace({1, {1, 1}})
+        s:update({1}, {{'=', 2, {1}}})
+        box.commit()
+        box.snapshot()
+        local data = {{1, {1}}}
+        t.assert_equals(s.index.i1:select({}, {fullscan = true}), data)
+        t.assert_equals(s.index.i2:select({}, {fullscan = true}), data)
+        local stat = {memory = {rows = 0}, disk = {rows = 1}}
+        t.assert_covers(s.index.i1:stat(), stat)
+        t.assert_covers(s.index.i2:stat(), stat)
+    end)
+end