diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 246654cb71257a09926b67b9fe22f5113057e2c5..17ab19078d568755ba53922e8f6267fb2b912d72 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -2823,13 +2823,9 @@ case OP_Count: {         /* out2 */
 case OP_Savepoint: {
 	int p1;                         /* Value of P1 operand */
 	char *zName;                    /* Name of savepoint */
-	Savepoint *pNew;
-	Savepoint *pSavepoint;
-	Savepoint *pTmp;
 	struct txn *txn = in_txn();
-	struct sql_txn *psql_txn = txn != NULL ? txn->psql_txn : NULL;
 
-	if (psql_txn == NULL) {
+	if (txn == NULL) {
 		assert(!box_txn());
 		diag_set(ClientError, ER_NO_TRANSACTION);
 		goto abort_due_to_error;
@@ -2840,69 +2836,31 @@ case OP_Savepoint: {
 	/* Assert that the p1 parameter is valid. Also that if there is no open
 	 * transaction, then there cannot be any savepoints.
 	 */
-	assert(psql_txn->pSavepoint == NULL || box_txn());
+	assert(rlist_empty(&txn->savepoints) || box_txn());
 	assert(p1==SAVEPOINT_BEGIN||p1==SAVEPOINT_RELEASE||p1==SAVEPOINT_ROLLBACK);
 
 	if (p1==SAVEPOINT_BEGIN) {
-		/* Create a new savepoint structure. */
-		pNew = sql_savepoint(p, zName);
-		/* Link the new savepoint into the database handle's list. */
-		pNew->pNext = psql_txn->pSavepoint;
-		psql_txn->pSavepoint = pNew;
+		/*
+		 * Savepoint is available by its name so we don't
+		 * care about object itself.
+		 */
+		if (txn_savepoint_new(txn, zName) == NULL)
+			goto abort_due_to_error;
 	} else {
 		/* Find the named savepoint. If there is no such savepoint, then an
 		 * an error is returned to the user.
 		 */
-		for(
-			pSavepoint = psql_txn->pSavepoint;
-			pSavepoint && sqlStrICmp(pSavepoint->zName, zName);
-			pSavepoint = pSavepoint->pNext
-			);
-		if (!pSavepoint) {
+		struct txn_savepoint *sv = txn_savepoint_by_name(txn, zName);
+		if (sv == NULL) {
 			diag_set(ClientError, ER_NO_SUCH_SAVEPOINT);
 			goto abort_due_to_error;
+		}
+		if (p1 == SAVEPOINT_RELEASE) {
+			txn_savepoint_release(sv);
 		} else {
-
-			/* Determine whether or not this is a transaction savepoint. If so,
-			 * and this is a RELEASE command, then the current transaction
-			 * is committed.
-			 */
-			int isTransaction = pSavepoint->pNext == 0;
-			if (isTransaction && p1==SAVEPOINT_RELEASE) {
-				if ((rc = sqlVdbeCheckFk(p, 1)) != 0)
-					goto vdbe_return;
-				sqlVdbeHalt(p);
-				if (p->is_aborted)
-					goto abort_due_to_error;
-			} else {
-				if (p1==SAVEPOINT_ROLLBACK)
-					box_txn_rollback_to_savepoint(pSavepoint->tnt_savepoint);
-			}
-
-			/* Regardless of whether this is a RELEASE or ROLLBACK, destroy all
-			 * savepoints nested inside of the savepoint being operated on.
-			 */
-			while (psql_txn->pSavepoint != pSavepoint) {
-				pTmp = psql_txn->pSavepoint;
-				psql_txn->pSavepoint = pTmp->pNext;
-				/*
-				 * Since savepoints are stored in region, we do not
-				 * have to destroy them
-				 */
-			}
-
-			/* If it is a RELEASE, then destroy the savepoint being operated on
-			 * too. If it is a ROLLBACK TO, then set the number of deferred
-			 * constraint violations present in the database to the value stored
-			 * when the savepoint was created.
-			 */
-			if (p1==SAVEPOINT_RELEASE) {
-				assert(pSavepoint == psql_txn->pSavepoint);
-				psql_txn->pSavepoint = pSavepoint->pNext;
-			} else {
-				txn->fk_deferred_count =
-					pSavepoint->tnt_savepoint->fk_deferred_count;
-			}
+			assert(p1 == SAVEPOINT_ROLLBACK);
+			if (box_txn_rollback_to_savepoint(sv) != 0)
+				goto abort_due_to_error;
 		}
 	}
 
@@ -2940,7 +2898,11 @@ case OP_CheckViewReferences: {
  * Otherwise, raise an error with appropriate error message.
  */
 case OP_TransactionBegin: {
-	if (sql_txn_begin() != 0)
+	if (in_txn()) {
+		diag_set(ClientError, ER_ACTIVE_TRANSACTION);
+		goto abort_due_to_error;
+	}
+	if (txn_begin() == NULL)
 		goto abort_due_to_error;
 	p->auto_commit = false	;
 	break;
@@ -2996,7 +2958,7 @@ case OP_TransactionRollback: {
  */
 case OP_TTransaction: {
 	if (!box_txn()) {
-		if (sql_txn_begin() != 0)
+		if (txn_begin() == NULL)
 			goto abort_due_to_error;
 	} else {
 		p->anonymous_savepoint = sql_savepoint(p, NULL);
@@ -4843,7 +4805,6 @@ case OP_FkCounter: {
 	if (((p->sql_flags & SQL_DeferFKs) != 0 || pOp->p1 != 0) &&
 	    !p->auto_commit) {
 		struct txn *txn = in_txn();
-		assert(txn != NULL && txn->psql_txn != NULL);
 		txn->fk_deferred_count += pOp->p2;
 	} else {
 		p->nFkConstraint += pOp->p2;
@@ -4867,7 +4828,6 @@ case OP_FkIfZero: {         /* jump */
 	if (((p->sql_flags & SQL_DeferFKs) != 0 || pOp->p1 != 0) &&
 	    !p->auto_commit) {
 		struct txn *txn = in_txn();
-		assert(txn != NULL && txn->psql_txn != NULL);
 		if (txn->fk_deferred_count == 0)
 			goto jump_to_p2;
 	} else {
diff --git a/src/box/sql/vdbe.h b/src/box/sql/vdbe.h
index 8f16202ba47499c9244da4e26e3903ce94e150f0..06f25880560317d87a98d2a5b82caf91919835a4 100644
--- a/src/box/sql/vdbe.h
+++ b/src/box/sql/vdbe.h
@@ -172,15 +172,6 @@ struct SubProgram {
  */
 Vdbe *sqlVdbeCreate(Parse *);
 
-/**
- * Allocate and initialize SQL-specific struct which completes
- * original Tarantool's txn struct using region allocator.
- *
- * @retval NULL on OOM, new psql_txn struct on success.
- **/
-struct sql_txn *
-sql_alloc_txn();
-
 /**
  * Prepare given VDBE to execution: initialize structs connected
  * with transaction routine: autocommit mode, deferred foreign
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index 5582d9506bfe825bceb1193fb9d28faf5fdc0c8c..3050f75b787f12a6665b5df4793b0d1e5488b70c 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -453,8 +453,7 @@ u32 sqlVdbeSerialGet(const unsigned char *, u32, Mem *);
 
 int sqlVdbeExec(Vdbe *);
 int sqlVdbeList(Vdbe *);
-int
-sql_txn_begin();
+
 Savepoint *
 sql_savepoint(Vdbe *p,
 	      const char *zName);
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index bbbb99c7048d780faadaa36c97eb03c915821641..b77fffaec2cafa768af0a15456da5c17c8d4d243 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -76,39 +76,12 @@ sqlVdbeCreate(Parse * pParse)
 	return p;
 }
 
-struct sql_txn *
-sql_alloc_txn()
-{
-	struct sql_txn *txn = region_alloc_object(&fiber()->gc,
-						  struct sql_txn);
-	if (txn == NULL) {
-		diag_set(OutOfMemory, sizeof(struct sql_txn), "region",
-			 "struct sql_txn");
-		return NULL;
-	}
-	txn->pSavepoint = NULL;
-	return txn;
-}
-
 int
 sql_vdbe_prepare(struct Vdbe *vdbe)
 {
 	assert(vdbe != NULL);
 	struct txn *txn = in_txn();
 	vdbe->auto_commit = txn == NULL;
-	if (txn != NULL) {
-		/*
-		 * If transaction has been started in Lua, then
-		 * sql_txn is NULL. On the other hand, it is not
-		 * critical, since in Lua it is impossible to
-		 * check FK violations, at least now.
-		 */
-		if (txn->psql_txn == NULL) {
-			txn->psql_txn = sql_alloc_txn();
-			if (txn->psql_txn == NULL)
-				return -1;
-		}
-	}
 	return 0;
 }
 
@@ -1998,24 +1971,6 @@ sqlVdbeCheckFk(Vdbe * p, int deferred)
 	return 0;
 }
 
-int
-sql_txn_begin()
-{
-	if (in_txn()) {
-		diag_set(ClientError, ER_ACTIVE_TRANSACTION);
-		return -1;
-	}
-	struct txn *ptxn = txn_begin(false);
-	if (ptxn == NULL)
-		return -1;
-	ptxn->psql_txn = sql_alloc_txn();
-	if (ptxn->psql_txn == NULL) {
-		box_txn_rollback();
-		return -1;
-	}
-	return 0;
-}
-
 Savepoint *
 sql_savepoint(MAYBE_UNUSED Vdbe *p, const char *zName)
 {
diff --git a/src/box/txn.c b/src/box/txn.c
index b57240846fb3166804dce0872f7e6f62140d34a3..1002c2136878f13becc5f56019d3c236e56faac5 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -221,8 +221,8 @@ txn_begin()
 	txn->signature = -1;
 	txn->engine = NULL;
 	txn->engine_tx = NULL;
-	txn->psql_txn = NULL;
 	txn->fk_deferred_count = 0;
+	rlist_create(&txn->savepoints);
 	txn->fiber = NULL;
 	fiber_set_txn(fiber(), txn);
 	/* fiber_on_yield is initialized by engine on demand */
@@ -734,28 +734,53 @@ box_txn_alloc(size_t size)
 	                            alignof(union natural_align));
 }
 
-box_txn_savepoint_t *
-box_txn_savepoint()
+struct txn_savepoint *
+txn_savepoint_new(struct txn *txn, const char *name)
 {
-	struct txn *txn = in_txn();
-	if (txn == NULL) {
-		diag_set(ClientError, ER_NO_TRANSACTION);
-		return NULL;
-	}
+	assert(txn == in_txn());
+	size_t svp_sz = sizeof(struct txn_savepoint);
+	int name_len = name != NULL ? strlen(name) : 0;
+	svp_sz += name_len;
 	struct txn_savepoint *svp =
-		(struct txn_savepoint *) region_alloc_object(&txn->region,
-							struct txn_savepoint);
+		(struct txn_savepoint *) region_alloc(&txn->region, svp_sz);
 	if (svp == NULL) {
-		diag_set(OutOfMemory, sizeof(*svp),
-			 "region", "struct txn_savepoint");
+		diag_set(OutOfMemory, svp_sz, "region", "svp");
 		return NULL;
 	}
 	svp->stmt = stailq_last(&txn->stmts);
 	svp->in_sub_stmt = txn->in_sub_stmt;
 	svp->fk_deferred_count = txn->fk_deferred_count;
+	if (name != NULL)
+		memcpy(svp->name, name, name_len + 1);
+	else
+		svp->name[0] = 0;
+	rlist_add_entry(&txn->savepoints, svp, link);
 	return svp;
 }
 
+struct txn_savepoint *
+txn_savepoint_by_name(struct txn *txn, const char *name)
+{
+	assert(txn == in_txn());
+	struct txn_savepoint *sv;
+	rlist_foreach_entry(sv, &txn->savepoints, link) {
+		if (strcmp(sv->name, name) == 0)
+			return sv;
+	}
+	return NULL;
+}
+
+box_txn_savepoint_t *
+box_txn_savepoint()
+{
+	struct txn *txn = in_txn();
+	if (txn == NULL) {
+		diag_set(ClientError, ER_NO_TRANSACTION);
+		return NULL;
+	}
+	return txn_savepoint_new(txn, NULL);
+}
+
 int
 box_txn_rollback_to_savepoint(box_txn_savepoint_t *svp)
 {
@@ -779,10 +804,31 @@ box_txn_rollback_to_savepoint(box_txn_savepoint_t *svp)
 		return -1;
 	}
 	txn_rollback_to_svp(txn, svp->stmt);
+	/* Discard from list all newer savepoints. */
+	RLIST_HEAD(discard);
+	rlist_cut_before(&discard, &txn->savepoints, &svp->link);
 	txn->fk_deferred_count = svp->fk_deferred_count;
 	return 0;
 }
 
+void
+txn_savepoint_release(struct txn_savepoint *svp)
+{
+	struct txn *txn = in_txn();
+	assert(txn != NULL);
+	/* Make sure that savepoint hasn't been released yet. */
+	struct txn_stmt *stmt = svp->stmt == NULL ? NULL :
+				stailq_entry(svp->stmt, struct txn_stmt, next);
+	assert(stmt == NULL || (stmt->space != NULL && stmt->row != NULL));
+	(void) stmt;
+	/*
+	 * Discard current savepoint alongside with all
+	 * created after it savepoints.
+	 */
+	RLIST_HEAD(discard);
+	rlist_cut_before(&discard, &txn->savepoints, rlist_next(&svp->link));
+}
+
 static void
 txn_on_stop(struct trigger *trigger, void *event)
 {
diff --git a/src/box/txn.h b/src/box/txn.h
index f795cb76fb9d82f6908cd566f747051fc4dc2049..da12feebfc248d03eefab86ffef79a561002c23d 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -43,7 +43,6 @@ extern "C" {
 
 /** box statistics */
 extern struct rmean *rmean_box;
-struct Savepoint;
 
 struct engine;
 struct space;
@@ -128,15 +127,21 @@ struct txn_savepoint {
 	 * state violating any number of deferred FK constraints.
 	 */
 	uint32_t fk_deferred_count;
+	/** Organize savepoints into linked list. */
+	struct rlist link;
+	/**
+	 * Optional name of savepoint. If savepoint lacks
+	 * name (i.e. anonymous savepoint available only by
+	 * reference to the object), name[0] == ''. Otherwise,
+	 * memory for name is reserved in the same memory chunk
+	 * as struct txn_savepoint itself - name is placed
+	 * right after structure (see txn_savepoint_new()).
+	 */
+	char name[1];
 };
 
 extern double too_long_threshold;
 
-struct sql_txn {
-	/** List of active SQL savepoints. */
-	struct Savepoint *pSavepoint;
-};
-
 /**
  * An element of list of autogenerated ids, being returned as SQL
  * response metadata.
@@ -220,7 +225,8 @@ struct txn {
 	 * SQL specific property.
 	 */
 	uint32_t fk_deferred_count;
-	struct sql_txn *psql_txn;
+	/** List of savepoints to find savepoint by name. */
+	struct rlist savepoints;
 };
 
 static inline bool
@@ -466,6 +472,22 @@ txn_current_stmt(struct txn *txn)
 	return stailq_entry(stmt, struct txn_stmt, next);
 }
 
+/**
+ * Allocate new savepoint object using region allocator.
+ * Savepoint is allowed to be anonymous (i.e. without
+ * name).
+ */
+struct txn_savepoint *
+txn_savepoint_new(struct txn *txn, const char *name);
+
+/** Find savepoint by its name in savepoint list. */
+struct txn_savepoint *
+txn_savepoint_by_name(struct txn *txn, const char *name);
+
+/** Remove given and all newer entries from savepoint list. */
+void
+txn_savepoint_release(struct txn_savepoint *svp);
+
 /**
  * FFI bindings: do not throw exceptions, do not accept extra
  * arguments
diff --git a/test/sql/savepoints.result b/test/sql/savepoints.result
index 78957173eb64ab7dd444c621b16db78985829671..b5a0b7f4640f9642fe15f8477e2fbefd633ddf15 100644
--- a/test/sql/savepoints.result
+++ b/test/sql/savepoints.result
@@ -58,6 +58,23 @@ release_sv();
 box.commit();
 ---
 ...
+release_sv_2 = function()
+    box.begin()
+    box.execute('SAVEPOINT t1;')
+    box.execute('SAVEPOINT t2;')
+    box.execute('SAVEPOINT t3;')
+    box.execute('RELEASE SAVEPOINT t2;')
+    local _, err = box.execute('ROLLBACK TO t1;')
+    assert(err == nil)
+end;
+---
+...
+release_sv_2();
+---
+...
+box.commit();
+---
+...
 release_sv_fail = function()
     box.begin()
     box.execute('SAVEPOINT t1;')
diff --git a/test/sql/savepoints.test.lua b/test/sql/savepoints.test.lua
index d96b04600bdbeda0a05b9e8092fe36cc3529ac40..a4312c114cdabeb0b4f7ee37217629a4885a0893 100644
--- a/test/sql/savepoints.test.lua
+++ b/test/sql/savepoints.test.lua
@@ -31,6 +31,18 @@ end;
 release_sv();
 box.commit();
 
+release_sv_2 = function()
+    box.begin()
+    box.execute('SAVEPOINT t1;')
+    box.execute('SAVEPOINT t2;')
+    box.execute('SAVEPOINT t3;')
+    box.execute('RELEASE SAVEPOINT t2;')
+    local _, err = box.execute('ROLLBACK TO t1;')
+    assert(err == nil)
+end;
+release_sv_2();
+box.commit();
+
 release_sv_fail = function()
     box.begin()
     box.execute('SAVEPOINT t1;')