diff --git a/changelogs/unreleased/gh-8793-rework-sql-defaults.md b/changelogs/unreleased/gh-8793-rework-sql-defaults.md
index 50f52d39038c7bd9807b7b25f713738d1d0ec994..ce92025b31bb96036ae1f6561b6c2a224676fee4 100644
--- a/changelogs/unreleased/gh-8793-rework-sql-defaults.md
+++ b/changelogs/unreleased/gh-8793-rework-sql-defaults.md
@@ -4,3 +4,5 @@
 * `SQL_EXPR` functions can now be set as a default value (gh-8793).
 * A literal set as the default value can no longer have a `+` or `-` sign unless
   the literal is numeric (gh-8793).
+* **[Breaking change]** SQL now uses the BOX mechanism for default
+  values (gh-8793).
diff --git a/src/box/sql.c b/src/box/sql.c
index 1a3168c6ce08917f96f1d92461096f38a4849a76..74c63f522b44c578f702eca0655cfd9aac753989 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1619,6 +1619,20 @@ sql_check_create(const char *name, uint32_t space_id, uint32_t func_id,
 	return sql_constraint_create(name, space_id, path, value);
 }
 
+int
+sql_add_default(uint32_t space_id, uint32_t fieldno, uint32_t func_id)
+{
+	const char *path = tt_sprintf("format[%u].default_func", fieldno + 1);
+	const int ops_size = 128;
+	char ops[ops_size];
+	const char *ops_end = ops + mp_format(ops, ops_size, "[[%s%s%u]]", "!",
+					      path, func_id);
+	const int key_size = 16;
+	char key[key_size];
+	const char *key_end = key + mp_format(key, key_size, "[%u]", space_id);
+	return box_update(BOX_SPACE_ID, 0, key, key_end, ops, ops_end, 0, NULL);
+}
+
 const struct space *
 sql_space_by_token(const struct Token *name)
 {
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 6f544980a788903a89272142cf2f4b87ea56064d..908c751e558b6e71195e0021d38d54e53b97b629 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -366,6 +366,18 @@ sql_create_column_start(struct Parse *parse)
 static void
 vdbe_emit_create_constraints(struct Parse *parse, int reg_space_id);
 
+/** Emit VDBE code to add SQL_EXPR function as default value. */
+static void
+vdbe_emit_create_defaults(struct Parse *parser, int reg_space_id)
+{
+	struct Vdbe *v = sqlGetVdbe(parser);
+	for (uint32_t i = 0; i < parser->default_func_count; ++i) {
+		sqlVdbeAddOp3(v, OP_AddFuncDefault, reg_space_id,
+			      parser->default_funcs[i].reg_func_id,
+			      parser->default_funcs[i].fieldno);
+	}
+}
+
 void
 sql_create_column_end(struct Parse *parse)
 {
@@ -424,6 +436,7 @@ sql_create_column_end(struct Parse *parse)
 	sqlVdbeChangeP5(v, OPFLAG_NCHANGE);
 	sqlReleaseTempRange(parse, tuple_reg, box_space_field_MAX + 1);
 	vdbe_emit_create_constraints(parse, key_reg);
+	vdbe_emit_create_defaults(parse, key_reg);
 }
 
 void
@@ -450,45 +463,6 @@ sql_column_add_nullable_action(struct Parse *parser,
 	field->is_nullable = action_is_nullable(nullable_action);
 }
 
-/*
- * The expression is the default value for the most recently added
- * column.
- *
- * Default value expressions must be constant.  Raise an exception if this
- * is not the case.
- *
- * This routine is called by the parser while in the middle of
- * parsing a <CREATE TABLE> or an <ALTER TABLE ADD COLUMN>
- * statement.
- */
-void
-sqlAddDefaultValue(Parse * pParse, ExprSpan * pSpan)
-{
-	struct space *p = pParse->create_column_def.space;
-	if (p != NULL) {
-		assert(p->def->opts.is_ephemeral);
-		struct space_def *def = p->def;
-		if (!sqlExprIsConstantOrFunction
-		    (pSpan->pExpr, sql_get()->init.busy)) {
-			const char *column_name =
-				def->fields[def->field_count - 1].name;
-			diag_set(ClientError, ER_CREATE_SPACE, def->name,
-				 tt_sprintf("default value of column '%s' is "\
-					    "not constant", column_name));
-			pParse->is_aborted = true;
-		} else {
-			assert(def != NULL);
-			struct field_def *field =
-				&def->fields[def->field_count - 1];
-			struct region *region = &pParse->region;
-			size_t len = pSpan->zEnd - pSpan->zStart + 1;
-			field->sql_default_value = xregion_alloc(region, len);
-			strlcpy(field->sql_default_value, pSpan->zStart, len);
-		}
-	}
-	sql_expr_delete(pSpan->pExpr);
-}
-
 /** Fetch negative integer value from the expr. */
 static int
 sql_expr_nint(const struct Expr *expr, int64_t *res)
@@ -1261,6 +1235,38 @@ vdbe_emit_ck_constraint_create(struct Parse *parser,
 	sqlVdbeCountChanges(v);
 }
 
+void
+sql_add_func_default(struct Parse *parser, struct ExprSpan *span)
+{
+	assert(parser->create_column_def.space != NULL);
+	struct space_def *def = parser->create_column_def.space->def;
+	uint32_t fieldno = def->field_count - 1;
+	const char *field_name = def->fields[fieldno].name;
+
+	if (!sqlExprIsConstantOrFunction(span->pExpr, sql_get()->init.busy)) {
+		diag_set(ClientError, ER_CREATE_SPACE, def->name,
+			 tt_sprintf("default value of column '%s' is not "
+				    "constant", field_name));
+		parser->is_aborted = true;
+		sql_expr_delete(span->pExpr);
+		return;
+	}
+	sql_expr_delete(span->pExpr);
+
+	char *name = sqlMPrintf("default_%s_%s", def->name, field_name);
+	char *body = sql_xstrndup(span->zStart, span->zEnd - span->zStart);
+	int reg_id = ++parser->nMem;
+	vdbe_emit_create_function(parser, reg_id, name, body);
+	sql_xfree(name);
+	sql_xfree(body);
+	int id = parser->default_func_count++;
+	size_t size = parser->default_func_count *
+		      sizeof(*parser->default_funcs);
+	parser->default_funcs = sql_xrealloc(parser->default_funcs, size);
+	parser->default_funcs[id].fieldno = fieldno;
+	parser->default_funcs[id].reg_func_id = reg_id;
+}
+
 /**
  * Generate opcodes to create foreign key constraint.
  *
@@ -1508,6 +1514,7 @@ sqlEndTable(struct Parse *pParse)
 	int reg_space_id = getNewSpaceId(pParse);
 	vdbe_emit_space_create(pParse, reg_space_id, name_reg, new_space);
 	vdbe_emit_create_constraints(pParse, reg_space_id);
+	vdbe_emit_create_defaults(pParse, reg_space_id);
 }
 
 void
diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y
index be67cb53a2ed2b4d9ce6b637f31dc4345b217827..bef339117933fadf05e139f5faf808a506a2fda0 100644
--- a/src/box/sql/parse.y
+++ b/src/box/sql/parse.y
@@ -293,7 +293,7 @@ ccons ::= DEFAULT LP expr(X) RP.      {
   if (sql_expr_is_term(X.pExpr))
     sql_add_term_default(pParse, &X);
   else
-    sqlAddDefaultValue(pParse, &X);
+    sql_add_func_default(pParse, &X);
 }
 ccons ::= DEFAULT PLUS number(X).     {sql_add_term_default(pParse, &X);}
 ccons ::= DEFAULT MINUS(A) number(X). {
diff --git a/src/box/sql/prepare.c b/src/box/sql/prepare.c
index 6b10db2fd632223327b655195795a661eaad82d1..2e2647641c3ec6b4ce17b03b17f339c8039ec007 100644
--- a/src/box/sql/prepare.c
+++ b/src/box/sql/prepare.c
@@ -202,6 +202,7 @@ sql_parser_destroy(Parse *parser)
 {
 	assert(parser != NULL);
 	assert(!parser->parse_only || parser->pVdbe == NULL);
+	sql_xfree(parser->default_funcs);
 	sql_xfree(parser->aLabel);
 	sql_expr_list_delete(parser->pConstExpr);
 	struct create_fk_constraint_parse_def *create_fk_constraint_parse_def =
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 4244b7a2d9028bb16ad03e077120c2fa706cbdb6..8d2325e06102ab7a2e7670df992c4d062bccaf33 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -1942,6 +1942,14 @@ enum ast_type {
 	ast_type_MAX
 };
 
+/** Information about the expressions that will be used as default values. */
+struct sql_default_func {
+	/** Fieldno of the field to which default value will be added. */
+	uint32_t fieldno;
+	/** Register that will contain the ID of the new SQL EXPR functions. */
+	int reg_func_id;
+};
+
 /*
  * An SQL parser context.  A copy of this structure is passed through
  * the parser and down into all the parser action routine in order to
@@ -2068,6 +2076,10 @@ struct Parse {
 	 */
 	struct create_table_def create_table_def;
 	struct create_column_def create_column_def;
+	/** Array of default function descriptions. */
+	struct sql_default_func *default_funcs;
+	/** Length of array of default function descriptions. */
+	uint32_t default_func_count;
 	/** AST of parsed SQL statement. */
 	struct sql_ast ast;
 	/*
@@ -2750,7 +2762,10 @@ sql_create_check_contraint(struct Parse *parser, bool is_field_ck);
 void
 sql_add_term_default(struct Parse *parser, struct ExprSpan *expr_span);
 
-void sqlAddDefaultValue(Parse *, ExprSpan *);
+/** Add a default expression to the last created column. */
+void
+sql_add_func_default(struct Parse *parser, struct ExprSpan *span);
+
 void sqlAddCollateType(Parse *, Token *);
 
 /**
diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h
index 6677bac4c8a5a271a8935dc2d28adac45a3125cc..79cdc6b236f5e3fbc0e030446c3f3a580a7cb03f 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -229,6 +229,10 @@ int
 sql_check_create(const char *name, uint32_t space_id, uint32_t func_id,
 		 uint32_t fieldno, bool is_field_ck);
 
+/** Add SQL_EXPR function as the field default value to space format. */
+int
+sql_add_default(uint32_t space_id, uint32_t fieldno, uint32_t func_id);
+
 /**
  * Encode index parts of given foreign key constraint into
  * MsgPack on @region.
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 6f4f27d316a4e3aacb5e53ad17db80b89d279e7a..bc49ca82f03b900031b0fac6909a51ea40f31874 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -2246,6 +2246,21 @@ case OP_DropFieldCheck: {
 	break;
 }
 
+/**
+ * Opcode: AddFuncDefault P1 P2 P3 * *
+ * Synopsis: Add function r[P2] as default for field P3 of box.space[r[P1]]
+ */
+case OP_AddFuncDefault: {
+	assert(aMem[pOp->p1].type == MEM_TYPE_UINT);
+	uint32_t space_id = aMem[pOp->p1].u.u;
+	uint32_t fieldno = pOp->p3;
+	assert(aMem[pOp->p2].type == MEM_TYPE_UINT);
+	uint32_t func_id = aMem[pOp->p2].u.u;
+	if (sql_add_default(space_id, fieldno, func_id) != 0)
+		goto abort_due_to_error;
+	break;
+}
+
 /* Opcode: Savepoint P1 * P3 P4 *
  *
  * Open, release or rollback the savepoint named by parameter P4, depending
diff --git a/test/sql-luatest/defaults_test.lua b/test/sql-luatest/defaults_test.lua
index b4e2543b3161ed9d38aea84e4fd2fa988aca24f0..9f864a8bed026a0ddaecc90fc64193552b157c25 100644
--- a/test/sql-luatest/defaults_test.lua
+++ b/test/sql-luatest/defaults_test.lua
@@ -245,3 +245,21 @@ g.test_new_defaults = function()
         box.space.T:drop()
     end)
 end
+
+g.test_new_func_defaults = function()
+    g.server:exec(function()
+        -- Make sure an expression works correctly as a default value.
+        local sql = [[CREATE TABLE T(I INT PRIMARY KEY, A ANY DEFAULT(9 * 7));]]
+        box.execute(sql)
+        local s = box.space.T
+        local func = box.func.default_T_A
+        t.assert_equals(func.body, '9 * 7')
+        t.assert_equals(s:format()[2].default_func, func.id)
+        s:insert({1})
+        t.assert_equals(s:select(), {{1, 63}})
+        box.execute([[INSERT INTO T VALUES (2, NULL);]])
+        t.assert_equals(s:select(), {{1, 63}, {2, 63}})
+        s:drop()
+        func:drop()
+    end)
+end
diff --git a/test/sql-tap/func.test.lua b/test/sql-tap/func.test.lua
index 43c71b9ee5c995069657a8e987cb12676706af5c..599428bcd3f372a737c7a1f4d02ab33165735b19 100755
--- a/test/sql-tap/func.test.lua
+++ b/test/sql-tap/func.test.lua
@@ -2426,11 +2426,8 @@ test:do_catchsql_test(
 test:do_test(
     "func-28.1",
     function()
-        test:execsql([[
-            CREATE TABLE t28(id INT primary key, x INT, y INT DEFAULT(nosuchfunc(1)));
-        ]])
         return test:catchsql([[
-            INSERT INTO t28(id, x) VALUES(1, 1);
+            CREATE TABLE t28(id INT primary key, x INT, y INT DEFAULT(nosuchfunc(1)));
         ]])
     end, {
         -- <func-28.1>
diff --git a/test/sql-tap/func5.test.lua b/test/sql-tap/func5.test.lua
index 7fb5bfadb030f7be11515801b6b902f9c79fb087..f0aff08bca70e949c5705ea83f564814791bdc32 100755
--- a/test/sql-tap/func5.test.lua
+++ b/test/sql-tap/func5.test.lua
@@ -323,30 +323,26 @@ box.func.counter2:drop()
 local body = 'function(x) return 1 end'
 box.schema.func.create('f1', {language = 'Lua', returns = 'number', body = body,
                        param_list = {}, exports = {'LUA'}});
-box.execute([[CREATE TABLE t01(i INT PRIMARY KEY, a INT DEFAULT(f1(1)));]])
 test:do_catchsql_test(
     "func-7.1",
     [[
-        INSERT INTO t01(i) VALUES(1);
+        CREATE TABLE t01(i INT PRIMARY KEY, a INT DEFAULT(f1(1)));
     ]], {
         1, "function f1() is not available in SQL"
     })
 
 box.schema.func.create('f2', {language = 'Lua', returns = 'number', body = body,
                        exports = {'LUA', 'SQL'}});
-box.execute([[CREATE TABLE t02(i INT PRIMARY KEY, a INT DEFAULT(f2(1)));]])
 test:do_catchsql_test(
     "func-7.2",
     [[
-        INSERT INTO t02(i) VALUES(1);
+        CREATE TABLE t02(i INT PRIMARY KEY, a INT DEFAULT(f2(1)));
     ]], {
         1, "Wrong number of arguments is passed to f2(): expected 0, got 1"
     })
 
 box.func.f1:drop()
 box.func.f2:drop()
-box.space.t01:drop()
-box.space.t02:drop()
 
 --
 -- gh-6105:  Make sure that functions that were described in _func but were not
diff --git a/test/sql-tap/uuid.test.lua b/test/sql-tap/uuid.test.lua
index 6f694933c7edab042e08c964c3f61a6f0329ab79..485c1d54a42060befa9fdd7e553ababa0248be76 100755
--- a/test/sql-tap/uuid.test.lua
+++ b/test/sql-tap/uuid.test.lua
@@ -900,7 +900,7 @@ test:do_execsql_test(
         INSERT INTO t10 VALUES (4, NULL);
         SELECT * FROM t10 WHERE i = 4;
     ]], {
-        4, ''
+        4, uuid1
     })
 
 -- Check that UPDATE of UUID field works.