diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index d4929a372a47b74dbdd5dd8701ac97d3e9429955..9372e5f7f7c22de2150245f1deffe4219a14dc93 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1277,9 +1277,17 @@ vinyl_index_compact(struct index *index)
  * If the LSM tree is going to be dropped or truncated on WAL
  * recovery, there's no point in replaying statements for it,
  * either.
+ *
+ * Note, although we may skip secondary index update in certain
+ * cases (e.g.  UPDATE that doesn't touch secondary index parts
+ * or DELETE for which generation of secondary index statement
+ * is deferred), a DML request of any kind always updates the
+ * primary index. Also, we always dump the primary index after
+ * secondary indexes. So we may skip recovery of a DML request
+ * if it has been dumped for the primary index.
  */
 static inline bool
-vy_is_committed_one(struct vy_env *env, struct vy_lsm *lsm)
+vy_is_committed(struct vy_env *env, struct vy_lsm *lsm)
 {
 	if (likely(env->status != VINYL_FINAL_RECOVERY_LOCAL))
 		return false;
@@ -1290,23 +1298,6 @@ vy_is_committed_one(struct vy_env *env, struct vy_lsm *lsm)
 	return false;
 }
 
-/**
- * Check if a request has already been committed to a space.
- * See also vy_is_committed_one().
- */
-static inline bool
-vy_is_committed(struct vy_env *env, struct space *space)
-{
-	if (likely(env->status != VINYL_FINAL_RECOVERY_LOCAL))
-		return false;
-	for (uint32_t iid = 0; iid < space->index_count; iid++) {
-		struct vy_lsm *lsm = vy_lsm(space->index[iid]);
-		if (!vy_is_committed_one(env, lsm))
-			return false;
-	}
-	return true;
-}
-
 /**
  * Get a full tuple by a tuple read from a secondary index.
  * @param lsm         LSM tree from which the tuple was read.
@@ -1772,11 +1763,11 @@ static int
 vy_delete(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 	  struct space *space, struct request *request)
 {
-	if (vy_is_committed(env, space))
-		return 0;
 	struct vy_lsm *pk = vy_lsm_find(space, 0);
 	if (pk == NULL)
 		return -1;
+	if (vy_is_committed(env, pk))
+		return 0;
 	struct vy_lsm *lsm = vy_lsm_find_unique(space, request->index_id);
 	if (lsm == NULL)
 		return -1;
@@ -1807,7 +1798,7 @@ vy_delete(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 			return -1;
 		for (uint32_t i = 0; i < space->index_count; i++) {
 			struct vy_lsm *lsm = vy_lsm(space->index[i]);
-			if (vy_is_committed_one(env, lsm))
+			if (vy_is_committed(env, lsm))
 				continue;
 			rc = vy_tx_set(tx, lsm, delete);
 			if (rc != 0)
@@ -1891,7 +1882,7 @@ vy_perform_update(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 
 	for (uint32_t i = 1; i < space->index_count; ++i) {
 		struct vy_lsm *lsm = vy_lsm(space->index[i]);
-		if (vy_is_committed_one(env, lsm))
+		if (vy_is_committed(env, lsm))
 			continue;
 		if (vy_tx_set_with_colmask(tx, lsm, delete, column_mask) != 0)
 			goto error;
@@ -1924,7 +1915,10 @@ vy_update(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 	  struct space *space, struct request *request)
 {
 	assert(tx != NULL && tx->state == VINYL_TX_READY);
-	if (vy_is_committed(env, space))
+	struct vy_lsm *pk = vy_lsm_find(space, 0);
+	if (pk == NULL)
+		return -1;
+	if (vy_is_committed(env, pk))
 		return 0;
 	struct vy_lsm *lsm = vy_lsm_find_unique(space, request->index_id);
 	if (lsm == NULL)
@@ -1942,11 +1936,6 @@ vy_update(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 		return 0;
 
 	/* Apply update operations. */
-	struct vy_lsm *pk = vy_lsm(space->index[0]);
-	assert(pk != NULL);
-	assert(pk->index_id == 0);
-	/* Primary key is dumped last. */
-	assert(!vy_is_committed_one(env, pk));
 	uint64_t column_mask = 0;
 	const char *new_tuple, *new_tuple_end;
 	uint32_t new_size, old_size;
@@ -2126,7 +2115,10 @@ vy_upsert(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 	  struct space *space, struct request *request)
 {
 	assert(tx != NULL && tx->state == VINYL_TX_READY);
-	if (vy_is_committed(env, space))
+	struct vy_lsm *pk = vy_lsm_find(space, 0);
+	if (pk == NULL)
+		return -1;
+	if (vy_is_committed(env, pk))
 		return 0;
 	/* Check update operations. */
 	if (tuple_update_check_ops(region_aligned_alloc_cb, &fiber()->gc,
@@ -2143,11 +2135,6 @@ vy_upsert(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 	const char *tuple_end = request->tuple_end;
 	const char *ops = request->ops;
 	const char *ops_end = request->ops_end;
-	struct vy_lsm *pk = vy_lsm_find(space, 0);
-	if (pk == NULL)
-		return -1;
-	/* Primary key is dumped last. */
-	assert(!vy_is_committed_one(env, pk));
 	if (tuple_validate_raw(pk->mem_format, tuple))
 		return -1;
 
@@ -2240,14 +2227,12 @@ static int
 vy_insert(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 	  struct space *space, struct request *request)
 {
-	assert(stmt != NULL);
+	assert(tx != NULL && tx->state == VINYL_TX_READY);
 	struct vy_lsm *pk = vy_lsm_find(space, 0);
 	if (pk == NULL)
-		/* The space hasn't the primary index. */
 		return -1;
-	assert(pk->index_id == 0);
-	/* Primary key is dumped last. */
-	assert(!vy_is_committed_one(env, pk));
+	if (vy_is_committed(env, pk))
+		return 0;
 	if (tuple_validate_raw(pk->mem_format, request->tuple))
 		return -1;
 	/* First insert into the primary index. */
@@ -2263,7 +2248,7 @@ vy_insert(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 
 	for (uint32_t iid = 1; iid < space->index_count; ++iid) {
 		struct vy_lsm *lsm = vy_lsm(space->index[iid]);
-		if (vy_is_committed_one(env, lsm))
+		if (vy_is_committed(env, lsm))
 			continue;
 		if (vy_tx_set(tx, lsm, stmt->new_tuple) != 0)
 			return -1;
@@ -2290,16 +2275,11 @@ vy_replace(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 	   struct space *space, struct request *request)
 {
 	assert(tx != NULL && tx->state == VINYL_TX_READY);
-	if (vy_is_committed(env, space))
-		return 0;
-	if (request->type == IPROTO_INSERT)
-		return vy_insert(env, tx, stmt, space, request);
-
 	struct vy_lsm *pk = vy_lsm_find(space, 0);
 	if (pk == NULL)
 		return -1;
-	/* Primary key is dumped last. */
-	assert(!vy_is_committed_one(env, pk));
+	if (vy_is_committed(env, pk))
+		return 0;
 
 	/* Validate and create a statement for the new tuple. */
 	if (tuple_validate_raw(pk->mem_format, request->tuple))
@@ -2352,7 +2332,7 @@ vy_replace(struct vy_env *env, struct vy_tx *tx, struct txn_stmt *stmt,
 	}
 	for (uint32_t i = 1; i < space->index_count; i++) {
 		struct vy_lsm *lsm = vy_lsm(space->index[i]);
-		if (vy_is_committed_one(env, lsm))
+		if (vy_is_committed(env, lsm))
 			continue;
 		if (delete != NULL) {
 			rc = vy_tx_set(tx, lsm, delete);
@@ -2376,7 +2356,12 @@ vinyl_space_execute_replace(struct space *space, struct txn *txn,
 	struct vy_env *env = vy_env(space->engine);
 	struct vy_tx *tx = txn->engine_tx;
 	struct txn_stmt *stmt = txn_current_stmt(txn);
-	if (vy_replace(env, tx, stmt, space, request))
+	int rc;
+	if (request->type == IPROTO_INSERT)
+		rc = vy_insert(env, tx, stmt, space, request);
+	else
+		rc = vy_replace(env, tx, stmt, space, request);
+	if (rc != 0)
 		return -1;
 	*result = stmt->new_tuple;
 	return 0;
@@ -3300,6 +3285,8 @@ vinyl_space_apply_initial_join_row(struct space *space, struct request *request)
 	int rc = -1;
 	switch (request->type) {
 	case IPROTO_INSERT:
+		rc = vy_insert(env, tx, &stmt, space, request);
+		break;
 	case IPROTO_REPLACE:
 		rc = vy_replace(env, tx, &stmt, space, request);
 		break;
@@ -4488,7 +4475,7 @@ vy_deferred_delete_on_rollback(struct trigger *trigger, void *event)
  * to restore deferred DELETE statements that haven't been dumped
  * to disk. To skip deferred DELETEs that have been dumped, we
  * use the same technique we employ for normal WAL statements,
- * i.e. we filter them by LSN, see vy_is_committed_one(). To do
+ * i.e. we filter them by LSN, see vy_is_committed(). To do
  * that, we need to account the LSN of a WAL row that generated
  * a deferred DELETE statement to vy_lsm::dump_lsn, so we install
  * an on_commit trigger that propagates the LSN of the WAL row to
@@ -4575,7 +4562,7 @@ vy_deferred_delete_on_replace(struct trigger *trigger, void *event)
 	struct tuple *region_stmt = NULL;
 	for (uint32_t i = 1; i < space->index_count; i++) {
 		struct vy_lsm *lsm = vy_lsm(space->index[i]);
-		if (vy_is_committed_one(env, lsm))
+		if (vy_is_committed(env, lsm))
 			continue;
 		/*
 		 * As usual, rotate the active in-memory index if
diff --git a/test/vinyl/recover.result b/test/vinyl/recover.result
index a6313f225fa5b523d7c577a5605ae8da72898655..345a019ddc1b017e88eb78ef42675edfdf886b71 100644
--- a/test/vinyl/recover.result
+++ b/test/vinyl/recover.result
@@ -479,3 +479,63 @@ test_run:cmd('cleanup server force_recovery')
 ---
 - true
 ...
+--
+-- gh-4222: assertion failure while recovering dumped statement
+-- that isn't present in secondary index.
+--
+test_run:cmd('create server test with script = "vinyl/low_quota.lua"')
+---
+- true
+...
+test_run:cmd('start server test with args="1048576"')
+---
+- true
+...
+test_run:cmd('switch test')
+---
+- true
+...
+s = box.schema.space.create('test', {engine = 'vinyl'})
+---
+...
+pk = s:create_index('primary')
+---
+...
+sk = s:create_index('secondary', {unique = false, parts = {2, 'unsigned'}})
+---
+...
+s:insert{1, 1, 1}
+---
+- [1, 1, 1]
+...
+box.snapshot()
+---
+- ok
+...
+for i = 1, 1024 do s:update(1, {{'=', 3, string.rep('x', 1024)}}) end
+---
+...
+test_run:wait_cond(function() return pk:stat().disk.dump.count == 2 end)
+---
+- true
+...
+sk:stat().disk.dump.count -- 1
+---
+- 1
+...
+test_run:cmd('restart server test with args="1048576"')
+box.space.test:drop()
+---
+...
+test_run:cmd('switch default')
+---
+- true
+...
+test_run:cmd('stop server test')
+---
+- true
+...
+test_run:cmd('cleanup server test')
+---
+- true
+...
diff --git a/test/vinyl/recover.test.lua b/test/vinyl/recover.test.lua
index 1ed266a1af745dcf84f58c9dc0359b1813b6913f..7bdba17826b201ec496301de76ca755325256136 100644
--- a/test/vinyl/recover.test.lua
+++ b/test/vinyl/recover.test.lua
@@ -164,3 +164,30 @@ sum
 test_run:cmd('switch default')
 test_run:cmd('stop server force_recovery')
 test_run:cmd('cleanup server force_recovery')
+
+--
+-- gh-4222: assertion failure while recovering dumped statement
+-- that isn't present in secondary index.
+--
+test_run:cmd('create server test with script = "vinyl/low_quota.lua"')
+test_run:cmd('start server test with args="1048576"')
+test_run:cmd('switch test')
+
+s = box.schema.space.create('test', {engine = 'vinyl'})
+pk = s:create_index('primary')
+sk = s:create_index('secondary', {unique = false, parts = {2, 'unsigned'}})
+
+s:insert{1, 1, 1}
+box.snapshot()
+
+for i = 1, 1024 do s:update(1, {{'=', 3, string.rep('x', 1024)}}) end
+test_run:wait_cond(function() return pk:stat().disk.dump.count == 2 end)
+sk:stat().disk.dump.count -- 1
+
+test_run:cmd('restart server test with args="1048576"')
+
+box.space.test:drop()
+
+test_run:cmd('switch default')
+test_run:cmd('stop server test')
+test_run:cmd('cleanup server test')