diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index b5af1c7d821631fb6b98e00a162b6e75565fe29a..725e27bbb7e909ccc8961954fbbf5ff5d6274e65 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -93,6 +93,7 @@ add_library(box STATIC
     space.c
     space_def.c
     sequence.c
+    fkey.c
     func.c
     func_def.c
     alter.cc
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 0a17a0c3552fe18fa4c1fcb6ef21567fa86574ae..edcbe7b1d92ef00ef2f33f526be85178101e474f 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -33,6 +33,7 @@
 #include "user.h"
 #include "space.h"
 #include "index.h"
+#include "fkey.h"
 #include "func.h"
 #include "coll_id_cache.h"
 #include "coll_id_def.h"
@@ -578,6 +579,14 @@ space_swap_triggers(struct space *new_space, struct space *old_space)
 	old_space->sql_triggers = new_value;
 }
 
+/** The same as for triggers - swap lists of FK constraints. */
+static void
+space_swap_fkeys(struct space *new_space, struct space *old_space)
+{
+	rlist_swap(&new_space->child_fkey, &old_space->child_fkey);
+	rlist_swap(&new_space->parent_fkey, &old_space->parent_fkey);
+}
+
 /**
  * True if the space has records identified by key 'uid'.
  * Uses 'iid' index.
@@ -788,9 +797,10 @@ alter_space_rollback(struct trigger *trigger, void * /* event */)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	struct space *new_space = space_cache_replace(alter->old_space);
 	assert(new_space == alter->new_space);
 	(void) new_space;
@@ -886,9 +896,10 @@ alter_space_do(struct txn *txn, struct alter_space *alter)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	/*
 	 * The new space is ready. Time to update the space
 	 * cache with it.
@@ -1749,6 +1760,18 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 				  space_name(old_space),
 				  "other views depend on this space");
 		}
+		/*
+		 * No need to check existence of parent keys,
+		 * since if we went so far, space would'n have
+		 * any indexes. But referenced space has at least
+		 * one referenced index which can't be dropped
+		 * before constraint itself.
+		 */
+		if (! rlist_empty(&old_space->child_fkey)) {
+			tnt_raise(ClientError, ER_DROP_SPACE,
+				  space_name(old_space),
+				  "the space has foreign key constraints");
+		}
 		/**
 		 * The space must be deleted from the space
 		 * cache right away to achieve linearisable
@@ -1849,6 +1872,28 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 	}
 }
 
+/**
+ * Check whether given index is referenced by some foreign key
+ * constraint or not.
+ * @param fkey_head List of FK constraints belonging to parent
+ *        space.
+ * @param iid Index id which belongs to parent space and to be
+ *        tested.
+ *
+ * @retval True if at least one FK constraint references this
+ *         index; false otherwise.
+ */
+static inline bool
+index_is_fkey_referenced(struct rlist *fkey_head, uint32_t iid)
+{
+	struct fkey *fk;
+	rlist_foreach_entry(fk, fkey_head, parent_link) {
+		if (fk->index_id == iid)
+			return true;
+	}
+	return false;
+}
+
 /**
  * Just like with _space, 3 major cases:
  *
@@ -1958,12 +2003,18 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	 * 3. Change of an index which does not require a rebuild.
 	 * 4. Change of an index which does require a rebuild.
 	 */
-	/*
-	 * First, move all unchanged indexes from the old space
-	 * to the new one.
-	 */
 	/* Case 1: drop the index, if it is dropped. */
 	if (old_index != NULL && new_tuple == NULL) {
+		/*
+		 * Can't drop index if foreign key constraints
+		 * references this index.
+		 */
+		if (index_is_fkey_referenced(&old_space->parent_fkey,
+					     iid)) {
+			tnt_raise(ClientError, ER_ALTER_SPACE,
+				  space_name(old_space),
+				  "can not drop referenced index");
+		}
 		alter_space_move_indexes(alter, 0, iid);
 		(void) new DropIndex(alter, old_index->def);
 	}
@@ -2024,6 +2075,12 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 			(void) new MoveIndex(alter, old_index->def->iid);
 		} else if (index_def_change_requires_rebuild(old_index,
 							     index_def)) {
+			if (index_is_fkey_referenced(&old_space->parent_fkey,
+						     iid)) {
+				tnt_raise(ClientError, ER_ALTER_SPACE,
+					  space_name(old_space),
+					  "can not alter referenced index");
+			}
 			/*
 			 * Operation demands an index rebuild.
 			 */
@@ -3477,6 +3534,392 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
 	txn_on_commit(txn, on_commit);
 }
 
+/**
+ * Decode MsgPack arrays of links. They are stored as two
+ * separate arrays filled with unsigned fields numbers.
+ *
+ * @param tuple Tuple to be inserted into _fk_constraints.
+ * @param[out] out_count Count of links.
+ * @param constraint_name Constraint name to use in error
+ *        messages.
+ * @param constraint_len Length of constraint name.
+ * @param errcode Errcode for client errors.
+ * @retval Array of links.
+ */
+static struct field_link *
+fkey_links_decode(const struct tuple *tuple, uint32_t *out_count,
+		  const char *constraint_name, uint32_t constraint_len,
+		  uint32_t errcode)
+{
+	const char *parent_cols =
+		tuple_field_with_type_xc(tuple,
+					 BOX_FK_CONSTRAINT_FIELD_PARENT_COLS,
+					 MP_ARRAY);
+	uint32_t count = mp_decode_array(&parent_cols);
+	if (count == 0) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(constraint_name, constraint_len),
+			  "at least one link must be specified");
+	}
+	const char *child_cols =
+		tuple_field_with_type_xc(tuple,
+					 BOX_FK_CONSTRAINT_FIELD_CHILD_COLS,
+					 MP_ARRAY);
+	if (mp_decode_array(&child_cols) != count) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(constraint_name, constraint_len),
+			  "number of referenced and referencing fields "
+			  "must be the same");
+	}
+	*out_count = count;
+	size_t size = count * sizeof(struct field_link);
+	struct field_link *region_links =
+		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
+	memset(region_links, 0, size);
+	for (uint32_t i = 0; i < count; ++i) {
+		if (mp_typeof(*parent_cols) != MP_UINT ||
+		    mp_typeof(*child_cols) != MP_UINT) {
+			tnt_raise(ClientError, errcode,
+				  tt_cstr(constraint_name, constraint_len),
+				  tt_sprintf("value of %d link is not unsigned",
+					     i));
+		}
+		region_links[i].parent_field = mp_decode_uint(&parent_cols);
+		region_links[i].child_field = mp_decode_uint(&child_cols);
+	}
+	return region_links;
+}
+
+/** Create an instance of foreign key def constraint from tuple. */
+static struct fkey_def *
+fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
+{
+	uint32_t name_len;
+	const char *name =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_NAME,
+				   &name_len);
+	if (name_len > BOX_NAME_MAX) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(name, BOX_INVALID_NAME_MAX),
+			  "constraint name is too long");
+	}
+	identifier_check_xc(name, name_len);
+	uint32_t link_count;
+	struct field_link *links = fkey_links_decode(tuple, &link_count, name,
+						     name_len, errcode);
+	size_t fkey_sz = fkey_def_sizeof(link_count, name_len);
+	struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz);
+	if (fk_def == NULL)
+		tnt_raise(OutOfMemory, fkey_sz, "malloc", "fk_def");
+	auto def_guard = make_scoped_guard([=] { free(fk_def); });
+	memcpy(fk_def->name, name, name_len);
+	fk_def->name[name_len] = '\0';
+	fk_def->links = (struct field_link *)((char *)&fk_def->name +
+					      name_len + 1);
+	memcpy(fk_def->links, links, link_count * sizeof(struct field_link));
+	fk_def->field_count = link_count;
+	fk_def->child_id = tuple_field_u32_xc(tuple,
+					      BOX_FK_CONSTRAINT_FIELD_CHILD_ID);
+	fk_def->parent_id =
+		tuple_field_u32_xc(tuple, BOX_FK_CONSTRAINT_FIELD_PARENT_ID);
+	fk_def->is_deferred =
+		tuple_field_bool_xc(tuple, BOX_FK_CONSTRAINT_FIELD_DEFERRED);
+	const char *match = tuple_field_str_xc(tuple,
+					       BOX_FK_CONSTRAINT_FIELD_MATCH,
+					       &name_len);
+	fk_def->match = STRN2ENUM(fkey_match, match, name_len);
+	if (fk_def->match == fkey_match_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown MATCH clause");
+	}
+	const char *on_delete_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_DELETE,
+				   &name_len);
+	fk_def->on_delete = STRN2ENUM(fkey_action, on_delete_action, name_len);
+	if (fk_def->on_delete == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON DELETE action");
+	}
+	const char *on_update_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_UPDATE,
+				   &name_len);
+	fk_def->on_update = STRN2ENUM(fkey_action, on_update_action, name_len);
+	if (fk_def->on_update == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON UPDATE action");
+	}
+	def_guard.is_active = false;
+	return fk_def;
+}
+
+/**
+ * Remove FK constraint from child's and parent's lists and
+ * return it. Entries in child list are supposed to be
+ * unique by their name.
+ *
+ * @param list List of child FK constraints.
+ * @param fkey_name Name of constraint to be removed.
+ * @retval FK being removed.
+ */
+static struct fkey *
+fkey_grab_by_name(struct rlist *list, const char *fkey_name)
+{
+	struct fkey *fk;
+	rlist_foreach_entry(fk, list, child_link) {
+		if (strcmp(fkey_name, fk->def->name) == 0) {
+			rlist_del_entry(fk, child_link);
+			rlist_del_entry(fk, parent_link);
+			return fk;
+		}
+	}
+	unreachable();
+	return NULL;
+}
+
+/**
+ * On rollback of creation we remove FK constraint from DD, i.e.
+ * from parent's and child's lists of constraints and
+ * release memory.
+ */
+static void
+on_create_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	rlist_del_entry(fk, parent_link);
+	rlist_del_entry(fk, child_link);
+	fkey_delete(fk);
+}
+
+/** Return old FK and release memory for the new one. */
+static void
+on_replace_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *old_fk = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(old_fk->def->parent_id);
+	struct space *child = space_by_id(old_fk->def->child_id);
+	struct fkey *new_fkey = fkey_grab_by_name(&child->child_fkey,
+						  old_fk->def->name);
+	fkey_delete(new_fkey);
+	rlist_add_entry(&child->child_fkey, old_fk, child_link);
+	rlist_add_entry(&parent->parent_fkey, old_fk, parent_link);
+}
+
+/** On rollback of drop simply return back FK to DD. */
+static void
+on_drop_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk_to_restore = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk_to_restore->def->parent_id);
+	struct space *child = space_by_id(fk_to_restore->def->child_id);
+	rlist_add_entry(&child->child_fkey, fk_to_restore, child_link);
+	rlist_add_entry(&parent->parent_fkey, fk_to_restore, parent_link);
+}
+
+/**
+ * On commit of drop or replace we have already deleted old
+ * foreign key entry from both (parent's and child's) lists,
+ * so just release memory.
+ */
+static void
+on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event)
+{
+	(void) event;
+	fkey_delete((struct fkey *) trigger->data);
+}
+
+/**
+ * ANSI SQL doesn't allow list of referenced fields to contain
+ * duplicates. Firstly, we try to follow the easiest way:
+ * if all referenced fields numbers are less than 63, we can
+ * use bit mask. Otherwise, fall through slow check where we
+ * use O(field_cont^2) simple nested cycle iterations.
+ */
+static void
+fkey_links_check_duplicates(struct fkey_def *fk_def)
+{
+	uint64_t field_mask = 0;
+	for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+		uint32_t parent_field = fk_def->links[i].parent_field;
+		if (parent_field > 63)
+			goto slow_check;
+		parent_field = ((uint64_t) 1) << parent_field;
+		if ((field_mask & parent_field) != 0)
+			goto error;
+		field_mask |= parent_field;
+	}
+	return;
+slow_check:
+	for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+		uint32_t parent_field = fk_def->links[i].parent_field;
+		for (uint32_t j = i + 1; j < fk_def->field_count; ++j) {
+			if (parent_field == fk_def->links[j].parent_field)
+				goto error;
+		}
+	}
+	return;
+error:
+	tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT, fk_def->name,
+		  "referenced fields can not contain duplicates");
+}
+
+/** A trigger invoked on replace in the _fk_constraint space. */
+static void
+on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
+{
+	struct txn *txn = (struct txn *) event;
+	txn_check_singlestatement_xc(txn, "Space _fk_constraint");
+	struct txn_stmt *stmt = txn_current_stmt(txn);
+	struct tuple *old_tuple = stmt->old_tuple;
+	struct tuple *new_tuple = stmt->new_tuple;
+	if (new_tuple != NULL) {
+		/* Create or replace foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(new_tuple,
+						ER_CREATE_FK_CONSTRAINT);
+		auto fkey_def_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		if (child_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space can't be VIEW");
+		}
+		struct space *parent_space =
+			space_cache_find_xc(fk_def->parent_id);
+		if (parent_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referenced space can't be VIEW");
+		}
+		/*
+		 * FIXME: until SQL triggers are completely
+		 * integrated into server (i.e. we are able to
+		 * invoke triggers even if DML occurred via Lua
+		 * interface), it makes no sense to provide any
+		 * checks on existing data in space.
+		 */
+		struct index *pk = space_index(child_space, 0);
+		if (index_size(pk) > 0) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space must be empty");
+		}
+		/* Check types of referenced fields. */
+		for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+			uint32_t child_fieldno = fk_def->links[i].child_field;
+			uint32_t parent_fieldno = fk_def->links[i].parent_field;
+			if (child_fieldno >= child_space->def->field_count ||
+			    parent_fieldno >= parent_space->def->field_count) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "foreign key refers to "
+						        "nonexistent field");
+			}
+			struct field_def *child_field =
+				&child_space->def->fields[child_fieldno];
+			struct field_def *parent_field =
+				&parent_space->def->fields[parent_fieldno];
+			if (! field_type1_contains_type2(parent_field->type,
+							 child_field->type)) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "field type mismatch");
+			}
+			if (child_field->coll_id != parent_field->coll_id) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name,
+					  "field collation mismatch");
+			}
+		}
+		fkey_links_check_duplicates(fk_def);
+		/*
+		 * Search for suitable index in parent space:
+		 * it must be unique and consist exactly from
+		 * referenced columns (but order may be
+		 * different).
+		 */
+		struct index *fk_index = NULL;
+		for (uint32_t i = 0; i < parent_space->index_count; ++i) {
+			struct index *idx = space_index(parent_space, i);
+			if (!idx->def->opts.is_unique)
+				continue;
+			if (idx->def->key_def->part_count !=
+			    fk_def->field_count)
+				continue;
+			uint32_t j;
+			for (j = 0; j < fk_def->field_count; ++j) {
+				if (key_def_find(idx->def->key_def,
+						 fk_def->links[j].parent_field)
+				    == NULL)
+					break;
+			}
+			if (j != fk_def->field_count)
+				continue;
+			fk_index = idx;
+			break;
+		}
+		if (fk_index == NULL) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name, "referenced fields don't "
+						"compose unique index");
+		}
+		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));
+		if (fkey == NULL)
+			tnt_raise(OutOfMemory, sizeof(*fkey), "malloc", "fkey");
+		auto fkey_guard = make_scoped_guard([=] { free(fkey); });
+		memset(fkey, 0, sizeof(*fkey));
+		fkey->def = fk_def;
+		fkey->index_id = fk_index->def->iid;
+		if (old_tuple == NULL) {
+			rlist_add_entry(&child_space->child_fkey, fkey,
+					child_link);
+			rlist_add_entry(&parent_space->parent_fkey, fkey,
+					parent_link);
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_create_fkey_rollback,
+						      fkey);
+			txn_on_rollback(txn, on_rollback);
+		} else {
+			struct fkey *old_fk =
+				fkey_grab_by_name(&child_space->child_fkey,
+						  fk_def->name);
+			rlist_add_entry(&child_space->child_fkey, fkey,
+					child_link);
+			rlist_add_entry(&parent_space->parent_fkey, fkey,
+					parent_link);
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_replace_fkey_rollback,
+						      old_fk);
+			txn_on_rollback(txn, on_rollback);
+			struct trigger *on_commit =
+				txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
+						      old_fk);
+			txn_on_commit(txn, on_commit);
+		}
+		fkey_def_guard.is_active = false;
+		fkey_guard.is_active = false;
+	} else if (new_tuple == NULL && old_tuple != NULL) {
+		/* Drop foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(old_tuple,
+						ER_DROP_FK_CONSTRAINT);
+		auto fkey_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		struct fkey *old_fkey =
+			fkey_grab_by_name(&child_space->child_fkey,
+					  fk_def->name);
+		struct trigger *on_commit =
+			txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
+					      old_fkey);
+		txn_on_commit(txn, on_commit);
+		struct trigger *on_rollback =
+			txn_alter_trigger_new(on_drop_fkey_rollback, old_fkey);
+		txn_on_rollback(txn, on_rollback);
+	}
+}
+
 struct trigger alter_space_on_replace_space = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL
 };
@@ -3541,4 +3984,8 @@ struct trigger on_replace_trigger = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_trigger, NULL, NULL
 };
 
+struct trigger on_replace_fk_constraint = {
+	RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL
+};
+
 /* vim: set foldmethod=marker */
diff --git a/src/box/alter.h b/src/box/alter.h
index 8ea29c77bf47f5e1cb9eecd102df2108ec058563..4108fa47ccbe1699f82979bd8ebd4a4e0d461622 100644
--- a/src/box/alter.h
+++ b/src/box/alter.h
@@ -45,6 +45,7 @@ extern struct trigger on_replace_sequence;
 extern struct trigger on_replace_sequence_data;
 extern struct trigger on_replace_space_sequence;
 extern struct trigger on_replace_trigger;
+extern struct trigger on_replace_fk_constraint;
 extern struct trigger on_stmt_begin_space;
 extern struct trigger on_stmt_begin_index;
 extern struct trigger on_stmt_begin_truncate;
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index a8a00ec29e7106afbd06294cf6ffe291f90a2e10..10f77f641b6308209ba01e6449bb22cc35963a9f 100644
Binary files a/src/box/bootstrap.snap and b/src/box/bootstrap.snap differ
diff --git a/src/box/errcode.h b/src/box/errcode.h
index b61b387f2e7516bb6f3bf3f1f761e631bbb86739..213a1864bbb2947a68600f70b69757242de0148a 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -219,6 +219,8 @@ struct errcode_record {
 	/*164 */_(ER_NO_SUCH_GROUP,		"Replication group '%s' does not exist") \
 	/*165 */_(ER_NO_SUCH_MODULE,		"Module '%s' does not exist") \
 	/*166 */_(ER_NO_SUCH_COLLATION,		"Collation '%s' does not exist") \
+	/*167 */_(ER_CREATE_FK_CONSTRAINT,	"Failed to create foreign key constraint '%s': %s") \
+	/*168 */_(ER_DROP_FK_CONSTRAINT,	"Failed to drop foreign key constraint '%s': %s") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/fkey.c b/src/box/fkey.c
new file mode 100644
index 0000000000000000000000000000000000000000..0bdccb52110908f25add65007a8f28279321cb8a
--- /dev/null
+++ b/src/box/fkey.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "fkey.h"
+#include "sql.h"
+#include "sql/sqliteInt.h"
+
+const char *fkey_action_strs[] = {
+	/* [FKEY_ACTION_RESTRICT]    = */ "no_action",
+	/* [FKEY_ACTION_SET_NULL]    = */ "set_null",
+	/* [FKEY_ACTION_SET_DEFAULT] = */ "set_default",
+	/* [FKEY_ACTION_CASCADE]     = */ "cascade",
+	/* [FKEY_ACTION_NO_ACTION]   = */ "restrict"
+};
+
+const char *fkey_match_strs[] = {
+	/* [FKEY_MATCH_SIMPLE]  = */ "simple",
+	/* [FKEY_MATCH_PARTIAL] = */ "partial",
+	/* [FKEY_MATCH_FULL]    = */ "full"
+};
+
+void
+fkey_delete(struct fkey *fkey)
+{
+	sql_trigger_delete(sql_get(), fkey->on_delete_trigger);
+	sql_trigger_delete(sql_get(), fkey->on_update_trigger);
+	free(fkey->def);
+	free(fkey);
+}
diff --git a/src/box/fkey.h b/src/box/fkey.h
new file mode 100644
index 0000000000000000000000000000000000000000..ed99617ca592afc13c5c77a44455333d5942d52e
--- /dev/null
+++ b/src/box/fkey.h
@@ -0,0 +1,138 @@
+#ifndef TARANTOOL_BOX_FKEY_H_INCLUDED
+#define TARANTOOL_BOX_FKEY_H_INCLUDED
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "space.h"
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct sqlite3;
+
+enum fkey_action {
+	FKEY_NO_ACTION = 0,
+	FKEY_ACTION_SET_NULL,
+	FKEY_ACTION_SET_DEFAULT,
+	FKEY_ACTION_CASCADE,
+	FKEY_ACTION_RESTRICT,
+	fkey_action_MAX
+};
+
+enum fkey_match {
+	FKEY_MATCH_SIMPLE = 0,
+	FKEY_MATCH_PARTIAL,
+	FKEY_MATCH_FULL,
+	fkey_match_MAX
+};
+
+extern const char *fkey_action_strs[];
+
+extern const char *fkey_match_strs[];
+
+/** Structure describing field dependencies for foreign keys. */
+struct field_link {
+	uint32_t parent_field;
+	uint32_t child_field;
+};
+
+/** Definition of foreign key constraint. */
+struct fkey_def {
+	/** Id of space containing the REFERENCES clause (child). */
+	uint32_t child_id;
+	/** Id of space that the key points to (parent). */
+	uint32_t parent_id;
+	/** Number of fields in this key. */
+	uint32_t field_count;
+	/** True if constraint checking is deferred till COMMIT. */
+	bool is_deferred;
+	/** Match condition for foreign key. SIMPLE by default. */
+	enum fkey_match match;
+	/** ON DELETE action. NO ACTION by default. */
+	enum fkey_action on_delete;
+	/** ON UPDATE action. NO ACTION by default. */
+	enum fkey_action on_update;
+	/** Mapping of fields in child to fields in parent. */
+	struct field_link *links;
+	/** Name of the constraint. */
+	char name[0];
+};
+
+/** Structure representing foreign key relationship. */
+struct fkey {
+	struct fkey_def *def;
+	/** Index id of referenced index in parent space. */
+	uint32_t index_id;
+	/** Triggers for actions. */
+	struct sql_trigger *on_delete_trigger;
+	struct sql_trigger *on_update_trigger;
+	/** Links for parent and child lists. */
+	struct rlist parent_link;
+	struct rlist child_link;
+};
+
+/**
+ * Alongside with struct fkey_def itself, we reserve memory for
+ * string containing its name and for array of links.
+ * Memory layout:
+ * +-------------------------+ <- Allocated memory starts here
+ * |     struct fkey_def     |
+ * |-------------------------|
+ * |        name + \0        |
+ * |-------------------------|
+ * |          links          |
+ * +-------------------------+
+ */
+static inline size_t
+fkey_def_sizeof(uint32_t links_count, uint32_t name_len)
+{
+	return sizeof(struct fkey) + links_count * sizeof(struct field_link) +
+	       name_len + 1;
+}
+
+static inline bool
+fkey_is_self_referenced(const struct fkey_def *fkey)
+{
+	return fkey->child_id == fkey->parent_id;
+}
+
+/** Release memory for foreign key and its triggers, if any. */
+void
+fkey_delete(struct fkey *fkey);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* TARANTOOL_BOX_FKEY_H_INCLUDED */
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index d14dd748bdd32563eb634e57527aeacdf558a504..b73d9ab7852c5e608977f4ac67fa281b0a9a58e0 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -508,6 +508,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local _vindex = box.space[box.schema.VINDEX_ID]
     local _truncate = box.space[box.schema.TRUNCATE_ID]
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
+    local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
     local sequence_tuple = _space_sequence:delete{space_id}
     if sequence_tuple ~= nil and sequence_tuple[3] == true then
         -- Delete automatically generated sequence.
@@ -521,6 +522,9 @@ box.schema.space.drop = function(space_id, space_name, opts)
         local v = keys[i]
         _index:delete{v[1], v[2]}
     end
+    for _, t in _fk_constraint.index.child_id:pairs({space_id}) do
+        _fk_constraint:delete({t.name, space_id})
+    end
     revoke_object_privs('space', space_id)
     _truncate:delete{space_id}
     if _space:delete{space_id} == nil then
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index ca3fefc0dbd53ede96fbc678d969f990dcb3ba5b..d07560d6c493134e6211a77334329182a76227a9 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -551,6 +551,8 @@ box_lua_space_init(struct lua_State *L)
 	lua_setfield(L, -2, "SQL_STAT1_ID");
 	lua_pushnumber(L, BOX_SQL_STAT4_ID);
 	lua_setfield(L, -2, "SQL_STAT4_ID");
+	lua_pushnumber(L, BOX_FK_CONSTRAINT_ID);
+	lua_setfield(L, -2, "FK_CONSTRAINT_ID");
 	lua_pushnumber(L, BOX_TRUNCATE_ID);
 	lua_setfield(L, -2, "TRUNCATE_ID");
 	lua_pushnumber(L, BOX_SEQUENCE_ID);
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index f112a93aebd88d49f54cb521278b106bfa89e1ab..8a30e9f7df5b900a2acb891a3201b2f5f574e9e3 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -509,6 +509,27 @@ local function upgrade_to_2_1_0()
                   {unique = true}, {{0, 'string'}, {1, 'string'},
                                     {5, 'scalar'}}}
 
+    local fk_constr_ft = {{name='name', type='string'},
+                          {name='child_id', type='unsigned'},
+                          {name='parent_id', type='unsigned'},
+                          {name='is_deferred', type='boolean'},
+                          {name='match', type='string'},
+                          {name='on_delete', type='string'},
+                          {name='on_update', type='string'},
+                          {name='child_cols', type='array'},
+                          {name='parent_cols', type='array'}}
+    log.info("create space _fk_constraint")
+    _space:insert{box.schema.FK_CONSTRAINT_ID, ADMIN, '_fk_constraint', 'memtx',
+                  0, setmap({}), fk_constr_ft}
+
+    log.info("create index primary on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 0, 'primary', 'tree',
+                  {unique = true}, {{0, 'string'}, {1, 'unsigned'}}}
+
+    log.info("create secondary index child_id on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 1, 'child_id', 'tree',
+                  {unique = false}, {{1, 'unsigned'}}}
+
     -- Nullability wasn't skipable. This was fixed in 1-7.
     -- Now, abscent field means NULL, so we can safely set second
     -- field in format, marking it nullable.
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 86c56ee2ecf8975a144dee6800c612036a8638af..faad537003d622dc3b2b2801354876f559430831 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -412,6 +412,22 @@ schema_init()
 			 COLL_NONE, SORT_ORDER_ASC);
 	/* _sql_stat4 - extensive statistics on space, seen in SQL. */
 	sc_space_new(BOX_SQL_STAT4_ID, "_sql_stat4", key_def, NULL, NULL);
+
+	key_def_delete(key_def);
+	key_def = key_def_new(2);
+	if (key_def == NULL)
+		diag_raise();
+	/* Constraint name. */
+	key_def_set_part(key_def, 0, BOX_FK_CONSTRAINT_FIELD_NAME,
+			 FIELD_TYPE_STRING, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* Child space. */
+	key_def_set_part(key_def, 1, BOX_FK_CONSTRAINT_FIELD_CHILD_ID,
+			 FIELD_TYPE_UNSIGNED, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* _fk_сonstraint - foreign keys constraints. */
+	sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_def,
+		     &on_replace_fk_constraint, NULL);
 }
 
 void
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 5ab4bb002c4d6c9110e34952c7c6fdbd3d2258cd..fd57d22b94e68b88a93c74b10fda384ffd6c4aff 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -107,6 +107,8 @@ enum {
 	/** Space ids for SQL statictics. */
 	BOX_SQL_STAT1_ID = 348,
 	BOX_SQL_STAT4_ID = 349,
+	/** Space id of _fk_constraint. */
+	BOX_FK_CONSTRAINT_ID = 356,
 	/** End of the reserved range of system spaces. */
 	BOX_SYSTEM_ID_MAX = 511,
 	BOX_ID_NIL = 2147483647
@@ -224,6 +226,19 @@ enum {
 	BOX_TRIGGER_FIELD_OPTS = 2,
 };
 
+/** _fk_constraint fields. */
+enum {
+	BOX_FK_CONSTRAINT_FIELD_NAME = 0,
+	BOX_FK_CONSTRAINT_FIELD_CHILD_ID = 1,
+	BOX_FK_CONSTRAINT_FIELD_PARENT_ID = 2,
+	BOX_FK_CONSTRAINT_FIELD_DEFERRED = 3,
+	BOX_FK_CONSTRAINT_FIELD_MATCH = 4,
+	BOX_FK_CONSTRAINT_FIELD_ON_DELETE = 5,
+	BOX_FK_CONSTRAINT_FIELD_ON_UPDATE = 6,
+	BOX_FK_CONSTRAINT_FIELD_CHILD_COLS = 7,
+	BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8,
+};
+
 /*
  * Different objects which can be subject to access
  * control.
diff --git a/src/box/space.c b/src/box/space.c
index dad47e6fc630f818defb4dad8c5d569bc29b9f40..548f6678775ea57f156d9d3636124aa1ae0c8b97 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -163,6 +163,8 @@ space_create(struct space *space, struct engine *engine,
 		space->index_map[index_def->iid] = index;
 	}
 	space_fill_index_map(space);
+	rlist_create(&space->parent_fkey);
+	rlist_create(&space->child_fkey);
 	return 0;
 
 fail_free_indexes:
@@ -220,6 +222,8 @@ space_delete(struct space *space)
 	 * on_replace_dd_trigger on deletion from _trigger.
 	 */
 	assert(space->sql_triggers == NULL);
+	assert(rlist_empty(&space->parent_fkey));
+	assert(rlist_empty(&space->child_fkey));
 	space->vtab->destroy(space);
 }
 
diff --git a/src/box/space.h b/src/box/space.h
index 01a4af726a4fa11681f68be6234e20d7ae513be1..d60ba6c5673d076a61c1f2e6372456948f7a8aa3 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -183,6 +183,15 @@ struct space {
 	 * of index id.
 	 */
 	struct index **index;
+	/**
+	 * Lists of foreign key constraints. In SQL terms child
+	 * space is the "from" table i.e. the table that contains
+	 * the REFERENCES clause. Parent space is "to" table, in
+	 * other words the table that is named in the REFERENCES
+	 * clause.
+	 */
+	struct rlist parent_fkey;
+	struct rlist child_fkey;
 };
 
 /** Initialize a base space instance. */
diff --git a/src/box/sql.c b/src/box/sql.c
index 2b93c3d4502588d823fb5311bd5dd2cc84a0d7fb..8b3bda358166dfde3f3c384b38c33d5197d6c4f0 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1260,6 +1260,14 @@ void tarantoolSqlite3LoadSchema(struct init_data *init)
 			       "\"sample\","
 			       "PRIMARY KEY(\"tbl\", \"idx\", \"sample\"))");
 
+	sql_init_callback(init, TARANTOOL_SYS_FK_CONSTRAINT_NAME,
+			  BOX_FK_CONSTRAINT_ID, 0,
+			  "CREATE TABLE \""TARANTOOL_SYS_FK_CONSTRAINT_NAME
+			  "\"(\"name\" TEXT, \"parent_id\" INT, \"child_id\" INT,"
+			  "\"deferred\" INT, \"match\" TEXT, \"on_delete\" TEXT,"
+			  "\"on_update\" TEXT, \"child_cols\", \"parent_cols\","
+			  "PRIMARY KEY(\"name\", \"child_id\"))");
+
 	/* Read _space */
 	if (space_foreach(space_foreach_put_cb, init) != 0) {
 		init->rc = SQL_TARANTOOL_ERROR;
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index 248bd31bfc91a0a187bd7ae171c6a6c94d391612..3973f435a0d7c3c580527d7d9884c9def0521b43 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -35,6 +35,7 @@
  */
 #include "coll.h"
 #include "sqliteInt.h"
+#include "box/fkey.h"
 #include "box/schema.h"
 #include "box/session.h"
 #include "tarantoolInt.h"
@@ -709,31 +710,6 @@ sqlite3FkReferences(Table * pTab)
 					pTab->def->name);
 }
 
-/**
- * The second argument is a Trigger structure allocated by the
- * fkActionTrigger() routine. This function deletes the sql_trigger
- * structure and all of its sub-components.
- *
- * The Trigger structure or any of its sub-components may be
- * allocated from the lookaside buffer belonging to database
- * handle dbMem.
- *
- * @param db Database connection.
- * @param trigger AST object.
- */
-static void
-sql_fk_trigger_delete(struct sqlite3 *db, struct sql_trigger *trigger)
-{
-	if (trigger == NULL)
-		return;
-	struct TriggerStep *trigger_step = trigger->step_list;
-	sql_expr_delete(db, trigger_step->pWhere, false);
-	sql_expr_list_delete(db, trigger_step->pExprList);
-	sql_select_delete(db, trigger_step->pSelect);
-	sql_expr_delete(db, trigger->pWhen, false);
-	sqlite3DbFree(db, trigger);
-}
-
 /*
  * The second argument points to an FKey object representing a foreign key
  * for which pTab is the child table. An UPDATE statement against pTab
@@ -1277,20 +1253,14 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 						   pWhere, 0, 0, 0, 0, 0, 0);
 			pWhere = 0;
 		}
-
-		/* Disable lookaside memory allocation */
-		db->lookaside.bDisable++;
-
-		size_t trigger_size = sizeof(struct sql_trigger) +
-				      sizeof(TriggerStep) + nFrom + 1;
-		trigger =
-			(struct sql_trigger *)sqlite3DbMallocZero(db,
-								  trigger_size);
+		trigger = (struct sql_trigger *)sqlite3DbMallocZero(db,
+								    sizeof(*trigger));
 		if (trigger != NULL) {
-			pStep = trigger->step_list = (TriggerStep *)&trigger[1];
+			size_t step_size = sizeof(TriggerStep) + nFrom + 1;
+			trigger->step_list = sqlite3DbMallocZero(db, step_size);
+			pStep = trigger->step_list;
 			pStep->zTarget = (char *)&pStep[1];
-			memcpy((char *)pStep->zTarget, zFrom, nFrom);
-
+			memcpy(pStep->zTarget, zFrom, nFrom);
 			pStep->pWhere =
 			    sqlite3ExprDup(db, pWhere, EXPRDUP_REDUCE);
 			pStep->pExprList =
@@ -1304,15 +1274,12 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 			}
 		}
 
-		/* Re-enable the lookaside buffer, if it was disabled earlier. */
-		db->lookaside.bDisable--;
-
 		sql_expr_delete(db, pWhere, false);
 		sql_expr_delete(db, pWhen, false);
 		sql_expr_list_delete(db, pList);
 		sql_select_delete(db, pSelect);
 		if (db->mallocFailed == 1) {
-			sql_fk_trigger_delete(db, trigger);
+			sql_trigger_delete(db, trigger);
 			return 0;
 		}
 		assert(pStep != 0);
@@ -1410,8 +1377,8 @@ sqlite3FkDelete(sqlite3 * db, Table * pTab)
 		assert(pFKey->isDeferred == 0 || pFKey->isDeferred == 1);
 
 		/* Delete any triggers created to implement actions for this FK. */
-		sql_fk_trigger_delete(db, pFKey->apTrigger[0]);
-		sql_fk_trigger_delete(db, pFKey->apTrigger[1]);
+		sql_trigger_delete(db, pFKey->apTrigger[0]);
+		sql_trigger_delete(db, pFKey->apTrigger[1]);
 
 		pNext = pFKey->pNextFrom;
 		sqlite3DbFree(db, pFKey);
diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h
index e1430a398605f7ec8bcad82775166f37183fd682..bc61e8426d7b20fb723921b82f01d1d1cb5a789d 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -20,6 +20,7 @@
 #define TARANTOOL_SYS_TRUNCATE_NAME "_truncate"
 #define TARANTOOL_SYS_SQL_STAT1_NAME "_sql_stat1"
 #define TARANTOOL_SYS_SQL_STAT4_NAME "_sql_stat4"
+#define TARANTOOL_SYS_FK_CONSTRAINT_NAME "_fk_constraint"
 
 /* Max space id seen so far. */
 #define TARANTOOL_SYS_SCHEMA_MAXID_KEY "max_id"
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 53af3e8bdfb488d12dbceefccfcbf8267816fc44..3ba33ee97592453a31ae8476d88b8f56ec9e5245 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -815,6 +815,11 @@ box.space._space:select()
   - [349, 1, '_sql_stat4', 'memtx', 0, {}, [{'name': 'tbl', 'type': 'string'}, {'name': 'idx',
         'type': 'string'}, {'name': 'neq', 'type': 'string'}, {'name': 'nlt', 'type': 'string'},
       {'name': 'ndlt', 'type': 'string'}, {'name': 'sample', 'type': 'scalar'}]]
+  - [356, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
+      {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
+      {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
+      {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'},
+      {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]]
 ...
 box.space._func:select()
 ---
diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result
index ae042664a2271220fdd7824abb9a8f8799e10c9d..77a24b425d0a4c4cdbc79163f4550902fb51ee49 100644
--- a/test/box/access_sysview.result
+++ b/test/box/access_sysview.result
@@ -230,11 +230,11 @@ box.session.su('guest')
 ...
 #box.space._vspace:select{}
 ---
-- 22
+- 23
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 50
 ...
 #box.space._vuser:select{}
 ---
@@ -262,7 +262,7 @@ box.session.su('guest')
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 50
 ...
 #box.space._vuser:select{}
 ---
diff --git a/test/box/alter.result b/test/box/alter.result
index c41b52f489d684727e1742fdba953d844f42adfb..0d50855d2586741528d0d2b58a5a2dcada4602f5 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -107,7 +107,7 @@ space = box.space[t[1]]
 ...
 space.id
 ---
-- 350
+- 357
 ...
 space.field_count
 ---
@@ -152,7 +152,7 @@ space_deleted
 ...
 space:replace{0}
 ---
-- error: Space '350' does not exist
+- error: Space '357' does not exist
 ...
 _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}}
 ---
@@ -231,6 +231,8 @@ _index:select{}
   - [348, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string']]]
   - [349, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string'], [
         5, 'scalar']]]
+  - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]]
+  - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]]
 ...
 -- modify indexes of a system space
 _index:delete{_index.id, 0}
diff --git a/test/box/misc.result b/test/box/misc.result
index 892851823db5b6e6959695c885075b1e1e7d0760..a680f752e6dcf05825bf4a8cea1e533a5baaf424 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -491,6 +491,8 @@ t;
   164: box.error.NO_SUCH_GROUP
   165: box.error.NO_SUCH_MODULE
   166: box.error.NO_SUCH_COLLATION
+  167: box.error.CREATE_FK_CONSTRAINT
+  168: box.error.DROP_FK_CONSTRAINT
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/engine/iterator.result b/test/engine/iterator.result
index 98b0b3e7d3a7090daca7a4b6779a99111788ed8a..6e03fbbbdc074ecdc4fd66923c7fbe9db35e723e 100644
--- a/test/engine/iterator.result
+++ b/test/engine/iterator.result
@@ -4211,9 +4211,15 @@ s:replace{35}
 ---
 - [35]
 ...
-state, value = gen(param,state)
+f = function() return gen(param, state) end
+---
+...
+_, errmsg = pcall(f)
+---
+...
+errmsg:match('usage: next%(param, state%)')
 ---
-- error: 'builtin/box/schema.lua:1051: usage: next(param, state)'
+- 'usage: next(param, state)'
 ...
 value
 ---
diff --git a/test/engine/iterator.test.lua b/test/engine/iterator.test.lua
index fcf753f46438685bbbde4bbcb42f29ba55caa1d4..9ff51ebe143b1983e59fc9e52ddcb935ba2cf80e 100644
--- a/test/engine/iterator.test.lua
+++ b/test/engine/iterator.test.lua
@@ -400,7 +400,9 @@ gen,param,state = i:pairs({35})
 state, value = gen(param,state)
 value
 s:replace{35}
-state, value = gen(param,state)
+f = function() return gen(param, state) end
+_, errmsg = pcall(f)
+errmsg:match('usage: next%(param, state%)')
 value
 
 s:drop()
diff --git a/test/sql/foreign-keys.result b/test/sql/foreign-keys.result
new file mode 100644
index 0000000000000000000000000000000000000000..c2ec429c3477783b6c8d006e9918f645b8a81d25
--- /dev/null
+++ b/test/sql/foreign-keys.result
@@ -0,0 +1,336 @@
+env = require('test_run')
+---
+...
+test_run = env.new()
+---
+...
+test_run:cmd('restart server default with cleanup=1')
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+---
+...
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+---
+...
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+---
+...
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+---
+...
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: Space '666' does not exist
+...
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('T2')[1]['id']
+---
+...
+view_id = box.space._space.index.name:select('V1')[1]['id']
+---
+...
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced space can''t
+    be VIEW'
+...
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space can''t
+    be VIEW'
+...
+box.sql.execute("DROP VIEW v1;")
+---
+...
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown MATCH clause'
+...
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON DELETE action'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON UPDATE action'
+...
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space must
+    be empty'
+...
+box.sql.execute("DELETE FROM t2;")
+---
+...
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {}, {}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': at least one link must
+    be specified'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1,2}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': number of referenced and
+    referencing fields must be the same'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {13}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': foreign key refers to
+    nonexistent field'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {'crash'}, {'crash'}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': value of 0 link is not
+    unsigned'
+...
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 2}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced fields don''t
+    compose unique index'
+...
+-- Referencing and referenced fields must feature compatible types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': field type mismatch'
+...
+-- Each referenced column must appear once.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced fields can
+    not contain duplicates'
+...
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+---
+- error: 'Can''t modify space ''T1'': can not drop referenced index'
+...
+-- Referenced index can't be altered as well, if alter leads to
+-- rebuild of index (e.g. index still can be renamed).
+box.space._index:replace{512, 1, 'I1', 'tree', {unique = true}, {{field = 0, type = 'unsigned', is_nullable = true}}}
+---
+- error: 'Can''t modify space ''T1'': can not alter referenced index'
+...
+box.space._index:replace{512, 1, 'I2', 'tree', {unique = true}, {{field = 1, type = 'unsigned', is_nullable = true}}}
+---
+- [512, 1, 'I2', 'tree', {'unique': true}, [{'field': 1, 'type': 'unsigned', 'is_nullable': true}]]
+...
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+---
+...
+box.space._space:delete(child_id)
+---
+- error: 'Can''t drop space ''T2'': the space has foreign key constraints'
+...
+box.space.T2:drop()
+---
+...
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+---
+- []
+...
+box.space.T1:drop()
+---
+...
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+---
+...
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+---
+...
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {0}, {0}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:count()
+---
+- 5
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [0], [1]]
+...
+box.space._fk_constraint:delete{'fk_1', child_id}
+---
+- ['fk_1', 515, 516, false, 'simple', 'restrict', 'restrict', [0], [1]]
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [0], [1]]
+...
+box.space._fk_constraint:delete{'self_2', parent_id}
+---
+- ['self_2', 516, 516, false, 'simple', 'restrict', 'restrict', [1], [1]]
+...
+box.space._fk_constraint:delete{'self_1', child_id}
+---
+- ['self_1', 515, 515, false, 'simple', 'restrict', 'restrict', [0], [0]]
+...
+box.space._fk_constraint:delete{'fk_3', parent_id}
+---
+- ['fk_3', 516, 515, false, 'simple', 'restrict', 'restrict', [1], [0]]
+...
+box.space._fk_constraint:count()
+---
+- 0
+...
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+---
+- cascade
+...
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['is_deferred']
+---
+- true
+...
+box.space.CHILD:drop()
+---
+...
+box.space.PARENT:drop()
+---
+...
+-- Clean-up SQL DD hash.
+test_run:cmd('restart server default with cleanup=1')
diff --git a/test/sql/foreign-keys.test.lua b/test/sql/foreign-keys.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..a7a242bc256e45a4d1fcb804e6eb4ddd5173ae68
--- /dev/null
+++ b/test/sql/foreign-keys.test.lua
@@ -0,0 +1,154 @@
+env = require('test_run')
+test_run = env.new()
+test_run:cmd('restart server default with cleanup=1')
+
+
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+child_id = box.space._space.index.name:select('T2')[1]['id']
+view_id = box.space._space.index.name:select('V1')[1]['id']
+
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DROP VIEW v1;")
+
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {0}, {1}}
+box.space._fk_constraint:insert(t)
+
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DELETE FROM t2;")
+
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {}, {}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1,2}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {13}, {1}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {'crash'}, {'crash'}}
+box.space._fk_constraint:insert(t)
+
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 2}}
+box.space._fk_constraint:insert(t)
+
+-- Referencing and referenced fields must feature compatible types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+box.space._fk_constraint:insert(t)
+
+-- Each referenced column must appear once.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 1}}
+box.space._fk_constraint:insert(t)
+
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+
+-- Referenced index can't be altered as well, if alter leads to
+-- rebuild of index (e.g. index still can be renamed).
+box.space._index:replace{512, 1, 'I1', 'tree', {unique = true}, {{field = 0, type = 'unsigned', is_nullable = true}}}
+box.space._index:replace{512, 1, 'I2', 'tree', {unique = true}, {{field = 1, type = 'unsigned', is_nullable = true}}}
+
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+box.space._space:delete(child_id)
+box.space.T2:drop()
+
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+box.space.T1:drop()
+
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {0}, {0}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {1}}
+t = box.space._fk_constraint:insert(t)
+
+box.space._fk_constraint:count()
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'fk_1', child_id}
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'self_2', parent_id}
+box.space._fk_constraint:delete{'self_1', child_id}
+box.space._fk_constraint:delete{'fk_3', parent_id}
+box.space._fk_constraint:count()
+
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:replace(t)
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:replace(t)
+box.space._fk_constraint:select({'fk_1', child_id})[1]['is_deferred']
+
+box.space.CHILD:drop()
+box.space.PARENT:drop()
+
+-- Clean-up SQL DD hash.
+test_run:cmd('restart server default with cleanup=1')