Skip to content
Snippets Groups Projects
Commit 93a8edbc authored by Vladimir Davydov's avatar Vladimir Davydov
Browse files

vinyl: enable exact match optimization for unique secondary indexes

If the iterator type is EQ/REQ/LE/GE and the search key is exact (that
is, there may be at most one tuple matching the key in the index),
there's no need to scan disk levels if we found a statement for this
key in the memory level. We've had this optimization for ages but it
worked only for full keys in terms `cmp_def` (key definition extended
with primary key parts). Apparently, a lookup in a secondary index
performed by the user wouldn't match these criteria unless the secondary
index explicitly included all primary key parts.

This commit improves on that. Now, we enable the optimization if the
search key is **exact**. We consider a key **exact** if either of the
following conditions is true:

 - The key statement is a tuple (tuple has all key parts).
 - The key statement is a full key in terms of `cmp_def`.
 - The key statement is a full key in terms of `key_def`, it doesn't
   contain nulls, and the index is unique. The check for nulls is
   necessary because even a unique nullable index may contain more than
   one equal key with nulls.

Note, this patch slightly refactors the optimization, adding a few
comments and hopefully making it more understandable. In particular,
we remove the one-result-tuple optimization for exact EQ/REQ from
`vy_read_iterator_advance` and put it in `vy_read_iterator_evaluate_src`
instead. This way the whole optimization resides in one place.

Closes #10442

NO_DOC=bug fix

(cherry picked from commit 850673db5a69df2c7250d174ab15305624b2634a)
parent 3067139b
No related branches found
No related tags found
No related merge requests found
## bugfix/vinyl
* Eliminated an unnecessary disk read when a key that was recently updated or
deleted was accessed via a unique secondary index (gh-10442).
......@@ -52,6 +52,11 @@ struct vy_read_src {
};
/** Set if the iterator was started. */
bool is_started;
/**
* Set if this is the last (deepest) source that may store tuples
* matching the search criteria.
*/
bool is_last;
/** See vy_read_iterator->front_id. */
uint32_t front_id;
/** History of the key the iterator is positioned at. */
......@@ -188,29 +193,6 @@ vy_read_iterator_cmp_stmt(struct vy_read_iterator *itr,
vy_entry_compare(a, b, itr->lsm->cmp_def);
}
/**
* Return true if the statement matches search criteria
* and older sources don't need to be scanned.
*/
static bool
vy_read_iterator_is_exact_match(struct vy_read_iterator *itr,
struct vy_entry entry)
{
enum iterator_type type = itr->iterator_type;
struct key_def *cmp_def = itr->lsm->cmp_def;
/*
* If the index is unique and the search key is full,
* we can avoid disk accesses on the first iteration
* in case the key is found in memory.
*/
return itr->last.stmt == NULL && entry.stmt != NULL &&
(type == ITER_EQ || type == ITER_REQ ||
type == ITER_GE || type == ITER_LE) &&
vy_stmt_is_full_key(itr->key.stmt, cmp_def) &&
vy_entry_compare(entry, itr->key, cmp_def) == 0;
}
/**
* Check if the statement at which the given read source
* is positioned precedes the current candidate for the
......@@ -234,13 +216,44 @@ vy_read_iterator_evaluate_src(struct vy_read_iterator *itr,
if (cmp <= 0)
src->front_id = itr->front_id;
itr->skipped_src = MAX(itr->skipped_src, src_id + 1);
if (src->is_last)
goto stop;
if (cmp < 0 && vy_history_is_terminal(&src->history) &&
vy_read_iterator_is_exact_match(itr, entry)) {
itr->skipped_src = src_id + 1;
*stop = true;
if (itr->check_exact_match &&
cmp < 0 && vy_history_is_terminal(&src->history)) {
/*
* So this is a terminal statement that might be the first one
* in the output and the iterator may return at most one tuple
* equal to the search key. Let's check if this statement
* equals the search key. If it is, there cannot be a better
* candidate in deeper sources so we may skip them.
*
* No need to check for equality if it's EQ iterator because
* it must have been already checked by the source iterator.
* Sic: for REQ the check is still required (see need_check_eq).
*/
if (itr->iterator_type == ITER_EQ ||
vy_entry_compare(entry, itr->key, itr->lsm->cmp_def) == 0) {
/*
* If we get an exact match for EQ/REQ search, we don't
* need to check deeper sources on next iterations so
* mark this source last. Note that we might still need
* to scan this source again though - if we encounter
* a DELETE statement - because in this case there may
* be a newer REPLACE statement for the same key in it.
*/
if (itr->iterator_type == ITER_EQ ||
itr->iterator_type == ITER_REQ)
src->is_last = true;
goto stop;
}
}
itr->skipped_src = MAX(itr->skipped_src, src_id + 1);
return;
stop:
itr->skipped_src = src_id + 1;
*stop = true;
}
/**
......@@ -490,16 +503,6 @@ vy_read_iterator_next_range(struct vy_read_iterator *itr);
static NODISCARD int
vy_read_iterator_advance(struct vy_read_iterator *itr)
{
if (itr->last.stmt != NULL && (itr->iterator_type == ITER_EQ ||
itr->iterator_type == ITER_REQ) &&
vy_stmt_is_full_key(itr->key.stmt, itr->lsm->cmp_def)) {
/*
* There may be one statement at max satisfying
* EQ with a full key.
*/
itr->front_id++;
return 0;
}
/*
* Restore the iterator position if the LSM tree has changed
* since the last iteration or this is the first iteration.
......@@ -778,6 +781,12 @@ vy_read_iterator_open_after(struct vy_read_iterator *itr, struct vy_lsm *lsm,
*/
itr->need_check_eq = true;
}
itr->check_exact_match =
(iterator_type == ITER_EQ || iterator_type == ITER_REQ ||
iterator_type == ITER_GE || iterator_type == ITER_LE) &&
vy_stmt_is_exact_key(key.stmt, lsm->cmp_def, lsm->key_def,
lsm->opts.is_unique);
}
/**
......@@ -955,6 +964,7 @@ vy_read_iterator_next(struct vy_read_iterator *itr, struct vy_entry *result)
vy_stmt_type(entry.stmt) == IPROTO_INSERT ||
vy_stmt_type(entry.stmt) == IPROTO_REPLACE);
itr->check_exact_match = false;
*result = entry;
return 0;
}
......
......@@ -63,6 +63,14 @@ struct vy_read_iterator {
* checked to match the search key.
*/
bool need_check_eq;
/**
* Set if (a) the iterator hasn't returned anything yet and (b) there
* may be at most one tuple equal to the search key and the iterator
* will return it if it exists (i.e. it's GE/LE/EQ/REQ). This flag
* enables the optimization that skips deeper read sources if a tuple
* exactly matching the search key is found.
*/
bool check_exact_match;
/** Set to true on the first iteration. */
bool is_started;
/**
......
......@@ -138,6 +138,39 @@ vy_simple_stmt_format_new(struct vy_stmt_env *env,
env, keys, key_count);
}
bool
vy_stmt_is_exact_key(struct tuple *stmt, struct key_def *cmp_def,
struct key_def *key_def, bool is_unique)
{
/* A tuple has all primary key parts => exact. */
if (!vy_stmt_is_key(stmt))
return true;
const char *data = tuple_data(stmt);
uint32_t part_count = mp_decode_array(&data);
/* Extended key (with primary key parts) => exact. */
if (part_count == cmp_def->part_count)
return true;
/* Non-unique index => not exact. */
if (!is_unique)
return false;
/* Partial key => not exact. */
if (part_count < key_def->part_count)
return false;
/*
* For a unique nullable index, presence of all key parts doesn't
* guarantee that the key is exact - we also have to check that
* the key doesn't have nulls.
*/
if (!key_def->is_nullable)
return true;
for (uint32_t i = 0; i < key_def->part_count; i++) {
if (mp_typeof(*data) == MP_NIL)
return false;
mp_next(&data);
}
return true;
}
/**
* Allocate a vinyl statement object on base of the struct tuple
* with malloc() and the reference counter equal to 1.
......
......@@ -313,6 +313,14 @@ vy_stmt_is_empty_key(struct tuple *stmt)
return tuple_field_count(stmt) == 0;
}
/**
* Return true if there cannot be more than one tuple equal to
* the given vinyl statement in an index.
*/
bool
vy_stmt_is_exact_key(struct tuple *stmt, struct key_def *cmp_def,
struct key_def *key_def, bool is_unique);
/**
* Duplicate the statememnt.
*
......
local server = require('luatest.server')
local t = require('luatest')
local g = t.group()
g.before_all(function(cg)
cg.server = server:new({
box_cfg = {
-- Disable the tuple cache and bloom filters.
vinyl_cache = 0,
vinyl_bloom_fpr = 1,
},
})
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 ~= nil then
box.space.test:drop()
end
end)
end)
g.test_exact_match_optimization = function(cg)
cg.server:exec(function()
local s = box.schema.space.create('test', {engine = 'vinyl'})
s:create_index('i1')
s:create_index('i2', {
unique = true,
parts = {{2, 'unsigned'}},
})
s:create_index('i3', {
unique = true,
parts = {{3, 'unsigned', is_nullable = true}},
})
s:replace({10, 10, 10})
s:replace({20, 20, 20})
s:replace({30, 30, 30})
s:replace({40, 40, 40})
box.snapshot()
s:delete({10})
s:delete({20})
s:replace({20, 20, 20})
s:delete({30})
s:replace({31, 30, 30})
s:delete({40})
s:replace({39, 40, 40})
box.stat.reset()
local function check(index, op, ret)
local json = require('json')
local msg = string.format("%s %s", index.name, json.encode(op))
t.assert_equals(index[op[1]](index, unpack(op, 2)), ret, msg)
t.assert_equals(index:stat().disk.iterator.lookup, 0, msg)
end
for i = 0, 2 do
local index = s.index[i]
check(index, {'get', {10}}, nil)
check(index, {'select', {10}}, {})
check(index, {'select', {10}, {iterator = 'req'}}, {})
local tuple = {20, 20, 20}
check(index, {'get', {20}}, tuple)
check(index, {'select', {20}}, {tuple})
check(index, {'select', {20}, {iterator = 'req'}}, {tuple})
check(index, {'select', {20},
{iterator = 'ge', limit = 1}}, {tuple})
check(index, {'select', {20},
{iterator = 'le', limit = 1}}, {tuple})
if i > 0 then
tuple = {31, 30, 30}
check(index, {'get', {30}}, tuple)
check(index, {'select', {30}}, {tuple})
check(index, {'select', {30}, {iterator = 'req'}}, {tuple})
check(index, {'select', {30},
{iterator = 'ge', limit = 1}}, {tuple})
check(index, {'select', {30},
{iterator = 'le', limit = 1}}, {tuple})
tuple = {39, 40, 40}
check(index, {'get', {40}}, tuple)
check(index, {'select', {40}}, {tuple})
check(index, {'select', {40}, {iterator = 'req'}}, {tuple})
check(index, {'select', {40},
{iterator = 'ge', limit = 1}}, {tuple})
check(index, {'select', {40},
{iterator = 'le', limit = 1}}, {tuple})
end
end
end)
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment