diff --git a/changelogs/unreleased/gh-9994-next-prefix-iterator.md b/changelogs/unreleased/gh-9994-next-prefix-iterator.md
new file mode 100644
index 0000000000000000000000000000000000000000..e341bd13ee9e77aa0f50245e9b5f3b8dfe480571
--- /dev/null
+++ b/changelogs/unreleased/gh-9994-next-prefix-iterator.md
@@ -0,0 +1,4 @@
+## feature/core
+
+* Implemented new iterators for the memtx tree index: 'np' (next prefix)
+  and 'pp' (previous prefix) (gh-9994).
diff --git a/src/box/iterator_type.c b/src/box/iterator_type.c
index 5d6b55f21cef7359729704cdb49cbe65688558f1..0979a16916d3dbcec06856ef5f1b0792a89dd92e 100644
--- a/src/box/iterator_type.c
+++ b/src/box/iterator_type.c
@@ -44,6 +44,8 @@ const char *iterator_type_strs[] = {
 	/* [ITER_BITS_ALL_NOT_SET] = */ "BITS_ALL_NOT_SET",
 	/* [ITER_OVERLAPS] = */ "OVERLAPS",
 	/* [ITER_NEIGHBOR] = */ "NEIGHBOR",
+	/* [ITER_NP] = */ "NP",
+	/* [ITER_PP] = */ "PP",
 };
 
 static_assert(sizeof(iterator_type_strs) / sizeof(const char *) ==
diff --git a/src/box/iterator_type.h b/src/box/iterator_type.h
index c57e61407d33cd196af148486e65a3e986b2e61a..7aa9f5c15f75fec0b8ab4163e1aca8b9426e5080 100644
--- a/src/box/iterator_type.h
+++ b/src/box/iterator_type.h
@@ -72,7 +72,9 @@ enum iterator_type {
 	ITER_BITS_ANY_SET     =  8, /* at least one x's bit is set         */
 	ITER_BITS_ALL_NOT_SET =  9, /* all bits are not set                */
 	ITER_OVERLAPS         = 10, /* key overlaps x                      */
-	ITER_NEIGHBOR         = 11, /* tuples in distance ascending order from specified point */
+	ITER_NEIGHBOR         = 11, /* tuples as they move away from x point */
+	ITER_NP               = 12, /* next prefix, ASC order              */
+	ITER_PP               = 13, /* previous prefix, DESC order         */
 	iterator_type_MAX
 };
 
@@ -81,21 +83,25 @@ enum iterator_type {
 extern const char *iterator_type_strs[];
 
 /**
- * Determine a direction of the given iterator type.
- * That is -1 for REQ, LT and LE and +1 for all others.
+ * Determine whether direction of given iterator type is reverse,
+ * That is true for REQ, LT and LE etc and false for all others.
  */
-static inline int
-iterator_direction(enum iterator_type type)
+static inline bool
+iterator_type_is_reverse(enum iterator_type type)
 {
-	const unsigned reverse =
-		(1u << ITER_REQ) | (1u << ITER_LT) | (1u << ITER_LE);
-	return (reverse & (1u << type)) ? -1 : 1;
+	const unsigned reverse = (1u << ITER_REQ) | (1u << ITER_LT) |
+				 (1u << ITER_LE) | (1u << ITER_PP);
+	return reverse & (1u << type);
 }
 
-static inline bool
-iterator_type_is_reverse(enum iterator_type type)
+/**
+ * Determine a direction of given iterator type.
+ * That is -1 for REQ, LT and LE etc and +1 for all others.
+ */
+static inline int
+iterator_direction(enum iterator_type type)
 {
-	return type == ITER_REQ || type == ITER_LT || type == ITER_LE;
+	return iterator_type_is_reverse(type) ? -1 : 1;
 }
 
 #if defined(__cplusplus)
diff --git a/src/box/memtx_tree.cc b/src/box/memtx_tree.cc
index 1e6716d7594236529731e6e297e80794926ecef0..94962439be71192dde027031becc8e818f868fb9 100644
--- a/src/box/memtx_tree.cc
+++ b/src/box/memtx_tree.cc
@@ -646,10 +646,12 @@ tree_iterator_set_next_method(struct tree_iterator<USE_HINT> *it)
 		break;
 	case ITER_LT:
 	case ITER_LE:
+	case ITER_PP:
 		it->base.next_internal = tree_iterator_prev<USE_HINT>;
 		break;
 	case ITER_GE:
 	case ITER_GT:
+	case ITER_NP:
 		it->base.next_internal = tree_iterator_next<USE_HINT>;
 		break;
 	default:
@@ -659,10 +661,77 @@ tree_iterator_set_next_method(struct tree_iterator<USE_HINT> *it)
 	it->base.next = memtx_iterator_next;
 }
 
+/**
+ * Having iterator @a type as ITER_NP or ITER_PP, transform initial search key
+ * @a start_data and the @a type so that normal initial search in iterator would
+ * find exactly what needed for next prefix or previous prefix iterator.
+ * The resulting type is one of ITER_GT/ITER_LT/ITER_GE/ITER_LE.
+ * In the most common case a new search key is allocated on @a region, so
+ * region cleanup is needed after the key is no more needed.
+ * @retval true if @a start_data and @a type are ready for search.
+ * @retval false if the iteration must be stopped without an error.
+ */
+template <bool USE_HINT>
+static bool
+prepare_start_prefix_iterator(struct memtx_tree_key_data<USE_HINT> *start_data,
+			      enum iterator_type *type, struct key_def *cmp_def,
+			      struct region *region)
+{
+	assert(*type == ITER_NP || *type == ITER_PP);
+	assert(start_data->part_count > 0);
+	*type = (*type == ITER_NP) ? ITER_GT : ITER_LT;
+
+	/* PP with ASC and NP with DESC works exactly as LT and GT. */
+	bool part_order = cmp_def->parts[start_data->part_count - 1].sort_order;
+	if ((*type == ITER_LT) == (part_order == SORT_ORDER_ASC))
+		return true;
+
+	/* Find the last part of given key. */
+	const char *c = start_data->key;
+	for (uint32_t i = 1; i < start_data->part_count; i++)
+		mp_next(&c);
+	/* If the last part is not a string the iterator degrades to GT/LT. */
+	if (mp_typeof(*c) != MP_STR)
+		return true;
+
+	uint32_t str_size = mp_decode_strl(&c);
+	/* Any string logically starts with empty string; iteration is over. */
+	if (str_size == 0)
+		return false;
+	size_t prefix_size = c - start_data->key;
+	size_t total_size = prefix_size + str_size;
+
+	unsigned char *p = (unsigned char *)xregion_alloc(region, total_size);
+	memcpy(p, start_data->key, total_size);
+
+	/* Increase the key to the least greater value. */
+	unsigned char *str = p + prefix_size;
+	for (uint32_t i = str_size - 1; ; i--) {
+		if (str[i] != UCHAR_MAX) {
+			str[i]++;
+			break;
+		} else if (i == 0) {
+			/* If prefix consists of CHAR_MAX, there's no next. */
+			return false;
+		}
+		str[i] = 0;
+	}
+
+	/* With increased key we can continue the GE/LE search. */
+	*type = (*type == ITER_GT) ? ITER_GE : ITER_LE;
+	start_data->key = (char *)p;
+	if (USE_HINT)
+		start_data->set_hint(key_hint(start_data->key,
+					      start_data->part_count, cmp_def));
+	return true;
+}
+
 template <bool USE_HINT>
 static int
 tree_iterator_start(struct iterator *iterator, struct tuple **ret)
 {
+	struct region *region = &fiber()->gc;
+	RegionGuard region_guard(region);
 	*ret = NULL;
 	struct space *space;
 	struct index *index_base;
@@ -677,6 +746,12 @@ tree_iterator_start(struct iterator *iterator, struct tuple **ret)
 	struct memtx_tree_key_data<USE_HINT> start_data =
 		it->after_data.key != NULL ? it->after_data : it->key_data;
 	enum iterator_type type = it->type;
+	if ((type == ITER_NP || type == ITER_PP) &&
+	    it->after_data.key == NULL) {
+		if (!prepare_start_prefix_iterator(&start_data, &type,
+						   cmp_def, region))
+			return 0;
+	}
 	/*
 	 * Since iteration with equality iterators returns first found tuple,
 	 * we need a special flag for EQ and REQ if we want to start iteration
@@ -1577,11 +1652,21 @@ memtx_tree_index_create_iterator(struct index *base, enum iterator_type type,
 	struct key_def *cmp_def = memtx_tree_cmp_def(&index->tree);
 
 	assert(part_count == 0 || key != NULL);
-	if (type > ITER_GT) {
+	assert(type >= 0 && type < iterator_type_MAX);
+	static_assert(iterator_type_MAX < 32, "Too big for bit logic");
+	const uint32_t supported_mask = ((1u << (ITER_GT + 1)) - 1) |
+		(1u << ITER_NP) | (1u << ITER_PP);
+	if (((1u << type) & supported_mask) == 0) {
 		diag_set(UnsupportedIndexFeature, base->def,
 			 "requested iterator type");
 		return NULL;
 	}
+	if ((type == ITER_NP || type == ITER_PP) && part_count > 0 &&
+	    cmp_def->parts[part_count - 1].coll != NULL) {
+		diag_set(UnsupportedIndexFeature, base->def,
+			 "requested iterator type along with collation");
+		return NULL;
+	}
 	if (part_count == 0) {
 		/*
 		 * If no key is specified, downgrade equality
diff --git a/test/box-luatest/gh_9994_next_prefix_iterator_test.lua b/test/box-luatest/gh_9994_next_prefix_iterator_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..8dd14996953dc5c87e588ae426865f7128c21b32
--- /dev/null
+++ b/test/box-luatest/gh_9994_next_prefix_iterator_test.lua
@@ -0,0 +1,405 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all(function(cg)
+    cg.server = server:new()
+    cg.server:start()
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+g.after_each(function(cg)
+    cg.server:exec(function()
+        if box.space.test then
+            box.space.test:drop()
+        end
+        if box.func.func then
+            box.func.func:drop()
+        end
+    end)
+end)
+
+-- Check some cases when NP and PP iterators are unsupported..
+g.test_next_prefix_unsupported = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {type = 'hash', parts = {{1, 'string'}}})
+        local m = "Index 'pk' (HASH) of space 'test' (memtx) " ..
+                  "does not support requested iterator type"
+        t.assert_error_msg_content_equals(m, s.select, s, '', {iterator = 'np'})
+        t.assert_error_msg_content_equals(m, s.select, s, '', {iterator = 'pp'})
+
+        s:create_index('sk', {parts = {{1, 'str', collation = 'unicode_ci'}}})
+        local i = s.index.sk
+        m = "Index 'sk' (TREE) of space 'test' (memtx) " ..
+            "does not support requested iterator type along with collation"
+        t.assert_error_msg_content_equals(m, i.select, i, '', {iterator = 'np'})
+        t.assert_error_msg_content_equals(m, i.select, i, '', {iterator = 'pp'})
+
+        s:drop()
+        s = box.schema.space.create('test', {engine = 'vinyl'})
+        s:create_index('pk', {parts = {{1, 'string'}}})
+        m = "Index 'pk' (TREE) of space 'test' (vinyl) " ..
+            "does not support requested iterator type"
+        t.assert_error_msg_content_equals(m, s.select, s, '', {iterator = 'np'})
+        t.assert_error_msg_content_equals(m, s.select, s, '', {iterator = 'pp'})
+        s:drop()
+    end)
+end
+
+-- Simple test of next prefix and previous prefix iterators.
+g.test_next_prefix_simple = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1, 'string'}}})
+        s:replace{'a'}
+        s:replace{'aa'}
+        s:replace{'ab'}
+        s:replace{'b'}
+        s:replace{'ba'}
+        s:replace{'bb'}
+        s:replace{'c'}
+        s:replace{'ca'}
+        s:replace{'cb'}
+        local all = s:select({}, {fullscan = true})
+        local rall = s:select({}, {fullscan = true, iterator = 'le'})
+
+        t.assert_equals(s:select({}, {iterator = 'np', fullscan = true}), all)
+        t.assert_equals(s:select('', {iterator = 'np'}), {})
+        t.assert_equals(s:select('a', {iterator = 'np'}),
+                        {{'b'}, {'ba'}, {'bb'}, {'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(s:select('b', {iterator = 'np'}),
+                        {{'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(s:select('c', {iterator = 'np'}), {})
+        t.assert_equals(s:select('b', {iterator = 'np', limit = 1}), {{'c'}})
+
+        t.assert_equals(s:select({}, {iterator = 'pp', fullscan = true}), rall)
+        t.assert_equals(s:select('', {iterator = 'pp'}), {})
+        t.assert_equals(s:select('a', {iterator = 'pp'}), {})
+        t.assert_equals(s:select('b', {iterator = 'pp'}),
+                        {{'ab'}, {'aa'}, {'a'}})
+        t.assert_equals(s:select('c', {iterator = 'pp'}),
+                        {{'bb'}, {'ba'}, {'b'}, {'ab'}, {'aa'}, {'a'}})
+        t.assert_equals(s:select('b', {iterator = 'pp', limit = 1}), {{'ab'}})
+
+        local function get_pairs(key, opts)
+            local res = {}
+            for _, t in s:pairs(key, opts) do
+                table.insert(res, t)
+            end
+            return res
+        end
+
+        t.assert_equals(get_pairs({}, {iterator = 'np', fullscan = true}), all)
+        t.assert_equals(get_pairs('', {iterator = 'np'}), {})
+        t.assert_equals(get_pairs('a', {iterator = 'np'}),
+                        {{'b'}, {'ba'}, {'bb'}, {'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(get_pairs('b', {iterator = 'np'}),
+                        {{'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(get_pairs('c', {iterator = 'np'}), {})
+
+        t.assert_equals(get_pairs({}, {iterator = 'pp', fullscan = true}), rall)
+        t.assert_equals(get_pairs('', {iterator = 'pp'}), {})
+        t.assert_equals(get_pairs('a', {iterator = 'pp'}), {})
+        t.assert_equals(get_pairs('b', {iterator = 'pp'}),
+                        {{'ab'}, {'aa'}, {'a'}})
+        t.assert_equals(get_pairs('c', {iterator = 'pp'}),
+                        {{'bb'}, {'ba'}, {'b'}, {'ab'}, {'aa'}, {'a'}})
+    end)
+end
+
+-- Simple test of next prefix and previous prefix iterators with desc order.
+g.test_next_prefix_simple_reverse = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1, 'string', sort_order = 'desc'}}})
+        s:replace{'a'}
+        s:replace{'aa'}
+        s:replace{'ab'}
+        s:replace{'b'}
+        s:replace{'ba'}
+        s:replace{'bb'}
+        s:replace{'c'}
+        s:replace{'ca'}
+        s:replace{'cb'}
+        local all = s:select({}, {fullscan = true})
+        local rall = s:select({}, {fullscan = true, iterator = 'le'})
+
+        t.assert_equals(s:select({}, {iterator = 'np', fullscan = true}), all)
+        t.assert_equals(s:select('', {iterator = 'np'}), {})
+        t.assert_equals(s:select('a', {iterator = 'np'}), {})
+        t.assert_equals(s:select('b', {iterator = 'np'}),
+                        {{'ab'}, {'aa'}, {'a'}})
+        t.assert_equals(s:select('c', {iterator = 'np'}),
+                        {{'bb'}, {'ba'}, {'b'}, {'ab'}, {'aa'}, {'a'}})
+        t.assert_equals(s:select('b', {iterator = 'np', limit = 1}), {{'ab'}})
+
+        t.assert_equals(s:select({}, {iterator = 'pp', fullscan = true}), rall)
+        t.assert_equals(s:select('', {iterator = 'pp'}), {})
+        t.assert_equals(s:select('a', {iterator = 'pp'}),
+                        {{'b'}, {'ba'}, {'bb'}, {'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(s:select('b', {iterator = 'pp'}),
+                        {{'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(s:select('c', {iterator = 'pp'}), {})
+        t.assert_equals(s:select('b', {iterator = 'pp', limit = 1}), {{'c'}})
+
+        local function get_pairs(key, opts)
+            local res = {}
+            for _, t in s:pairs(key, opts) do
+                table.insert(res, t)
+            end
+            return res
+        end
+
+        t.assert_equals(get_pairs({}, {iterator = 'np', fullscan = true}), all)
+        t.assert_equals(get_pairs('', {iterator = 'np'}), {})
+        t.assert_equals(get_pairs('a', {iterator = 'np'}), {})
+        t.assert_equals(get_pairs('b', {iterator = 'np'}),
+                        {{'ab'}, {'aa'}, {'a'}})
+        t.assert_equals(get_pairs('c', {iterator = 'np'}),
+                        {{'bb'}, {'ba'}, {'b'}, {'ab'}, {'aa'}, {'a'}})
+
+        t.assert_equals(get_pairs({}, {iterator = 'pp', fullscan = true}), rall)
+        t.assert_equals(get_pairs('', {iterator = 'pp'}), {})
+        t.assert_equals(get_pairs('a', {iterator = 'pp'}),
+                        {{'b'}, {'ba'}, {'bb'}, {'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(get_pairs('b', {iterator = 'pp'}),
+                        {{'c'}, {'ca'}, {'cb'}})
+        t.assert_equals(get_pairs('c', {iterator = 'pp'}), {})
+    end)
+end
+
+-- Next prefix in scalar index.
+g.test_next_prefix_scalar = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1, 'scalar'}}})
+        s:replace{1}
+        s:replace{2}
+        s:replace{3}
+        s:replace{'a'}
+        s:replace{'ab'}
+        s:replace{'b'}
+        s:replace{'ba'}
+
+        t.assert_equals(s:select('a', {iterator = 'np'}), {{'b'}, {'ba'}})
+        t.assert_equals(s:select('b', {iterator = 'pp'}),
+                        {{'ab'}, {'a'}, {3}, {2}, {1}})
+        t.assert_equals(s:select(2, {iterator = 'np'}),
+                        {{3}, {'a'}, {'ab'}, {'b'}, {'ba'}})
+        t.assert_equals(s:select(2, {iterator = 'pp'}), {{1}})
+    end)
+end
+
+-- Next prefix in functional index.
+g.test_next_prefix_func = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk')
+        local body = [[
+            function(tuple)
+                local res = {}
+                for _, s in pairs(string.split(tuple[2], ' ')) do
+                    table.insert(res, {s})
+                end
+                return res
+            end
+        ]]
+        box.schema.func.create('func',
+                               {body = body,
+                                is_deterministic = true,
+                                is_sandboxed = true,
+                                is_multikey = true})
+        local i = s:create_index('sk', {parts = {{1, 'string'}}, func = 'func'})
+        s:replace{1, 'a aa ab'}
+        s:replace{2, 'b ba bb'}
+        s:replace{3, 'c ca cb'}
+        t.assert_equals(i:select({}, {iterator = 'np', fullscan = true}),
+                        i:select({}, {iterator = 'ge', fullscan = true}))
+        t.assert_equals(i:select('', {iterator = 'np'}), {})
+        t.assert_equals(i:select('b', {iterator = 'np'}),
+                        {{3, 'c ca cb'}, {3, 'c ca cb'}, {3, 'c ca cb'}})
+        t.assert_equals(i:select('c', {iterator = 'np'}), {})
+
+        t.assert_equals(i:select({}, {iterator = 'pp', fullscan = true}),
+                        i:select({}, {iterator = 'le', fullscan = true}))
+        t.assert_equals(i:select('', {iterator = 'pp'}), {})
+        t.assert_equals(i:select('b', {iterator = 'pp'}),
+                        {{1, 'a aa ab'}, {1, 'a aa ab'}, {1, 'a aa ab'}})
+        t.assert_equals(i:select('a', {iterator = 'pp'}), {})
+    end)
+end
+
+-- Next prefix in json index.
+g.test_next_prefix_json = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1, 'string', path = 'data'}}})
+        s:replace{{data = 'a'}}
+        s:replace{{data = 'ab'}}
+        s:replace{{data = 'b'}}
+        s:replace{{data = 'ba'}}
+
+        t.assert_equals(s:select('a', {iterator = 'np'}),
+                        {{{data = 'b'}}, {{data = 'ba'}}})
+        t.assert_equals(s:select('a', {iterator = 'pp'}), {})
+        t.assert_equals(s:select('b', {iterator = 'np'}), {})
+        t.assert_equals(s:select('b', {iterator = 'pp'}),
+                        {{{data = 'ab'}}, {{data = 'a'}}})
+    end)
+end
+
+-- Strange but valid cases.
+g.test_next_prefix_strange = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1, 'unsigned'}, {2, 'string'},
+                                       {3, 'unsigned'}}})
+        local sk = s:create_index('sk', {parts = {{1, 'unsigned'}}})
+        s:replace{1, '', 1}
+        s:replace{2, 'a', 2}
+        s:replace{3, 'b', 3}
+
+        -- Non-string parts works as gt/lt.
+        t.assert_equals(s:select({2}, {iterator = 'np'}), {{3, 'b', 3}})
+        t.assert_equals(s:select({2}, {iterator = 'pp'}), {{1, '', 1}})
+        t.assert_equals(s:select({2, 'a', 2}, {iterator = 'np'}), {{3, 'b', 3}})
+        t.assert_equals(s:select({2, 'a', 2}, {iterator = 'pp'}), {{1, '', 1}})
+        t.assert_equals(sk:select({2}, {iterator = 'np'}), {{3, 'b', 3}})
+        t.assert_equals(sk:select({2}, {iterator = 'pp'}), {{1, '', 1}})
+
+        sk:drop()
+        s:replace{1, 'a', 1}
+        s:replace{1, 'aa', 1}
+        s:replace{1, 'ab', 1}
+        s:replace{1, 'b', 1}
+        s:replace{1, 'ba', 1}
+        s:replace{1, 'bb', 1}
+
+        -- Previous prefix takes all tuples with string.sub('', 1, 3) < 'a'
+        t.assert_equals(s:select({1, 'aaa'}, {iterator = 'pp'}),
+                        {{1, 'aa', 1}, {1, 'a', 1}, {1, '', 1}})
+        s:drop()
+
+        s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1, 'string'}}})
+        s:replace{'\x00'}
+        s:replace{'\x00\x00'}
+        s:replace{'\x00\x00\x01'}
+        s:replace{'\x00\x00\x02'}
+        s:replace{'\x7f'}
+        s:replace{'\x7f\x7f'}
+        s:replace{'\x7f\x7f\x01'}
+        s:replace{'\x7f\x7f\x02'}
+        s:replace{'\x80'}
+        s:replace{'\x80\x80'}
+        s:replace{'\x80\x80\x01'}
+        s:replace{'\x80\x80\x02'}
+        s:replace{'\xff'}
+        s:replace{'\xff\xff'}
+        s:replace{'\xff\xff\x01'}
+        s:replace{'\xff\xff\x02'}
+
+        local opts = {iterator = 'np', limit = 1}
+        t.assert_equals(s:select('\x00', opts), {{'\x7f'}})
+        t.assert_equals(s:select('\x00\x00', opts), {{'\x7f'}})
+        t.assert_equals(s:select('\x7f', opts), {{'\x80'}})
+        t.assert_equals(s:select('\x7f\x7f', opts), {{'\x80'}})
+        t.assert_equals(s:select('\x80', opts), {{'\xff'}})
+        t.assert_equals(s:select('\x80\x80', opts), {{'\xff'}})
+        t.assert_equals(s:select('\xff', opts), {})
+        t.assert_equals(s:select('\xff\xff', opts), {})
+
+        local opts = {iterator = 'pp', limit = 1}
+        t.assert_equals(s:select('\x00', opts), {})
+        t.assert_equals(s:select('\x00\x00', opts), {{'\x00'}})
+        t.assert_equals(s:select('\x7f', opts), {{'\x00\x00\x02'}})
+        t.assert_equals(s:select('\x7f\x7f', opts), {{'\x7f'}})
+        t.assert_equals(s:select('\x80', opts), {{'\x7f\x7f\x02'}})
+        t.assert_equals(s:select('\x80\x80', opts), {{'\x80'}})
+        t.assert_equals(s:select('\xff', opts), {{'\x80\x80\x02'}})
+        t.assert_equals(s:select('\xff\xff', opts), {{'\xff'}})
+    end)
+end
+
+-- Practical case. List all files and directories in given directory.
+g.test_directory_list = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:format{{'division', 'unsigned'},
+                 {'path', 'string'},
+                 {'version', 'unsigned'}}
+        s:create_index('pk', {parts = {{'division'}, {'path'}, {'version'}}})
+        s:replace{0, '/home/', 0}
+        s:replace{1, '/home/another_user/file.txt', 0}
+        s:replace{1, '/home/who_is_that/file.txt', 0}
+        s:replace{1, '/home/user/file.txt', 0}
+        s:replace{1, '/home/user/file.txt', 1}
+        s:replace{1, '/home/user/data', 0}
+        s:replace{1, '/home/user/data1', 0}
+        s:replace{1, '/home/user/data2', 0}
+        s:replace{1, '/home/user/folder/', 0}
+        s:replace{1, '/home/user/folder/subfolder/data.txt', 0}
+        s:replace{1, '/home/user/folder1/subfolder/data.txt', 0}
+        s:replace{1, '/home/user/bin/tarantool', 1}
+        s:replace{1, '/home/user/bin/tarantool_ctl', 1}
+        s:replace{1, '/home/user/work/tarantool/src/main.cc', 0}
+        s:replace{1, '/home/user/work/tarantool/src/main.cc', 1}
+        s:replace{1, '/home/user/work/tarantool/test/prefix_test.lua', 0}
+        s:replace{1, '/home/user/work/tarantool/README.md', 0}
+        s:replace{1, '/home/user/work/small/README.md', 0}
+        s:replace{1, '/home/user/work/folder/', 0}
+        s:replace{1, '/home/user/work/tarantool/folder/', 0}
+        s:replace{2, '/home/user/secret.txt', 0}
+
+        local function list(division, path)
+            local res = {}
+            if not string.endswith(path, '/') then
+                path = path .. '/'
+            end
+            local function is_good(t)
+                return t and t.division == division and
+                       string.startswith(t.path, path)
+            end
+            local function get_name(subpath)
+                local name = string.sub(subpath, #path + 1)
+                local pos = string.find(name, '/')
+                if pos then
+                    name = string.sub(name, 1, pos)
+                end
+                return name
+            end
+
+            local opts = {iterator = 'gt', limit = 1}
+            local t = s:select({division, path}, opts)[1]
+            if not is_good(t) then
+                return res
+            end
+            local name = get_name(t.path)
+            table.insert(res, name)
+            while true do
+                opts.iterator = string.endswith(name, '/') and 'np' or 'gt'
+                t = s:select({division, path .. name}, opts)[1]
+                if not is_good(t) then
+                    return res
+                end
+                name = get_name(t.path)
+                table.insert(res, name)
+            end
+        end
+
+        t.assert_equals(list(1, '/home/user'),
+                        {'bin/', 'data', 'data1', 'data2',
+                         'file.txt', 'folder/', 'folder1/', 'work/'})
+        t.assert_equals(list(1, '/home/user/bin/'),
+                        {'tarantool', 'tarantool_ctl'})
+        t.assert_equals(list(1, '/home/user/work/'),
+                        {'folder/', 'small/', 'tarantool/'})
+        t.assert_equals(list(1, '/home/user/work/tarantool'),
+                        {'README.md', 'folder/', 'src/', 'test/'})
+    end)
+end