Написание метода отбора выборки из таблицы
В дополнение к методам BERNOULLI и SYSTEM, требуемым стандартом SQL, реализация предложения TABLESAMPLE в QHB поддерживает пользовательские методы отбора выборки из таблицы. Метод отбора выборки определяет, какие строки таблицы будут выбраны при использовании предложения TABLESAMPLE.
На уровне SQL метод отбора выборки представлен единственной функцией SQL (как правило, реализованной на C/RUST) с сигнатурой
method_name(internal) RETURNS tsm_handler
Имя функции совпадает с именем метода, фигурирующим в предложении TABLESAMPLE. Аргумент internal является фиктивным (всегда принимающим нулевое значение) и просто предотвращает вызов этой функции непосредственно из команды SQL. Результатом функции должна быть выделенная palloc структура типа TsmRoutine, содержащая указатели на вспомогательные функции для метода отбора выборки. Эти вспомогательные функции представляют собой простые функции на C, которые не видны и не могут вызываться на уровне SQL. Эти вспомогательные функции описаны в разделе Вспомогательные функции метода отбора выборки.
Кроме указателей на функций структура TsmRoutine должна содержать следующие поля:
-
List *parameterTypes
Это список OID типов данных параметров, которые будет принимать предложение TABLESAMPLE при использовании данного метода отбора выборки. Например, у встроенных методов этот список содержит один элемент со значением FLOAT4OID, представляющим собой процент выборки. Пользовательские методы могут иметь дополнительные или иные параметры. -
bool repeatable_across_queries
При значении true этот метод отбора выборки может возвращать идентичные выборки в последовательных запросах, если каждый раз предоставляются одни и те же параметры и начальное значение REPEATABLE, а содержимое таблицы не изменяется. Если же значение равно false, предложение REPEATABLE не принимается для использования с этим методом отбора выборки. -
bool repeatable_across_scans
При значении true этот метод отбора выборки может возвращать идентичные выборки при последовательных сканированиях в одном запросе (при неизменных параметрах, начальном значении и снимке). Если же значение равно false, то планировщик не будет выбирать планы, требующие более одного сканирования таблицы с выборкой, так как это может привести к несогласованному результату запроса.
Структура TsmRoutine объявляется следующим образом:
typedef struct TsmRoutine
{
NodeTag type;
/* Список OID типов данных для аргументов предложения TABLESAMPLE */
List *parameterTypes;
/* Может ли метод воспроизводить отбор выборки в разных (или даже в одном) запросах? */
bool repeatable_across_queries;
bool repeatable_across_scans;
/* Функции для планирования SampleScan в физической таблице */
SampleScanGetSampleSize_function SampleScanGetSampleSize;
/* Функции для выполнения SampleScan в физической таблице */
InitSampleScan_function InitSampleScan; /* может быть NULL */
BeginSampleScan_function BeginSampleScan;
NextSampleBlock_function NextSampleBlock; /* может быть NULL */
NextSampleTuple_function NextSampleTuple;
EndSampleScan_function EndSampleScan; /* может быть NULL */
} TsmRoutine;
Примечание
Скорее всего, в будущем будет добавлено больше указателей на функции. Поэтому рекомендуется, чтобы обработчик инициализировал эту структуру cmakeNode(TsmRoutine), чтобы во всех полях был установлен NULL. Это гарантирует, что ни одно поле не останется по случайности неопределенным.
Методы отбора выборки из таблицы, входящие в стандартный дистрибутив, могут послужить хорошим примером при реализации собственных методов.
Вспомогательные функции метода отбора выборки
Функция-обработчик TSM (Table Sampling Method, метод отбора выборки) возвращает аллоцированную palloc структуру TsmRoutine, содержащую указатели на вспомогательные функции, описанные ниже. Большинство этих функций являются обязательными, однако некоторые — нет, и соответствующие им указатели могут иметь значение NULL.
void
SampleScanGetSampleSize (PlannerInfo *root,
RelOptInfo *baserel,
List *paramexprs,
BlockNumber *pages,
double *tuples);
Эта функция вызывается во время планирования. Она должна оценить количество страниц отношения, которые будут прочитаны во время сканирования выборки, и количество кортежей, которые будут выбраны при сканировании. (Например, их можно определить, оценив процент выборки и умножив baserel->pages и baserel->tuples на этот процент с округлением до целых.) Список paramexprs содержит выражения, являющиеся параметрами к предложению TABLESAMPLE. Если значения этих выражений нужны для целей оценки, рекомендуется использовать функцию estimate_expression_value(), чтобы преобразовать их в константы, однако функция должна предоставить оценку размера и в случае, когда выражения нельзя преобразовать, и она не должна выдавать ошибку, даже если эти значения кажутся некорректными (помните, что это только приблизительные оценки значений, которые будут получены во время выполнения). pages и tuples являются выходными параметрами.
void
InitSampleScan (SampleScanState *node,
int eflags);
Провести инициализацию для выполнения узла плана SampleScan. Эта функция вызывается во время запуска исполнителя. Она должна выполнить всю необходимую подготовительную работу до начала обработки. Узел SampleScanState уже создан, но его поле tsm_state равно NULL. Функция InitSampleScan может выделить с помощью palloc область для любых данных внутреннего состояния, требуемых для метода отбора выборки, и сохранить указатель на нее в node->tsm_state. Информация о сканируемой таблице доступна через иные поля узла SampleScanState (но обратите внимание, что дескриптор сканирования node->ss.ss_currentScanDesc еще не установлен). Аргумент eflags содержит битовые флаги, описывающие рабочий режим исполнителя для данного узла плана.
Когда (eflags & EXEC_FLAG_EXPLAIN_ONLY) равно true, сканирование фактически
не будет выполняться, и функция должна сделать только необходимый минимум, чтобы
состояние узла стало допустимым для EXPLAIN и EndSampleScan.
Эту функцию можно опустить (присвоить указателю NULL), тогда всю подготовительную работу, необходимую для метода отбора выборки, должна выполнять функция BeginSampleScan.
void
BeginSampleScan (SampleScanState *node,
Datum *params,
int nparams,
uint32 seed);
Начать выполнение сканирования выборки. Это функция вызывается непосредственно перед первой попыткой извлечения кортежа и может быть вызвана снова, если сканирование необходимо перезапустить. Информация о сканируемой таблице доступна через поля узла SampleScanState (но обратите внимание, что дескриптор сканирования node->ss.ss_currentScanDesc еще не установлен). Массив params длиной nparams содержит значения параметров, переданных предложению TABLESAMPLE. Их количество и тип будут определяться списком parameterTypes метода отбора выборки, и они были проверены на отличие от NULL. Аргумент seed содержит затравочное значение для всех случайных чисел, сгенерированных внутри метода отбора выборки; это либо хеш на основе значения REPEATABLE (если таковое имеется), либо результат вызова random() (в противном случае).
Эта функция может корректировать значения полей node->use_bulkread и node->use_pagemode. Если node->use_bulkread равно true (по умолчанию), то при сканирование будет использоваться стратегия доступа к буферу, предусматривающая переработку буферов после использования. Может иметь смысл установить в этом поле false, если при сканировании будет просматриваться лишь малая часть страниц таблицы. Если node->use_bulkread равно true (по умолчанию), то при сканировании проверка видимости будет выполняться за один проход для всех кортежей на каждой просмотренной странице. Может иметь смысл установить в этом поле false, если при сканировании будет выбираться лишь малая часть кортежей на каждой просмотренной странице. Это снизит число выполняемых проверок видимости кортежей, хотя каждая из них будет дороже, так как потребует больше блокировок.
Если метод отбора выборки помечен как repeatable_across_scans, он должен быть способен выбирать во время повторного сканирования тот же набор кортежей, что и в первый раз, то есть вызов BeginSampleScan должен приводить к выбору тех же кортежей, что и раньше (если параметры TABLESPACE и начальное значение не изменились).
BlockNumber
NextSampleBlock (SampleScanState *node, BlockNumber nblocks);
Возвращает номер блока следующей сканируемой страницы или InvalidBlockNumber, если не осталось страниц для сканирования.
Эту функцию можно опустить (присвоив указателю NULL), тогда код ядра выполнит последовательное сканирование всего отношения. Такое сканирование может быть синхронизированным, поэтому метод отбора выборки не может предполагать, что при каждом сканировании страницы отношения просматриваются в одном порядке.
OffsetNumber
NextSampleTuple (SampleScanState *node,
BlockNumber blockno,
OffsetNumber maxoffset);
Возвращает номер смещения следующего кортежа, отбираемого с заданной страницы, или InvalidOffsetNumber, если не осталось кортежей для отбора. Аргумент maxoffset является наибольшим номером смещения, возможным на этой странице.
Примечание
NextSampleTuple не говорит явно, в каких номерах смещения в диапазоне 1 .. maxoffset действительно содержатся допустимые кортежи. Как правило, это не является проблемой, поскольку код ядра игнорирует запросы на отбор отсутствующих или скрытых кортежей; это не должно привести к отклонениям в выборке. Однако при необходимости эта функция может использовать node->donetuples для проверки того, сколько из возвращенных кортежей были допустимы и видимы.
Примечание
NextSampleTuple не должна предполагать, что blockno — это тот же номер страницы, что был возвращен при последнем вызове NextSampleBlock. Он точно был возвращен при каким-то предыдущем вызове NextSampleBlock, однако код ядра может вызывать NextSampleBlock до фактического сканирования страниц, для поддержки упреждающего извлечения. Допустимо предположить, что когда начнется отбор с данной страницы, все последующие вызовы NextSampleTuple будут обращаться к этой же странице, пока не будет возвращен InvalidOffsetNumber.
void
EndSampleScan (SampleScanState *node);
Завершить сканирование и освободить ресурсы. Как правило, нет нужды освобождать память, выделенную palloc, но, например, открытые файлы и соединения с удаленными серверами следует очистить. В рядовых случаях, когда таких ресурсов нет, эту функцию можно опустить (присвоив указателю NULL).
Встроенные методы отбора выборки
Метод отбора BERNOULLI
Для обеспечения воспроизводимости выборки необходимо, чтобы выбор заданного кортежа не зависел от предыстории, иначе синхронное сканирование нарушит воспроизводимость, не говоря уже о логически нерелевантной эксплуатации, например, физическом истощении или укорочении отношения.
Чтобы добиться этого, мы хешируем TID всех кандидатов вместе с активным затравочным значением, а затем выбираем кандидата, если хеш меньше порогового значения, вычисленного из вероятности выбора с помощью BeginSampleScan.
/* Внутреннее состояние */
typedef struct
{
uint64 cutoff; /* выбрать кортежи с хешем меньше этого значения */
uint32 seed; /* случайное затравочное значение */
OffsetNumber lt; /* последний кортеж, возвращенный из текущего блока */
} BernoulliSamplerData;
static void bernoulli_samplescangetsamplesize(PlannerInfo *root,
RelOptInfo *baserel,
List *paramexprs,
BlockNumber *pages,
double *tuples);
static void bernoulli_initsamplescan(SampleScanState *node,
int eflags);
static void bernoulli_beginsamplescan(SampleScanState *node,
Datum *params,
int nparams,
uint32 seed);
static OffsetNumber bernoulli_nextsampletuple(SampleScanState *node,
BlockNumber blockno,
OffsetNumber maxoffset);
/*
* Создать дескриптор TsmRoutine для метода BERNOULLI.
*/
Datum
tsm_bernoulli_handler(PG_FUNCTION_ARGS)
{
TsmRoutine *tsm = makeNode(TsmRoutine);
tsm->parameterTypes = list_make1_oid(FLOAT4OID);
tsm->repeatable_across_queries = true;
tsm->repeatable_across_scans = true;
tsm->SampleScanGetSampleSize = bernoulli_samplescangetsamplesize;
tsm->InitSampleScan = bernoulli_initsamplescan;
tsm->BeginSampleScan = bernoulli_beginsamplescan;
tsm->NextSampleBlock = NULL;
tsm->NextSampleTuple = bernoulli_nextsampletuple;
tsm->EndSampleScan = NULL;
PG_RETURN_POINTER(tsm);
}
/*
* Оценка размера выборки.
*/
static void
bernoulli_samplescangetsamplesize(PlannerInfo *root,
RelOptInfo *baserel,
List *paramexprs,
BlockNumber *pages,
double *tuples)
{
Node *pctnode;
float4 samplefract;
/* Попытаться извлечь оценочное значение для процента выборки */
pctnode = (Node *) linitial(paramexprs);
pctnode = estimate_expression_value(root, pctnode);
if (IsA(pctnode, Const) &&
!((Const *) pctnode)->constisnull)
{
samplefract = DatumGetFloat4(((Const *) pctnode)->constvalue);
if (samplefract >= 0 && samplefract <= 100 && !isnan(samplefract))
samplefract /= 100.0f;
else
{
/* Значение samplefract по умолчанию, если значение ложно */
samplefract = 0.1f;
}
}
else
{
/* Значение samplefract по умолчанию, если мы получили Const, равную NULL */
samplefract = 0.1f;
}
/* Мы зайдем на все страницы baserel */
*pages = baserel->pages;
*tuples = clamp_row_est(baserel->tuples * samplefract);
}
/*
* Инициализировать во время настройки исполнителя.
*/
static void
bernoulli_initsamplescan(SampleScanState *node, int eflags)
{
node->tsm_state = palloc0(sizeof(BernoulliSamplerData));
}
/*
* Проверить параметры и подготовиться к сканированию выборки.
*/
static void
bernoulli_beginsamplescan(SampleScanState *node,
Datum *params,
int nparams,
uint32 seed)
{
BernoulliSamplerData *sampler = (BernoulliSamplerData *) node->tsm_state;
double percent = DatumGetFloat4(params[0]);
double dcutoff;
if (percent < 0 || percent > 100 || isnan(percent))
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLESAMPLE_ARGUMENT),
errmsg("sample percentage must be between 0 and 100")));
/*
* Порог — это вероятность выборки, умноженная на (PG_UINT32_MAX + 1); разумеется
* мы должны хранить его в виде uint64. Обратите внимание, что это дает строго
* корректное поведение при пределах вероятности, равных нулю и единице.
*/
dcutoff = rint(((double) PG_UINT32_MAX + 1) * percent / 100);
sampler->cutoff = (uint64) dcutoff;
sampler->seed = seed;
sampler->lt = InvalidOffsetNumber;
/*
* Использовать bulkread, так как мы сканируем все страницы. Но проверка видимости
* pagemode выигрышна только в более крупных фракциях выборки. Здесь порог в 25%
* основан на крайне небольшом числе экспериментов.
*/
node->use_bulkread = true;
node->use_pagemode = (percent >= 25);
}
/*
* Выбрать следующий отбираемый кортеж в текущем блоке.
*
* Здесь нормально возвращать смещение, не зная, является ли этот кортеж видимым
* (и существует ли вообще). Причина этого в том, что мы действуем наудачу с каждым
* смещением кортежа в таблице. Поскольку все кортежи могут быть возвращены с равной
* вероятностью, неважно, действуем ли мы наудачу еще и с невидимыми кортежами.
*
* Когда мы достигаем конца блока, возвращается значение InvalidOffsetNumber,
* которое указывает SampleScan перейти к следующему блоку.
*/
static OffsetNumber
bernoulli_nextsampletuple(SampleScanState *node,
BlockNumber blockno,
OffsetNumber maxoffset)
{
BernoulliSamplerData *sampler = (BernoulliSamplerData *) node->tsm_state;
OffsetNumber tupoffset = sampler->lt;
uint32 hashinput[3];
/* Перейти к первому/следующему кортежу в блоке */
if (tupoffset == InvalidOffsetNumber)
tupoffset = FirstOffsetNumber;
else
tupoffset++;
/*
* Мы вычисляем хеш путем применения hash_any к массиву из 3 значений uint32,
* составляющих блок, смещение и затравку. Это эффективно для настройки,
* и с текущей реализацией hash_any дает аппаратно-независимые результаты,
* что является полезным свойством для регрессионного тестирования.
*
* Эти слова во входном хеше остаются неизменными во всем блоке:
*/
hashinput[0] = blockno;
hashinput[2] = sampler->seed;
/*
* Пройтись по смещениям кортежей, пока не найдем подходящий TID или не
* достигнем конца блока.
*/
for (; tupoffset <= maxoffset; tupoffset++)
{
uint32 hash;
hashinput[1] = tupoffset;
hash = DatumGetUInt32(hash_any((const unsigned char *) hashinput,
(int) sizeof(hashinput)));
if (hash < sampler->cutoff)
break;
}
if (tupoffset > maxoffset)
tupoffset = InvalidOffsetNumber;
sampler->lt = tupoffset;
return tupoffset;
}
Метод отбора SYSTEM
Для обеспечения воспроизводимости выборки необходимо, чтобы выбор заданного кортежа не зависел от предыстории, иначе синхронное сканирование нарушит воспроизводимость, не говоря уже о логически нерелевантной эксплуатации, например, физическом истощении или укорочении отношения.
Чтобы добиться этого, мы хешируем номера блоков всех кандидатов вместе с активным затравочным значением, а затем выбираем кандидата, если хеш меньше порогового значения, вычисленного из вероятности выбора с помощью BeginSampleScan.
/* Внетреннее состояние */
typedef struct
{
uint64 cutoff; /* выбрать блоки с хешем меньше этого значения */
uint32 seed; /* случайное затравочное значение */
BlockNumber nextblock; /* следующий блок для рассмотрения отбора выборки */
OffsetNumber lt; /* последний кортеж, возвращенный из текущего блока */
} SystemSamplerData;
static void system_samplescangetsamplesize(PlannerInfo *root,
RelOptInfo *baserel,
List *paramexprs,
BlockNumber *pages,
double *tuples);
static void system_initsamplescan(SampleScanState *node,
int eflags);
static void system_beginsamplescan(SampleScanState *node,
Datum *params,
int nparams,
uint32 seed);
static BlockNumber system_nextsampleblock(SampleScanState *node, BlockNumber nblocks);
static OffsetNumber system_nextsampletuple(SampleScanState *node,
BlockNumber blockno,
OffsetNumber maxoffset);
/*
* Создать дескриптор TsmRoutine для метода SYSTEM.
*/
Datum
tsm_system_handler(PG_FUNCTION_ARGS)
{
TsmRoutine *tsm = makeNode(TsmRoutine);
tsm->parameterTypes = list_make1_oid(FLOAT4OID);
tsm->repeatable_across_queries = true;
tsm->repeatable_across_scans = true;
tsm->SampleScanGetSampleSize = system_samplescangetsamplesize;
tsm->InitSampleScan = system_initsamplescan;
tsm->BeginSampleScan = system_beginsamplescan;
tsm->NextSampleBlock = system_nextsampleblock;
tsm->NextSampleTuple = system_nextsampletuple;
tsm->EndSampleScan = NULL;
PG_RETURN_POINTER(tsm);
}
/*
* Оценка размера выборки.
*/
static void
system_samplescangetsamplesize(PlannerInfo *root,
RelOptInfo *baserel,
List *paramexprs,
BlockNumber *pages,
double *tuples)
{
Node *pctnode;
float4 samplefract;
/* Попытаться извлечь оценочное значение для процента выборки */
pctnode = (Node *) linitial(paramexprs);
pctnode = estimate_expression_value(root, pctnode);
if (IsA(pctnode, Const) &&
!((Const *) pctnode)->constisnull)
{
samplefract = DatumGetFloat4(((Const *) pctnode)->constvalue);
if (samplefract >= 0 && samplefract <= 100 && !isnan(samplefract))
samplefract /= 100.0f;
else
{
/* Значение samplefract по умолчанию, если значение ложно */
samplefract = 0.1f;
}
}
else
{
/* Значение samplefract по умолчанию, если мы получили Const, равную NULL */
samplefract = 0.1f;
}
/* Мы зайдем на выборку страниц ... */
*pages = clamp_row_est(baserel->pages * samplefract);
/* ... и, хотелось бы надеяться, получим из них репрезентативное число кортежей */
*tuples = clamp_row_est(baserel->tuples * samplefract);
}
/*
* Инициализировать во время настройки исполнителя.
*/
static void
system_initsamplescan(SampleScanState *node, int eflags)
{
node->tsm_state = palloc0(sizeof(SystemSamplerData));
}
/*
* Проверить параметры и подготовиться к сканированию выборки.
*/
static void
system_beginsamplescan(SampleScanState *node,
Datum *params,
int nparams,
uint32 seed)
{
SystemSamplerData *sampler = (SystemSamplerData *) node->tsm_state;
double percent = DatumGetFloat4(params[0]);
double dcutoff;
if (percent < 0 || percent > 100 || isnan(percent))
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLESAMPLE_ARGUMENT),
errmsg("sample percentage must be between 0 and 100")));
/*
* Порог — это вероятность выборки, умноженная на (PG_UINT32_MAX + 1); разумеется
* мы должны хранить его в виде uint64. Обратите внимание, что это дает строго
* корректное поведение при пределах вероятности, равных нулю и единице.
*/
dcutoff = rint(((double) PG_UINT32_MAX + 1) * percent / 100);
sampler->cutoff = (uint64) dcutoff;
sampler->seed = seed;
sampler->nextblock = 0;
sampler->lt = InvalidOffsetNumber;
/*
* Стратегия обращения к буферу bulkread, вероятно, имеет смысл, если только мы
* не сканируем очень маленькую часть таблицы. Здесь порог в 1% является
* предположительным. Нам следует использовать проверку видимости pagemode,
* поскольку мы сканируем все кортежи на каждой выбранной странице.
*/
node->use_bulkread = (percent >= 1);
node->use_pagemode = true;
}
/*
* Выбрать следующий блок для отбора.
*/
static BlockNumber
system_nextsampleblock(SampleScanState *node, BlockNumber nblocks)
{
SystemSamplerData *sampler = (SystemSamplerData *) node->tsm_state;
BlockNumber nextblock = sampler->nextblock;
uint32 hashinput[2];
/*
* Мы вычисляем хеш путем применения hash_any к массиву из 2 значений uint32,
* содержащих номер блока и затравку. Это эффективно для настройки,
* и с текущей реализацией hash_any дает аппаратно-независимые результаты,
* что является полезным свойством для регрессионного тестирования.
*
* Эти слова во входном хеше остаются неизменными во всем блоке:
*/
hashinput[1] = sampler->seed;
/*
* Пройтись по номерам блоков, пока не найдем подходящий блок или не
* достигнем конца отношения.
*/
for (; nextblock < nblocks; nextblock++)
{
uint32 hash;
hashinput[0] = nextblock;
hash = DatumGetUInt32(hash_any((const unsigned char *) hashinput,
(int) sizeof(hashinput)));
if (hash < sampler->cutoff)
break;
}
if (nextblock < nblocks)
{
/* Найти подходящий блок; запомнить, откуда мы должны начать в следующий раз */
sampler->nextblock = nextblock + 1;
return nextblock;
}
/* Готово, но давайте из соображений безопасности сбросим nextblock до 0. */
sampler->nextblock = 0;
return InvalidBlockNumber;
}
/*
* Выбрать следующий отбираемый кортеж в текущем блоке.
*
* При выборке блока мы хотим просто отобрать все кортежи в каждом выбранном блоке.
*
* Здесь нормально возвращать смещение, не зная, является ли кортеж видимым
* (или существует ли вообще); nodeSamplescan.c с этим разберется.
*
* Когда мы достигаем конца блока, возвращается значение InvalidOffsetNumber,
* которое указывает SampleScan перейти к следующему блоку.
*/
static OffsetNumber
system_nextsampletuple(SampleScanState *node,
BlockNumber blockno,
OffsetNumber maxoffset)
{
SystemSamplerData *sampler = (SystemSamplerData *) node->tsm_state;
OffsetNumber tupoffset = sampler->lt;
/* Перейти к следующему возможному смещению на странице */
if (tupoffset == InvalidOffsetNumber)
tupoffset = FirstOffsetNumber;
else
tupoffset++;
/* Готово? */
if (tupoffset > maxoffset)
tupoffset = InvalidOffsetNumber;
sampler->lt = tupoffset;
return tupoffset;
}