Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancing copy throughput by retrieving datums with checkpointing available in TupleTableSlot #21929

Merged
merged 1 commit into from May 22, 2024

Conversation

naanagon
Copy link
Contributor

@naanagon naanagon commented Apr 11, 2024

In the sample table we have almost all char columns and few varchar columns for both of these data types attlen in pg_attribute table is -1. Since we have varlen attributes in nocachegetattr function it is going to this part of the code (nocachecode, ybcModifyTable) . In this way the time complexity is O(n*n) where n is number of attributes.

there is an another for loop in (nocachecode) as it is doing the same work every time, whey can't we build a prefix sum array once for the row and just use that for all the attributes.

@CLAassistant
Copy link

CLAassistant commented Apr 11, 2024

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

@jasonyb jasonyb left a comment

Choose a reason for hiding this comment

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

It seems the core issue is with upstream PG code, unless this access pattern by YB is discouraged by upstream PG:

for (AttrNumber attnum = minattr; attnum <= natts; attnum++)
	column->datum = heap_getattr(tuple, attnum, tupleDesc, &column->is_null);

Comment on lines 732 to 734
* Otherwise, check for non-fixed-length attrs up to and including
* target. If there aren't any, it's safe to cheaply initialize the
* cached offsets for these attrs.
Copy link
Contributor

Choose a reason for hiding this comment

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

It appears that, ever since the beginning as far as Git history shows, caching attr offsets was disabled for variable-length attributes. I don't know why exactly that is the case, but it calls into question whether your caching is safe.

Copy link
Contributor Author

@naanagon naanagon Apr 14, 2024

Choose a reason for hiding this comment

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

Since they are varlen attributes the offsets of attributes will changes from one tuple and another tuple and i think that is reason offsets aren't cached. I'm caching for each tuple and marking the array NULL after that.


for (j = 0; j <= attnum; j++)
{
if (TupleDescAttr(tupleDesc, j)->attlen <= 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Supposing it is safe to cache varlen offsets for the case you are targetting, I think a cleaner solution is to reuse the existing attcacheoff rather than duplicate code. You could add a YB exception here (perhaps by a boolean argument to the function) so that slow is not set to true, and the caller can reset these attcacheoff back to -1 when done? Note I haven't given the idea much thought and there could be holes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is safe to cacheoffsets for each row. Even i was thinking in the same line but i was stuck on how to reset to -1 only for this varlen case at ybcModifyTable level. But if we throw exception then that requires changes at many places which is hard for now to review.. Let me also rethink on this.

Comment on lines 759 to 763
* ... only we don't have it yet, or we'd not have got here. Since
* it's cheap to compute offsets for fixed-width columns, we take the
* opportunity to initialize the cached offsets for *all* the leading
* fixed-width columns, in hope of avoiding future visits to this
* routine.
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems this is the O(n^2) behavior in case all your columns are not fixed-width.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

slow becomes true as we have varlen attributes. This is the loop that is causing O(n^2) - link

naanagon

This comment was marked as duplicate.

@andrei-mart
Copy link
Contributor

It is correctly pointed out that repeatable calls to heap_getattr are generally inefficient.
However, in cases like this, when all or most of values are fetched, you can simply use heap_deform_tuple.
Same or even better efficiency as heap_getattr with cache, no need to add anything to the library.

@naanagon naanagon force-pushed the enhancing-copy-thoughput branch 4 times, most recently from e0f846e to 107e5d4 Compare April 20, 2024 17:20
@naanagon
Copy link
Contributor Author

naanagon commented Apr 20, 2024

It is correctly pointed out that repeatable calls to heap_getattr are generally inefficient. However, in cases like this, when all or most of values are fetched, you can simply use heap_deform_tuple. Same or even better efficiency as heap_getattr with cache, no need to add anything to the library.

Thanks andrei for taking a quick look. Based on your suggestion given in slack, using TupleTableSlot is the best option. I have updated the pr accordingly. Didn't combine tupDesc and slot because in classes like matview.c the tupleDesc is retrieved from diff struct like DR_transientrel, which i'm thinking may not be updated in slot.

Please lmk if i'm missing anything.

Copy link

netlify bot commented May 1, 2024

Deploy Preview for infallible-bardeen-164bc9 ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 2ea43a7
🔍 Latest deploy log https://app.netlify.com/sites/infallible-bardeen-164bc9/deploys/66326314d88c55000922e5a7
😎 Deploy Preview https://deploy-preview-21929--infallible-bardeen-164bc9.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@@ -824,8 +824,12 @@ InsertOneTuple(Oid objectid)
if (objectid != (Oid) 0)
HeapTupleSetOid(tuple, objectid);

if (IsYugaByteEnabled())
YBCExecuteInsert(boot_reldesc, tupDesc, tuple, ONCONFLICT_NONE);
if (IsYugaByteEnabled()){
Copy link
Contributor

Choose a reason for hiding this comment

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

New line before the opening brace

@@ -626,8 +626,9 @@ intorel_receive(TupleTableSlot *slot, DestReceiver *self)

if (IsYBRelation(myState->rel))
{
slot->tts_tupleDescriptor=RelationGetDescr(myState->rel);
Copy link
Contributor

Choose a reason for hiding this comment

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

ExecSetSlotDescriptor should be used.
However, it is a bad idea to modify passed in slot.
Why do we think that slot does not have a descriptor already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't used ExecSetSlotDescriptor as this clears the tuple from the slot.
slot should have descriptor otherwise it won't work in slot_getattr as that retrieves tupleDesc from slot->tts_tupleDescriptor. But, I have done a check

RelationGetDescr(rel)!=slot->tts_tupleDescriptor -> this is true as it has different tdtypeid and tdrefcount. So, I tried to update it.

Based on your comments i have done a recheck and I hope we aren't using tdtypeid and tdrefcount anywhere.

@@ -485,8 +485,9 @@ transientrel_receive(TupleTableSlot *slot, DestReceiver *self)
tuple = ExecMaterializeSlot(slot);
if (IsYBRelation(myState->transientrel))
{
slot->tts_tupleDescriptor=RelationGetDescr(myState->transientrel);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above

@@ -5417,11 +5417,13 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
/* Write the tuple out to the new relation */
if (newrel)
{
if (IsYBRelation(newrel))
if (IsYBRelation(newrel)){
Copy link
Contributor

Choose a reason for hiding this comment

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

New line before brace

@@ -5417,11 +5417,13 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
/* Write the tuple out to the new relation */
if (newrel)
{
if (IsYBRelation(newrel))
if (IsYBRelation(newrel)){
newslot->tts_tupleDescriptor=RelationGetDescr(newrel);
Copy link
Contributor

Choose a reason for hiding this comment

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

Unsafe tuple descriptor assignment again

@@ -1573,6 +1573,7 @@ ExecUpdate(ModifyTableState *mtstate,
ModifyTable *plan = (ModifyTable *) mtstate->ps.plan;
if (is_pk_updated)
{
planSlot->tts_tupleDescriptor=RelationGetDescr(resultRelationDesc);
Copy link
Contributor

Choose a reason for hiding this comment

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

and again

HeapTuple tuple, OnConflictAction onConflictAction, Datum *ybctid,
YBCPgTransactionSetting transaction_setting) {
Oid relfileNodeId = YbGetRelfileNodeId(rel);
AttrNumber minattr = YBGetFirstLowInvalidAttributeNumber(rel);
int natts = RelationGetNumberOfAttributes(rel);
Bitmapset *pkey = YBGetTablePrimaryKeyBms(rel);

TupleDesc tupleDesc = slot->tts_tupleDescriptor;
Copy link
Contributor

Choose a reason for hiding this comment

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

if slot->tts_tupleDescriptor is NULL you probably can use RelationGetDescr(rel)

@andrei-mart
Copy link
Contributor

yugabyte=# CREATE TABLE test (r1 int, r2 int, i int, PRIMARY KEY (r1 ASC, r2 DESC));
CREATE TABLE
yugabyte=# INSERT INTO test VALUES (1, 1, 1), (2, 2, 2), (3, 3, 3);
INSERT 0 3
yugabyte=# INSERT INTO test  VALUES (3, 3, 31) ON CONFLICT (r1, r2) DO UPDATE SET r1 = EXCLUDED.i, r2 = EXCLUDED.i;
INSERT 0 1
yugabyte=# select * from test;
 r1 | r2 | i  
----+----+----
  1 |  1 |  1
  2 |  2 |  2
 31 | 31 | 31
(3 rows)

Expected result is

yugabyte=# select * from test;
 r1 | r2 | i  
----+----+---
  1 |  1 | 1
  2 |  2 | 2
 31 | 31 | 3
(3 rows)

There are more failures of ./yb_build.sh --java-test 'org.yb.pgsql.TestPgUpdatePrimaryKey'

@@ -1573,7 +1573,8 @@ ExecUpdate(ModifyTableState *mtstate,
ModifyTable *plan = (ModifyTable *) mtstate->ps.plan;
if (is_pk_updated)
{
YBCExecuteUpdateReplace(resultRelationDesc, planSlot, tuple, estate);
slot->tts_tuple->t_ybctid = YBCGetYBTupleIdFromSlot(planSlot);
YBCExecuteUpdateReplace(resultRelationDesc, slot, tuple, estate);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a TODO already here to pass a transformed slot. I guess slot is the right option instead of planSlot

@naanagon naanagon force-pushed the enhancing-copy-thoughput branch 3 times, most recently from f71115d to e572a17 Compare May 14, 2024 14:34
YBCPgTransactionSetting transaction_setting) {
Oid relfileNodeId = YbGetRelfileNodeId(rel);
AttrNumber minattr = YBGetFirstLowInvalidAttributeNumber(rel);
int natts = RelationGetNumberOfAttributes(rel);
Bitmapset *pkey = YBGetTablePrimaryKeyBms(rel);
TupleDesc tupleDesc = slot->tts_tupleDescriptor;
HeapTuple tuple = slot->tts_tuple;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Based on the definition of ExecMaterializeSlot, I haven't used it. So, ybctid and oid are updated in tuple itself. Please lmk if i'm missing anything.

HeapTuple tuple, OnConflictAction onConflictAction, Datum *ybctid,
YBCPgTransactionSetting transaction_setting) {
Oid relfileNodeId = YbGetRelfileNodeId(rel);
AttrNumber minattr = YBGetFirstLowInvalidAttributeNumber(rel);
int natts = RelationGetNumberOfAttributes(rel);
Bitmapset *pkey = YBGetTablePrimaryKeyBms(rel);

TupleDesc tupleDesc = slot->tts_tupleDescriptor;
if(tupleDesc == NULL || tupleDesc != RelationGetDescr(rel))
Copy link
Contributor

Choose a reason for hiding this comment

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

If tupleDesc has to be equal to rel's descriptor, why not just to assign rel's descriptor to that variable?

@@ -18211,7 +18210,7 @@ YbATCopyTableRowsUnchecked(Relation old_rel, Relation new_rel,
ExecStoreHeapTuple(tuple, newslot, false);

/* Write the tuple out to the new relation */
YBCExecuteInsert(new_rel, newslot->tts_tupleDescriptor, tuple,
YBCExecuteInsert(new_rel, newslot,
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: the statement is shorter now, can fit into single line.

@@ -342,7 +347,7 @@ static Oid YBCApplyInsertRow(
if (IsCatalogRelation(rel))
{
MarkCurrentCommandUsed();
CacheInvalidateHeapTuple(rel, tuple, NULL);
CacheInvalidateHeapTuple(rel, slot->tts_tuple, NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

If I'm not missing something, local variable tuple is defined here and equal to slot->tts_tuple

@naanagon naanagon changed the title Enhancing-copy-throughput-by-storing-offsets-for-tuple Enhancing-copy-throughput-by-retrieving-datums-with-checkpointing-available-in-slot May 22, 2024
@naanagon naanagon changed the title Enhancing-copy-throughput-by-retrieving-datums-with-checkpointing-available-in-slot Enhancing copy throughput by retrieving datums with checkpointing available in TupleTableSlot May 22, 2024
@andrei-mart andrei-mart merged commit 5751825 into yugabyte:master May 22, 2024
2 checks passed
jharveysmith pushed a commit that referenced this pull request May 24, 2024
… a TupleTableSlot

A TupleTableSlot parameter replaces HeapTuple and TupleDescriptor parameters in all DocDB insert procedures, including bulk insert.   

In Postgres insert procedures work with a HeapTuple, because it represent on-disk format, so tuple data was simply copied into the data page. DocDB store individual values (Datums). In Yugabyte insert procedures operated on HeapTuple too, because it was ready available, however HeapTuple is less efficient to retrieve individual values, especially if tuple has variable length arguments. Fixed size arguments allow to calculate data offsets once, but since variable size value occur in the tuple, to retrieve a value from a HeapTuple it is necessary to iterate over all columns from the beginning and calculate offsets. 

This impacted a lot extremely wide tables with hundreds of columns. Values were added to the DocDB statement, one by one, and each time columns were iterated to find the offset. Hundreds of columns required up to hundreds iterations each.
With new approach tuple may be already in slot and dematerialized at the time when it is about to be sent to DocDB for insert. If so, all values are readily available and materialization step is not needed. In worst case, if there is a HeapTuple to insert, access to values via TupleTableSlot does dematerialization in one pass, no multiple iterations over columns. 

Effect is barely noticeable for tables with a few/several columns, but load tests into tables with 1000+ columns show up to 3x improvement.
svarnau pushed a commit that referenced this pull request May 25, 2024
… a TupleTableSlot

A TupleTableSlot parameter replaces HeapTuple and TupleDescriptor parameters in all DocDB insert procedures, including bulk insert.

In Postgres insert procedures work with a HeapTuple, because it represent on-disk format, so tuple data was simply copied into the data page. DocDB store individual values (Datums). In Yugabyte insert procedures operated on HeapTuple too, because it was ready available, however HeapTuple is less efficient to retrieve individual values, especially if tuple has variable length arguments. Fixed size arguments allow to calculate data offsets once, but since variable size value occur in the tuple, to retrieve a value from a HeapTuple it is necessary to iterate over all columns from the beginning and calculate offsets.

This impacted a lot extremely wide tables with hundreds of columns. Values were added to the DocDB statement, one by one, and each time columns were iterated to find the offset. Hundreds of columns required up to hundreds iterations each.
With new approach tuple may be already in slot and dematerialized at the time when it is about to be sent to DocDB for insert. If so, all values are readily available and materialization step is not needed. In worst case, if there is a HeapTuple to insert, access to values via TupleTableSlot does dematerialization in one pass, no multiple iterations over columns.

Effect is barely noticeable for tables with a few/several columns, but load tests into tables with 1000+ columns show up to 3x improvement.
svarnau pushed a commit that referenced this pull request May 25, 2024
… a TupleTableSlot

A TupleTableSlot parameter replaces HeapTuple and TupleDescriptor parameters in all DocDB insert procedures, including bulk insert.

In Postgres insert procedures work with a HeapTuple, because it represent on-disk format, so tuple data was simply copied into the data page. DocDB store individual values (Datums). In Yugabyte insert procedures operated on HeapTuple too, because it was ready available, however HeapTuple is less efficient to retrieve individual values, especially if tuple has variable length arguments. Fixed size arguments allow to calculate data offsets once, but since variable size value occur in the tuple, to retrieve a value from a HeapTuple it is necessary to iterate over all columns from the beginning and calculate offsets.

This impacted a lot extremely wide tables with hundreds of columns. Values were added to the DocDB statement, one by one, and each time columns were iterated to find the offset. Hundreds of columns required up to hundreds iterations each.
With new approach tuple may be already in slot and dematerialized at the time when it is about to be sent to DocDB for insert. If so, all values are readily available and materialization step is not needed. In worst case, if there is a HeapTuple to insert, access to values via TupleTableSlot does dematerialization in one pass, no multiple iterations over columns.

Effect is barely noticeable for tables with a few/several columns, but load tests into tables with 1000+ columns show up to 3x improvement.
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.

None yet

4 participants