Skip to content

New implementation of bulk insert#8990

Open
hvlad wants to merge 2 commits intomasterfrom
work/bulk_insert
Open

New implementation of bulk insert#8990
hvlad wants to merge 2 commits intomasterfrom
work/bulk_insert

Conversation

@hvlad
Copy link
Copy Markdown
Member

@hvlad hvlad commented Apr 15, 2026

In Firebird 5 parallel restore was introduced. It contains a "bulk insert" code that allows to lower contention on PP by concurrent writers. Also it makes each writer to use own dedicated DP to fill with records.

With shared metadata cache in v6 that code became broken and parallel restore creates a lot of unused data pages. Instead of fixing that first attempt to have bulk insert ability, I offer a new approach that fixes the issue, have better performance and could be used more widely.

Note, patch doesn't remove old code, it could be done a bit later - after agreement on the new code.

@hvlad hvlad self-assigned this Apr 15, 2026
@dyemanov
Copy link
Copy Markdown
Member

Some details about how the new approach is different from the old one would be appreciated.

@hvlad
Copy link
Copy Markdown
Member Author

hvlad commented Apr 15, 2026

The insert code path now avoids a lot of not necessary steps, such as triggers, validations (except of NOT NULL on field level) index maintenance - all of this is abcent during restore. Also, all records inserted into dedicated in-memory buffer to avoid endless latches on data page buffers. When in-memory buffer is full, its contents is copied into actual DB buffers and go to the disk in usual way. In-memory buffer size start from 1 page and resized to the 8 pages when first 8 pages of relation is filled. Also, blob contens are put into separate in-memory buffer that works in the same way.

The code is put into two main classes:

  • BulkInsert - implements in-memory buffers and code that works with records (mostly analog/copy of some DPM parts) , and
  • BulkInsertNode - replaces StoreNode and contains some further optimizations, such as pre-calculated target descriptors.

@AlexPeshkoff
Copy link
Copy Markdown
Member

AlexPeshkoff commented Apr 15, 2026 via email

@hvlad
Copy link
Copy Markdown
Member Author

hvlad commented Apr 15, 2026

But what about relPages->rel_*_data_space stored at database, not attachment level? From new code it seems that they should be moved back to attachment from database? Am I missing something?

These fields not used in bulk insert code and thus was not affected in this patch.

At next step I'm going to:

leave as is:

	ULONG rel_index_root;		// index root page number
	USHORT rel_pg_space_id;

make atomic:

	ULONG rel_data_pages;		// count of relation data pages
	ULONG rel_slot_space;		// lowest pointer page with slot space
	ULONG rel_pri_data_space;	// lowest pointer page with primary data page space
	ULONG rel_sec_data_space;	// lowest pointer page with secondary data page space

remove:

	ULONG rel_last_free_pri_dp;	// last primary data page found with space
	ULONG rel_last_free_blb_dp;	// last blob data page found with space

@dyemanov dyemanov self-requested a review April 15, 2026 16:54
@aafemt
Copy link
Copy Markdown
Contributor

aafemt commented Apr 16, 2026

I think term "bulk insert" is a little misleading here. Oracle uses "direct-path insert" for the path where data go straight into data pages.

@hvlad
Copy link
Copy Markdown
Member Author

hvlad commented May 1, 2026

@dyemanov: did you have a chance to look at it ?

@dyemanov
Copy link
Copy Markdown
Member

dyemanov commented May 1, 2026

@dyemanov: did you have a chance to look at it ?

Halfway done, hopefully to be completed during the weekend.

Copy link
Copy Markdown
Member

@dyemanov dyemanov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing (besides my other comments) that I don't really like is a code duplication between dpm.epp and BulkInsert.cpp. For example, storage of a big record seems to share about 80% of code. Do you think it could be improved (i.e. by adding separate routines to dpm.epp that deal with a preallocated page buffer)?

Comment thread src/dsql/StmtNodes.cpp
auto node = FB_NEW_POOL(pool) BulkInsertNode(pool);

if (csb->csb_blr_reader.peekByte() == blr_marks)
/*node->marks |= */PAR_marks(csb);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If marks are skipped, why blr_marks can be generated after blr_bulk_insert? Or is it just reserved for possible future use?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, just in case it will be used later

Comment thread src/dsql/StmtNodes.cpp
tail.csb_flags |= csb_store;

jrd_rel* const relation = tail.csb_relation(tdbb);
if (relation)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could a relation be missing here, given that the target stream is always RelationSourceNode or LocalTableSourceNode? Maybe an assert instead? Or at least add an fb_assert(false) to the else part?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add assert, thanks.

Comment thread src/dsql/StmtNodes.cpp
record->setTransactionNumber(transaction->tra_number);

assignValues(tdbb, request, relation, record, toDescs.begin());
cleanupRpb(tdbb, rpb);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose cleanupRpb() makes no sense after record->nullify() which not only set NULL flags but also zeroes the record data.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, will remove it.

Comment thread src/dsql/StmtNodes.cpp
bulk->putRecord(tdbb, rpb, transaction);
REPL_store(tdbb, rpb, transaction);

JRD_reschedule(tdbb);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JRD_reschedule() is usually called per every record inside cursor->fetchNext() -> RecordSource::internalGetRecord(), is it really necessary calling it again after store?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, will remove

Comment thread src/dsql/StmtNodes.cpp
}
auto bulk = transaction->getBulkInsert(tdbb, relation, true);
assignValues(tdbb, request, relation, record, impure->descs->begin());
cleanupRpb(tdbb, rpb);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above re. cleanupRpb()

Comment thread src/dsql/StmtNodes.cpp
ERR_post(Arg::Gds(isc_not_valid) << Arg::Str(name) << Arg::Str(NULL_STRING_MARK));
}

record->setNull(toField->fieldId);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assignValues() seems to be always called after record->nullify(), so all fields are already initialized to NULLs and it shouldn't be necessary to do that again.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

Comment thread src/dsql/StmtNodes.cpp
record->nullify();
record->setTransactionNumber(transaction->tra_number);

if (!(impure->flags & INIT_DONE))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you could check for !impure->descs and thus avoid impure->flags at all?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

Comment thread src/jrd/ods.h
inline constexpr UCHAR ppg_dp_swept = 0x04; // Sweep has nothing to do on data page
inline constexpr UCHAR ppg_dp_secondary = 0x08; // Primary record versions not stored on data page
inline constexpr UCHAR ppg_dp_empty = 0x10; // Data page is empty
inline constexpr UCHAR ppg_dp_reserved = 0x20; // Slot is reserved for bulk insert
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the slot was reserved, but the server crashed before the slot has been actually used, can it be reused for regular (non-bulk) operations? If not, shouldn't gfix clear this flag if such a case is detected?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the slot was reserved, but the server crashed before the slot has been actually used, can it be reused for regular (non-bulk) operations?

No, it can't

If not, shouldn't gfix clear this flag if such a case is detected?

Yes, and validation always clears it, see:

  • Validation::walk_data_page() never set ppg_dp_reserved at pp_bits
  • thus, Validation::walk_pointer_page() always clear this bit

Comment thread src/jrd/BulkInsert.cpp
CCH_RELEASE(tdbb, &dpWindow);

if (m_isPrimary)
tdbb->bumpStats(RecordStatType::INSERTS, m_relation->getId(), m_current->dpg_count);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this record-level counter increased per page rather than per record (inside putRecord(), I guess)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until flush() records actually not yet inserted into relation.
Also, this is a kind of small optimization.

@hvlad
Copy link
Copy Markdown
Member Author

hvlad commented May 3, 2026

The only thing (besides my other comments) that I don't really like is a code duplication between dpm.epp and BulkInsert.cpp. For example, storage of a big record seems to share about 80% of code.

Yes, this is not an easy question and I considered it, of course. The current code is best compromiіe I can think of.
I think the amount of duplicated code is relatively small.
Do you have clear idea how to not duplicate code and not create an additional branches in existing one ?

Do you think it could be improved (i.e. by adding separate routines to dpm.epp that deal with a preallocated page buffer)?

You mean - move (half of) BulkInsert into DPM ? :)

@hvlad
Copy link
Copy Markdown
Member Author

hvlad commented May 3, 2026

I'll implement suggested changes and re-test, then commit its.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants