Как работает база данных Firebird, часть 3

В предыдущей части мы изучали, как выполняется выборка строки из таблицы базы данных при выполнении запроса. Мы доизучались до того, что подсистема EXE в функции EXE_looper() в цикле выполняет метод StmtNode::execute(), специфические имплементации которого есть в каждом классе, унаследованном от StmtNode. В этой части мы пройдём по цепочке выполняющихся узлов.

Dsql@2

Мы снова оказываемся в подсистеме Dsql, именно тут реализованы классы, являющиеся потомками абстрактного класса StmtNode. Первым экземпляром, с которым мы столкнулись, будет StallNode.

const StmtNode* StallNode::execute(thread_db* /*tdbb*/, Request* request, ExeState* /*exeState*/) const {
	switch (request->req_operation) {
		case Request::req_evaluate:
		case Request::req_return:
			request->req_message = this;
			request->req_operation = Request::req_return;
			request->req_flags |= req_stall;
			return this;

		case Request::req_proceed:
			request->req_operation = Request::req_return;
			return parentStmt;

		default:
			return parentStmt;
	}
}

Ага, тут что-то типа конечного автомата, event-ом выступает request->req_operation, в результате выполнения req_operation может поменяться. Возвращаемое значение - это следующее состояние. Самым первым значением req_operation будет req_sync, и в этом случае StallNode::execute() возвращает parentStmt, в качестве которого выступает ForNode.

const StmtNode* ForNode::execute(thread_db* tdbb, Request* request, ExeState* /*exeState*/) const {
	jrd_tra* transaction = request->req_transaction;
	ImpureMerge* merge = request->getImpure<ImpureMerge>(impureOffset);
	Impure* impure = merge;

	switch (request->req_operation) {
		case Request::req_evaluate:
			// initialize impure values
			impure->savepoint = 0;
			impure->writeLockMode = false;
			if (marks & MARK_MERGE)
				merge->recUpdated = nullptr;

			if (!(transaction->tra_flags & TRA_system) &&
				transaction->tra_save_point &&
				transaction->tra_save_point->hasChanges()) {
				const Savepoint* const savepoint = transaction->startSavepoint();
				impure->savepoint = savepoint->getNumber();
			}

			cursor->open(tdbb);

			if (cursor->isUpdateCounters())
				request->req_records_affected.clear();

			// fall into

		case Request::req_return:
			if (stall)
				return stall;

			// fall into

		case Request::req_sync:
			{

				const bool fetched = cursor->fetchNext(tdbb);
				if (withLock) {
					const Request* top_request = request->req_snapshot.m_owner;
					if ((top_request) && (top_request->req_flags & req_update_conflict))
						impure->writeLockMode = true;
				}

				if (fetched) {
					if (impure->writeLockMode && withLock) {
						// Skip statement execution and fetch (and try to lock) next record.
						request->req_operation = Request::req_sync;
						return this;
					}

					request->req_operation = Request::req_evaluate;
					return statement;
				}
			}

			if (impure->writeLockMode)
				restartRequest(request, transaction);

			request->req_operation = Request::req_return;

			if (impure->savepoint) {
				while (transaction->tra_save_point &&
					transaction->tra_save_point->getNumber() >= impure->savepoint)
				{
					fb_assert(!transaction->tra_save_point->isChanging());
					transaction->releaseSavepoint(tdbb);
				}
			}

			// fall into

		default:
		{
			if (request->req_operation == Request::req_unwind) {
				if (request->req_flags & (req_leave | req_continue_loop)) {
					const auto label = nodeAs<LabelNode>(parentStmt.getObject());

					// If CONTINUE matches our label, restart fetching records

					if (label && request->req_label == label->labelNumber &&
						(request->req_flags & req_continue_loop))
					{
						request->req_flags &= ~req_continue_loop;
						request->req_operation = Request::req_sync;
						return this;
					}

					// Otherwise (BREAK/LEAVE/EXIT or mismatched CONTINUE), we should unwind further.
					// Thus cleanup our savepoint.

					if (impure->savepoint) {
						while (transaction->tra_save_point &&
							transaction->tra_save_point->getNumber() >= impure->savepoint)
						{
							transaction->releaseSavepoint(tdbb);
						}
					}
				}
			}

			cursor->close(tdbb);

			if (marks & MARK_MERGE) {
				delete merge->recUpdated;
				merge->recUpdated = nullptr;
			}

			return parentStmt;
		}
	}

	return NULL;
}

Как мы видим, ForNode - это обёртка над cursor, и вызов ForNode::execute() в том случае, когда req_operation равен Request::req_sync , приведёт к вызову cursor->fetchNext(), после чего req_operation переведётся в Request::req_evaluate, и будет возвращено значение поля statement, которое в нашем случае указывает на экземпляр класса SuspendNode.

RecordSource@2

Метод Cursor::fetchNext() выглядит так:

bool Cursor::fetchNext(thread_db* tdbb) const {
	if (m_rse->isScrollable())
		return fetchRelative(tdbb, 1);

	if (!validate(tdbb))
		return false;

	const auto request = tdbb->getRequest();
	Impure* const impure = request->getImpure<Impure>(m_impure);

	if (!impure->irsb_active) {
		// error: invalid cursor state
		status_exception::raise(Arg::Gds(isc_cursor_not_open));
	}

	if (impure->irsb_state == EOS)
		return false;

	if (!m_root->getRecord(tdbb)) {
		impure->irsb_state = EOS;
		return false;
	}

	if (m_updateCounters) {
		request->req_records_selected++;
		request->req_records_affected.bumpFetched();
	}

	impure->irsb_state = POSITIONED;
	return true;
}

Здесь очень мешается некий impure, который выглядит как ещё один контекст. Его поле irsb_state помечается как EOS (end of stream), как только не получилось извлечь следующую строку, и это выглядит как оптимизация. Собственно извлечение строки происходит в m_root->getRecord(), а m_root - это экземпляр класса RecordSource. В нашем случае, это будет FullTableScan. Вот код метода:

bool FullTableScan::internalGetRecord(thread_db* tdbb) const
{
	JRD_reschedule(tdbb);

	Request* const request = tdbb->getRequest();
	record_param* const rpb = &request->req_rpb[m_stream];
	Impure* const impure = request->getImpure<Impure>(m_impure);

	if (!(impure->irsb_flags & irsb_open)) {
		rpb->rpb_number.setValid(false);
		return false;
	}

	const RecordNumber* upper = impure->irsb_upper.isValid() ? &impure->irsb_upper : nullptr;

	if (VIO_next_record(tdbb, rpb, request->req_transaction, request->req_pool, DPM_next_all, upper))
	{
		rpb->rpb_number.setValid(true);
		return true;
	}

	rpb->rpb_number.setValid(false);
	return false;
}

Здесь мы видим, что всю работу выполняет VIO_next_record(). Углубляться в него мы сейчас не будем, но запишем на будущее вопросы, на которые мы хотим получить ответы:

  1. Кто создал Cursor и присвоил m_root значение FullTableScan ?

  2. Если вспомнить, что мы видели в первой части в функциях подсистемы VIO, то там было критически важно, что в rpb хранится Relation, в котом закеширован Format, который прицепляется к строке при её чтении. Значит, для быстрой работы rpb должен быть одним и тем же для всех читаемых строк. Здесь мы видим, что действительно, один и тот же rpb берётся из некоего массива request->req_rpb по индексу m_stream , который присваивается в конструкторе. Я предположу, что если request - это один SQL-запрос с группировкой (select securityId, avg(price) from orders group by securityId), то при выполнении такого запроса будут использованы несколько RecordSource: один читает исходные данные, второй группирует. И тогда m_stream используется, чтобы выбирать RecordSource. Это нужно проверить.

  3. В продолжение предыдущего пункта: по исходникам можно убедиться, что m_stream используется только для обращения к req_rpb. Я, конечно, понимаю, что это константный доступ и всё такое, но почему бы не отказаться от m_stream и не хранить сразу rpb, на пару операций меньше для каждой строки ? Это нужно проверить. Первая идея: request недоступен на том шаге, когда вызывается конструктор. Но я могу ответить: в FullTableScan::internalOpen() , который точно вызывается раньше, чем internalGetRecord(), уже есть обращение к rpb, я бы его сохранил в this.

  4. Номер строки, который нужно прочитать, задаётся через impure->irsb_upper . Что это и откуда берётся ?

Dsql@3

Следующим узлом в нашей цепочке будет SuspendNode, вызванный в режиме req_evaluate

const StmtNode* SuspendNode::execute(thread_db* tdbb, Request* request, ExeState* /*exeState*/) const
{
	switch (request->req_operation) {
		case Request::req_evaluate:
		{
			// ASF: If this is the send in the tail of a procedure and the procedure was called
			// with a SELECT, don't run all the send statements. It may make validations fail when
			// the procedure didn't return any rows. See CORE-2204.
			// But we should run the last assignment, as it's the one who make the procedure stop.

			if (!(request->req_flags & req_proc_fetch))
				return statement;

			const CompoundStmtNode* list = nodeAs<CompoundStmtNode>(parentStmt);

			if (list && !list->parentStmt && list->statements[list->statements.getCount() - 1] == this)
			{
				list = nodeAs<CompoundStmtNode>(statement);

				if (list && list->onlyAssignments && list->statements.hasData())
				{
					// This is the assignment that sets the EOS parameter.
					const AssignmentNode* assign = static_cast<const AssignmentNode*>(
						list->statements[list->statements.getCount() - 1].getObject());
					EXE_assignment(tdbb, assign);
				}
				else
					return statement;
			}
			else
				return statement;

			// fall into
		}

		case Request::req_return:
			request->req_operation = Request::req_send;
			request->req_message = message;
			request->req_flags |= req_stall;
			return this;

		case Request::req_proceed:
			request->req_operation = Request::req_return;
			return parentStmt;

		default:
			return parentStmt;
	}
}

Тут много кода, но в нашем случае всё просто: поскольку у request->req_flags не выставлен флаг req_proc_fetch (потому что выполняется запрос, а не хранимая процедура), то сразу выполнится первый return statement. И управление передаётся в CompondStmtNode.

const StmtNode* CompoundStmtNode::execute(thread_db* tdbb, Request* request, ExeState* /*exeState*/) const {
	const NestConst<StmtNode>* end = statements.end();

	if (onlyAssignments && !request->req_attachment->isProfilerActive()) 	{
		if (request->req_operation == Request::req_evaluate) 	{
			for (const NestConst<StmtNode>* i = statements.begin(); i != end; ++i) {
				const StmtNode* stmt = i->getObject();
				EXE_assignment(tdbb, static_cast<const AssignmentNode*>(stmt));
			}

			request->req_operation = Request::req_return;
		}

		return parentStmt;
	}

	impure_state* impure = request->getImpure<impure_state>(impureOffset);

	switch (request->req_operation)
	{
		case Request::req_evaluate:
			impure->sta_state = 0;
			// fall into

		case Request::req_return:
		case Request::req_sync:
			if (impure->sta_state < statements.getCount())
			{
				request->req_operation = Request::req_evaluate;
				return statements[impure->sta_state++];
			}
			request->req_operation = Request::req_return;
			// fall into

		default:
			return parentStmt;
	}
}

Для нас интересным тут является первая часть функции, которая проверяет атрибут onlyAssignments, потому что в нашем случае он выставлен в true. Раз так, в цикле вызывается EXE_assignment() для всех вложенных AssignmentNode.

void EXE_assignment(thread_db* tdbb, const AssignmentNode* node)
{
	DEV_BLKCHK(node, type_nod);

	SET_TDBB(tdbb);
	Request* request = tdbb->getRequest();

	// Get descriptors of src field/parameter/variable, etc.
	request->req_flags &= ~req_null;
	dsc* from_desc = EVL_expr(tdbb, request, node->asgnFrom);

	EXE_assignment(tdbb, node->asgnTo, from_desc, (request->req_flags & req_null),
		node->missing, node->missing2);
}

// Evaluate a value expression.
inline dsc* EVL_expr(thread_db* tdbb, Request* request, const ValueExprNode* node)
	{
		if (!node)
			BUGCHECK(303);	// msg 303 Invalid expression for evaluation

		SET_TDBB(tdbb);
		JRD_reschedule(tdbb);
		request->req_flags &= ~req_null;

		dsc* desc = node->execute(tdbb, request);

		if (desc)
			request->req_flags &= ~req_null;
		else
			request->req_flags |= req_null;

		return desc;
}

dsc* LiteralNode::execute(thread_db* /*tdbb*/, Request* /*request*/) const
{
	return const_cast<dsc*>(&litDesc);
}

Каждый AssignmentNode содержит два вложенных узла: источник asgnFrom и получатель asgnTo.

Присваивание выполняется ожидаемо: сначала вычисляем выражение asgnFrom, результат которого будем присваивать. В нашем случае выражением будет LiteralNode, который возвращает dsc. Мы уже встречались с этой структурой во второй части статьи, она описывает поле в строке, включая тип, смещение в строке и длину. Для нас пока останется загадкой, как именно эта структура была создана, видимо, это происходит на этапе подготовки запроса, а не чтения строк, и для заполнения этой структуры очевидно анализируются метаданные таблицы, например Format. Именно поэтому очень удобно, что структура dsc совпадает по полям со структурой Ods::Descriptor, которая нам встречалась в первой части, то есть одну в другую можно превратить очень быстро и без копирований.

Это описание передаётся в другой вариант EXE_assignment(). Первым существенным действием в нём является вызов EVL_assign_to(), чтобы получить dsc для узла-получателя.

// Perform an assignment.
void EXE_assignment(thread_db* tdbb, const ValueExprNode* to, dsc* from_desc, bool from_null,
	const ValueExprNode* missing_node, const ValueExprNode* missing2_node)
{
	SET_TDBB(tdbb);
	Request* request = tdbb->getRequest();

	const auto toVar = nodeAs<VariableNode>(to);

	if (toVar && toVar->outerDecl)
		request = toVar->getVarRequest(request);

	AutoSetRestore2<Request*, thread_db> autoSetRequest(
		tdbb, &thread_db::getRequest, &thread_db::setRequest, request);

	// Get descriptors of receiving and sending fields/parameters, variables, etc.

	dsc* missing = NULL;
	if (missing_node)
		missing = EVL_expr(tdbb, request, missing_node);

	// Get descriptor of target field/parameter/variable, etc.
	DSC* to_desc = EVL_assign_to(tdbb, to);

	request->req_flags &= ~req_null;

	// NS: If we are assigning to NULL, we finished.
	// This functionality is currently used to allow calling UDF routines
	// without assigning resulting value anywhere.
	if (!to_desc)
		return;

	SSHORT null = from_null ? -1 : 0;

	if (!null && missing && MOV_compare(tdbb, missing, from_desc) == 0)
		null = -1;

	USHORT* impure_flags = NULL;
	const auto toParam = nodeAs<ParameterNode>(to);

	if (toParam) {
		const MessageNode* message = toParam->message;
		const auto paramRequest = toParam->getParamRequest(request);

		if (toParam->argInfo) {
			AutoSetRestore2<Request*, thread_db> autoSetRequest(
				tdbb, &thread_db::getRequest, &thread_db::setRequest, paramRequest);

			EVL_validate(tdbb, Item(Item::TYPE_PARAMETER, message->messageNumber, toParam->argNumber),
				toParam->argInfo, from_desc, null == -1);
		}

		impure_flags = paramRequest->getImpure<USHORT>(
			message->impureFlags + (sizeof(USHORT) * toParam->argNumber));
	} else if (toVar) {
		const auto varRequest = toVar->getVarRequest(request);

		if (toVar->varInfo) {
			AutoSetRestore2<Request*, thread_db> autoSetRequest(
				tdbb, &thread_db::getRequest, &thread_db::setRequest, varRequest);

			EVL_validate(tdbb, Item(Item::TYPE_VARIABLE, toVar->varId),
				toVar->varInfo, from_desc, null == -1);
		}

		impure_flags = &varRequest->getImpure<impure_value>(
			toVar->varDecl->impureOffset)->vlu_flags;
	}

	if (impure_flags != NULL)
		*impure_flags |= VLU_checked;

	// If the value is non-missing, move/convert it.  Otherwise fill the
	// field with appropriate nulls.
	dsc temp;

	if (!null) {
		if (!DSC_EQUIV(from_desc, to_desc, false)) {
			MOV_move(tdbb, from_desc, to_desc);
		} else if (from_desc->dsc_dtype == dtype_short) {
			*((SSHORT*) to_desc->dsc_address) = *((SSHORT*) from_desc->dsc_address);
		} else if (from_desc->dsc_dtype == dtype_long) {
			*((SLONG*) to_desc->dsc_address) = *((SLONG*) from_desc->dsc_address);
		} else if (from_desc->dsc_dtype == dtype_int64) {
			*((SINT64*) to_desc->dsc_address) = *((SINT64*) from_desc->dsc_address);
		} else {
			memcpy(to_desc->dsc_address, from_desc->dsc_address, from_desc->dsc_length);
		}

		to_desc->dsc_flags &= ~DSC_null;
	} else {
		if (missing2_node && (missing = EVL_expr(tdbb, request, missing2_node)))
			MOV_move(tdbb, missing, to_desc);
		else
			memset(to_desc->dsc_address, 0, to_desc->dsc_length);

		to_desc->dsc_flags |= DSC_null;
	}

	// Handle the null flag as appropriate for fields and message arguments.

	const FieldNode* toField = nodeAs<FieldNode>(to);
	if (toField) {
		Record* record = request->req_rpb[toField->fieldStream].rpb_record;

		if (null)
			record->setNull(toField->fieldId);
		else
			record->clearNull(toField->fieldId); 
	} else if (toParam && toParam->argFlag) {
		to_desc = EVL_assign_to(tdbb, toParam->argFlag);

		// If the null flag is a string with an effective length of one,
		// then -1 will not fit.  Therefore, store 1 instead.

		if (null && to_desc->dsc_dtype <= dtype_varying) {
			USHORT minlen;

			switch (to_desc->dsc_dtype)
			{
			case dtype_text:
				minlen = 1;
				break;
			case dtype_cstring:
				minlen = 2;
				break;
			case dtype_varying:
				minlen = 3;
				break;
			}

			if (to_desc->dsc_length <= minlen)
				null = 1;
		}

		temp.dsc_dtype = dtype_short;
		temp.dsc_length = sizeof(SSHORT);
		temp.dsc_scale = 0;
		temp.dsc_sub_type = 0;
		temp.dsc_address = (UCHAR*) &null;
		MOV_move(tdbb, &temp, to_desc);
	}
}




dsc* EVL_assign_to(thread_db* tdbb, const ValueExprNode* node) {

	SET_TDBB(tdbb);

	DEV_BLKCHK(node, type_nod);

	Request* request = tdbb->getRequest();

	// The only nodes that can be assigned to are: argument, field and variable.
	if (auto paramNode = nodeAs<ParameterNode>(node)) {
		auto message = paramNode->message;
		auto arg_number = paramNode->argNumber;
		auto desc = &message->format->fmt_desc[arg_number];

		auto impure = request->getImpure<impure_value>(node->impureOffset);

		impure->vlu_desc.dsc_address = paramNode->getParamRequest(request)->getImpure<UCHAR>(message->impureOffset + (IPTR) desc->dsc_address);
		impure->vlu_desc.dsc_dtype = desc->dsc_dtype;
		impure->vlu_desc.dsc_length = desc->dsc_length;
		impure->vlu_desc.dsc_scale = desc->dsc_scale;
		impure->vlu_desc.dsc_sub_type = desc->dsc_sub_type;

		if (DTYPE_IS_TEXT(desc->dsc_dtype) &&
			((INTL_TTYPE(desc) == ttype_dynamic) || (INTL_GET_CHARSET(desc) == CS_dynamic)))
		{
			// Value is a text value, we're assigning it back to the user
			// process, user process has not specified a subtype, user
			// process specified dynamic translation and the dsc isn't from
			// a 3.3 type request (blr_cstring2 instead of blr_cstring) so
			// convert the charset to the declared charset of the process.

			impure->vlu_desc.setTextType(tdbb->getCharSet());
		}

		return &impure->vlu_desc;
	} else if (nodeIs<NullNode>(node))
		return NULL;
	else if (auto varNode = nodeAs<VariableNode>(node)) {
		auto impure = varNode->getVarRequest(request)->getImpure<impure_value>(varNode->varDecl->impureOffset);
		return &impure->vlu_desc;
	} else if (auto fieldNode = nodeAs<FieldNode>(node)) {
		auto record = request->req_rpb[fieldNode->fieldStream].rpb_record;
		auto impure = request->getImpure<impure_value>(node->impureOffset);

		if (!EVL_field(0, record, fieldNode->fieldId, &impure->vlu_desc))
		{
			// The below condition means that EVL_field() returned
			// a read-only dummy value which cannot be assigned to.
			// The usual reason is a field being unexpectedly dropped.
			if (impure->vlu_desc.dsc_address && !(impure->vlu_desc.dsc_flags & DSC_null))
				ERR_post(Arg::Gds(isc_field_disappeared));
		}

		if (!impure->vlu_desc.dsc_address)
			ERR_post(Arg::Gds(isc_read_only_field) << "<unknown>");

		return &impure->vlu_desc;
	}

	SOFT_BUGCHECK(229);	// msg 229 EVL_assign_to: invalid operation
	return NULL;
}

В EVL_assign_to() выполнение зайдёт в if-ветку, которая обрабатывает paramNode. И что же мы видим внутри: получаем paramNode->message, у которого берём format, из которого, зная порядковый номер параметра, получаем dsc, описывающий то, куда значение параметра нужно записать . Но возвращаем из функции мы не его, а его копию, которая формируется в impure->vlu_desc. Зачем нужно копирование, непонятно, но самое интересное для нас тут то, что dsc_address копируется как смещение от message->impureOffset, то есть копия impure->vlu_desc указывает на то же самое расположение содержимого.

В середине EXE_assignment() отлично видно if по типам данных, который выполняет собственно присваивание через копирование.

После завершения всех присваиваний CompondStmtNode::execute() переведёт режим req_operation в значение req_return, и вернёт parentStmt, которым является тот же самый SuspendNode, который перевёл выполнение на CompoundStmtNode. Но, поскольку req_operation уже другой, то и действия в SuspendNode::execute() будут другими: режим req_operation будет установлен в req_send, выставится флаг stall, и сохраняет в request->req_message значение своего поля message, которое является экземпляром класса MessageNode. Некоторая загадка состоит в том, что этот message нигде не фигурировал. Но я смог в отладчике проверить, что в функции EVL_assign_to() значения paramNode->message будут указывать на тот же самый экземпляр MessageNode, ох уж эти сто указателей на одно и то же, потом проверю, как это так вышло. В итоге SuspendNode::execute() в режиме req_return возвращает себя. Флаг stall заставляет цикл обработки узлов прерваться.

Все эти перескоки из одного StmtNode к другому выглядят очень запутанно, поэтому я нарисовал схему. Те шаги, которые мы уже рассмотрели, отмечены синими стрелками с номерами от 1 до 6. На стрелках указаны значения req_operation.

Цепочка StmtNode
Цепочка StmtNode

Как можно подсмотреть в части 2, цепочка вызовов раскрутится до EXE_receive(), который обработает request->req_message , значит я ответил на вопрос, который я же задал в заключении второй части о том, откуда берётся request->req_message. Кроме этого мы увидим, что EXE_receive() вызовет execute_looper() второй раз, уже с параметром Request::req_proceed. Пришло время понять, зачем это нужно. Шаги, которые мы рассмотрим, показаны на картинке зелёными стрелками с номерами от 7 до 10

Выполнение продолжится с SuspendNode, который выставит req_operation в req_return, и в качестве следующего выполняющегося узла вернёт свой parentStmt, которым является тот самый ForNode, который ещё на первом проходе передавал управление на SuspendNode. А ForNode тупо передаёт управление на своё поле stall, которым выступает тот самый StallNode, с которого всё начиналось.

Пока я разбирался с этими узлами выполнения, я наткнулся на вот какую штуку. Оказывается, инструмент командной строки isql имеет настройку "SET EXEC_PATH_DISPLAY BLR;" , при включении которой он начинает показывать BLR, некий внутренний формат Firebird. Если включить его и выполнить наш запрос, то выводится очень интересное содержимое:

SQL> select orderid, securityid,qty,price from orders;

Execution path (BLR):

    0 blr_version5,
    1 blr_begin,
    2    blr_message, 1, 9,0,
    6       blr_short, 0,
    8       blr_short, 0,
   10       blr_double,
   11       blr_short, 0,
   13       blr_long, 0,
   15       blr_short, 0,
   17       blr_long, 0,
   19       blr_short, 0,
   21       blr_varying2, 0,0, 0,2,
   26    blr_for,
   27       blr_stall,
   28       blr_rse, 1,
   30          blr_rid, 128,0, 0,
   34          blr_end,
   35    blr_send, 1,
   37       blr_begin,
   38          blr_assignment,
   39             blr_literal, blr_short, 0, 1,0,
   44             blr_parameter, 1, 0,0,
   48          blr_assignment,
   49             blr_fid, 0, 3,0,
   53             blr_parameter2, 1, 2,0, 1,0,
   59          blr_assignment,
   60             blr_fid, 0, 2,0,
   64             blr_parameter2, 1, 4,0, 3,0,
   70          blr_assignment,
   71             blr_fid, 0, 4,0,
   75             blr_parameter2, 1, 6,0, 5,0,
   81          blr_assignment,
   82             blr_fid, 0, 1,0,
   86             blr_parameter2, 1, 8,0, 7,0,
   92          blr_end,
   93    blr_send, 1,
   95       blr_assignment,
   96          blr_literal, blr_short, 0, 0,0,
  101          blr_parameter, 1, 0,0,
  105    blr_end,
  106 blr_eoc

И тут мы отлично видим, что в начале идёт MessageNode, соответствующий запрошенным столбцам. Потом идёт ForNode, внутри которого есть StallNode и ещё какой-то RSE (наверное это SuspendNode). Дальше идёт Send, который, похоже, соответствует нашему CompoundStmtNode, потому что внутри идут AssignmentNode. Вот такие пироги. Если почитать файл StmtNodes.cpp , то можно увидеть, что многие потомки StmtNode реализуют метод genBlr(), вот например вариант ForNode::genBlr() .

Заключение

Мы прошлись по интерпретатору операций, которые выполняются при чтении данных из строки базы данных при выполнении пользовательского запроса. Интерпретатор реализован так, что каждая инструкция реализована как C++ класс, наследующийся от StmtNode, с логикой в методе execute().

Чтение полей из строки выполняется в цикле по количеству читаемых полей, и чтение представляет собой копирование из буфера строки таблицы в буфер результата, который в процессе анализа запроса выделен в узле MessageNode. В следующей части мы попробуем понять, как формируются все эти StmtNode, и самый загадочный вопрос: как получается, что ParameterNode, который описывает, куда сохранять значение из поля таблицы, ссылается на тот же самый message, что и SuspendNode.

Итак, сколько раз перекладываются данные при чтении строки? Давайте посчитаем:

  1. В VIO cтрока распаковывается из страницы данных из rle-закодированного вида, в буфер, содержимое которого описано Format-ом.

  2. При выполнении EXE_assignment() для каждого запрошенного столбца значение будет скопировано в буфер, выделенный для строки результата где то в request->impure.

  3. EXE_receive() скопирует буфер строки-результата в свой буфер.

  4. Метод DsqlDmlRequest::mapInOut() скопирует параметры в цикле в выходной буфер.

Самостоятельная работа

  1. Мы рассмотрели вариант, когда пользовательский запрос хотел получить строку из таблицы, и строка нашлась. А что произойдёт, если больше строк в таблице нет? Подсказка: нужно искать в ForNode::execute().

@kmatveev
26.07.2025 01:10 UTC
Первоисточник

Комментарии

@Siemargl
28.07.2025 21:22 UTC
+2

Когда я 10 лет назад смотрел, как оно устроено в 3.0, запомнилось несколько моментов

  1. хороший чистый код С++98

  2. запутанная модель памяти сервера (супер классики итп)

  3. GC-модель хранения данных

  4. отсутствие отдельного лога, изменения пишутся прямо в файл базы, что прогнозируется в проблемы с онлайн бэкапом и хрупкостью самой базы.

  5. простой и незатейливый оптимизатор запросов, и отсутствие CBO

    Собственно, я делал вывод, что ФБ хорошая для небольших систем, но где то дальше начнутся затыки, которые неустранимы принципиально.

    А все комьюнити утверждалось, что годится для задач любой сложности и объёма БД =)

Сейчас везде Ssd, будет конечно быстрее, но что будет с износом при GC-подходе при обязательном fsync?

Может что то изменилось в версиях 4 и 5, но по релнотам не заметил.

@sim_84
11.01.2026 18:48 UTC
0
  1. Разный код был написан в разное время. В 3.0 местами используется фичи c++11. В 5-ке c++14/17. В 6.0 разрешено при написании кода использовать фичи c++20. Использование своих контейнеров вместо стандартных там по иным причинам, прежде всего для того чтобы использовать свой пул памяти.

  2. Ну не такая уж она запутанная. Ты просто не въехал.

  3. Она такая во всех СУБД с mvcc. Отдельный undo также требует очистки если что. А если не требует, а просто кольцевой буфер, то рано или поздно натыкаемся на проблему старого снапшота (Oracle).

  4. Не то не другое оно не вызывает. Без redo лога есть всего две проблемы. Постоянный fsync м рпндомной записью, и не возможность делать физический hot stanby на основе этого лога. Начиная с 4.0 последний можно сделать с помощью встроенной логической репликации.

  5. Оптимизатор в фб всегда был смешанный. Ты не поверишь но в других СУБД также. Ибо в некоторых случаях заведомо известно что дешевле. Но таки да сквозной оценки стоимости еще нет, но с каждым релизом доля стоимостной оценки повышается относительно доли на основе правил.