pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feedFast COPY FROM based on batch insert
39+ messages / 7 participants
[nested] [flat]
Commits from this thread:
2022-10-13 97da48246d Allow batch insertion during COPY into a foreign table.
* Fast COPY FROM based on batch insert
@ 2021-06-04 08:26 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 2 replies; 39+ messages in thread
From: Andrey Lepikhov @ 2021-06-04 08:26 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; Justin Pryzby <pryzby@telsasoft.com>; +Cc: tsunakawa.takay <tsunakawa.takay@fujitsu.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
Hi,
We still have slow 'COPY FROM' operation for foreign tables in current
master.
Now we have a foreign batch insert operation And I tried to rewrite the
patch [1] with this machinery.
The patch (see in attachment) smaller than [1] and no changes required
in FDW API.
Benchmarking
============
I used two data sets: with a number of 1E6 and 1E7 tuples. As a foreign
server emulation I used loopback FDW links.
Test table:
CREATE TABLE test(a int, payload varchar(80));
Execution time of COPY FROM into single foreign table:
version | 1E6 tuples | 1E7 tuples |
master: | 64s | 775s |
Patch [1]: | 5s | 50s |
Current: | 4s | 42s |
Execution time of the COPY operation into a plane table is 0.8s for 1E6
tuples and 8s for 1E7 tuples.
Execution time of COPY FROM into the table partitioned by three foreign
partitions:
version | 1E6 tuples | 1E7 tuples |
master: | 85s | 900s |
Patch [1]: | 10s | 100s |
Current: | 3.5s | 34s |
But the bulk insert execution time in current implementation strongly
depends on MAX_BUFFERED_TUPLES/BYTES value and in my experiments was
reduced to 50s.
[1]
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru
--
regards,
Andrey Lepikhov
Postgres Professional
Attachments:
0001-Implementation-of-a-Bulk-COPY-FROM.patch (text/plain, 23.7K) download
From 715406ce4a98df4e0aecdfdf9d9f59cd3a13101e Mon Sep 17 00:00:00 2001
From: "Andrey V. Lepikhov" <a.lepikhov@postgrespro.ru>
Date: Fri, 4 Jun 2021 13:21:43 +0500
Subject: [PATCH] Implementation of a Bulk 'COPY FROM ...' operation into
foreign/distributed table.
---
.../postgres_fdw/expected/postgres_fdw.out | 46 +++-
contrib/postgres_fdw/sql/postgres_fdw.sql | 45 +++
src/backend/commands/copyfrom.c | 259 ++++++++----------
src/backend/executor/execMain.c | 45 +++
src/backend/executor/execPartition.c | 8 +
src/include/commands/copyfrom_internal.h | 10 -
src/include/executor/executor.h | 1 +
src/include/nodes/execnodes.h | 8 +-
8 files changed, 261 insertions(+), 161 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f320a7578d..cb2680c6bd 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8164,8 +8164,9 @@ copy rem2 from stdin;
copy rem2 from stdin; -- ERROR
ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
DETAIL: Failing row contains (-1, xyzzy).
-CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1 xyzzy"
+CONTEXT: COPY loc2, line 1: "-1 xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN
+COPY rem2, line 2
select * from rem2;
f1 | f2
----+-----
@@ -8176,6 +8177,19 @@ select * from rem2;
alter foreign table rem2 drop constraint rem2_f1positive;
alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE: (1)
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -8284,6 +8298,34 @@ drop trigger rem2_trig_row_before on rem2;
drop trigger rem2_trig_row_after on rem2;
drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR: column "f1" of relation "loc2" does not exist
+CONTEXT: remote SQL command: COPY public.loc2(f1, f2) FROM STDIN
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 17dba77d7e..5576328348 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2258,6 +2258,23 @@ alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -2358,6 +2375,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1 foo
+2 bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 40a54ad0bd..4f65601bac 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -317,54 +317,78 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
cstate->line_buf_valid = false;
save_cur_lineno = cstate->cur_lineno;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
-
- for (i = 0; i < nused; i++)
+ if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ int sent = 0;
+
+ Assert(resultRelInfo->ri_BatchSize > 1 &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL);
+
+ /* Flush into foreign table or partition */
+ do {
+ int batch_size = (resultRelInfo->ri_BatchSize < nused - sent) ?
+ resultRelInfo->ri_BatchSize : (nused - sent);
+
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ slots,
+ NULL,
+ &batch_size);
+ sent += batch_size;
+ } while (sent < nused);
+ }
+ else
{
/*
- * If there are any indexes, update them for all the inserted tuples,
- * and run AFTER ROW INSERT triggers.
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
*/
- if (resultRelInfo->ri_NumIndices > 0)
- {
- List *recheckIndexes;
-
- cstate->cur_lineno = buffer->linenos[i];
- recheckIndexes =
- ExecInsertIndexTuples(resultRelInfo,
- buffer->slots[i], estate, false, false,
- NULL, NIL);
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], recheckIndexes,
- cstate->transition_capture);
- list_free(recheckIndexes);
- }
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
- /*
- * There's no indexes, but see if we need to run AFTER ROW INSERT
- * triggers anyway.
- */
- else if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ for (i = 0; i < nused; i++)
{
- cstate->cur_lineno = buffer->linenos[i];
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], NIL, cstate->transition_capture);
- }
+ /*
+ * If there are any indexes, update them for all the inserted tuples,
+ * and run AFTER ROW INSERT triggers.
+ */
+ if (resultRelInfo->ri_NumIndices > 0)
+ {
+ List *recheckIndexes;
+
+ cstate->cur_lineno = buffer->linenos[i];
+ recheckIndexes =
+ ExecInsertIndexTuples(resultRelInfo,
+ buffer->slots[i], estate, false, false,
+ NULL, NIL);
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], recheckIndexes,
+ cstate->transition_capture);
+ list_free(recheckIndexes);
+ }
+
+ /*
+ * There's no indexes, but see if we need to run AFTER ROW INSERT
+ * triggers anyway.
+ */
+ else if (resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ cstate->cur_lineno = buffer->linenos[i];
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], NIL, cstate->transition_capture);
+ }
- ExecClearTuple(slots[i]);
+ ExecClearTuple(slots[i]);
+ }
}
/* Mark that all slots are free */
@@ -538,13 +562,11 @@ CopyFrom(CopyFromState cstate)
CommandId mycid = GetCurrentCommandId(true);
int ti_options = 0; /* start with default options for insert */
BulkInsertState bistate = NULL;
- CopyInsertMethod insertMethod;
CopyMultiInsertInfo multiInsertInfo = {0}; /* pacify compiler */
int64 processed = 0;
int64 excluded = 0;
bool has_before_insert_row_trig;
bool has_instead_insert_row_trig;
- bool leafpart_use_multi_insert = false;
Assert(cstate->rel);
Assert(list_length(cstate->range_table) == 1);
@@ -654,6 +676,33 @@ CopyFrom(CopyFromState cstate)
resultRelInfo = target_resultRelInfo = makeNode(ResultRelInfo);
ExecInitResultRelation(estate, resultRelInfo, 1);
+ Assert(!target_resultRelInfo->ri_usesMultiInsert);
+
+ /*
+ * It's generally more efficient to prepare a bunch of tuples for
+ * insertion, and insert them in bulk, for example, with one
+ * table_multi_insert() call than call table_tuple_insert() separately for
+ * every tuple. However, there are a number of reasons why we might not be
+ * able to do this. For example, if there any volatile expressions in the
+ * table's default values or in the statement's WHERE clause, which may
+ * query the table we are inserting into, buffering tuples might produce
+ * wrong results. Also, the relation we are trying to insert into itself
+ * may not be amenable to buffered inserts.
+ *
+ * Note: For partitions, this flag is set considering the target table's
+ * flag that is being set here and partition's own properties which are
+ * checked by calling ExecMultiInsertAllowed(). It does not matter
+ * whether partitions have any volatile default expressions as we use the
+ * defaults from the target of the COPY command.
+ * Also, the COPY command requires a non-zero input list of attributes.
+ * Therefore, the length of the attribute list is checked here.
+ */
+ if (!cstate->volatile_defexprs &&
+ list_length(cstate->attnumlist) > 0 &&
+ !contain_volatile_functions(cstate->whereClause))
+ target_resultRelInfo->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(target_resultRelInfo);
+
/* Verify the named relation is a valid target for INSERT */
CheckValidResultRel(resultRelInfo, CMD_INSERT);
@@ -676,6 +725,12 @@ CopyFrom(CopyFromState cstate)
resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
resultRelInfo);
+ if (target_resultRelInfo->ri_usesMultiInsert &&
+ resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -703,83 +758,9 @@ CopyFrom(CopyFromState cstate)
cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
&mtstate->ps);
- /*
- * It's generally more efficient to prepare a bunch of tuples for
- * insertion, and insert them in one table_multi_insert() call, than call
- * table_tuple_insert() separately for every tuple. However, there are a
- * number of reasons why we might not be able to do this. These are
- * explained below.
- */
- if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
- {
- /*
- * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
- * triggers on the table. Such triggers might query the table we're
- * inserting into and act differently if the tuples that have already
- * been processed and prepared for insertion are not there.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
- resultRelInfo->ri_TrigDesc->trig_insert_new_table)
- {
- /*
- * For partitioned tables we can't support multi-inserts when there
- * are any statement level insert triggers. It might be possible to
- * allow partitioned tables with such triggers in the future, but for
- * now, CopyMultiInsertInfoFlush expects that any before row insert
- * and statement level insert triggers are on the same relation.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
- {
- /*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
- *
- * Note: It does not matter if any partitions have any volatile
- * default expressions as we use the defaults from the target of the
- * COPY command.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (contain_volatile_functions(cstate->whereClause))
- {
- /*
- * Can't support multi-inserts if there are any volatile function
- * expressions in WHERE clause. Similarly to the trigger case above,
- * such expressions may query the table we're inserting into.
- */
- insertMethod = CIM_SINGLE;
- }
- else
- {
- /*
- * For partitioned tables, we may still be able to perform bulk
- * inserts. However, the possibility of this depends on which types
- * of triggers exist on the partition. We must disable bulk inserts
- * if the partition is a foreign table or it has any before row insert
- * or insert instead triggers (same as we checked above for the parent
- * table). Since the partition's resultRelInfos are initialized only
- * when we actually need to insert the first tuple into them, we must
- * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
- * flag that we must later determine if we can use bulk-inserts for
- * the partition being inserted into.
- */
- if (proute)
- insertMethod = CIM_MULTI_CONDITIONAL;
- else
- insertMethod = CIM_MULTI;
-
+ if (resultRelInfo->ri_usesMultiInsert)
CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
estate, mycid, ti_options);
- }
/*
* If not using batch mode (which allocates slots as needed) set up a
@@ -787,7 +768,7 @@ CopyFrom(CopyFromState cstate)
* one, even if we might batch insert, to read the tuple in the root
* partition's form.
*/
- if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+ if (!resultRelInfo->ri_usesMultiInsert || proute)
{
singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
&estate->es_tupleTable);
@@ -830,7 +811,7 @@ CopyFrom(CopyFromState cstate)
ResetPerTupleExprContext(estate);
/* select slot to (initially) load row into */
- if (insertMethod == CIM_SINGLE || proute)
+ if (!target_resultRelInfo->ri_usesMultiInsert || proute)
{
myslot = singleslot;
Assert(myslot != NULL);
@@ -838,7 +819,6 @@ CopyFrom(CopyFromState cstate)
else
{
Assert(resultRelInfo == target_resultRelInfo);
- Assert(insertMethod == CIM_MULTI);
myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -905,24 +885,14 @@ CopyFrom(CopyFromState cstate)
has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
- /*
- * Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
- */
- leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
- !has_before_insert_row_trig &&
- !has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
-
/* Set the multi-insert buffer to use for this partition. */
- if (leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
resultRelInfo);
}
- else if (insertMethod == CIM_MULTI_CONDITIONAL &&
- !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
{
/*
* Flush pending inserts if this partition can't use
@@ -952,7 +922,7 @@ CopyFrom(CopyFromState cstate)
* rowtype.
*/
map = resultRelInfo->ri_RootToPartitionMap;
- if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+ if (!resultRelInfo->ri_usesMultiInsert)
{
/* non batch insert */
if (map != NULL)
@@ -971,9 +941,6 @@ CopyFrom(CopyFromState cstate)
*/
TupleTableSlot *batchslot;
- /* no other path available for partitioned table */
- Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -1045,7 +1012,7 @@ CopyFrom(CopyFromState cstate)
ExecPartitionCheck(resultRelInfo, myslot, estate, true);
/* Store the slot in the multi-insert buffer, when enabled. */
- if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
/*
* The slot previously might point into the per-tuple
@@ -1124,11 +1091,8 @@ CopyFrom(CopyFromState cstate)
}
/* Flush any remaining buffered tuples */
- if (insertMethod != CIM_SINGLE)
- {
- if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
- }
+ if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
/* Done, clean up */
error_context_stack = errcallback.previous;
@@ -1149,12 +1113,11 @@ CopyFrom(CopyFromState cstate)
/* Allow the FDW to shut down */
if (target_resultRelInfo->ri_FdwRoutine != NULL &&
target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
- target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
- target_resultRelInfo);
+ target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+ target_resultRelInfo);
/* Tear down the multi-insert buffer data */
- if (insertMethod != CIM_SINGLE)
- CopyMultiInsertInfoCleanup(&multiInsertInfo);
+ CopyMultiInsertInfoCleanup(&multiInsertInfo);
/* Close all the partitioned tables, leaf partitions, and their indices */
if (proute)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..922e80089c 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1256,9 +1256,54 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_PartitionTupleSlot = NULL; /* ditto */
resultRelInfo->ri_ChildToRootMap = NULL;
resultRelInfo->ri_ChildToRootMapValid = false;
+ resultRelInfo->ri_usesMultiInsert = false;
resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
}
+/*
+ * ExecMultiInsertAllowed
+ * Does this relation allow caller to use multi-insert mode when
+ * inserting rows into it?
+ */
+bool
+ExecMultiInsertAllowed(const ResultRelInfo *rri)
+{
+ /*
+ * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+ * triggers on the table. Such triggers might query the table we're
+ * inserting into and act differently if the tuples that have already
+ * been processed and prepared for insertion are not there.
+ */
+ if (rri->ri_TrigDesc != NULL &&
+ (rri->ri_TrigDesc->trig_insert_before_row ||
+ rri->ri_TrigDesc->trig_insert_instead_row))
+ return false;
+
+ /*
+ * For partitioned tables we can't support multi-inserts when there are
+ * any statement level insert triggers. It might be possible to allow
+ * partitioned tables with such triggers in the future, but for now,
+ * CopyMultiInsertInfoFlush expects that any before row insert and
+ * statement level insert triggers are on the same relation.
+ */
+ if (rri->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ rri->ri_TrigDesc != NULL &&
+ rri->ri_TrigDesc->trig_insert_new_table)
+ return false;
+
+ if (rri->ri_FdwRoutine != NULL &&
+ (rri->ri_FdwRoutine->ExecForeignBatchInsert == NULL ||
+ rri->ri_FdwRoutine->GetForeignModifyBatchSize == NULL))
+ /*
+ * Foreign tables don't support multi-inserts, unless their FDW
+ * provides the necessary bulk insert interface.
+ */
+ return false;
+
+ /* OK, caller can use multi-insert on this relation. */
+ return true;
+}
+
/*
* ExecGetTriggerResultRel
* Get a ResultRelInfo for a trigger target relation.
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 606c920b06..d784692bf8 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -514,6 +514,14 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
rootResultRelInfo,
estate->es_instrument);
+ /*
+ * If a partition's root parent isn't allowed to use it, neither is the
+ * partition.
+ */
+ if (rootResultRelInfo->ri_usesMultiInsert)
+ leaf_part_rri->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(leaf_part_rri);
+
/*
* Verify result relation is a valid target for an INSERT. An UPDATE of a
* partition-key becomes a DELETE+INSERT operation, so this check is still
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index 4d68d9cceb..598a68a6f1 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -39,16 +39,6 @@ typedef enum EolType
EOL_CRNL
} EolType;
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
- CIM_SINGLE, /* use table_tuple_insert or fdw routine */
- CIM_MULTI, /* always use table_multi_insert */
- CIM_MULTI_CONDITIONAL /* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
/*
* This struct contains all the state variables used throughout a COPY FROM
* operation.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 3dc03c913e..46a79c5ad8 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,6 +203,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
+extern bool ExecMultiInsertAllowed(const ResultRelInfo *rri);
extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 7795a69490..58d5df9874 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -521,7 +521,13 @@ typedef struct ResultRelInfo
TupleConversionMap *ri_ChildToRootMap;
bool ri_ChildToRootMapValid;
- /* for use by copyfrom.c when performing multi-inserts */
+ /*
+ * The following fields are currently only relevant to copyfrom.c.
+ * True if okay to use multi-insert on this relation
+ */
+ bool ri_usesMultiInsert;
+
+ /* Buffer allocated to this relation when using multi-insert mode */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
} ResultRelInfo;
--
2.31.1
^ permalink raw reply [nested|flat] 39+ messages in thread
* RE: Fast COPY FROM based on batch insert
@ 2021-06-04 08:45 tsunakawa.takay@fujitsu.com <tsunakawa.takay@fujitsu.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
1 sibling, 1 reply; 39+ messages in thread
From: tsunakawa.takay@fujitsu.com @ 2021-06-04 08:45 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Etsuro Fujita <etsuro.fujita@gmail.com>; Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com <houzj.fnst@fujitsu.com>
From: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
> We still have slow 'COPY FROM' operation for foreign tables in current master.
> Now we have a foreign batch insert operation And I tried to rewrite the patch [1]
> with this machinery.
I haven't looked at the patch, but nice performance.
However, I see the following problems. What do you think about them?
1)
No wonder why the user would think like "Why are INSERTs run on the remote server? I ran COPY."
2)
Without the FDW API for COPY, other FDWs won't get a chance to optimize for bulk data loading. For example, oracle_fdw might use conventional path insert for the FDW batch insert, and the direct path insert for the FDW COPY.
3)
INSERT and COPY in Postgres differs in whether the rule is invoked:
https://www.postgresql.org/docs/devel/sql-copy.html
"COPY FROM will invoke any triggers and check constraints on the destination table. However, it will not invoke rules."
Regards
Takayuki Tsunakawa
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2021-06-07 06:36 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: tsunakawa.takay@fujitsu.com <tsunakawa.takay@fujitsu.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2021-06-07 06:36 UTC (permalink / raw)
To: tsunakawa.takay@fujitsu.com <tsunakawa.takay@fujitsu.com>; +Cc: Etsuro Fujita <etsuro.fujita@gmail.com>; Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com <houzj.fnst@fujitsu.com>
On 4/6/21 13:45, tsunakawa.takay@fujitsu.com wrote:
> From: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
>> We still have slow 'COPY FROM' operation for foreign tables in current master.
>> Now we have a foreign batch insert operation And I tried to rewrite the patch [1]
>> with this machinery.
>
> I haven't looked at the patch, but nice performance.
>
> However, I see the following problems. What do you think about them?
I agree with your fears.
Think about this patch as an intermediate step on the way to fast COPY
FROM. This patch contains all logic of the previous patch, except of
transport machinery (bulk insertion api).
It may be simpler to understand advantages of proposed 'COPY' FDW API
having committed 'COPY FROM ...' feature based on the bulk insert FDW API.
--
regards,
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2021-06-07 11:16 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2021-06-07 11:16 UTC (permalink / raw)
To: tsunakawa.takay@fujitsu.com <tsunakawa.takay@fujitsu.com>; +Cc: Etsuro Fujita <etsuro.fujita@gmail.com>; Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com <houzj.fnst@fujitsu.com>
Second version of the patch fixes problems detected by the FDW
regression tests and shows differences of error reports in
tuple-by-tuple and batched COPY approaches.
--
regards,
Andrey Lepikhov
Postgres Professional
Attachments:
v2-0001-Implementation-of-a-Bulk-COPY-FROM.patch (text/plain, 23.6K) download
From 68ad02038d7477e005b65bf5aeeac4efbb41073e Mon Sep 17 00:00:00 2001
From: "Andrey V. Lepikhov" <a.lepikhov@postgrespro.ru>
Date: Fri, 4 Jun 2021 13:21:43 +0500
Subject: [PATCH] Implementation of a Bulk COPY FROM operation into foreign
table.
---
.../postgres_fdw/expected/postgres_fdw.out | 45 +++-
contrib/postgres_fdw/sql/postgres_fdw.sql | 47 ++++
src/backend/commands/copyfrom.c | 216 ++++++++----------
src/backend/executor/execMain.c | 45 ++++
src/backend/executor/execPartition.c | 8 +
src/include/commands/copyfrom_internal.h | 10 -
src/include/executor/executor.h | 1 +
src/include/nodes/execnodes.h | 8 +-
8 files changed, 246 insertions(+), 134 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f320a7578d..146a3be576 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8143,6 +8143,7 @@ drop table loct2;
-- ===================================================================
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -8165,7 +8166,7 @@ copy rem2 from stdin; -- ERROR
ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
DETAIL: Failing row contains (-1, xyzzy).
CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1 xyzzy"
+COPY rem2, line 2
select * from rem2;
f1 | f2
----+-----
@@ -8176,6 +8177,19 @@ select * from rem2;
alter foreign table rem2 drop constraint rem2_f1positive;
alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE: (1)
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -8284,6 +8298,34 @@ drop trigger rem2_trig_row_before on rem2;
drop trigger rem2_trig_row_after on rem2;
drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR: column "f1" of relation "loc2" does not exist
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2), ($3, $4)
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -8300,6 +8342,7 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 17dba77d7e..8371d16c6a 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2226,6 +2226,7 @@ drop table loct2;
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -2258,6 +2259,23 @@ alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -2358,6 +2376,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1 foo
+2 bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -2371,6 +2417,7 @@ commit;
select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 40a54ad0bd..c4f858e2b7 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -317,18 +317,43 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
cstate->line_buf_valid = false;
save_cur_lineno = cstate->cur_lineno;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ int sent = 0;
+
+ Assert(resultRelInfo->ri_BatchSize > 1 &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL);
+
+ /* Flush into foreign table or partition */
+ do {
+ int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
+ resultRelInfo->ri_BatchSize : (nused - sent);
+ int inserted = size;
+
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+ sent += size;
+ } while (sent < nused);
+ }
+ else
+ {
+ /*
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+ }
for (i = 0; i < nused; i++)
{
@@ -340,6 +365,8 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
{
List *recheckIndexes;
+ Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind != RELKIND_FOREIGN_TABLE);
+
cstate->cur_lineno = buffer->linenos[i];
recheckIndexes =
ExecInsertIndexTuples(resultRelInfo,
@@ -359,6 +386,12 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
(resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
resultRelInfo->ri_TrigDesc->trig_insert_new_table))
{
+ /*
+ * AFTER ROW triggers aren't allowed with the foreign bulk insert
+ * method.
+ */
+ Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind != RELKIND_FOREIGN_TABLE);
+
cstate->cur_lineno = buffer->linenos[i];
ExecARInsertTriggers(estate, resultRelInfo,
slots[i], NIL, cstate->transition_capture);
@@ -538,13 +571,11 @@ CopyFrom(CopyFromState cstate)
CommandId mycid = GetCurrentCommandId(true);
int ti_options = 0; /* start with default options for insert */
BulkInsertState bistate = NULL;
- CopyInsertMethod insertMethod;
CopyMultiInsertInfo multiInsertInfo = {0}; /* pacify compiler */
int64 processed = 0;
int64 excluded = 0;
bool has_before_insert_row_trig;
bool has_instead_insert_row_trig;
- bool leafpart_use_multi_insert = false;
Assert(cstate->rel);
Assert(list_length(cstate->range_table) == 1);
@@ -671,10 +702,43 @@ CopyFrom(CopyFromState cstate)
mtstate->resultRelInfo = resultRelInfo;
mtstate->rootResultRelInfo = resultRelInfo;
- if (resultRelInfo->ri_FdwRoutine != NULL &&
- resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
- resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
- resultRelInfo);
+ if (resultRelInfo->ri_FdwRoutine != NULL)
+ {
+ if (resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+
+ if (resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+ resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+ resultRelInfo);
+ }
+
+ Assert(!target_resultRelInfo->ri_usesMultiInsert);
+
+ /*
+ * It's generally more efficient to prepare a bunch of tuples for
+ * insertion, and insert them in bulk, for example, with one
+ * table_multi_insert() call than call table_tuple_insert() separately for
+ * every tuple. However, there are a number of reasons why we might not be
+ * able to do this. For example, if there any volatile expressions in the
+ * table's default values or in the statement's WHERE clause, which may
+ * query the table we are inserting into, buffering tuples might produce
+ * wrong results. Also, the relation we are trying to insert into itself
+ * may not be amenable to buffered inserts.
+ *
+ * Note: For partitions, this flag is set considering the target table's
+ * flag that is being set here and partition's own properties which are
+ * checked by calling ExecMultiInsertAllowed(). It does not matter
+ * whether partitions have any volatile default expressions as we use the
+ * defaults from the target of the COPY command.
+ * Also, the COPY command requires a non-zero input list of attributes.
+ * Therefore, the length of the attribute list is checked here.
+ */
+ if (!cstate->volatile_defexprs &&
+ list_length(cstate->attnumlist) > 0 &&
+ !contain_volatile_functions(cstate->whereClause))
+ target_resultRelInfo->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(target_resultRelInfo);
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -703,83 +767,9 @@ CopyFrom(CopyFromState cstate)
cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
&mtstate->ps);
- /*
- * It's generally more efficient to prepare a bunch of tuples for
- * insertion, and insert them in one table_multi_insert() call, than call
- * table_tuple_insert() separately for every tuple. However, there are a
- * number of reasons why we might not be able to do this. These are
- * explained below.
- */
- if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
- {
- /*
- * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
- * triggers on the table. Such triggers might query the table we're
- * inserting into and act differently if the tuples that have already
- * been processed and prepared for insertion are not there.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
- resultRelInfo->ri_TrigDesc->trig_insert_new_table)
- {
- /*
- * For partitioned tables we can't support multi-inserts when there
- * are any statement level insert triggers. It might be possible to
- * allow partitioned tables with such triggers in the future, but for
- * now, CopyMultiInsertInfoFlush expects that any before row insert
- * and statement level insert triggers are on the same relation.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
- {
- /*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
- *
- * Note: It does not matter if any partitions have any volatile
- * default expressions as we use the defaults from the target of the
- * COPY command.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (contain_volatile_functions(cstate->whereClause))
- {
- /*
- * Can't support multi-inserts if there are any volatile function
- * expressions in WHERE clause. Similarly to the trigger case above,
- * such expressions may query the table we're inserting into.
- */
- insertMethod = CIM_SINGLE;
- }
- else
- {
- /*
- * For partitioned tables, we may still be able to perform bulk
- * inserts. However, the possibility of this depends on which types
- * of triggers exist on the partition. We must disable bulk inserts
- * if the partition is a foreign table or it has any before row insert
- * or insert instead triggers (same as we checked above for the parent
- * table). Since the partition's resultRelInfos are initialized only
- * when we actually need to insert the first tuple into them, we must
- * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
- * flag that we must later determine if we can use bulk-inserts for
- * the partition being inserted into.
- */
- if (proute)
- insertMethod = CIM_MULTI_CONDITIONAL;
- else
- insertMethod = CIM_MULTI;
-
+ if (resultRelInfo->ri_usesMultiInsert)
CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
estate, mycid, ti_options);
- }
/*
* If not using batch mode (which allocates slots as needed) set up a
@@ -787,7 +777,7 @@ CopyFrom(CopyFromState cstate)
* one, even if we might batch insert, to read the tuple in the root
* partition's form.
*/
- if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+ if (!resultRelInfo->ri_usesMultiInsert || proute)
{
singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
&estate->es_tupleTable);
@@ -830,7 +820,7 @@ CopyFrom(CopyFromState cstate)
ResetPerTupleExprContext(estate);
/* select slot to (initially) load row into */
- if (insertMethod == CIM_SINGLE || proute)
+ if (!target_resultRelInfo->ri_usesMultiInsert || proute)
{
myslot = singleslot;
Assert(myslot != NULL);
@@ -838,7 +828,6 @@ CopyFrom(CopyFromState cstate)
else
{
Assert(resultRelInfo == target_resultRelInfo);
- Assert(insertMethod == CIM_MULTI);
myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -905,24 +894,14 @@ CopyFrom(CopyFromState cstate)
has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
- /*
- * Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
- */
- leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
- !has_before_insert_row_trig &&
- !has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
-
/* Set the multi-insert buffer to use for this partition. */
- if (leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
resultRelInfo);
}
- else if (insertMethod == CIM_MULTI_CONDITIONAL &&
- !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
{
/*
* Flush pending inserts if this partition can't use
@@ -952,7 +931,7 @@ CopyFrom(CopyFromState cstate)
* rowtype.
*/
map = resultRelInfo->ri_RootToPartitionMap;
- if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+ if (!resultRelInfo->ri_usesMultiInsert)
{
/* non batch insert */
if (map != NULL)
@@ -971,9 +950,6 @@ CopyFrom(CopyFromState cstate)
*/
TupleTableSlot *batchslot;
- /* no other path available for partitioned table */
- Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -1045,7 +1021,7 @@ CopyFrom(CopyFromState cstate)
ExecPartitionCheck(resultRelInfo, myslot, estate, true);
/* Store the slot in the multi-insert buffer, when enabled. */
- if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
/*
* The slot previously might point into the per-tuple
@@ -1124,11 +1100,8 @@ CopyFrom(CopyFromState cstate)
}
/* Flush any remaining buffered tuples */
- if (insertMethod != CIM_SINGLE)
- {
- if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
- }
+ if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
/* Done, clean up */
error_context_stack = errcallback.previous;
@@ -1149,12 +1122,11 @@ CopyFrom(CopyFromState cstate)
/* Allow the FDW to shut down */
if (target_resultRelInfo->ri_FdwRoutine != NULL &&
target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
- target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
- target_resultRelInfo);
+ target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+ target_resultRelInfo);
/* Tear down the multi-insert buffer data */
- if (insertMethod != CIM_SINGLE)
- CopyMultiInsertInfoCleanup(&multiInsertInfo);
+ CopyMultiInsertInfoCleanup(&multiInsertInfo);
/* Close all the partitioned tables, leaf partitions, and their indices */
if (proute)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..92947eaecd 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1256,9 +1256,54 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_PartitionTupleSlot = NULL; /* ditto */
resultRelInfo->ri_ChildToRootMap = NULL;
resultRelInfo->ri_ChildToRootMapValid = false;
+ resultRelInfo->ri_usesMultiInsert = false;
resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
}
+/*
+ * ExecMultiInsertAllowed
+ * Does this relation allow caller to use multi-insert mode when
+ * inserting rows into it?
+ */
+bool
+ExecMultiInsertAllowed(const ResultRelInfo *rri)
+{
+ /*
+ * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+ * triggers on the table. Such triggers might query the table we're
+ * inserting into and act differently if the tuples that have already
+ * been processed and prepared for insertion are not there.
+ */
+ if (rri->ri_TrigDesc != NULL &&
+ (rri->ri_TrigDesc->trig_insert_before_row ||
+ rri->ri_TrigDesc->trig_insert_instead_row))
+ return false;
+
+ /*
+ * For partitioned tables we can't support multi-inserts when there are
+ * any statement level insert triggers. It might be possible to allow
+ * partitioned tables with such triggers in the future, but for now,
+ * CopyMultiInsertInfoFlush expects that any before row insert and
+ * statement level insert triggers are on the same relation.
+ */
+ if (rri->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ rri->ri_TrigDesc != NULL &&
+ rri->ri_TrigDesc->trig_insert_new_table)
+ return false;
+
+ if (rri->ri_FdwRoutine != NULL &&
+ (rri->ri_FdwRoutine->ExecForeignBatchInsert == NULL ||
+ rri->ri_BatchSize <= 1))
+ /*
+ * Foreign tables don't support multi-inserts, unless their FDW
+ * provides the necessary bulk insert interface.
+ */
+ return false;
+
+ /* OK, caller can use multi-insert on this relation. */
+ return true;
+}
+
/*
* ExecGetTriggerResultRel
* Get a ResultRelInfo for a trigger target relation.
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 606c920b06..e2ea101cd9 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -661,6 +661,14 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
ExecInitRoutingInfo(mtstate, estate, proute, dispatch,
leaf_part_rri, partidx, false);
+ /*
+ * If a partition's root parent isn't allowed to use it, neither is the
+ * partition.
+ */
+ if (rootResultRelInfo->ri_usesMultiInsert)
+ leaf_part_rri->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(leaf_part_rri);
+
/*
* If there is an ON CONFLICT clause, initialize state for it.
*/
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index 4d68d9cceb..598a68a6f1 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -39,16 +39,6 @@ typedef enum EolType
EOL_CRNL
} EolType;
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
- CIM_SINGLE, /* use table_tuple_insert or fdw routine */
- CIM_MULTI, /* always use table_multi_insert */
- CIM_MULTI_CONDITIONAL /* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
/*
* This struct contains all the state variables used throughout a COPY FROM
* operation.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 3dc03c913e..46a79c5ad8 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,6 +203,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
+extern bool ExecMultiInsertAllowed(const ResultRelInfo *rri);
extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 7795a69490..58d5df9874 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -521,7 +521,13 @@ typedef struct ResultRelInfo
TupleConversionMap *ri_ChildToRootMap;
bool ri_ChildToRootMapValid;
- /* for use by copyfrom.c when performing multi-inserts */
+ /*
+ * The following fields are currently only relevant to copyfrom.c.
+ * True if okay to use multi-insert on this relation
+ */
+ bool ri_usesMultiInsert;
+
+ /* Buffer allocated to this relation when using multi-insert mode */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
} ResultRelInfo;
--
2.31.1
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-03-21 23:58 Andres Freund <andres@anarazel.de>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Andres Freund @ 2022-03-21 23:58 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: tsunakawa.takay@fujitsu.com <tsunakawa.takay@fujitsu.com>; Etsuro Fujita <etsuro.fujita@gmail.com>; Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com <houzj.fnst@fujitsu.com>
On 2021-06-07 16:16:58 +0500, Andrey Lepikhov wrote:
> Second version of the patch fixes problems detected by the FDW regression
> tests and shows differences of error reports in tuple-by-tuple and batched
> COPY approaches.
Patch doesn't apply and likely hasn't for a while...
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-03-22 01:54 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
1 sibling, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-03-22 01:54 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; tsunakawa.takay <tsunakawa.takay@fujitsu.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Fri, Jun 4, 2021 at 5:26 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> We still have slow 'COPY FROM' operation for foreign tables in current
> master.
> Now we have a foreign batch insert operation And I tried to rewrite the
> patch [1] with this machinery.
I’d been reviewing the previous version of the patch without noticing
this. (Gmail grouped it in a new thread due to the subject change,
but I overlooked the whole thread.)
I agree with you that the first step for fast copy into foreign
tables/partitions is to use the foreign-batch-insert API. (Actually,
I was also thinking the same while reviewing the previous version.)
Thanks for the new version of the patch!
The patch has been rewritten to something essentially different, but
no one reviewed it. (Tsunakawa-san gave some comments without looking
at it, though.) So the right status of the patch is “Needs review”,
rather than “Ready for Committer”? Anyway, here are a few review
comments from me:
* I don’t think this assumption is correct:
@@ -359,6 +386,12 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
(resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
resultRelInfo->ri_TrigDesc->trig_insert_new_table))
{
+ /*
+ * AFTER ROW triggers aren't allowed with the foreign bulk insert
+ * method.
+ */
+ Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind !=
RELKIND_FOREIGN_TABLE);
+
In postgres_fdw we disable foreign batch insert when the target table
has AFTER ROW triggers, but the core allows it even in that case. No?
* To allow foreign multi insert, the patch made an invasive change to
the existing logic to determine whether to use multi insert for the
target relation, adding a new member ri_usesMultiInsert to the
ResultRelInfo struct, as well as introducing a new function
ExecMultiInsertAllowed(). But I’m not sure we really need such a
change. Isn’t it reasonable to *adjust* the existing logic to allow
foreign multi insert when possible?
I didn’t finish my review, but I’ll mark this as “Waiting on Author”.
My apologies for the long long delay.
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-03-22 02:36 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andres Freund <andres@anarazel.de>
0 siblings, 0 replies; 39+ messages in thread
From: Etsuro Fujita @ 2022-03-22 02:36 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; tsunakawa.takay@fujitsu.com <tsunakawa.takay@fujitsu.com>; Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com <houzj.fnst@fujitsu.com>
On Tue, Mar 22, 2022 at 8:58 AM Andres Freund <andres@anarazel.de> wrote:
>
> On 2021-06-07 16:16:58 +0500, Andrey Lepikhov wrote:
> > Second version of the patch fixes problems detected by the FDW regression
> > tests and shows differences of error reports in tuple-by-tuple and batched
> > COPY approaches.
>
> Patch doesn't apply and likely hasn't for a while...
Actually, it has bit-rotted due to the recent fix for cross-partition
updates (i.e., commit ba9a7e392).
Thanks!
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-03-24 06:43 Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 2 replies; 39+ messages in thread
From: Andrey V. Lepikhov @ 2022-03-24 06:43 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 3/22/22 06:54, Etsuro Fujita wrote:
> On Fri, Jun 4, 2021 at 5:26 PM Andrey Lepikhov
> <a.lepikhov@postgrespro.ru> wrote:
>> We still have slow 'COPY FROM' operation for foreign tables in current
>> master.
>> Now we have a foreign batch insert operation And I tried to rewrite the
>> patch [1] with this machinery.
>
> The patch has been rewritten to something essentially different, but
> no one reviewed it. (Tsunakawa-san gave some comments without looking
> at it, though.) So the right status of the patch is “Needs review”,
> rather than “Ready for Committer”? Anyway, here are a few review
> comments from me:
>
> * I don’t think this assumption is correct:
>
> @@ -359,6 +386,12 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
> (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
> resultRelInfo->ri_TrigDesc->trig_insert_new_table))
> {
> + /*
> + * AFTER ROW triggers aren't allowed with the foreign bulk insert
> + * method.
> + */
> + Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind !=
> RELKIND_FOREIGN_TABLE);
> +
>
> In postgres_fdw we disable foreign batch insert when the target table
> has AFTER ROW triggers, but the core allows it even in that case. No?
Agree
> * To allow foreign multi insert, the patch made an invasive change to
> the existing logic to determine whether to use multi insert for the
> target relation, adding a new member ri_usesMultiInsert to the
> ResultRelInfo struct, as well as introducing a new function
> ExecMultiInsertAllowed(). But I’m not sure we really need such a
> change. Isn’t it reasonable to *adjust* the existing logic to allow
> foreign multi insert when possible?
Of course, such approach would look much better, if we implemented it.
I'll ponder how to do it.
> I didn’t finish my review, but I’ll mark this as “Waiting on Author”.
I rebased the patch onto current master. Now it works correctly. I'll
mark it as "Waiting for review".
--
regards,
Andrey Lepikhov
Postgres Professional
Attachments:
v3-0001-Implementation-of-a-Bulk-COPY-FROM.patch (text/x-patch, 23.1K) download
From 2d51d0f5d94a3e4b3400714b5841228d1896fb56 Mon Sep 17 00:00:00 2001
From: "Andrey V. Lepikhov" <a.lepikhov@postgrespro.ru>
Date: Fri, 4 Jun 2021 13:21:43 +0500
Subject: [PATCH] Implementation of a Bulk COPY FROM operation into foreign
table.
---
.../postgres_fdw/expected/postgres_fdw.out | 45 +++-
contrib/postgres_fdw/sql/postgres_fdw.sql | 47 ++++
src/backend/commands/copyfrom.c | 210 ++++++++----------
src/backend/executor/execMain.c | 45 ++++
src/backend/executor/execPartition.c | 8 +
src/include/commands/copyfrom_internal.h | 10 -
src/include/executor/executor.h | 1 +
src/include/nodes/execnodes.h | 5 +-
8 files changed, 237 insertions(+), 134 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f210f91188..a803029f2f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8415,6 +8415,7 @@ drop table loct2;
-- ===================================================================
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -8437,7 +8438,7 @@ copy rem2 from stdin; -- ERROR
ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
DETAIL: Failing row contains (-1, xyzzy).
CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1 xyzzy"
+COPY rem2, line 2
select * from rem2;
f1 | f2
----+-----
@@ -8448,6 +8449,19 @@ select * from rem2;
alter foreign table rem2 drop constraint rem2_f1positive;
alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE: (1)
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -8556,6 +8570,34 @@ drop trigger rem2_trig_row_before on rem2;
drop trigger rem2_trig_row_after on rem2;
drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR: column "f1" of relation "loc2" does not exist
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2), ($3, $4)
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -8572,6 +8614,7 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 95b6b7192e..847b869629 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2322,6 +2322,7 @@ drop table loct2;
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -2354,6 +2355,23 @@ alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -2454,6 +2472,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1 foo
+2 bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -2467,6 +2513,7 @@ commit;
select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index db6eb6fae7..ba5ccf8908 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -320,18 +320,43 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
cstate->line_buf_valid = false;
save_cur_lineno = cstate->cur_lineno;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ int sent = 0;
+
+ Assert(resultRelInfo->ri_BatchSize > 1 &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL);
+
+ /* Flush into foreign table or partition */
+ do {
+ int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
+ resultRelInfo->ri_BatchSize : (nused - sent);
+ int inserted = size;
+
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+ sent += size;
+ } while (sent < nused);
+ }
+ else
+ {
+ /*
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+ }
for (i = 0; i < nused; i++)
{
@@ -343,6 +368,8 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
{
List *recheckIndexes;
+ Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind != RELKIND_FOREIGN_TABLE);
+
cstate->cur_lineno = buffer->linenos[i];
recheckIndexes =
ExecInsertIndexTuples(resultRelInfo,
@@ -541,13 +568,11 @@ CopyFrom(CopyFromState cstate)
CommandId mycid = GetCurrentCommandId(true);
int ti_options = 0; /* start with default options for insert */
BulkInsertState bistate = NULL;
- CopyInsertMethod insertMethod;
CopyMultiInsertInfo multiInsertInfo = {0}; /* pacify compiler */
int64 processed = 0;
int64 excluded = 0;
bool has_before_insert_row_trig;
bool has_instead_insert_row_trig;
- bool leafpart_use_multi_insert = false;
Assert(cstate->rel);
Assert(list_length(cstate->range_table) == 1);
@@ -674,10 +699,43 @@ CopyFrom(CopyFromState cstate)
mtstate->resultRelInfo = resultRelInfo;
mtstate->rootResultRelInfo = resultRelInfo;
- if (resultRelInfo->ri_FdwRoutine != NULL &&
- resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
- resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
- resultRelInfo);
+ if (resultRelInfo->ri_FdwRoutine != NULL)
+ {
+ if (resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+
+ if (resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+ resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+ resultRelInfo);
+ }
+
+ Assert(!target_resultRelInfo->ri_usesMultiInsert);
+
+ /*
+ * It's generally more efficient to prepare a bunch of tuples for
+ * insertion, and insert them in bulk, for example, with one
+ * table_multi_insert() call than call table_tuple_insert() separately for
+ * every tuple. However, there are a number of reasons why we might not be
+ * able to do this. For example, if there any volatile expressions in the
+ * table's default values or in the statement's WHERE clause, which may
+ * query the table we are inserting into, buffering tuples might produce
+ * wrong results. Also, the relation we are trying to insert into itself
+ * may not be amenable to buffered inserts.
+ *
+ * Note: For partitions, this flag is set considering the target table's
+ * flag that is being set here and partition's own properties which are
+ * checked by calling ExecMultiInsertAllowed(). It does not matter
+ * whether partitions have any volatile default expressions as we use the
+ * defaults from the target of the COPY command.
+ * Also, the COPY command requires a non-zero input list of attributes.
+ * Therefore, the length of the attribute list is checked here.
+ */
+ if (!cstate->volatile_defexprs &&
+ list_length(cstate->attnumlist) > 0 &&
+ !contain_volatile_functions(cstate->whereClause))
+ target_resultRelInfo->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(target_resultRelInfo);
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -706,83 +764,9 @@ CopyFrom(CopyFromState cstate)
cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
&mtstate->ps);
- /*
- * It's generally more efficient to prepare a bunch of tuples for
- * insertion, and insert them in one table_multi_insert() call, than call
- * table_tuple_insert() separately for every tuple. However, there are a
- * number of reasons why we might not be able to do this. These are
- * explained below.
- */
- if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
- {
- /*
- * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
- * triggers on the table. Such triggers might query the table we're
- * inserting into and act differently if the tuples that have already
- * been processed and prepared for insertion are not there.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
- resultRelInfo->ri_TrigDesc->trig_insert_new_table)
- {
- /*
- * For partitioned tables we can't support multi-inserts when there
- * are any statement level insert triggers. It might be possible to
- * allow partitioned tables with such triggers in the future, but for
- * now, CopyMultiInsertInfoFlush expects that any before row insert
- * and statement level insert triggers are on the same relation.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
- {
- /*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
- *
- * Note: It does not matter if any partitions have any volatile
- * default expressions as we use the defaults from the target of the
- * COPY command.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (contain_volatile_functions(cstate->whereClause))
- {
- /*
- * Can't support multi-inserts if there are any volatile function
- * expressions in WHERE clause. Similarly to the trigger case above,
- * such expressions may query the table we're inserting into.
- */
- insertMethod = CIM_SINGLE;
- }
- else
- {
- /*
- * For partitioned tables, we may still be able to perform bulk
- * inserts. However, the possibility of this depends on which types
- * of triggers exist on the partition. We must disable bulk inserts
- * if the partition is a foreign table or it has any before row insert
- * or insert instead triggers (same as we checked above for the parent
- * table). Since the partition's resultRelInfos are initialized only
- * when we actually need to insert the first tuple into them, we must
- * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
- * flag that we must later determine if we can use bulk-inserts for
- * the partition being inserted into.
- */
- if (proute)
- insertMethod = CIM_MULTI_CONDITIONAL;
- else
- insertMethod = CIM_MULTI;
-
+ if (resultRelInfo->ri_usesMultiInsert)
CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
estate, mycid, ti_options);
- }
/*
* If not using batch mode (which allocates slots as needed) set up a
@@ -790,7 +774,7 @@ CopyFrom(CopyFromState cstate)
* one, even if we might batch insert, to read the tuple in the root
* partition's form.
*/
- if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+ if (!resultRelInfo->ri_usesMultiInsert || proute)
{
singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
&estate->es_tupleTable);
@@ -833,7 +817,7 @@ CopyFrom(CopyFromState cstate)
ResetPerTupleExprContext(estate);
/* select slot to (initially) load row into */
- if (insertMethod == CIM_SINGLE || proute)
+ if (!target_resultRelInfo->ri_usesMultiInsert || proute)
{
myslot = singleslot;
Assert(myslot != NULL);
@@ -841,7 +825,6 @@ CopyFrom(CopyFromState cstate)
else
{
Assert(resultRelInfo == target_resultRelInfo);
- Assert(insertMethod == CIM_MULTI);
myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -908,24 +891,14 @@ CopyFrom(CopyFromState cstate)
has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
- /*
- * Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
- */
- leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
- !has_before_insert_row_trig &&
- !has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
-
/* Set the multi-insert buffer to use for this partition. */
- if (leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
resultRelInfo);
}
- else if (insertMethod == CIM_MULTI_CONDITIONAL &&
- !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
{
/*
* Flush pending inserts if this partition can't use
@@ -955,7 +928,7 @@ CopyFrom(CopyFromState cstate)
* rowtype.
*/
map = resultRelInfo->ri_RootToPartitionMap;
- if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+ if (!resultRelInfo->ri_usesMultiInsert)
{
/* non batch insert */
if (map != NULL)
@@ -974,9 +947,6 @@ CopyFrom(CopyFromState cstate)
*/
TupleTableSlot *batchslot;
- /* no other path available for partitioned table */
- Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -1048,7 +1018,7 @@ CopyFrom(CopyFromState cstate)
ExecPartitionCheck(resultRelInfo, myslot, estate, true);
/* Store the slot in the multi-insert buffer, when enabled. */
- if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
/*
* The slot previously might point into the per-tuple
@@ -1127,11 +1097,8 @@ CopyFrom(CopyFromState cstate)
}
/* Flush any remaining buffered tuples */
- if (insertMethod != CIM_SINGLE)
- {
- if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
- }
+ if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
/* Done, clean up */
error_context_stack = errcallback.previous;
@@ -1152,12 +1119,11 @@ CopyFrom(CopyFromState cstate)
/* Allow the FDW to shut down */
if (target_resultRelInfo->ri_FdwRoutine != NULL &&
target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
- target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
- target_resultRelInfo);
+ target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+ target_resultRelInfo);
/* Tear down the multi-insert buffer data */
- if (insertMethod != CIM_SINGLE)
- CopyMultiInsertInfoCleanup(&multiInsertInfo);
+ CopyMultiInsertInfoCleanup(&multiInsertInfo);
/* Close all the partitioned tables, leaf partitions, and their indices */
if (proute)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 473d2e00a2..8727c5ca89 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1257,9 +1257,54 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_PartitionTupleSlot = NULL; /* ditto */
resultRelInfo->ri_ChildToRootMap = NULL;
resultRelInfo->ri_ChildToRootMapValid = false;
+ resultRelInfo->ri_usesMultiInsert = false;
resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
}
+/*
+ * ExecMultiInsertAllowed
+ * Does this relation allow caller to use multi-insert mode when
+ * inserting rows into it?
+ */
+bool
+ExecMultiInsertAllowed(const ResultRelInfo *rri)
+{
+ /*
+ * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+ * triggers on the table. Such triggers might query the table we're
+ * inserting into and act differently if the tuples that have already
+ * been processed and prepared for insertion are not there.
+ */
+ if (rri->ri_TrigDesc != NULL &&
+ (rri->ri_TrigDesc->trig_insert_before_row ||
+ rri->ri_TrigDesc->trig_insert_instead_row))
+ return false;
+
+ /*
+ * For partitioned tables we can't support multi-inserts when there are
+ * any statement level insert triggers. It might be possible to allow
+ * partitioned tables with such triggers in the future, but for now,
+ * CopyMultiInsertInfoFlush expects that any before row insert and
+ * statement level insert triggers are on the same relation.
+ */
+ if (rri->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ rri->ri_TrigDesc != NULL &&
+ rri->ri_TrigDesc->trig_insert_new_table)
+ return false;
+
+ if (rri->ri_FdwRoutine != NULL &&
+ (rri->ri_FdwRoutine->ExecForeignBatchInsert == NULL ||
+ rri->ri_BatchSize <= 1))
+ /*
+ * Foreign tables don't support multi-inserts, unless their FDW
+ * provides the necessary bulk insert interface.
+ */
+ return false;
+
+ /* OK, caller can use multi-insert on this relation. */
+ return true;
+}
+
/*
* ExecGetTriggerResultRel
* Get a ResultRelInfo for a trigger target relation.
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 90ed1485d1..942bd506f8 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -661,6 +661,14 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
ExecInitRoutingInfo(mtstate, estate, proute, dispatch,
leaf_part_rri, partidx, false);
+ /*
+ * If a partition's root parent isn't allowed to use it, neither is the
+ * partition.
+ */
+ if (rootResultRelInfo->ri_usesMultiInsert)
+ leaf_part_rri->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(leaf_part_rri);
+
/*
* If there is an ON CONFLICT clause, initialize state for it.
*/
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index 3df1c5a97c..1c733bb9a4 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -39,16 +39,6 @@ typedef enum EolType
EOL_CRNL
} EolType;
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
- CIM_SINGLE, /* use table_tuple_insert or fdw routine */
- CIM_MULTI, /* always use table_multi_insert */
- CIM_MULTI_CONDITIONAL /* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
/*
* This struct contains all the state variables used throughout a COPY FROM
* operation.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 82925b4b63..67d338920f 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,6 +203,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
+extern bool ExecMultiInsertAllowed(const ResultRelInfo *rri);
extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
ResultRelInfo *rootRelInfo);
extern List *ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 44dd73fc80..5617810279 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -528,7 +528,10 @@ typedef struct ResultRelInfo
TupleConversionMap *ri_ChildToRootMap;
bool ri_ChildToRootMapValid;
- /* for use by copyfrom.c when performing multi-inserts */
+ /* True if okay to use multi-insert on this relation */
+ bool ri_usesMultiInsert;
+
+ /* Buffer allocated to this relation when using multi-insert mode */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
/*
--
2.25.1
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-07 03:14 Ian Barwick <ian.barwick@enterprisedb.com>
parent: Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>
1 sibling, 1 reply; 39+ messages in thread
From: Ian Barwick @ 2022-07-07 03:14 UTC (permalink / raw)
To: Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>; Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
2022年3月24日(木) 15:44 Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>:
>
> On 3/22/22 06:54, Etsuro Fujita wrote:
> > On Fri, Jun 4, 2021 at 5:26 PM Andrey Lepikhov
> > <a.lepikhov@postgrespro.ru> wrote:
> >> We still have slow 'COPY FROM' operation for foreign tables in current
> >> master.
> >> Now we have a foreign batch insert operation And I tried to rewrite the
> >> patch [1] with this machinery.
> >
> > The patch has been rewritten to something essentially different, but
> > no one reviewed it. (Tsunakawa-san gave some comments without looking
> > at it, though.) So the right status of the patch is “Needs review”,
> > rather than “Ready for Committer”? Anyway, here are a few review
> > comments from me:
> >
> > * I don’t think this assumption is correct:
> >
> > @@ -359,6 +386,12 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
> > (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
> > resultRelInfo->ri_TrigDesc->trig_insert_new_table))
> > {
> > + /*
> > + * AFTER ROW triggers aren't allowed with the foreign bulk insert
> > + * method.
> > + */
> > + Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind !=
> > RELKIND_FOREIGN_TABLE);
> > +
> >
> > In postgres_fdw we disable foreign batch insert when the target table
> > has AFTER ROW triggers, but the core allows it even in that case. No?
> Agree
>
> > * To allow foreign multi insert, the patch made an invasive change to
> > the existing logic to determine whether to use multi insert for the
> > target relation, adding a new member ri_usesMultiInsert to the
> > ResultRelInfo struct, as well as introducing a new function
> > ExecMultiInsertAllowed(). But I’m not sure we really need such a
> > change. Isn’t it reasonable to *adjust* the existing logic to allow
> > foreign multi insert when possible?
> Of course, such approach would look much better, if we implemented it.
> I'll ponder how to do it.
>
> > I didn’t finish my review, but I’ll mark this as “Waiting on Author”.
> I rebased the patch onto current master. Now it works correctly. I'll
> mark it as "Waiting for review".
I took a look at this patch as it would a useful optimization to have.
It applies cleanly to current HEAD, but as-is, with a large data set, it
reproducibly fails like this (using postgres_fdw):
postgres=# COPY foo FROM '/tmp/fast-copy-from/test.csv' WITH (format csv);
ERROR: bind message supplies 0 parameters, but prepared statement "pgsql_fdw_prep_19422" requires 6
CONTEXT: remote SQL command: INSERT INTO public.foo_part_1(t, v1, v2, v3, v4, v5) VALUES ($1, $2, $3, $4, $5, $6)
COPY foo, line 17281589
This occurs because not all multi-insert buffers being flushed actually contain
tuples; the fix is simply not to call ExecForeignBatchInsert() if that's the case,
e.g:
/* Flush into foreign table or partition */
do {
int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
resultRelInfo->ri_BatchSize : (nused - sent);
if (size)
{
int inserted = size;
resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
resultRelInfo,
&slots[sent],
NULL,
&inserted);
sent += size;
}
} while (sent < nused);
There might a case for arguing that the respective FDW should check that it has
actually received some tuples to insert, but IMHO it's much preferable to catch
this as early as possible and avoid a superfluous call.
FWIW, with the above fix in place, with a simple local test the patch produces a
consistent speed-up of about 8 times compared to the existing functionality.
Regards
Ian Barwick
--
EnterpriseDB - https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-07 13:51 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Ian Barwick <ian.barwick@enterprisedb.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-07-07 13:51 UTC (permalink / raw)
To: Ian Barwick <ian.barwick@enterprisedb.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; Etsuro Fujita <etsuro.fujita@gmail.com>; houzj.fnst@fujitsu.com
On 7/7/2022 06:14, Ian Barwick wrote:
> 2022年3月24日(木) 15:44 Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>:
> >
> > On 3/22/22 06:54, Etsuro Fujita wrote:
> > > On Fri, Jun 4, 2021 at 5:26 PM Andrey Lepikhov
> > > <a.lepikhov@postgrespro.ru> wrote:
> > >> We still have slow 'COPY FROM' operation for foreign tables in
> current
> > >> master.
> > >> Now we have a foreign batch insert operation And I tried to
> rewrite the
> > >> patch [1] with this machinery.
> > >
> > > The patch has been rewritten to something essentially different, but
> > > no one reviewed it. (Tsunakawa-san gave some comments without looking
> > > at it, though.) So the right status of the patch is “Needs review”,
> > > rather than “Ready for Committer”? Anyway, here are a few review
> > > comments from me:
> > >
> > > * I don’t think this assumption is correct:
> > >
> > > @@ -359,6 +386,12 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo
> *miinfo,
> > >
> (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
> > > resultRelInfo->ri_TrigDesc->trig_insert_new_table))
> > > {
> > > + /*
> > > + * AFTER ROW triggers aren't allowed with the foreign
> bulk insert
> > > + * method.
> > > + */
> > > + Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind !=
> > > RELKIND_FOREIGN_TABLE);
> > > +
> > >
> > > In postgres_fdw we disable foreign batch insert when the target table
> > > has AFTER ROW triggers, but the core allows it even in that case. No?
> > Agree
> >
> > > * To allow foreign multi insert, the patch made an invasive change to
> > > the existing logic to determine whether to use multi insert for the
> > > target relation, adding a new member ri_usesMultiInsert to the
> > > ResultRelInfo struct, as well as introducing a new function
> > > ExecMultiInsertAllowed(). But I’m not sure we really need such a
> > > change. Isn’t it reasonable to *adjust* the existing logic to allow
> > > foreign multi insert when possible?
> > Of course, such approach would look much better, if we implemented it.
> > I'll ponder how to do it.
> >
> > > I didn’t finish my review, but I’ll mark this as “Waiting on Author”.
> > I rebased the patch onto current master. Now it works correctly. I'll
> > mark it as "Waiting for review".
>
> I took a look at this patch as it would a useful optimization to have.
>
> It applies cleanly to current HEAD, but as-is, with a large data set, it
> reproducibly fails like this (using postgres_fdw):
>
> postgres=# COPY foo FROM '/tmp/fast-copy-from/test.csv' WITH
> (format csv);
> ERROR: bind message supplies 0 parameters, but prepared statement
> "pgsql_fdw_prep_19422" requires 6
> CONTEXT: remote SQL command: INSERT INTO public.foo_part_1(t, v1,
> v2, v3, v4, v5) VALUES ($1, $2, $3, $4, $5, $6)
> COPY foo, line 17281589
>
> This occurs because not all multi-insert buffers being flushed actually
> contain
> tuples; the fix is simply not to call ExecForeignBatchInsert() if that's
> the case,
> e.g:
>
>
> /* Flush into foreign table or partition */
> do {
> int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
> resultRelInfo->ri_BatchSize : (nused - sent);
>
> if (size)
> {
> int inserted = size;
>
>
> resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
>
> resultRelInfo,
>
> &slots[sent],
> NULL,
>
> &inserted);
> sent += size;
> }
> } while (sent < nused);
>
>
> There might a case for arguing that the respective FDW should check that
> it has
> actually received some tuples to insert, but IMHO it's much preferable
> to catch
> this as early as possible and avoid a superfluous call.
>
> FWIW, with the above fix in place, with a simple local test the patch
> produces a
> consistent speed-up of about 8 times compared to the existing
> functionality.
Thank you for the attention to the patch.
I have a couple of questions:
1. It's a problem for me to reproduce the case you reported. Can you
give more details on the reproduction?
2. Have you tried to use previous version, based on bulk COPY machinery,
not bulk INSERT? Which approach looks better and have better performance
in your opinion?
--
regards,
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-08 02:12 Ian Barwick <ian.barwick@enterprisedb.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Ian Barwick @ 2022-07-08 02:12 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; Etsuro Fujita <etsuro.fujita@gmail.com>; houzj.fnst@fujitsu.com
On 07/07/2022 22:51, Andrey Lepikhov wrote:
> On 7/7/2022 06:14, Ian Barwick wrote:
>> 2022年3月24日(木) 15:44 Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>:
>> >
>> > On 3/22/22 06:54, Etsuro Fujita wrote:
>> > > On Fri, Jun 4, 2021 at 5:26 PM Andrey Lepikhov
>> > > <a.lepikhov@postgrespro.ru> wrote:
>> > >> We still have slow 'COPY FROM' operation for foreign tables in current
>> > >> master.
>> > >> Now we have a foreign batch insert operation And I tried to rewrite the
>> > >> patch [1] with this machinery.
>> > >
>> > > The patch has been rewritten to something essentially different, but
>> > > no one reviewed it. (Tsunakawa-san gave some comments without looking
>> > > at it, though.) So the right status of the patch is “Needs review”,
>> > > rather than “Ready for Committer”? Anyway, here are a few review
>> > > comments from me:
>> > >
>> > > * I don’t think this assumption is correct:
>> > >
>> > > @@ -359,6 +386,12 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
>> > > (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
>> > > resultRelInfo->ri_TrigDesc->trig_insert_new_table))
>> > > {
>> > > + /*
>> > > + * AFTER ROW triggers aren't allowed with the foreign bulk insert
>> > > + * method.
>> > > + */
>> > > + Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind !=
>> > > RELKIND_FOREIGN_TABLE);
>> > > +
>> > >
>> > > In postgres_fdw we disable foreign batch insert when the target table
>> > > has AFTER ROW triggers, but the core allows it even in that case. No?
>> > Agree
>> >
>> > > * To allow foreign multi insert, the patch made an invasive change to
>> > > the existing logic to determine whether to use multi insert for the
>> > > target relation, adding a new member ri_usesMultiInsert to the
>> > > ResultRelInfo struct, as well as introducing a new function
>> > > ExecMultiInsertAllowed(). But I’m not sure we really need such a
>> > > change. Isn’t it reasonable to *adjust* the existing logic to allow
>> > > foreign multi insert when possible?
>> > Of course, such approach would look much better, if we implemented it.
>> > I'll ponder how to do it.
>> >
>> > > I didn’t finish my review, but I’ll mark this as “Waiting on Author”.
>> > I rebased the patch onto current master. Now it works correctly. I'll
>> > mark it as "Waiting for review".
>>
>> I took a look at this patch as it would a useful optimization to have.
>>
>> It applies cleanly to current HEAD, but as-is, with a large data set, it
>> reproducibly fails like this (using postgres_fdw):
>>
>> postgres=# COPY foo FROM '/tmp/fast-copy-from/test.csv' WITH (format csv);
>> ERROR: bind message supplies 0 parameters, but prepared statement "pgsql_fdw_prep_19422" requires 6
>> CONTEXT: remote SQL command: INSERT INTO public.foo_part_1(t, v1, v2, v3, v4, v5) VALUES ($1, $2, $3, $4, $5, $6)
>> COPY foo, line 17281589
>>
>> This occurs because not all multi-insert buffers being flushed actually contain
>> tuples; the fix is simply not to call ExecForeignBatchInsert() if that's the case,
>> e.g:
>>
>>
>> /* Flush into foreign table or partition */
>> do {
>> int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
>> resultRelInfo->ri_BatchSize : (nused - sent);
>>
>> if (size)
>> {
>> int inserted = size;
>>
>> resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
>> resultRelInfo,
>> &slots[sent],
>> NULL,
>> &inserted);
>> sent += size;
>> }
>> } while (sent < nused);
>>
>>
>> There might a case for arguing that the respective FDW should check that it has
>> actually received some tuples to insert, but IMHO it's much preferable to catch
>> this as early as possible and avoid a superfluous call.
>>
>> FWIW, with the above fix in place, with a simple local test the patch produces a
>> consistent speed-up of about 8 times compared to the existing functionality.
>
> Thank you for the attention to the patch.
> I have a couple of questions:
>
> 1. It's a problem for me to reproduce the case you reported. Can you give more
> details on the reproduction?
The issue seems to occur when the data spans more than one foreign partition,
probably because the accumulated data for one partition needs to be flushed
before moving on to the next partition, but not all pre-allocated multi-insert
buffers have been filled.
The reproduction method I have, which is pared down from the original bulk insert
which triggered the error, is as follows:
1. Create some data using the attached script:
perl data.pl > /tmp/data.csv
2. Create two nodes (A and B)
3. On node B, create tables as follows:
CREATE TABLE foo_part_1 (t timestamptz, v1 int, v2 int, v3 int, v4 text, v5 text);
CREATE TABLE foo_part_2 (t timestamptz, v1 int, v2 int, v3 int, v4 text, v5 text);
4. On node A, create FDW and partitioned table as follows:
-- adjust parameters as appropriate
CREATE EXTENSION postgres_fdw;
CREATE SERVER pg_fdw
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (
host 'localhost',
port '6301',
dbname 'postgres',
batch_size '100'
);
CREATE USER MAPPING FOR CURRENT_USER SERVER pg_fdw
OPTIONS(user 'postgres');
-- create parition table and partitions
CREATE TABLE foo (t timestamptz, v1 int, v2 int, v3 int, v4 text, v5 text) PARTITION BY RANGE(t);
CREATE FOREIGN TABLE foo_part_1
PARTITION OF foo
FOR VALUES FROM ('2022-05-19 00:00:00') TO ('2022-05-20 00:00:00')
SERVER pg_fdw;
CREATE FOREIGN TABLE foo_part_2
PARTITION OF foo
FOR VALUES FROM ('2022-05-20 00:00:00') TO ('2022-05-21 00:00:00')
SERVER pg_fdw;
5. On node A, load the previously generated data with COPY:
COPY foo FROM '/tmp/data.csv' with (format 'csv');
This will fail like this:
ERROR: bind message supplies 0 parameters, but prepared statement "pgsql_fdw_prep_178" requires 6
CONTEXT: remote SQL command: INSERT INTO public.foo_part_1(t, v1, v2, v3, v4, v5) VALUES ($1, $2, $3, $4, $5, $6)
COPY foo, line 88160
> 2. Have you tried to use previous version, based on bulk COPY machinery, not
> bulk INSERT? > Which approach looks better and have better performance in
> your opinion?
Aha, I didn't see that, I'll take a look.
Regards
Ian Barwick
--
EnterpriseDB - https://www.enterprisedb.com
Attachments:
data.pl (application/x-perl, 1014B) download
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-08 15:09 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Ian Barwick <ian.barwick@enterprisedb.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-07-08 15:09 UTC (permalink / raw)
To: Ian Barwick <ian.barwick@enterprisedb.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; Etsuro Fujita <etsuro.fujita@gmail.com>; houzj.fnst@fujitsu.com
On 8/7/2022 05:12, Ian Barwick wrote:
> ERROR: bind message supplies 0 parameters, but prepared statement
> "pgsql_fdw_prep_178" requires 6
> CONTEXT: remote SQL command: INSERT INTO public.foo_part_1(t, v1,
> v2, v3, v4, v5) VALUES ($1, $2, $3, $4, $5, $6)
> COPY foo, line 88160
Thanks, I got it. MultiInsertBuffer are created on the first non-zero
flush of tuples into the partition and isn't deleted from the buffers
list until the end of COPY. And on a subsequent flush in the case of
empty buffer we catch the error.
Your fix is correct, but I want to propose slightly different change
(see in attachment).
--
regards,
Andrey Lepikhov
Postgres Professional
Attachments:
flush_nonzero_buffer.txt (text/plain, 1.2K) download
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 245a260982..203289f7f2 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -329,7 +329,8 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL);
/* Flush into foreign table or partition */
- do {
+ while(sent < nused)
+ {
int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
resultRelInfo->ri_BatchSize : (nused - sent);
int inserted = size;
@@ -340,7 +341,7 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
NULL,
&inserted);
sent += size;
- } while (sent < nused);
+ }
}
else
{
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-11 01:12 Ian Barwick <ian.barwick@enterprisedb.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Ian Barwick @ 2022-07-11 01:12 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; Etsuro Fujita <etsuro.fujita@gmail.com>; houzj.fnst@fujitsu.com
On 09/07/2022 00:09, Andrey Lepikhov wrote:
> On 8/7/2022 05:12, Ian Barwick wrote:
>> ERROR: bind message supplies 0 parameters, but prepared statement "pgsql_fdw_prep_178" requires 6
>> CONTEXT: remote SQL command: INSERT INTO public.foo_part_1(t, v1, v2, v3, v4, v5) VALUES ($1, $2, $3, $4, $5, $6)
>> COPY foo, line 88160
> Thanks, I got it. MultiInsertBuffer are created on the first non-zero flush of tuples into the partition and isn't deleted from the buffers list until the end of COPY. And on a subsequent flush in the case of empty buffer we catch the error.
> Your fix is correct, but I want to propose slightly different change (see in attachment).
LGTM.
Regards
Ian Barwick
--
https://www.enterprisedb.com/
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-11 03:54 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Ian Barwick <ian.barwick@enterprisedb.com>
0 siblings, 0 replies; 39+ messages in thread
From: Andrey Lepikhov @ 2022-07-11 03:54 UTC (permalink / raw)
To: Ian Barwick <ian.barwick@enterprisedb.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; Etsuro Fujita <etsuro.fujita@gmail.com>; houzj.fnst@fujitsu.com
On 11/7/2022 04:12, Ian Barwick wrote:
> On 09/07/2022 00:09, Andrey Lepikhov wrote:
>> On 8/7/2022 05:12, Ian Barwick wrote:
>>> ERROR: bind message supplies 0 parameters, but prepared
>>> statement "pgsql_fdw_prep_178" requires 6
>>> CONTEXT: remote SQL command: INSERT INTO public.foo_part_1(t,
>>> v1, v2, v3, v4, v5) VALUES ($1, $2, $3, $4, $5, $6)
>>> COPY foo, line 88160
>> Thanks, I got it. MultiInsertBuffer are created on the first non-zero
>> flush of tuples into the partition and isn't deleted from the buffers
>> list until the end of COPY. And on a subsequent flush in the case of
>> empty buffer we catch the error.
>> Your fix is correct, but I want to propose slightly different change
>> (see in attachment).
>
> LGTM.
New version (with aforementioned changes) is attached.
--
regards,
Andrey Lepikhov
Postgres Professional
Attachments:
v4-0001-Implementation-of-a-Bulk-COPY-FROM.patch (text/plain, 23.1K) download
From 976560f2ad406adba1aaf58a188b44302855ee12 Mon Sep 17 00:00:00 2001
From: "Andrey V. Lepikhov" <a.lepikhov@postgrespro.ru>
Date: Fri, 4 Jun 2021 13:21:43 +0500
Subject: [PATCH] Implementation of a Bulk COPY FROM operation into foreign
table.
---
.../postgres_fdw/expected/postgres_fdw.out | 45 +++-
contrib/postgres_fdw/sql/postgres_fdw.sql | 47 ++++
src/backend/commands/copyfrom.c | 211 ++++++++----------
src/backend/executor/execMain.c | 45 ++++
src/backend/executor/execPartition.c | 8 +
src/include/commands/copyfrom_internal.h | 10 -
src/include/executor/executor.h | 1 +
src/include/nodes/execnodes.h | 5 +-
8 files changed, 238 insertions(+), 134 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 44457f930c..aced9a6428 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8435,6 +8435,7 @@ drop table loct2;
-- ===================================================================
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -8457,7 +8458,7 @@ copy rem2 from stdin; -- ERROR
ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
DETAIL: Failing row contains (-1, xyzzy).
CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1 xyzzy"
+COPY rem2, line 2
select * from rem2;
f1 | f2
----+-----
@@ -8468,6 +8469,19 @@ select * from rem2;
alter foreign table rem2 drop constraint rem2_f1positive;
alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE: (1)
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -8576,6 +8590,34 @@ drop trigger rem2_trig_row_before on rem2;
drop trigger rem2_trig_row_after on rem2;
drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR: column "f1" of relation "loc2" does not exist
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2), ($3, $4)
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -8592,6 +8634,7 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 92d1212027..5c047ce8ee 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2330,6 +2330,7 @@ drop table loct2;
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -2362,6 +2363,23 @@ alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -2462,6 +2480,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1 foo
+2 bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -2475,6 +2521,7 @@ commit;
select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index a976008b3d..08d321f176 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -320,18 +320,44 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
cstate->line_buf_valid = false;
save_cur_lineno = cstate->cur_lineno;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ int sent = 0;
+
+ Assert(resultRelInfo->ri_BatchSize > 1 &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL);
+
+ /* Flush into foreign table or partition */
+ while (sent < nused)
+ {
+ int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
+ resultRelInfo->ri_BatchSize : (nused - sent);
+ int inserted = size;
+
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+ sent += size;
+ }
+ }
+ else
+ {
+ /*
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+ }
for (i = 0; i < nused; i++)
{
@@ -343,6 +369,8 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
{
List *recheckIndexes;
+ Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind != RELKIND_FOREIGN_TABLE);
+
cstate->cur_lineno = buffer->linenos[i];
recheckIndexes =
ExecInsertIndexTuples(resultRelInfo,
@@ -541,13 +569,11 @@ CopyFrom(CopyFromState cstate)
CommandId mycid = GetCurrentCommandId(true);
int ti_options = 0; /* start with default options for insert */
BulkInsertState bistate = NULL;
- CopyInsertMethod insertMethod;
CopyMultiInsertInfo multiInsertInfo = {0}; /* pacify compiler */
int64 processed = 0;
int64 excluded = 0;
bool has_before_insert_row_trig;
bool has_instead_insert_row_trig;
- bool leafpart_use_multi_insert = false;
Assert(cstate->rel);
Assert(list_length(cstate->range_table) == 1);
@@ -674,10 +700,43 @@ CopyFrom(CopyFromState cstate)
mtstate->resultRelInfo = resultRelInfo;
mtstate->rootResultRelInfo = resultRelInfo;
- if (resultRelInfo->ri_FdwRoutine != NULL &&
- resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
- resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
- resultRelInfo);
+ if (resultRelInfo->ri_FdwRoutine != NULL)
+ {
+ if (resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+
+ if (resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+ resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+ resultRelInfo);
+ }
+
+ Assert(!target_resultRelInfo->ri_usesMultiInsert);
+
+ /*
+ * It's generally more efficient to prepare a bunch of tuples for
+ * insertion, and insert them in bulk, for example, with one
+ * table_multi_insert() call than call table_tuple_insert() separately for
+ * every tuple. However, there are a number of reasons why we might not be
+ * able to do this. For example, if there any volatile expressions in the
+ * table's default values or in the statement's WHERE clause, which may
+ * query the table we are inserting into, buffering tuples might produce
+ * wrong results. Also, the relation we are trying to insert into itself
+ * may not be amenable to buffered inserts.
+ *
+ * Note: For partitions, this flag is set considering the target table's
+ * flag that is being set here and partition's own properties which are
+ * checked by calling ExecMultiInsertAllowed(). It does not matter
+ * whether partitions have any volatile default expressions as we use the
+ * defaults from the target of the COPY command.
+ * Also, the COPY command requires a non-zero input list of attributes.
+ * Therefore, the length of the attribute list is checked here.
+ */
+ if (!cstate->volatile_defexprs &&
+ list_length(cstate->attnumlist) > 0 &&
+ !contain_volatile_functions(cstate->whereClause))
+ target_resultRelInfo->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(target_resultRelInfo);
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -706,83 +765,9 @@ CopyFrom(CopyFromState cstate)
cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
&mtstate->ps);
- /*
- * It's generally more efficient to prepare a bunch of tuples for
- * insertion, and insert them in one table_multi_insert() call, than call
- * table_tuple_insert() separately for every tuple. However, there are a
- * number of reasons why we might not be able to do this. These are
- * explained below.
- */
- if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
- {
- /*
- * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
- * triggers on the table. Such triggers might query the table we're
- * inserting into and act differently if the tuples that have already
- * been processed and prepared for insertion are not there.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
- resultRelInfo->ri_TrigDesc->trig_insert_new_table)
- {
- /*
- * For partitioned tables we can't support multi-inserts when there
- * are any statement level insert triggers. It might be possible to
- * allow partitioned tables with such triggers in the future, but for
- * now, CopyMultiInsertInfoFlush expects that any before row insert
- * and statement level insert triggers are on the same relation.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
- {
- /*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
- *
- * Note: It does not matter if any partitions have any volatile
- * default expressions as we use the defaults from the target of the
- * COPY command.
- */
- insertMethod = CIM_SINGLE;
- }
- else if (contain_volatile_functions(cstate->whereClause))
- {
- /*
- * Can't support multi-inserts if there are any volatile function
- * expressions in WHERE clause. Similarly to the trigger case above,
- * such expressions may query the table we're inserting into.
- */
- insertMethod = CIM_SINGLE;
- }
- else
- {
- /*
- * For partitioned tables, we may still be able to perform bulk
- * inserts. However, the possibility of this depends on which types
- * of triggers exist on the partition. We must disable bulk inserts
- * if the partition is a foreign table or it has any before row insert
- * or insert instead triggers (same as we checked above for the parent
- * table). Since the partition's resultRelInfos are initialized only
- * when we actually need to insert the first tuple into them, we must
- * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
- * flag that we must later determine if we can use bulk-inserts for
- * the partition being inserted into.
- */
- if (proute)
- insertMethod = CIM_MULTI_CONDITIONAL;
- else
- insertMethod = CIM_MULTI;
-
+ if (resultRelInfo->ri_usesMultiInsert)
CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
estate, mycid, ti_options);
- }
/*
* If not using batch mode (which allocates slots as needed) set up a
@@ -790,7 +775,7 @@ CopyFrom(CopyFromState cstate)
* one, even if we might batch insert, to read the tuple in the root
* partition's form.
*/
- if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+ if (!resultRelInfo->ri_usesMultiInsert || proute)
{
singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
&estate->es_tupleTable);
@@ -833,7 +818,7 @@ CopyFrom(CopyFromState cstate)
ResetPerTupleExprContext(estate);
/* select slot to (initially) load row into */
- if (insertMethod == CIM_SINGLE || proute)
+ if (!target_resultRelInfo->ri_usesMultiInsert || proute)
{
myslot = singleslot;
Assert(myslot != NULL);
@@ -841,7 +826,6 @@ CopyFrom(CopyFromState cstate)
else
{
Assert(resultRelInfo == target_resultRelInfo);
- Assert(insertMethod == CIM_MULTI);
myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -908,24 +892,14 @@ CopyFrom(CopyFromState cstate)
has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
- /*
- * Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
- */
- leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
- !has_before_insert_row_trig &&
- !has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
-
/* Set the multi-insert buffer to use for this partition. */
- if (leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
resultRelInfo);
}
- else if (insertMethod == CIM_MULTI_CONDITIONAL &&
- !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
{
/*
* Flush pending inserts if this partition can't use
@@ -955,7 +929,7 @@ CopyFrom(CopyFromState cstate)
* rowtype.
*/
map = resultRelInfo->ri_RootToPartitionMap;
- if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+ if (!resultRelInfo->ri_usesMultiInsert)
{
/* non batch insert */
if (map != NULL)
@@ -974,9 +948,6 @@ CopyFrom(CopyFromState cstate)
*/
TupleTableSlot *batchslot;
- /* no other path available for partitioned table */
- Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
resultRelInfo);
@@ -1048,7 +1019,7 @@ CopyFrom(CopyFromState cstate)
ExecPartitionCheck(resultRelInfo, myslot, estate, true);
/* Store the slot in the multi-insert buffer, when enabled. */
- if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+ if (resultRelInfo->ri_usesMultiInsert)
{
/*
* The slot previously might point into the per-tuple
@@ -1127,11 +1098,8 @@ CopyFrom(CopyFromState cstate)
}
/* Flush any remaining buffered tuples */
- if (insertMethod != CIM_SINGLE)
- {
- if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
- }
+ if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
/* Done, clean up */
error_context_stack = errcallback.previous;
@@ -1152,12 +1120,11 @@ CopyFrom(CopyFromState cstate)
/* Allow the FDW to shut down */
if (target_resultRelInfo->ri_FdwRoutine != NULL &&
target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
- target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
- target_resultRelInfo);
+ target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+ target_resultRelInfo);
/* Tear down the multi-insert buffer data */
- if (insertMethod != CIM_SINGLE)
- CopyMultiInsertInfoCleanup(&multiInsertInfo);
+ CopyMultiInsertInfoCleanup(&multiInsertInfo);
/* Close all the partitioned tables, leaf partitions, and their indices */
if (proute)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index ef2fd46092..da95f2efb7 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1260,9 +1260,54 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_PartitionTupleSlot = NULL; /* ditto */
resultRelInfo->ri_ChildToRootMap = NULL;
resultRelInfo->ri_ChildToRootMapValid = false;
+ resultRelInfo->ri_usesMultiInsert = false;
resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
}
+/*
+ * ExecMultiInsertAllowed
+ * Does this relation allow caller to use multi-insert mode when
+ * inserting rows into it?
+ */
+bool
+ExecMultiInsertAllowed(const ResultRelInfo *rri)
+{
+ /*
+ * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+ * triggers on the table. Such triggers might query the table we're
+ * inserting into and act differently if the tuples that have already
+ * been processed and prepared for insertion are not there.
+ */
+ if (rri->ri_TrigDesc != NULL &&
+ (rri->ri_TrigDesc->trig_insert_before_row ||
+ rri->ri_TrigDesc->trig_insert_instead_row))
+ return false;
+
+ /*
+ * For partitioned tables we can't support multi-inserts when there are
+ * any statement level insert triggers. It might be possible to allow
+ * partitioned tables with such triggers in the future, but for now,
+ * CopyMultiInsertInfoFlush expects that any before row insert and
+ * statement level insert triggers are on the same relation.
+ */
+ if (rri->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ rri->ri_TrigDesc != NULL &&
+ rri->ri_TrigDesc->trig_insert_new_table)
+ return false;
+
+ if (rri->ri_FdwRoutine != NULL &&
+ (rri->ri_FdwRoutine->ExecForeignBatchInsert == NULL ||
+ rri->ri_BatchSize <= 1))
+ /*
+ * Foreign tables don't support multi-inserts, unless their FDW
+ * provides the necessary bulk insert interface.
+ */
+ return false;
+
+ /* OK, caller can use multi-insert on this relation. */
+ return true;
+}
+
/*
* ExecGetTriggerResultRel
* Get a ResultRelInfo for a trigger target relation.
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index e03ea27299..990d1fd306 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -669,6 +669,14 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
ExecInitRoutingInfo(mtstate, estate, proute, dispatch,
leaf_part_rri, partidx, false);
+ /*
+ * If a partition's root parent isn't allowed to use it, neither is the
+ * partition.
+ */
+ if (rootResultRelInfo->ri_usesMultiInsert)
+ leaf_part_rri->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(leaf_part_rri);
+
/*
* If there is an ON CONFLICT clause, initialize state for it.
*/
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index 3df1c5a97c..1c733bb9a4 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -39,16 +39,6 @@ typedef enum EolType
EOL_CRNL
} EolType;
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
- CIM_SINGLE, /* use table_tuple_insert or fdw routine */
- CIM_MULTI, /* always use table_multi_insert */
- CIM_MULTI_CONDITIONAL /* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
/*
* This struct contains all the state variables used throughout a COPY FROM
* operation.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index d68a6b9d28..b646f91883 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,6 +203,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
+extern bool ExecMultiInsertAllowed(const ResultRelInfo *rri);
extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
ResultRelInfo *rootRelInfo);
extern List *ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5728801379..80d97096b8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -548,7 +548,10 @@ typedef struct ResultRelInfo
TupleConversionMap *ri_ChildToRootMap;
bool ri_ChildToRootMapValid;
- /* for use by copyfrom.c when performing multi-inserts */
+ /* True if okay to use multi-insert on this relation */
+ bool ri_usesMultiInsert;
+
+ /* Buffer allocated to this relation when using multi-insert mode */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
/*
--
2.37.0
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-18 08:22 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>
1 sibling, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-07-18 08:22 UTC (permalink / raw)
To: Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Thu, Mar 24, 2022 at 3:43 PM Andrey V. Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> On 3/22/22 06:54, Etsuro Fujita wrote:
> > * To allow foreign multi insert, the patch made an invasive change to
> > the existing logic to determine whether to use multi insert for the
> > target relation, adding a new member ri_usesMultiInsert to the
> > ResultRelInfo struct, as well as introducing a new function
> > ExecMultiInsertAllowed(). But I’m not sure we really need such a
> > change. Isn’t it reasonable to *adjust* the existing logic to allow
> > foreign multi insert when possible?
> Of course, such approach would look much better, if we implemented it.
> I'll ponder how to do it.
I rewrote the decision logic to something much simpler and much less
invasive, which reduces the patch size significantly. Attached is an
updated patch. What do you think about that?
While working on the patch, I fixed a few issues as well:
+ if (resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL)
+ resultRelInfo->ri_BatchSize =
+
resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
When determining the batch size, I think we should check if the
ExecForeignBatchInsert callback routine is also defined, like other
places such as execPartition.c. For consistency I fixed this by
copying-and-pasting the code from that file.
+ * Also, the COPY command requires a non-zero input list of attributes.
+ * Therefore, the length of the attribute list is checked here.
+ */
+ if (!cstate->volatile_defexprs &&
+ list_length(cstate->attnumlist) > 0 &&
+ !contain_volatile_functions(cstate->whereClause))
+ target_resultRelInfo->ri_usesMultiInsert =
+ ExecMultiInsertAllowed(target_resultRelInfo);
I think “list_length(cstate->attnumlist) > 0” in the if-test would
break COPY FROM; it currently supports multi-inserting into *plain*
tables even in the case where they have no columns, but this would
disable the multi-insertion support in that case. postgres_fdw would
not be able to batch into zero-column foreign tables due to the INSERT
syntax limitation (i.e., the syntax does not allow inserting multiple
empty rows into a zero-column table in a single INSERT statement).
Which is the reason why this was added to the if-test? But I think
some other FDWs might be able to, so I think we should let the FDW
decide whether to allow batching even in that case, when called from
GetForeignModifyBatchSize. So I removed the attnumlist test from the
patch, and modified postgresGetForeignModifyBatchSize as such. I
might miss something, though.
Best regards,
Etsuro Fujita
Attachments:
v4-0001-Implementation-of-a-Bulk-COPY-FROM-efujita-1.patch (application/octet-stream, 11.2K) download
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index ebf9ea3598..3c50f739d0 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8481,6 +8481,7 @@ drop table loct2;
-- ===================================================================
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -8503,7 +8504,7 @@ copy rem2 from stdin; -- ERROR
ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
DETAIL: Failing row contains (-1, xyzzy).
CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1 xyzzy"
+COPY rem2, line 2
select * from rem2;
f1 | f2
----+-----
@@ -8514,6 +8515,19 @@ select * from rem2;
alter foreign table rem2 drop constraint rem2_f1positive;
alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE: (1)
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -8622,6 +8636,34 @@ drop trigger rem2_trig_row_before on rem2;
drop trigger rem2_trig_row_after on rem2;
drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR: column "f1" of relation "loc2" does not exist
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2), ($3, $4)
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -8638,6 +8680,7 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index f3b93954ee..3bee9d19a3 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -2057,6 +2057,15 @@ postgresGetForeignModifyBatchSize(ResultRelInfo *resultRelInfo)
resultRelInfo->ri_TrigDesc->trig_insert_after_row)))
return 1;
+ /*
+ * If the foreign table has no columns, disable batching as the INSERT
+ * syntax doesn't allow batching multiple empty rows into a zero-column
+ * table. This isn't needed in case of INSERT, but is in case of COPY.
+ * Note that in the latter case fmstate must be non-NULL.
+ */
+ if (fmstate && list_length(fmstate->target_attrs) == 0)
+ return 1;
+
/*
* Otherwise use the batch size specified for server/table. The number of
* parameters in a batch is limited to 65535 (uint16), so make sure we
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index b7817c5a41..e013d55313 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2350,6 +2350,7 @@ drop table loct2;
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -2382,6 +2383,23 @@ alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -2482,6 +2500,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1 foo
+2 bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -2495,6 +2541,7 @@ commit;
select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index a976008b3d..b1cb589d60 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -320,18 +320,44 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
cstate->line_buf_valid = false;
save_cur_lineno = cstate->cur_lineno;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ int sent = 0;
+
+ Assert(resultRelInfo->ri_BatchSize > 1 &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL);
+
+ /* Flush into foreign table or partition */
+ while (sent < nused)
+ {
+ int size = (resultRelInfo->ri_BatchSize < nused - sent) ?
+ resultRelInfo->ri_BatchSize : (nused - sent);
+ int inserted = size;
+
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+ sent += size;
+ }
+ }
+ else
+ {
+ /*
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+ }
for (i = 0; i < nused; i++)
{
@@ -343,6 +369,8 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
{
List *recheckIndexes;
+ Assert(resultRelInfo->ri_RelationDesc->rd_rel->relkind != RELKIND_FOREIGN_TABLE);
+
cstate->cur_lineno = buffer->linenos[i];
recheckIndexes =
ExecInsertIndexTuples(resultRelInfo,
@@ -679,6 +707,23 @@ CopyFrom(CopyFromState cstate)
resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
resultRelInfo);
+ /*
+ * Also, if the named relation is a foreign table, determine if the FDW
+ * supports batch insert and determine the batch size (a FDW may support
+ * batching, but it may be disabled for the server/table).
+ *
+ * If the FDW does not support batching, we set the batch size to 1.
+ */
+ if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+ else
+ resultRelInfo->ri_BatchSize = 1;
+
+ Assert(resultRelInfo->ri_BatchSize >= 1);
+
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -725,6 +770,15 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
+ else if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_BatchSize == 1)
+ {
+ /*
+ * Can't support multi-inserts to foreign tables if the FDW does not
+ * support batching.
+ */
+ insertMethod = CIM_SINGLE;
+ }
else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
resultRelInfo->ri_TrigDesc->trig_insert_new_table)
{
@@ -737,14 +791,12 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
+ else if (cstate->volatile_defexprs)
{
/*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
+ * Can't support multi-inserts if there are any volatile default
+ * expressions in the table. Similarly to the trigger case above,
+ * such expressions may query the table we're inserting into.
*
* Note: It does not matter if any partitions have any volatile
* default expressions as we use the defaults from the target of the
@@ -910,12 +962,14 @@ CopyFrom(CopyFromState cstate)
/*
* Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
+ * OF triggers, or if the partition is a foreign partition
+ * that can't use batching.
*/
leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
!has_before_insert_row_trig &&
!has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
+ (resultRelInfo->ri_FdwRoutine == NULL ||
+ resultRelInfo->ri_BatchSize > 1);
/* Set the multi-insert buffer to use for this partition. */
if (leafpart_use_multi_insert)
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-19 09:35 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-07-19 09:35 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 18/7/2022 13:22, Etsuro Fujita wrote:
> On Thu, Mar 24, 2022 at 3:43 PM Andrey V. Lepikhov
> <a.lepikhov@postgrespro.ru> wrote:
>> On 3/22/22 06:54, Etsuro Fujita wrote:
>>> * To allow foreign multi insert, the patch made an invasive change to
>>> the existing logic to determine whether to use multi insert for the
>>> target relation, adding a new member ri_usesMultiInsert to the
>>> ResultRelInfo struct, as well as introducing a new function
>>> ExecMultiInsertAllowed(). But I’m not sure we really need such a
>>> change. Isn’t it reasonable to *adjust* the existing logic to allow
>>> foreign multi insert when possible?
>> Of course, such approach would look much better, if we implemented it.
>
>> I'll ponder how to do it.
>
> I rewrote the decision logic to something much simpler and much less
> invasive, which reduces the patch size significantly. Attached is an
> updated patch. What do you think about that?
>
> While working on the patch, I fixed a few issues as well:
>
> + if (resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize != NULL)
> + resultRelInfo->ri_BatchSize =
> +
> resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
>
> When determining the batch size, I think we should check if the
> ExecForeignBatchInsert callback routine is also defined, like other
> places such as execPartition.c. For consistency I fixed this by
> copying-and-pasting the code from that file.
>
> + * Also, the COPY command requires a non-zero input list of attributes.
> + * Therefore, the length of the attribute list is checked here.
> + */
> + if (!cstate->volatile_defexprs &&
> + list_length(cstate->attnumlist) > 0 &&
> + !contain_volatile_functions(cstate->whereClause))
> + target_resultRelInfo->ri_usesMultiInsert =
> + ExecMultiInsertAllowed(target_resultRelInfo);
>
> I think “list_length(cstate->attnumlist) > 0” in the if-test would
> break COPY FROM; it currently supports multi-inserting into *plain*
> tables even in the case where they have no columns, but this would
> disable the multi-insertion support in that case. postgres_fdw would
> not be able to batch into zero-column foreign tables due to the INSERT
> syntax limitation (i.e., the syntax does not allow inserting multiple
> empty rows into a zero-column table in a single INSERT statement).
> Which is the reason why this was added to the if-test? But I think
> some other FDWs might be able to, so I think we should let the FDW
> decide whether to allow batching even in that case, when called from
> GetForeignModifyBatchSize. So I removed the attnumlist test from the
> patch, and modified postgresGetForeignModifyBatchSize as such. I
> might miss something, though.
Thanks a lot,
maybe you forgot this code:
/*
* If a partition's root parent isn't allowed to use it, neither is the
* partition.
*/
if (rootResultRelInfo->ri_usesMultiInsert)
leaf_part_rri->ri_usesMultiInsert =
ExecMultiInsertAllowed(leaf_part_rri);
Also, maybe to describe in documentation, if the value of batch_size is
more than 1, the ExecForeignBatchInsert routine have a chance to be called?
--
regards,
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-20 08:10 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-07-20 08:10 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Tue, Jul 19, 2022 at 6:35 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> On 18/7/2022 13:22, Etsuro Fujita wrote:
> > I rewrote the decision logic to something much simpler and much less
> > invasive, which reduces the patch size significantly. Attached is an
> > updated patch. What do you think about that?
> maybe you forgot this code:
> /*
> * If a partition's root parent isn't allowed to use it, neither is the
> * partition.
> */
> if (rootResultRelInfo->ri_usesMultiInsert)
> leaf_part_rri->ri_usesMultiInsert =
> ExecMultiInsertAllowed(leaf_part_rri);
I think the patch accounts for that. Consider this bit to determine
whether to use batching for the partition chosen by
ExecFindPartition():
@@ -910,12 +962,14 @@ CopyFrom(CopyFromState cstate)
/*
* Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
+ * OF triggers, or if the partition is a foreign partition
+ * that can't use batching.
*/
leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITION\
AL &&
!has_before_insert_row_trig &&
!has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
+ (resultRelInfo->ri_FdwRoutine == NULL ||
+ resultRelInfo->ri_BatchSize > 1);
If the root parent isn't allowed to use batching, then we have
insertMethod=CIM_SINGLE for the parent before we get here. So in that
case we have leafpart_use_multi_insert=false for the chosen partition,
meaning that the partition isn't allowed to use batching, either.
(The patch just extends the existing decision logic to the
foreign-partition case.)
> Also, maybe to describe in documentation, if the value of batch_size is
> more than 1, the ExecForeignBatchInsert routine have a chance to be called?
Yeah, but I think that is the existing behavior, and that the patch
doesn't change the behavior, so I would leave that for another patch.
Thanks for reviewing!
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-22 06:39 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-07-22 06:39 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 7/20/22 13:10, Etsuro Fujita wrote:
> On Tue, Jul 19, 2022 at 6:35 PM Andrey Lepikhov
> <a.lepikhov@postgrespro.ru> wrote:
>> On 18/7/2022 13:22, Etsuro Fujita wrote:
>>> I rewrote the decision logic to something much simpler and much less
>>> invasive, which reduces the patch size significantly. Attached is an
>>> updated patch. What do you think about that?
>
>> maybe you forgot this code:
>> /*
>> * If a partition's root parent isn't allowed to use it, neither is the
>> * partition.
>> */
>> if (rootResultRelInfo->ri_usesMultiInsert)
>> leaf_part_rri->ri_usesMultiInsert =
>> ExecMultiInsertAllowed(leaf_part_rri);
>
> I think the patch accounts for that. Consider this bit to determine
> whether to use batching for the partition chosen by
> ExecFindPartition():
Agreed.
Analyzing multi-level heterogeneous partitioned configurations I
realized, that single write into a partition with a trigger will flush
buffers for all other partitions of the parent table even if the parent
haven't any triggers.
It relates to the code:
else if (insertMethod == CIM_MULTI_CONDITIONAL &&
!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
{
/*
* Flush pending inserts if this partition can't use
* batching, so rows are visible to triggers etc.
*/
CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
}
Why such cascade flush is really necessary, especially for BEFORE and
INSTEAD OF triggers? AFTER Trigger should see all rows of the table, but
if it isn't exists for parent, I think, we wouldn't obligate to
guarantee order of COPY into two different tables.
--
Regards
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-22 08:14 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 2 replies; 39+ messages in thread
From: Etsuro Fujita @ 2022-07-22 08:14 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Fri, Jul 22, 2022 at 3:39 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> Analyzing multi-level heterogeneous partitioned configurations I
> realized, that single write into a partition with a trigger will flush
> buffers for all other partitions of the parent table even if the parent
> haven't any triggers.
> It relates to the code:
> else if (insertMethod == CIM_MULTI_CONDITIONAL &&
> !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
> {
> /*
> * Flush pending inserts if this partition can't use
> * batching, so rows are visible to triggers etc.
> */
> CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
> }
>
> Why such cascade flush is really necessary, especially for BEFORE and
> INSTEAD OF triggers?
BEFORE triggers on the chosen partition might query the parent table,
not just the partition, so I think we need to do this so that such
triggers can see all the rows that have been inserted into the parent
table until then.
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-22 08:42 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
1 sibling, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-07-22 08:42 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 7/22/22 13:14, Etsuro Fujita wrote:
> On Fri, Jul 22, 2022 at 3:39 PM Andrey Lepikhov
>> Why such cascade flush is really necessary, especially for BEFORE and
>> INSTEAD OF triggers?
>
> BEFORE triggers on the chosen partition might query the parent table,
> not just the partition, so I think we need to do this so that such
> triggers can see all the rows that have been inserted into the parent
> table until then.
Thanks for the explanation of your point of view. So, maybe switch
status of this patch to 'Ready for committer'?
--
Regards
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-26 10:19 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-07-26 10:19 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Fri, Jul 22, 2022 at 5:42 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> So, maybe switch
> status of this patch to 'Ready for committer'?
Yeah, I think the patch is getting better, but I noticed some issues,
so I'm working on them. I think I can post a new version in the next
few days.
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-07-27 05:42 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
1 sibling, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-07-27 05:42 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 7/22/22 13:14, Etsuro Fujita wrote:
> On Fri, Jul 22, 2022 at 3:39 PM Andrey Lepikhov
> <a.lepikhov@postgrespro.ru> wrote:
>> Analyzing multi-level heterogeneous partitioned configurations I
>> realized, that single write into a partition with a trigger will flush
>> buffers for all other partitions of the parent table even if the parent
>> haven't any triggers.
>> It relates to the code:
>> else if (insertMethod == CIM_MULTI_CONDITIONAL &&
>> !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
>> {
>> /*
>> * Flush pending inserts if this partition can't use
>> * batching, so rows are visible to triggers etc.
>> */
>> CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
>> }
>>
>> Why such cascade flush is really necessary, especially for BEFORE and
>> INSTEAD OF triggers?
>
> BEFORE triggers on the chosen partition might query the parent table,
> not just the partition, so I think we need to do this so that such
> triggers can see all the rows that have been inserted into the parent
> table until then.
if you'll excuse me, I will add one more argument.
It wasn't clear, so I've made an experiment: result of a SELECT in an
INSERT trigger function shows only data, existed in the parent table
before the start of COPY.
So, we haven't tools to access newly inserting rows in neighboring
partition and don't need to flush tuple buffers immediately.
Where am I wrong?
--
Regards
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-08-01 04:00 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 0 replies; 39+ messages in thread
From: Etsuro Fujita @ 2022-08-01 04:00 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Wed, Jul 27, 2022 at 2:42 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> On 7/22/22 13:14, Etsuro Fujita wrote:
> > On Fri, Jul 22, 2022 at 3:39 PM Andrey Lepikhov
> > <a.lepikhov@postgrespro.ru> wrote:
> >> Analyzing multi-level heterogeneous partitioned configurations I
> >> realized, that single write into a partition with a trigger will flush
> >> buffers for all other partitions of the parent table even if the parent
> >> haven't any triggers.
> >> It relates to the code:
> >> else if (insertMethod == CIM_MULTI_CONDITIONAL &&
> >> !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
> >> {
> >> /*
> >> * Flush pending inserts if this partition can't use
> >> * batching, so rows are visible to triggers etc.
> >> */
> >> CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
> >> }
> >>
> >> Why such cascade flush is really necessary, especially for BEFORE and
> >> INSTEAD OF triggers?
> >
> > BEFORE triggers on the chosen partition might query the parent table,
> > not just the partition, so I think we need to do this so that such
> > triggers can see all the rows that have been inserted into the parent
> > table until then.
> if you'll excuse me, I will add one more argument.
> It wasn't clear, so I've made an experiment: result of a SELECT in an
> INSERT trigger function shows only data, existed in the parent table
> before the start of COPY.
Is the trigger function declared VOLATILE? If so, the trigger should
see modifications to the parent table as well. See:
https://www.postgresql.org/docs/15/trigger-datachanges.html
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-08-09 11:44 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 2 replies; 39+ messages in thread
From: Etsuro Fujita @ 2022-08-09 11:44 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Tue, Jul 26, 2022 at 7:19 PM Etsuro Fujita <etsuro.fujita@gmail.com> wrote:
> Yeah, I think the patch is getting better, but I noticed some issues,
> so I'm working on them. I think I can post a new version in the next
> few days.
* When running AFTER ROW triggers in CopyMultiInsertBufferFlush(), the
patch uses the slots passed to ExecForeignBatchInsert(), not the ones
returned by the callback function, but I don't think that that is
always correct, as the documentation about the callback function says:
The return value is an array of slots containing the data that was
actually inserted (this might differ from the data supplied, for
example as a result of trigger actions.)
The passed-in <literal>slots</literal> can be re-used for this purpose.
postgres_fdw re-uses the passed-in slots, but other FDWs might not, so
I modified the patch to reference the returned slots when running the
AFTER ROW triggers. I also modified the patch to initialize the
tts_tableOid. Attached is an updated patch, in which I made some
minor adjustments to CopyMultiInsertBufferFlush() as well.
* The patch produces incorrect error context information:
create extension postgres_fdw;
create server loopback foreign data wrapper postgres_fdw options
(dbname 'postgres');
create user mapping for current_user server loopback;
create table t1 (f1 int, f2 text);
create foreign table ft1 (f1 int, f2 text) server loopback options
(table_name 't1');
alter table t1 add constraint t1_f1positive check (f1 >= 0);
alter foreign table ft1 add constraint ft1_f1positive check (f1 >= 0);
— single insert
copy ft1 from stdin;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> -1 foo
>> 1 bar
>> \.
ERROR: new row for relation "t1" violates check constraint "t1_f1positive"
DETAIL: Failing row contains (-1, foo).
CONTEXT: remote SQL command: INSERT INTO public.t1(f1, f2) VALUES ($1, $2)
COPY ft1, line 1: "-1 foo"
— batch insert
alter server loopback options (add batch_size '2');
copy ft1 from stdin;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> -1 foo
>> 1 bar
>> \.
ERROR: new row for relation "t1" violates check constraint "t1_f1positive"
DETAIL: Failing row contains (-1, foo).
CONTEXT: remote SQL command: INSERT INTO public.t1(f1, f2) VALUES
($1, $2), ($3, $4)
COPY ft1, line 3
In single-insert mode the error context information is correct, but in
batch-insert mode it isn’t (i.e., the line number isn’t correct).
The error occurs on the remote side, so I'm not sure if there is a
simple fix. What I came up with is to just suppress error context
information other than the relation name, like the attached. What do
you think about that?
(In CopyMultiInsertBufferFlush() your patch sets cstate->cur_lineno to
buffer->linenos[i] even when running AFTER ROW triggers for the i-th
row returned by ExecForeignBatchInsert(), but that wouldn’t always be
correct, as the i-th returned row might not correspond to the i-th row
originally stored in the buffer as the callback function returns only
the rows that were actually inserted on the remote side. I think the
proposed fix would address this issue as well.)
* The patch produces incorrect row count in cases where some/all of
the rows passed to ExecForeignBatchInsert() weren’t inserted on the
remote side:
create function trig_null() returns trigger as $$ begin return NULL;
end $$ language plpgsql;
create trigger trig_null before insert on t1 for each row execute
function trig_null();
— single insert
alter server loopback options (drop batch_size);
copy ft1 from stdin;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> 0 foo
>> 1 bar
>> \.
COPY 0
— batch insert
alter server loopback options (add batch_size '2');
copy ft1 from stdin;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> 0 foo
>> 1 bar
>> \.
COPY 2
The row count is correct in single-insert mode, but isn’t in batch-insert mode.
The reason is that in batch-insert mode the row counter is updated
immediately after adding the row to the buffer, not after doing
ExecForeignBatchInsert(), which might ignore the row. To fix, I
modified the patch to delay updating the row counter (and the progress
of the COPY command) until after doing the callback function. For
consistency, I also modified the patch to delay it even when batching
into plain tables. IMO I think that that would be more consistent
with the single-insert mode, as in that mode we update them after
writing the tuple out to the table or sending it to the remote side.
* I modified the patch so that when batching into foreign tables we
skip useless steps in CopyMultiInsertBufferInit() and
CopyMultiInsertBufferCleanup().
That’s all I have for now. Sorry for the delay.
Best regards,
Etsuro Fujita
Attachments:
v4-0001-Implementation-of-a-Bulk-COPY-FROM-efujita-2.patch (application/octet-stream, 20.0K) download
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 7bf35602b0..32c2eacf7a 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8533,6 +8533,7 @@ drop table loct2;
-- ===================================================================
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -8555,7 +8556,7 @@ copy rem2 from stdin; -- ERROR
ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
DETAIL: Failing row contains (-1, xyzzy).
CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1 xyzzy"
+COPY rem2
select * from rem2;
f1 | f2
----+-----
@@ -8566,6 +8567,19 @@ select * from rem2;
alter foreign table rem2 drop constraint rem2_f1positive;
alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE: (1)
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -8674,6 +8688,34 @@ drop trigger rem2_trig_row_before on rem2;
drop trigger rem2_trig_row_after on rem2;
drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR: column "f1" of relation "loc2" does not exist
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2), ($3, $4)
+COPY rem2
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -8690,6 +8732,7 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 16320170ce..7d18c70f1a 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -2059,6 +2059,15 @@ postgresGetForeignModifyBatchSize(ResultRelInfo *resultRelInfo)
resultRelInfo->ri_TrigDesc->trig_insert_after_row)))
return 1;
+ /*
+ * If the foreign table has no columns, disable batching as the INSERT
+ * syntax doesn't allow batching multiple empty rows into a zero-column
+ * table. This isn't needed in case of INSERT, but is in case of COPY.
+ * Note that in the latter case fmstate must be non-NULL.
+ */
+ if (fmstate && list_length(fmstate->target_attrs) == 0)
+ return 1;
+
/*
* Otherwise use the batch size specified for server/table. The number of
* parameters in a batch is limited to 65535 (uint16), so make sure we
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 42735ae78a..b0743098d4 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2368,6 +2368,7 @@ drop table loct2;
-- test COPY FROM
-- ===================================================================
+alter server loopback options (add batch_size '2');
create table loc2 (f1 int, f2 text);
alter table loc2 set (autovacuum_enabled = 'false');
create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
@@ -2400,6 +2401,23 @@ alter table loc2 drop constraint loc2_f1positive;
delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
-- Test local triggers
create trigger trig_stmt_before before insert on rem2
for each statement execute procedure trigger_func();
@@ -2500,6 +2518,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1 foo
+2 bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
-- test COPY FROM with foreign table created in the same transaction
create table loc3 (f1 int, f2 text);
begin;
@@ -2513,6 +2559,7 @@ commit;
select * from rem3;
drop foreign table rem3;
drop table loc3;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index a976008b3d..ced3b53191 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -116,6 +116,13 @@ CopyFromErrorCallback(void *arg)
{
CopyFromState cstate = (CopyFromState) arg;
+ if (cstate->relname_only)
+ {
+ errcontext("COPY %s",
+ cstate->cur_relname);
+ return;
+ }
+
if (cstate->opts.binary)
{
/* can't usefully display the data */
@@ -222,7 +229,7 @@ CopyMultiInsertBufferInit(ResultRelInfo *rri)
buffer = (CopyMultiInsertBuffer *) palloc(sizeof(CopyMultiInsertBuffer));
memset(buffer->slots, 0, sizeof(TupleTableSlot *) * MAX_BUFFERED_TUPLES);
buffer->resultRelInfo = rri;
- buffer->bistate = GetBulkInsertState();
+ buffer->bistate = (rri->ri_FdwRoutine == NULL) ? GetBulkInsertState() : NULL;
buffer->nused = 0;
return buffer;
@@ -299,83 +306,162 @@ CopyMultiInsertInfoIsEmpty(CopyMultiInsertInfo *miinfo)
*/
static inline void
CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
- CopyMultiInsertBuffer *buffer)
+ CopyMultiInsertBuffer *buffer,
+ int64 *processed)
{
- MemoryContext oldcontext;
- int i;
- uint64 save_cur_lineno;
CopyFromState cstate = miinfo->cstate;
EState *estate = miinfo->estate;
- CommandId mycid = miinfo->mycid;
- int ti_options = miinfo->ti_options;
- bool line_buf_valid = cstate->line_buf_valid;
int nused = buffer->nused;
ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
TupleTableSlot **slots = buffer->slots;
+ int i;
- /*
- * Print error context information correctly, if one of the operations
- * below fails.
- */
- cstate->line_buf_valid = false;
- save_cur_lineno = cstate->cur_lineno;
+ if (resultRelInfo->ri_FdwRoutine)
+ {
+ int batch_size = resultRelInfo->ri_BatchSize;
+ int sent = 0;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ Assert(batch_size > 1);
- for (i = 0; i < nused; i++)
- {
/*
- * If there are any indexes, update them for all the inserted tuples,
- * and run AFTER ROW INSERT triggers.
+ * We suppress error context information other than the relation name,
+ * if one of the operations below fails.
*/
- if (resultRelInfo->ri_NumIndices > 0)
+ Assert(!cstate->relname_only);
+ cstate->relname_only = true;
+
+ while (sent < nused)
{
- List *recheckIndexes;
-
- cstate->cur_lineno = buffer->linenos[i];
- recheckIndexes =
- ExecInsertIndexTuples(resultRelInfo,
- buffer->slots[i], estate, false, false,
- NULL, NIL);
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], recheckIndexes,
- cstate->transition_capture);
- list_free(recheckIndexes);
+ int size = (batch_size < nused - sent) ? batch_size : (nused - sent);
+ int inserted = size;
+ TupleTableSlot **rslots;
+
+ /* Batch insert into foreign table */
+ Assert(resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert);
+ rslots =
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+
+ /* If any rows were inserted, run AFTER ROW INSERT triggers. */
+ if (inserted > 0 &&
+ resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ for (i = 0; i < inserted; i++)
+ {
+ TupleTableSlot *slot = rslots[i];
+
+ /*
+ * AFTER ROW Triggers might reference the tableoid column,
+ * so (re-)initialize tts_tableOid before evaluating them.
+ */
+ slot->tts_tableOid =
+ RelationGetRelid(resultRelInfo->ri_RelationDesc);
+
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slot, NIL,
+ cstate->transition_capture);
+ }
+ }
+
+ sent += size;
+
+ /* Update the row counter and progress of the COPY command */
+ if (inserted > 0)
+ {
+ *processed += inserted;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
+ }
}
+ for (i = 0; i < nused; i++)
+ ExecClearTuple(slots[i]);
+
+ /* reset relname_only */
+ cstate->relname_only = false;
+ }
+ else
+ {
+ CommandId mycid = miinfo->mycid;
+ int ti_options = miinfo->ti_options;
+ bool line_buf_valid = cstate->line_buf_valid;
+ uint64 save_cur_lineno = cstate->cur_lineno;
+ MemoryContext oldcontext;
+
/*
- * There's no indexes, but see if we need to run AFTER ROW INSERT
- * triggers anyway.
+ * Print error context information correctly, if one of the operations
+ * below fails.
*/
- else if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ cstate->line_buf_valid = false;
+
+ /*
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+
+ for (i = 0; i < nused; i++)
{
- cstate->cur_lineno = buffer->linenos[i];
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], NIL, cstate->transition_capture);
+ /*
+ * If there are any indexes, update them for all the inserted
+ * tuples, and run AFTER ROW INSERT triggers.
+ */
+ if (resultRelInfo->ri_NumIndices > 0)
+ {
+ List *recheckIndexes;
+
+ cstate->cur_lineno = buffer->linenos[i];
+ recheckIndexes =
+ ExecInsertIndexTuples(resultRelInfo,
+ buffer->slots[i], estate, false,
+ false, NULL, NIL);
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], recheckIndexes,
+ cstate->transition_capture);
+ list_free(recheckIndexes);
+ }
+
+ /*
+ * There's no indexes, but see if we need to run AFTER ROW INSERT
+ * triggers anyway.
+ */
+ else if (resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ cstate->cur_lineno = buffer->linenos[i];
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], NIL,
+ cstate->transition_capture);
+ }
+
+ ExecClearTuple(slots[i]);
}
- ExecClearTuple(slots[i]);
+ /* Update the row counter and progress of the COPY command */
+ *processed += nused;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
+
+ /* reset cur_lineno and line_buf_valid to what they were */
+ cstate->line_buf_valid = line_buf_valid;
+ cstate->cur_lineno = save_cur_lineno;
}
/* Mark that all slots are free */
buffer->nused = 0;
-
- /* reset cur_lineno and line_buf_valid to what they were */
- cstate->line_buf_valid = line_buf_valid;
- cstate->cur_lineno = save_cur_lineno;
}
/*
@@ -387,22 +473,25 @@ static inline void
CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
CopyMultiInsertBuffer *buffer)
{
+ ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
int i;
/* Ensure buffer was flushed */
Assert(buffer->nused == 0);
/* Remove back-link to ourself */
- buffer->resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+ resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
- FreeBulkInsertState(buffer->bistate);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ FreeBulkInsertState(buffer->bistate);
/* Since we only create slots on demand, just drop the non-null ones. */
for (i = 0; i < MAX_BUFFERED_TUPLES && buffer->slots[i] != NULL; i++)
ExecDropSingleTupleTableSlot(buffer->slots[i]);
- table_finish_bulk_insert(buffer->resultRelInfo->ri_RelationDesc,
- miinfo->ti_options);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ table_finish_bulk_insert(resultRelInfo->ri_RelationDesc,
+ miinfo->ti_options);
pfree(buffer);
}
@@ -418,7 +507,8 @@ CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
* 'curr_rri'.
*/
static inline void
-CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
+CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri,
+ int64 *processed)
{
ListCell *lc;
@@ -426,7 +516,7 @@ CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
{
CopyMultiInsertBuffer *buffer = (CopyMultiInsertBuffer *) lfirst(lc);
- CopyMultiInsertBufferFlush(miinfo, buffer);
+ CopyMultiInsertBufferFlush(miinfo, buffer, processed);
}
miinfo->bufferedTuples = 0;
@@ -679,6 +769,23 @@ CopyFrom(CopyFromState cstate)
resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
resultRelInfo);
+ /*
+ * Also, if the named relation is a foreign table, determine if the FDW
+ * supports batch insert and determine the batch size (a FDW may support
+ * batching, but it may be disabled for the server/table).
+ *
+ * If the FDW does not support batching, we set the batch size to 1.
+ */
+ if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+ else
+ resultRelInfo->ri_BatchSize = 1;
+
+ Assert(resultRelInfo->ri_BatchSize >= 1);
+
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -725,6 +832,15 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
+ else if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_BatchSize == 1)
+ {
+ /*
+ * Can't support multi-inserts to foreign tables if the FDW does not
+ * support batching.
+ */
+ insertMethod = CIM_SINGLE;
+ }
else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
resultRelInfo->ri_TrigDesc->trig_insert_new_table)
{
@@ -737,14 +853,12 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
+ else if (cstate->volatile_defexprs)
{
/*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
+ * Can't support multi-inserts if there are any volatile default
+ * expressions in the table. Similarly to the trigger case above,
+ * such expressions may query the table we're inserting into.
*
* Note: It does not matter if any partitions have any volatile
* default expressions as we use the defaults from the target of the
@@ -910,12 +1024,14 @@ CopyFrom(CopyFromState cstate)
/*
* Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
+ * OF triggers, or if the partition is a foreign partition
+ * that can't use batching.
*/
leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
!has_before_insert_row_trig &&
!has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
+ (resultRelInfo->ri_FdwRoutine == NULL ||
+ resultRelInfo->ri_BatchSize > 1);
/* Set the multi-insert buffer to use for this partition. */
if (leafpart_use_multi_insert)
@@ -931,7 +1047,9 @@ CopyFrom(CopyFromState cstate)
* Flush pending inserts if this partition can't use
* batching, so rows are visible to triggers etc.
*/
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
}
if (bistate != NULL)
@@ -1067,7 +1185,17 @@ CopyFrom(CopyFromState cstate)
* buffers out to their tables.
*/
if (CopyMultiInsertInfoIsFull(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
+
+ /*
+ * We delay updating the row counter and the progress of
+ * the COPY command until after writing the tuples stored
+ * in the buffer out to the table. See
+ * CopyMultiInsertBufferFlush().
+ */
+ continue; /* next tuple please */
}
else
{
@@ -1130,7 +1258,7 @@ CopyFrom(CopyFromState cstate)
if (insertMethod != CIM_SINGLE)
{
if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL, &processed);
}
/* Done, clean up */
@@ -1349,6 +1477,7 @@ BeginCopyFrom(ParseState *pstate,
cstate->cur_lineno = 0;
cstate->cur_attname = NULL;
cstate->cur_attval = NULL;
+ cstate->relname_only = false;
/*
* Allocate buffers for the input pipeline.
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index e37c6032ae..21e8b89baa 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -81,6 +81,7 @@ typedef struct CopyFromStateData
uint64 cur_lineno; /* line number for error messages */
const char *cur_attname; /* current att for error messages */
const char *cur_attval; /* current att value for error messages */
+ bool relname_only; /* don't output line number, att, etc. */
/*
* Working state
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-08-09 16:13 Zhihong Yu <zyu@yugabyte.com>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
1 sibling, 1 reply; 39+ messages in thread
From: Zhihong Yu @ 2022-08-09 16:13 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Tue, Aug 9, 2022 at 4:45 AM Etsuro Fujita <etsuro.fujita@gmail.com>
wrote:
> On Tue, Jul 26, 2022 at 7:19 PM Etsuro Fujita <etsuro.fujita@gmail.com>
> wrote:
> > Yeah, I think the patch is getting better, but I noticed some issues,
> > so I'm working on them. I think I can post a new version in the next
> > few days.
>
> * When running AFTER ROW triggers in CopyMultiInsertBufferFlush(), the
> patch uses the slots passed to ExecForeignBatchInsert(), not the ones
> returned by the callback function, but I don't think that that is
> always correct, as the documentation about the callback function says:
>
> The return value is an array of slots containing the data that was
> actually inserted (this might differ from the data supplied, for
> example as a result of trigger actions.)
> The passed-in <literal>slots</literal> can be re-used for this
> purpose.
>
> postgres_fdw re-uses the passed-in slots, but other FDWs might not, so
> I modified the patch to reference the returned slots when running the
> AFTER ROW triggers. I also modified the patch to initialize the
> tts_tableOid. Attached is an updated patch, in which I made some
> minor adjustments to CopyMultiInsertBufferFlush() as well.
>
> * The patch produces incorrect error context information:
>
> create extension postgres_fdw;
> create server loopback foreign data wrapper postgres_fdw options
> (dbname 'postgres');
> create user mapping for current_user server loopback;
> create table t1 (f1 int, f2 text);
> create foreign table ft1 (f1 int, f2 text) server loopback options
> (table_name 't1');
> alter table t1 add constraint t1_f1positive check (f1 >= 0);
> alter foreign table ft1 add constraint ft1_f1positive check (f1 >= 0);
>
> — single insert
> copy ft1 from stdin;
> Enter data to be copied followed by a newline.
> End with a backslash and a period on a line by itself, or an EOF signal.
> >> -1 foo
> >> 1 bar
> >> \.
> ERROR: new row for relation "t1" violates check constraint "t1_f1positive"
> DETAIL: Failing row contains (-1, foo).
> CONTEXT: remote SQL command: INSERT INTO public.t1(f1, f2) VALUES ($1, $2)
> COPY ft1, line 1: "-1 foo"
>
> — batch insert
> alter server loopback options (add batch_size '2');
> copy ft1 from stdin;
> Enter data to be copied followed by a newline.
> End with a backslash and a period on a line by itself, or an EOF signal.
> >> -1 foo
> >> 1 bar
> >> \.
> ERROR: new row for relation "t1" violates check constraint "t1_f1positive"
> DETAIL: Failing row contains (-1, foo).
> CONTEXT: remote SQL command: INSERT INTO public.t1(f1, f2) VALUES
> ($1, $2), ($3, $4)
> COPY ft1, line 3
>
> In single-insert mode the error context information is correct, but in
> batch-insert mode it isn’t (i.e., the line number isn’t correct).
>
> The error occurs on the remote side, so I'm not sure if there is a
> simple fix. What I came up with is to just suppress error context
> information other than the relation name, like the attached. What do
> you think about that?
>
> (In CopyMultiInsertBufferFlush() your patch sets cstate->cur_lineno to
> buffer->linenos[i] even when running AFTER ROW triggers for the i-th
> row returned by ExecForeignBatchInsert(), but that wouldn’t always be
> correct, as the i-th returned row might not correspond to the i-th row
> originally stored in the buffer as the callback function returns only
> the rows that were actually inserted on the remote side. I think the
> proposed fix would address this issue as well.)
>
> * The patch produces incorrect row count in cases where some/all of
> the rows passed to ExecForeignBatchInsert() weren’t inserted on the
> remote side:
>
> create function trig_null() returns trigger as $$ begin return NULL;
> end $$ language plpgsql;
> create trigger trig_null before insert on t1 for each row execute
> function trig_null();
>
> — single insert
> alter server loopback options (drop batch_size);
> copy ft1 from stdin;
> Enter data to be copied followed by a newline.
> End with a backslash and a period on a line by itself, or an EOF signal.
> >> 0 foo
> >> 1 bar
> >> \.
> COPY 0
>
> — batch insert
> alter server loopback options (add batch_size '2');
> copy ft1 from stdin;
> Enter data to be copied followed by a newline.
> End with a backslash and a period on a line by itself, or an EOF signal.
> >> 0 foo
> >> 1 bar
> >> \.
> COPY 2
>
> The row count is correct in single-insert mode, but isn’t in batch-insert
> mode.
>
> The reason is that in batch-insert mode the row counter is updated
> immediately after adding the row to the buffer, not after doing
> ExecForeignBatchInsert(), which might ignore the row. To fix, I
> modified the patch to delay updating the row counter (and the progress
> of the COPY command) until after doing the callback function. For
> consistency, I also modified the patch to delay it even when batching
> into plain tables. IMO I think that that would be more consistent
> with the single-insert mode, as in that mode we update them after
> writing the tuple out to the table or sending it to the remote side.
>
> * I modified the patch so that when batching into foreign tables we
> skip useless steps in CopyMultiInsertBufferInit() and
> CopyMultiInsertBufferCleanup().
>
> That’s all I have for now. Sorry for the delay.
>
> Best regards,
> Etsuro Fujita
>
Hi,
+ /* If any rows were inserted, run AFTER ROW INSERT triggers. */
...
+ for (i = 0; i < inserted; i++)
+ {
+ TupleTableSlot *slot = rslots[i];
...
+ slot->tts_tableOid =
+ RelationGetRelid(resultRelInfo->ri_RelationDesc);
It seems the return value of
`RelationGetRelid(resultRelInfo->ri_RelationDesc)` can be stored in a
variable outside the for loop.
Inside the for loop, assign this variable to slot->tts_tableOid.
Cheers
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-08-10 08:30 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Zhihong Yu <zyu@yugabyte.com>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-08-10 08:30 UTC (permalink / raw)
To: Zhihong Yu <zyu@yugabyte.com>; +Cc: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
Hi,
On Wed, Aug 10, 2022 at 1:06 AM Zhihong Yu <zyu@yugabyte.com> wrote:
> On Tue, Aug 9, 2022 at 4:45 AM Etsuro Fujita <etsuro.fujita@gmail.com> wrote:
>> * When running AFTER ROW triggers in CopyMultiInsertBufferFlush(), the
>> patch uses the slots passed to ExecForeignBatchInsert(), not the ones
>> returned by the callback function, but I don't think that that is
>> always correct, as the documentation about the callback function says:
>>
>> The return value is an array of slots containing the data that was
>> actually inserted (this might differ from the data supplied, for
>> example as a result of trigger actions.)
>> The passed-in <literal>slots</literal> can be re-used for this purpose.
>>
>> postgres_fdw re-uses the passed-in slots, but other FDWs might not, so
>> I modified the patch to reference the returned slots when running the
>> AFTER ROW triggers.
I noticed that my explanation was not correct. Let me explain.
Before commit 82593b9a3, when batching into a view referencing a
postgres_fdw foreign table that has WCO constraints, postgres_fdw used
the passed-in slots to store the first tuple that was actually
inserted to the remote table. But that commit disabled batching in
that case, so postgres_fdw wouldn’t use the passed-in slots (until we
support batching when there are WCO constraints from the parent views
and/or AFTER ROW triggers on the foreign table).
> + /* If any rows were inserted, run AFTER ROW INSERT triggers. */
> ...
> + for (i = 0; i < inserted; i++)
> + {
> + TupleTableSlot *slot = rslots[i];
> ...
> + slot->tts_tableOid =
> + RelationGetRelid(resultRelInfo->ri_RelationDesc);
>
> It seems the return value of `RelationGetRelid(resultRelInfo->ri_RelationDesc)` can be stored in a variable outside the for loop.
> Inside the for loop, assign this variable to slot->tts_tableOid.
Actually, I did this to match the code in ExecBatchInsert(), but that
seems like a good idea, so I’ll update the patch as such in the next
version.
Thanks for reviewing!
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-08-15 05:29 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
1 sibling, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-08-15 05:29 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 8/9/22 16:44, Etsuro Fujita wrote:
>>> -1 foo
>>> 1 bar
>>> \.
> ERROR: new row for relation "t1" violates check constraint "t1_f1positive"
> DETAIL: Failing row contains (-1, foo).
> CONTEXT: remote SQL command: INSERT INTO public.t1(f1, f2) VALUES
> ($1, $2), ($3, $4)
> COPY ft1, line 3
>
> In single-insert mode the error context information is correct, but in
> batch-insert mode it isn’t (i.e., the line number isn’t correct).
>
> The error occurs on the remote side, so I'm not sure if there is a
> simple fix. What I came up with is to just suppress error context
> information other than the relation name, like the attached. What do
> you think about that?
I've spent many efforts to this problem too. Your solution have a
rationale and looks fine.
I only think, we should add a bit of info into an error report to
simplify comprehension why don't point specific line here. For example:
'COPY %s (buffered)'
or
'COPY FOREIGN TABLE %s'
or, if instead of relname_only field to save a MultiInsertBuffer
pointer, we might add min/max linenos into the report:
'COPY %s, line between %llu and %llu'
--
Regards
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-08-22 08:44 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-08-22 08:44 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Mon, Aug 15, 2022 at 2:29 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> On 8/9/22 16:44, Etsuro Fujita wrote:
> >>> -1 foo
> >>> 1 bar
> >>> \.
> > ERROR: new row for relation "t1" violates check constraint "t1_f1positive"
> > DETAIL: Failing row contains (-1, foo).
> > CONTEXT: remote SQL command: INSERT INTO public.t1(f1, f2) VALUES
> > ($1, $2), ($3, $4)
> > COPY ft1, line 3
> >
> > In single-insert mode the error context information is correct, but in
> > batch-insert mode it isn’t (i.e., the line number isn’t correct).
> >
> > The error occurs on the remote side, so I'm not sure if there is a
> > simple fix. What I came up with is to just suppress error context
> > information other than the relation name, like the attached. What do
> > you think about that?
> I've spent many efforts to this problem too. Your solution have a
> rationale and looks fine.
> I only think, we should add a bit of info into an error report to
> simplify comprehension why don't point specific line here. For example:
> 'COPY %s (buffered)'
> or
> 'COPY FOREIGN TABLE %s'
>
> or, if instead of relname_only field to save a MultiInsertBuffer
> pointer, we might add min/max linenos into the report:
> 'COPY %s, line between %llu and %llu'
I think the latter is more consistent with the existing error context
information when in CopyMultiInsertBufferFlush(). Actually, I thought
this too, and I think this would be useful when the COPY FROM command
is executed on a foreign table. My concern, however, is the case when
the command is executed on a partitioned table containing foreign
partitions; in that case the input data would not always be sorted in
the partition order, so the range for an error-occurring foreign
partition might contain many lines with rows from other partitions,
which I think makes the range information less useful. Maybe I'm too
worried about that, though.
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-08-23 05:58 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-08-23 05:58 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 22/8/2022 11:44, Etsuro Fujita wrote:
> I think the latter is more consistent with the existing error context
> information when in CopyMultiInsertBufferFlush(). Actually, I thought
> this too, and I think this would be useful when the COPY FROM command
> is executed on a foreign table. My concern, however, is the case when
> the command is executed on a partitioned table containing foreign
> partitions; in that case the input data would not always be sorted in
> the partition order, so the range for an error-occurring foreign
> partition might contain many lines with rows from other partitions,
> which I think makes the range information less useful. Maybe I'm too
> worried about that, though.
I got your point. Indeed, perharps such info doesn't really needed to be
included into the core, at least for now.
--
regards,
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-09-27 08:47 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 0 replies; 39+ messages in thread
From: Etsuro Fujita @ 2022-09-27 08:47 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Zhihong Yu <zyu@yugabyte.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Tue, Aug 23, 2022 at 2:58 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> On 22/8/2022 11:44, Etsuro Fujita wrote:
> > I think the latter is more consistent with the existing error context
> > information when in CopyMultiInsertBufferFlush(). Actually, I thought
> > this too, and I think this would be useful when the COPY FROM command
> > is executed on a foreign table. My concern, however, is the case when
> > the command is executed on a partitioned table containing foreign
> > partitions; in that case the input data would not always be sorted in
> > the partition order, so the range for an error-occurring foreign
> > partition might contain many lines with rows from other partitions,
> > which I think makes the range information less useful. Maybe I'm too
> > worried about that, though.
> I got your point. Indeed, perharps such info doesn't really needed to be
> included into the core, at least for now.
Ok. Sorry for the late response.
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-09-27 09:03 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-09-27 09:03 UTC (permalink / raw)
To: Zhihong Yu <zyu@yugabyte.com>; +Cc: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Wed, Aug 10, 2022 at 5:30 PM Etsuro Fujita <etsuro.fujita@gmail.com> wrote:
> On Wed, Aug 10, 2022 at 1:06 AM Zhihong Yu <zyu@yugabyte.com> wrote:
> > + /* If any rows were inserted, run AFTER ROW INSERT triggers. */
> > ...
> > + for (i = 0; i < inserted; i++)
> > + {
> > + TupleTableSlot *slot = rslots[i];
> > ...
> > + slot->tts_tableOid =
> > + RelationGetRelid(resultRelInfo->ri_RelationDesc);
> >
> > It seems the return value of `RelationGetRelid(resultRelInfo->ri_RelationDesc)` can be stored in a variable outside the for loop.
> > Inside the for loop, assign this variable to slot->tts_tableOid.
>
> Actually, I did this to match the code in ExecBatchInsert(), but that
> seems like a good idea, so I’ll update the patch as such in the next
> version.
Done. I also adjusted the code in CopyMultiInsertBufferFlush() a bit
further. No functional changes. I put back in the original position
an assertion ensuring the FDW supports batching. Sorry for the back
and forth. Attached is an updated version of the patch.
Other changes are:
* The previous patch modified postgres_fdw.sql so that the existing
test cases for COPY FROM were tested in batch-insert mode. But I
think we should keep them as-is to test the default behavior, so I
added test cases for this feature by copying-and-pasting some of the
existing test cases. Also, the previous patch added this:
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+ server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+ server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+ begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+ for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
Rather than doing so, I think it would be better to use a partitioned
table defined in the above section “test tuple routing for
foreign-table partitions”, to save cycles. So I changed this as such.
* I modified comments a bit further and updated docs.
That is it. I will review the patch a bit more, but I feel that it is
in good shape.
Best regards,
Etsuro Fujita
Attachments:
v4-0001-Implementation-of-a-Bulk-COPY-FROM-efujita-3.patch (application/octet-stream, 23.9K) download
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2e4e82a94f..d24e050f41 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8605,6 +8605,39 @@ select tableoid::regclass, * FROM remp1;
remp1 | 1 | bar
(2 rows)
+delete from ctrtest;
+-- Test copy tuple routing with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+copy ctrtest from stdin;
+select tableoid::regclass, * FROM ctrtest;
+ tableoid | a | b
+----------+---+-------
+ remp1 | 1 | foo
+ remp1 | 1 | bar
+ remp1 | 1 | test1
+ remp2 | 2 | baz
+ remp2 | 2 | qux
+ remp2 | 2 | test2
+(6 rows)
+
+select tableoid::regclass, * FROM remp1;
+ tableoid | a | b
+----------+---+-------
+ remp1 | 1 | foo
+ remp1 | 1 | bar
+ remp1 | 1 | test1
+(3 rows)
+
+select tableoid::regclass, * FROM remp2;
+ tableoid | b | a
+----------+-------+---
+ remp2 | baz | 2
+ remp2 | qux | 2
+ remp2 | test2 | 2
+(3 rows)
+
+delete from ctrtest;
+alter server loopback options (drop batch_size);
drop table ctrtest;
drop table loct1;
drop table loct2;
@@ -8768,6 +8801,78 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- Test COPY FROM with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+-- Test basic functionality
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+-----
+ 1 | foo
+ 2 | bar
+ 3 | baz
+(3 rows)
+
+delete from rem2;
+-- Test check constraints
+alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+-- check constraint is enforced on the remote side, not locally
+copy rem2 from stdin;
+copy rem2 from stdin; -- ERROR
+ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
+DETAIL: Failing row contains (-1, xyzzy).
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
+COPY rem2
+select * from rem2;
+ f1 | f2
+----+-----
+ 1 | foo
+ 2 | bar
+ 3 | baz
+(3 rows)
+
+alter foreign table rem2 drop constraint rem2_f1positive;
+alter table loc2 drop constraint loc2_f1positive;
+delete from rem2;
+-- Test remote triggers
+create trigger trig_row_before_insert before insert on loc2
+ for each row execute procedure trig_row_before_insupdate();
+-- The new values are concatenated with ' triggered !'
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+-----------------
+ 1 | foo triggered !
+ 2 | bar triggered !
+ 3 | baz triggered !
+(3 rows)
+
+drop trigger trig_row_before_insert on loc2;
+delete from rem2;
+create trigger trig_null before insert on loc2
+ for each row execute procedure trig_null();
+-- Nothing happens
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+drop trigger trig_null on loc2;
+delete from rem2;
+-- Check with zero-columns foreign table; batch insert will be disabled
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(3 rows)
+
+delete from rem2;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index dd858aba03..b0b548d30e 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -2059,6 +2059,15 @@ postgresGetForeignModifyBatchSize(ResultRelInfo *resultRelInfo)
resultRelInfo->ri_TrigDesc->trig_insert_after_row)))
return 1;
+ /*
+ * If the foreign table has no columns, disable batching as the INSERT
+ * syntax doesn't allow batching multiple empty rows into a zero-column
+ * table in a single statement. This is needed for COPY FROM, in which
+ * case fmstate must be non-NULL.
+ */
+ if (fmstate && list_length(fmstate->target_attrs) == 0)
+ return 1;
+
/*
* Otherwise use the batch size specified for server/table. The number of
* parameters in a batch is limited to 65535 (uint16), so make sure we
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index e48ccd286b..8c886d0b29 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2373,6 +2373,28 @@ copy remp1 from stdin;
select tableoid::regclass, * FROM remp1;
+delete from ctrtest;
+
+-- Test copy tuple routing with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+
+copy ctrtest from stdin;
+1 foo
+1 bar
+2 baz
+2 qux
+1 test1
+2 test2
+\.
+
+select tableoid::regclass, * FROM ctrtest;
+select tableoid::regclass, * FROM remp1;
+select tableoid::regclass, * FROM remp2;
+
+delete from ctrtest;
+
+alter server loopback options (drop batch_size);
+
drop table ctrtest;
drop table loct1;
drop table loct2;
@@ -2527,6 +2549,86 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- Test COPY FROM with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+
+-- Test basic functionality
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+delete from rem2;
+
+-- Test check constraints
+alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+
+-- check constraint is enforced on the remote side, not locally
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+copy rem2 from stdin; -- ERROR
+-1 xyzzy
+\.
+select * from rem2;
+
+alter foreign table rem2 drop constraint rem2_f1positive;
+alter table loc2 drop constraint loc2_f1positive;
+
+delete from rem2;
+
+-- Test remote triggers
+create trigger trig_row_before_insert before insert on loc2
+ for each row execute procedure trig_row_before_insupdate();
+
+-- The new values are concatenated with ' triggered !'
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+drop trigger trig_row_before_insert on loc2;
+
+delete from rem2;
+
+create trigger trig_null before insert on loc2
+ for each row execute procedure trig_null();
+
+-- Nothing happens
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+drop trigger trig_null on loc2;
+
+delete from rem2;
+
+-- Check with zero-columns foreign table; batch insert will be disabled
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+
+\.
+select * from rem2;
+
+delete from rem2;
+
+alter server loopback options (drop batch_size);
+
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index d0b5951019..94263c628f 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -665,7 +665,9 @@ ExecForeignBatchInsert(EState *estate,
<para>
Note that this function is also called when inserting routed tuples into
- a foreign-table partition. See the callback functions
+ a foreign-table partition or executing <command>COPY FROM</command> on
+ a foreign table, in which case it is called in a different way than it
+ is in the <command>INSERT</command> case. See the callback functions
described below that allow the FDW to support that.
</para>
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index bfd344cdc0..527f4deaaa 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -398,6 +398,10 @@ OPTIONS (ADD password_required 'false');
exceeds the limit, the <literal>batch_size</literal> will be adjusted to
avoid an error.
</para>
+
+ <para>
+ This option also applies when copying into foreign tables.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 175aa837f2..2fe4e0067e 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -116,6 +116,12 @@ CopyFromErrorCallback(void *arg)
{
CopyFromState cstate = (CopyFromState) arg;
+ if (cstate->relname_only)
+ {
+ errcontext("COPY %s",
+ cstate->cur_relname);
+ return;
+ }
if (cstate->opts.binary)
{
/* can't usefully display the data */
@@ -222,7 +228,7 @@ CopyMultiInsertBufferInit(ResultRelInfo *rri)
buffer = (CopyMultiInsertBuffer *) palloc(sizeof(CopyMultiInsertBuffer));
memset(buffer->slots, 0, sizeof(TupleTableSlot *) * MAX_BUFFERED_TUPLES);
buffer->resultRelInfo = rri;
- buffer->bistate = GetBulkInsertState();
+ buffer->bistate = (rri->ri_FdwRoutine == NULL) ? GetBulkInsertState() : NULL;
buffer->nused = 0;
return buffer;
@@ -299,83 +305,164 @@ CopyMultiInsertInfoIsEmpty(CopyMultiInsertInfo *miinfo)
*/
static inline void
CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
- CopyMultiInsertBuffer *buffer)
+ CopyMultiInsertBuffer *buffer,
+ int64 *processed)
{
- MemoryContext oldcontext;
- int i;
- uint64 save_cur_lineno;
CopyFromState cstate = miinfo->cstate;
EState *estate = miinfo->estate;
- CommandId mycid = miinfo->mycid;
- int ti_options = miinfo->ti_options;
- bool line_buf_valid = cstate->line_buf_valid;
int nused = buffer->nused;
ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
TupleTableSlot **slots = buffer->slots;
+ int i;
- /*
- * Print error context information correctly, if one of the operations
- * below fails.
- */
- cstate->line_buf_valid = false;
- save_cur_lineno = cstate->cur_lineno;
+ if (resultRelInfo->ri_FdwRoutine)
+ {
+ int batch_size = resultRelInfo->ri_BatchSize;
+ int sent = 0;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ /* Ensure that the FDW supports batching and it's enabled */
+ Assert(resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert);
+ Assert(batch_size > 1);
- for (i = 0; i < nused; i++)
- {
/*
- * If there are any indexes, update them for all the inserted tuples,
- * and run AFTER ROW INSERT triggers.
+ * We suppress error context information other than the relation name,
+ * if one of the operations below fails.
*/
- if (resultRelInfo->ri_NumIndices > 0)
+ Assert(!cstate->relname_only);
+ cstate->relname_only = true;
+
+ while (sent < nused)
{
- List *recheckIndexes;
-
- cstate->cur_lineno = buffer->linenos[i];
- recheckIndexes =
- ExecInsertIndexTuples(resultRelInfo,
- buffer->slots[i], estate, false, false,
- NULL, NIL);
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], recheckIndexes,
- cstate->transition_capture);
- list_free(recheckIndexes);
+ int size = (batch_size < nused - sent) ? batch_size : (nused - sent);
+ int inserted = size;
+ TupleTableSlot **rslots;
+
+ /* Batch insert into foreign table */
+ rslots =
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+
+ sent += size;
+
+ /* No need to do anything if there are no rows inserted */
+ if (inserted <= 0)
+ continue;
+
+ /* Run AFTER ROW INSERT triggers */
+ if (resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ Oid relid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
+
+ for (i = 0; i < inserted; i++)
+ {
+ TupleTableSlot *slot = rslots[i];
+
+ /*
+ * AFTER ROW Triggers might reference the tableoid column,
+ * so (re-)initialize tts_tableOid before evaluating them.
+ */
+ slot->tts_tableOid = relid;
+
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slot, NIL,
+ cstate->transition_capture);
+ }
+ }
+
+ /* Update the row counter and progress of the COPY command */
+ *processed += inserted;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
}
+ for (i = 0; i < nused; i++)
+ ExecClearTuple(slots[i]);
+
+ /* reset relname_only */
+ cstate->relname_only = false;
+ }
+ else
+ {
+ CommandId mycid = miinfo->mycid;
+ int ti_options = miinfo->ti_options;
+ bool line_buf_valid = cstate->line_buf_valid;
+ uint64 save_cur_lineno = cstate->cur_lineno;
+ MemoryContext oldcontext;
+
/*
- * There's no indexes, but see if we need to run AFTER ROW INSERT
- * triggers anyway.
+ * Print error context information correctly, if one of the operations
+ * below fails.
*/
- else if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ cstate->line_buf_valid = false;
+
+ /*
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+
+ for (i = 0; i < nused; i++)
{
- cstate->cur_lineno = buffer->linenos[i];
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], NIL, cstate->transition_capture);
+ /*
+ * If there are any indexes, update them for all the inserted
+ * tuples, and run AFTER ROW INSERT triggers.
+ */
+ if (resultRelInfo->ri_NumIndices > 0)
+ {
+ List *recheckIndexes;
+
+ cstate->cur_lineno = buffer->linenos[i];
+ recheckIndexes =
+ ExecInsertIndexTuples(resultRelInfo,
+ buffer->slots[i], estate, false,
+ false, NULL, NIL);
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], recheckIndexes,
+ cstate->transition_capture);
+ list_free(recheckIndexes);
+ }
+
+ /*
+ * There's no indexes, but see if we need to run AFTER ROW INSERT
+ * triggers anyway.
+ */
+ else if (resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ cstate->cur_lineno = buffer->linenos[i];
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], NIL,
+ cstate->transition_capture);
+ }
+
+ ExecClearTuple(slots[i]);
}
- ExecClearTuple(slots[i]);
+ /* Update the row counter and progress of the COPY command */
+ *processed += nused;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
+
+ /* reset cur_lineno and line_buf_valid to what they were */
+ cstate->line_buf_valid = line_buf_valid;
+ cstate->cur_lineno = save_cur_lineno;
}
/* Mark that all slots are free */
buffer->nused = 0;
-
- /* reset cur_lineno and line_buf_valid to what they were */
- cstate->line_buf_valid = line_buf_valid;
- cstate->cur_lineno = save_cur_lineno;
}
/*
@@ -387,22 +474,25 @@ static inline void
CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
CopyMultiInsertBuffer *buffer)
{
+ ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
int i;
/* Ensure buffer was flushed */
Assert(buffer->nused == 0);
/* Remove back-link to ourself */
- buffer->resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+ resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
- FreeBulkInsertState(buffer->bistate);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ FreeBulkInsertState(buffer->bistate);
/* Since we only create slots on demand, just drop the non-null ones. */
for (i = 0; i < MAX_BUFFERED_TUPLES && buffer->slots[i] != NULL; i++)
ExecDropSingleTupleTableSlot(buffer->slots[i]);
- table_finish_bulk_insert(buffer->resultRelInfo->ri_RelationDesc,
- miinfo->ti_options);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ table_finish_bulk_insert(resultRelInfo->ri_RelationDesc,
+ miinfo->ti_options);
pfree(buffer);
}
@@ -418,7 +508,8 @@ CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
* 'curr_rri'.
*/
static inline void
-CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
+CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri,
+ int64 *processed)
{
ListCell *lc;
@@ -426,7 +517,7 @@ CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
{
CopyMultiInsertBuffer *buffer = (CopyMultiInsertBuffer *) lfirst(lc);
- CopyMultiInsertBufferFlush(miinfo, buffer);
+ CopyMultiInsertBufferFlush(miinfo, buffer, processed);
}
miinfo->bufferedTuples = 0;
@@ -679,6 +770,23 @@ CopyFrom(CopyFromState cstate)
resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
resultRelInfo);
+ /*
+ * Also, if the named relation is a foreign table, determine if the FDW
+ * supports batch insert and determine the batch size (a FDW may support
+ * batching, but it may be disabled for the server/table).
+ *
+ * If the FDW does not support batching, we set the batch size to 1.
+ */
+ if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+ else
+ resultRelInfo->ri_BatchSize = 1;
+
+ Assert(resultRelInfo->ri_BatchSize >= 1);
+
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -708,10 +816,11 @@ CopyFrom(CopyFromState cstate)
/*
* It's generally more efficient to prepare a bunch of tuples for
- * insertion, and insert them in one table_multi_insert() call, than call
- * table_tuple_insert() separately for every tuple. However, there are a
- * number of reasons why we might not be able to do this. These are
- * explained below.
+ * insertion, and insert them in one
+ * table_multi_insert()/ExecForeignBatchInsert() call, than call
+ * table_tuple_insert()/ExecForeignInsert() separately for every tuple.
+ * However, there are a number of reasons why we might not be able to do
+ * this. These are explained below.
*/
if (resultRelInfo->ri_TrigDesc != NULL &&
(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
@@ -725,6 +834,15 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
+ else if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_BatchSize == 1)
+ {
+ /*
+ * Can't support multi-inserts to a foreign table if the FDW does not
+ * support batching, or it's disabled for the server or foreign table.
+ */
+ insertMethod = CIM_SINGLE;
+ }
else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
resultRelInfo->ri_TrigDesc->trig_insert_new_table)
{
@@ -737,14 +855,12 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
+ else if (cstate->volatile_defexprs)
{
/*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
+ * Can't support multi-inserts if there are any volatile default
+ * expressions in the table. Similarly to the trigger case above,
+ * such expressions may query the table we're inserting into.
*
* Note: It does not matter if any partitions have any volatile
* default expressions as we use the defaults from the target of the
@@ -767,13 +883,14 @@ CopyFrom(CopyFromState cstate)
* For partitioned tables, we may still be able to perform bulk
* inserts. However, the possibility of this depends on which types
* of triggers exist on the partition. We must disable bulk inserts
- * if the partition is a foreign table or it has any before row insert
- * or insert instead triggers (same as we checked above for the parent
- * table). Since the partition's resultRelInfos are initialized only
- * when we actually need to insert the first tuple into them, we must
- * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
- * flag that we must later determine if we can use bulk-inserts for
- * the partition being inserted into.
+ * if the partition is a foreign table that can't use batching or it
+ * has any before row insert or insert instead triggers (same as we
+ * checked above for the parent table). Since the partition's
+ * resultRelInfos are initialized only when we actually need to insert
+ * the first tuple into them, we must have the intermediate insert
+ * method of CIM_MULTI_CONDITIONAL to flag that we must later
+ * determine if we can use bulk-inserts for the partition being
+ * inserted into.
*/
if (proute)
insertMethod = CIM_MULTI_CONDITIONAL;
@@ -910,12 +1027,14 @@ CopyFrom(CopyFromState cstate)
/*
* Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
+ * OF triggers, or if the partition is a foreign table that
+ * can't use batching.
*/
leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
!has_before_insert_row_trig &&
!has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
+ (resultRelInfo->ri_FdwRoutine == NULL ||
+ resultRelInfo->ri_BatchSize > 1);
/* Set the multi-insert buffer to use for this partition. */
if (leafpart_use_multi_insert)
@@ -931,7 +1050,9 @@ CopyFrom(CopyFromState cstate)
* Flush pending inserts if this partition can't use
* batching, so rows are visible to triggers etc.
*/
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
}
if (bistate != NULL)
@@ -1067,7 +1188,17 @@ CopyFrom(CopyFromState cstate)
* buffers out to their tables.
*/
if (CopyMultiInsertInfoIsFull(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
+
+ /*
+ * We delay updating the row counter and progress of the
+ * COPY command until after writing the tuples stored in
+ * the buffer out to the table. See
+ * CopyMultiInsertBufferFlush().
+ */
+ continue; /* next tuple please */
}
else
{
@@ -1130,7 +1261,7 @@ CopyFrom(CopyFromState cstate)
if (insertMethod != CIM_SINGLE)
{
if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL, &processed);
}
/* Done, clean up */
@@ -1348,6 +1479,7 @@ BeginCopyFrom(ParseState *pstate,
cstate->cur_lineno = 0;
cstate->cur_attname = NULL;
cstate->cur_attval = NULL;
+ cstate->relname_only = false;
/*
* Allocate buffers for the input pipeline.
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index e37c6032ae..21e8b89baa 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -81,6 +81,7 @@ typedef struct CopyFromStateData
uint64 cur_lineno; /* line number for error messages */
const char *cur_attname; /* current att for error messages */
const char *cur_attval; /* current att value for error messages */
+ bool relname_only; /* don't output line number, att, etc. */
/*
* Working state
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-07 06:18 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-10-07 06:18 UTC (permalink / raw)
To: Zhihong Yu <zyu@yugabyte.com>; +Cc: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Tue, Sep 27, 2022 at 6:03 PM Etsuro Fujita <etsuro.fujita@gmail.com> wrote:
> I will review the patch a bit more, but I feel that it is
> in good shape.
One thing I noticed is this bit added to CopyMultiInsertBufferFlush()
to run triggers on the foreign table.
+ /* Run AFTER ROW INSERT triggers */
+ if (resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ Oid relid =
RelationGetRelid(resultRelInfo->ri_RelationDesc);
+
+ for (i = 0; i < inserted; i++)
+ {
+ TupleTableSlot *slot = rslots[i];
+
+ /*
+ * AFTER ROW Triggers might reference the tableoid column,
+ * so (re-)initialize tts_tableOid before evaluating them.
+ */
+ slot->tts_tableOid = relid;
+
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slot, NIL,
+ cstate->transition_capture);
+ }
+ }
Since foreign tables cannot have transition tables, we have
trig_insert_new_table=false. So I simplified the if test and added an
assertion ensuring trig_insert_new_table=false. Attached is a new
version of the patch. I tweaked some comments a bit as well. I think
the patch is committable. So I plan on committing it next week if
there are no objections.
Best regards,
Etsuro Fujita
Attachments:
v4-0001-Implementation-of-a-Bulk-COPY-FROM-efujita-4.patch (application/octet-stream, 24.0K) download
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2e4e82a94f..ffff8d066a 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8605,6 +8605,39 @@ select tableoid::regclass, * FROM remp1;
remp1 | 1 | bar
(2 rows)
+delete from ctrtest;
+-- Test copy tuple routing with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+copy ctrtest from stdin;
+select tableoid::regclass, * FROM ctrtest;
+ tableoid | a | b
+----------+---+-------
+ remp1 | 1 | foo
+ remp1 | 1 | bar
+ remp1 | 1 | test1
+ remp2 | 2 | baz
+ remp2 | 2 | qux
+ remp2 | 2 | test2
+(6 rows)
+
+select tableoid::regclass, * FROM remp1;
+ tableoid | a | b
+----------+---+-------
+ remp1 | 1 | foo
+ remp1 | 1 | bar
+ remp1 | 1 | test1
+(3 rows)
+
+select tableoid::regclass, * FROM remp2;
+ tableoid | b | a
+----------+-------+---
+ remp2 | baz | 2
+ remp2 | qux | 2
+ remp2 | test2 | 2
+(3 rows)
+
+delete from ctrtest;
+alter server loopback options (drop batch_size);
drop table ctrtest;
drop table loct1;
drop table loct2;
@@ -8768,6 +8801,78 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- Test COPY FROM with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+-- Test basic functionality
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+-----
+ 1 | foo
+ 2 | bar
+ 3 | baz
+(3 rows)
+
+delete from rem2;
+-- Test check constraints
+alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+-- check constraint is enforced on the remote side, not locally
+copy rem2 from stdin;
+copy rem2 from stdin; -- ERROR
+ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
+DETAIL: Failing row contains (-1, xyzzy).
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
+COPY rem2
+select * from rem2;
+ f1 | f2
+----+-----
+ 1 | foo
+ 2 | bar
+ 3 | baz
+(3 rows)
+
+alter foreign table rem2 drop constraint rem2_f1positive;
+alter table loc2 drop constraint loc2_f1positive;
+delete from rem2;
+-- Test remote triggers
+create trigger trig_row_before_insert before insert on loc2
+ for each row execute procedure trig_row_before_insupdate();
+-- The new values are concatenated with ' triggered !'
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+-----------------
+ 1 | foo triggered !
+ 2 | bar triggered !
+ 3 | baz triggered !
+(3 rows)
+
+drop trigger trig_row_before_insert on loc2;
+delete from rem2;
+create trigger trig_null before insert on loc2
+ for each row execute procedure trig_null();
+-- Nothing happens
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+drop trigger trig_null on loc2;
+delete from rem2;
+-- Check with zero-column foreign table; batch insert will be disabled
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(3 rows)
+
+delete from rem2;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index dd858aba03..b0b548d30e 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -2059,6 +2059,15 @@ postgresGetForeignModifyBatchSize(ResultRelInfo *resultRelInfo)
resultRelInfo->ri_TrigDesc->trig_insert_after_row)))
return 1;
+ /*
+ * If the foreign table has no columns, disable batching as the INSERT
+ * syntax doesn't allow batching multiple empty rows into a zero-column
+ * table in a single statement. This is needed for COPY FROM, in which
+ * case fmstate must be non-NULL.
+ */
+ if (fmstate && list_length(fmstate->target_attrs) == 0)
+ return 1;
+
/*
* Otherwise use the batch size specified for server/table. The number of
* parameters in a batch is limited to 65535 (uint16), so make sure we
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index e48ccd286b..1962051e54 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2373,6 +2373,28 @@ copy remp1 from stdin;
select tableoid::regclass, * FROM remp1;
+delete from ctrtest;
+
+-- Test copy tuple routing with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+
+copy ctrtest from stdin;
+1 foo
+1 bar
+2 baz
+2 qux
+1 test1
+2 test2
+\.
+
+select tableoid::regclass, * FROM ctrtest;
+select tableoid::regclass, * FROM remp1;
+select tableoid::regclass, * FROM remp2;
+
+delete from ctrtest;
+
+alter server loopback options (drop batch_size);
+
drop table ctrtest;
drop table loct1;
drop table loct2;
@@ -2527,6 +2549,86 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- Test COPY FROM with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+
+-- Test basic functionality
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+delete from rem2;
+
+-- Test check constraints
+alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+
+-- check constraint is enforced on the remote side, not locally
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+copy rem2 from stdin; -- ERROR
+-1 xyzzy
+\.
+select * from rem2;
+
+alter foreign table rem2 drop constraint rem2_f1positive;
+alter table loc2 drop constraint loc2_f1positive;
+
+delete from rem2;
+
+-- Test remote triggers
+create trigger trig_row_before_insert before insert on loc2
+ for each row execute procedure trig_row_before_insupdate();
+
+-- The new values are concatenated with ' triggered !'
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+drop trigger trig_row_before_insert on loc2;
+
+delete from rem2;
+
+create trigger trig_null before insert on loc2
+ for each row execute procedure trig_null();
+
+-- Nothing happens
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+drop trigger trig_null on loc2;
+
+delete from rem2;
+
+-- Check with zero-column foreign table; batch insert will be disabled
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+
+\.
+select * from rem2;
+
+delete from rem2;
+
+alter server loopback options (drop batch_size);
+
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index d0b5951019..94263c628f 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -665,7 +665,9 @@ ExecForeignBatchInsert(EState *estate,
<para>
Note that this function is also called when inserting routed tuples into
- a foreign-table partition. See the callback functions
+ a foreign-table partition or executing <command>COPY FROM</command> on
+ a foreign table, in which case it is called in a different way than it
+ is in the <command>INSERT</command> case. See the callback functions
described below that allow the FDW to support that.
</para>
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index bfd344cdc0..527f4deaaa 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -398,6 +398,10 @@ OPTIONS (ADD password_required 'false');
exceeds the limit, the <literal>batch_size</literal> will be adjusted to
avoid an error.
</para>
+
+ <para>
+ This option also applies when copying into foreign tables.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 175aa837f2..12de3255da 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -116,6 +116,12 @@ CopyFromErrorCallback(void *arg)
{
CopyFromState cstate = (CopyFromState) arg;
+ if (cstate->relname_only)
+ {
+ errcontext("COPY %s",
+ cstate->cur_relname);
+ return;
+ }
if (cstate->opts.binary)
{
/* can't usefully display the data */
@@ -222,7 +228,7 @@ CopyMultiInsertBufferInit(ResultRelInfo *rri)
buffer = (CopyMultiInsertBuffer *) palloc(sizeof(CopyMultiInsertBuffer));
memset(buffer->slots, 0, sizeof(TupleTableSlot *) * MAX_BUFFERED_TUPLES);
buffer->resultRelInfo = rri;
- buffer->bistate = GetBulkInsertState();
+ buffer->bistate = (rri->ri_FdwRoutine == NULL) ? GetBulkInsertState() : NULL;
buffer->nused = 0;
return buffer;
@@ -299,83 +305,167 @@ CopyMultiInsertInfoIsEmpty(CopyMultiInsertInfo *miinfo)
*/
static inline void
CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
- CopyMultiInsertBuffer *buffer)
+ CopyMultiInsertBuffer *buffer,
+ int64 *processed)
{
- MemoryContext oldcontext;
- int i;
- uint64 save_cur_lineno;
CopyFromState cstate = miinfo->cstate;
EState *estate = miinfo->estate;
- CommandId mycid = miinfo->mycid;
- int ti_options = miinfo->ti_options;
- bool line_buf_valid = cstate->line_buf_valid;
int nused = buffer->nused;
ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
TupleTableSlot **slots = buffer->slots;
+ int i;
- /*
- * Print error context information correctly, if one of the operations
- * below fails.
- */
- cstate->line_buf_valid = false;
- save_cur_lineno = cstate->cur_lineno;
+ if (resultRelInfo->ri_FdwRoutine)
+ {
+ int batch_size = resultRelInfo->ri_BatchSize;
+ int sent = 0;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ /* Ensure that the FDW supports batching and it's enabled */
+ Assert(resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert);
+ Assert(batch_size > 1);
- for (i = 0; i < nused; i++)
- {
/*
- * If there are any indexes, update them for all the inserted tuples,
- * and run AFTER ROW INSERT triggers.
+ * We suppress error context information other than the relation name,
+ * if one of the operations below fails.
*/
- if (resultRelInfo->ri_NumIndices > 0)
+ Assert(!cstate->relname_only);
+ cstate->relname_only = true;
+
+ while (sent < nused)
{
- List *recheckIndexes;
-
- cstate->cur_lineno = buffer->linenos[i];
- recheckIndexes =
- ExecInsertIndexTuples(resultRelInfo,
- buffer->slots[i], estate, false, false,
- NULL, NIL);
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], recheckIndexes,
- cstate->transition_capture);
- list_free(recheckIndexes);
+ int size = (batch_size < nused - sent) ? batch_size : (nused - sent);
+ int inserted = size;
+ TupleTableSlot **rslots;
+
+ /* insert into foreign table: let the FDW do it */
+ rslots =
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+
+ sent += size;
+
+ /* No need to do anything if there are no rows inserted */
+ if (inserted <= 0)
+ continue;
+
+ /* Triggers on foreign tables should not have transition tables */
+ Assert(resultRelInfo->ri_TrigDesc == NULL ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table == false);
+
+ /* Run AFTER ROW INSERT triggers */
+ if (resultRelInfo->ri_TrigDesc != NULL &&
+ resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+ {
+ Oid relid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
+
+ for (i = 0; i < inserted; i++)
+ {
+ TupleTableSlot *slot = rslots[i];
+
+ /*
+ * AFTER ROW Triggers might reference the tableoid column,
+ * so (re-)initialize tts_tableOid before evaluating them.
+ */
+ slot->tts_tableOid = relid;
+
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slot, NIL,
+ cstate->transition_capture);
+ }
+ }
+
+ /* Update the row counter and progress of the COPY command */
+ *processed += inserted;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
}
+ for (i = 0; i < nused; i++)
+ ExecClearTuple(slots[i]);
+
+ /* reset relname_only */
+ cstate->relname_only = false;
+ }
+ else
+ {
+ CommandId mycid = miinfo->mycid;
+ int ti_options = miinfo->ti_options;
+ bool line_buf_valid = cstate->line_buf_valid;
+ uint64 save_cur_lineno = cstate->cur_lineno;
+ MemoryContext oldcontext;
+
+ /*
+ * Print error context information correctly, if one of the operations
+ * below fails.
+ */
+ cstate->line_buf_valid = false;
+
/*
- * There's no indexes, but see if we need to run AFTER ROW INSERT
- * triggers anyway.
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
*/
- else if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+
+ for (i = 0; i < nused; i++)
{
- cstate->cur_lineno = buffer->linenos[i];
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], NIL, cstate->transition_capture);
+ /*
+ * If there are any indexes, update them for all the inserted
+ * tuples, and run AFTER ROW INSERT triggers.
+ */
+ if (resultRelInfo->ri_NumIndices > 0)
+ {
+ List *recheckIndexes;
+
+ cstate->cur_lineno = buffer->linenos[i];
+ recheckIndexes =
+ ExecInsertIndexTuples(resultRelInfo,
+ buffer->slots[i], estate, false,
+ false, NULL, NIL);
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], recheckIndexes,
+ cstate->transition_capture);
+ list_free(recheckIndexes);
+ }
+
+ /*
+ * There's no indexes, but see if we need to run AFTER ROW INSERT
+ * triggers anyway.
+ */
+ else if (resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ cstate->cur_lineno = buffer->linenos[i];
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], NIL,
+ cstate->transition_capture);
+ }
+
+ ExecClearTuple(slots[i]);
}
- ExecClearTuple(slots[i]);
+ /* Update the row counter and progress of the COPY command */
+ *processed += nused;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
+
+ /* reset cur_lineno and line_buf_valid to what they were */
+ cstate->line_buf_valid = line_buf_valid;
+ cstate->cur_lineno = save_cur_lineno;
}
/* Mark that all slots are free */
buffer->nused = 0;
-
- /* reset cur_lineno and line_buf_valid to what they were */
- cstate->line_buf_valid = line_buf_valid;
- cstate->cur_lineno = save_cur_lineno;
}
/*
@@ -387,22 +477,25 @@ static inline void
CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
CopyMultiInsertBuffer *buffer)
{
+ ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
int i;
/* Ensure buffer was flushed */
Assert(buffer->nused == 0);
/* Remove back-link to ourself */
- buffer->resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+ resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
- FreeBulkInsertState(buffer->bistate);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ FreeBulkInsertState(buffer->bistate);
/* Since we only create slots on demand, just drop the non-null ones. */
for (i = 0; i < MAX_BUFFERED_TUPLES && buffer->slots[i] != NULL; i++)
ExecDropSingleTupleTableSlot(buffer->slots[i]);
- table_finish_bulk_insert(buffer->resultRelInfo->ri_RelationDesc,
- miinfo->ti_options);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ table_finish_bulk_insert(resultRelInfo->ri_RelationDesc,
+ miinfo->ti_options);
pfree(buffer);
}
@@ -418,7 +511,8 @@ CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
* 'curr_rri'.
*/
static inline void
-CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
+CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri,
+ int64 *processed)
{
ListCell *lc;
@@ -426,7 +520,7 @@ CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
{
CopyMultiInsertBuffer *buffer = (CopyMultiInsertBuffer *) lfirst(lc);
- CopyMultiInsertBufferFlush(miinfo, buffer);
+ CopyMultiInsertBufferFlush(miinfo, buffer, processed);
}
miinfo->bufferedTuples = 0;
@@ -679,6 +773,23 @@ CopyFrom(CopyFromState cstate)
resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
resultRelInfo);
+ /*
+ * Also, if the named relation is a foreign table, determine if the FDW
+ * supports batch insert and determine the batch size (a FDW may support
+ * batching, but it may be disabled for the server/table).
+ *
+ * If the FDW does not support batching, we set the batch size to 1.
+ */
+ if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+ else
+ resultRelInfo->ri_BatchSize = 1;
+
+ Assert(resultRelInfo->ri_BatchSize >= 1);
+
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -708,10 +819,11 @@ CopyFrom(CopyFromState cstate)
/*
* It's generally more efficient to prepare a bunch of tuples for
- * insertion, and insert them in one table_multi_insert() call, than call
- * table_tuple_insert() separately for every tuple. However, there are a
- * number of reasons why we might not be able to do this. These are
- * explained below.
+ * insertion, and insert them in one
+ * table_multi_insert()/ExecForeignBatchInsert() call, than call
+ * table_tuple_insert()/ExecForeignInsert() separately for every tuple.
+ * However, there are a number of reasons why we might not be able to do
+ * this. These are explained below.
*/
if (resultRelInfo->ri_TrigDesc != NULL &&
(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
@@ -725,6 +837,15 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
+ else if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_BatchSize == 1)
+ {
+ /*
+ * Can't support multi-inserts to a foreign table if the FDW does not
+ * support batching, or it's disabled for the server or foreign table.
+ */
+ insertMethod = CIM_SINGLE;
+ }
else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
resultRelInfo->ri_TrigDesc->trig_insert_new_table)
{
@@ -737,14 +858,12 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
+ else if (cstate->volatile_defexprs)
{
/*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
+ * Can't support multi-inserts if there are any volatile default
+ * expressions in the table. Similarly to the trigger case above,
+ * such expressions may query the table we're inserting into.
*
* Note: It does not matter if any partitions have any volatile
* default expressions as we use the defaults from the target of the
@@ -767,13 +886,14 @@ CopyFrom(CopyFromState cstate)
* For partitioned tables, we may still be able to perform bulk
* inserts. However, the possibility of this depends on which types
* of triggers exist on the partition. We must disable bulk inserts
- * if the partition is a foreign table or it has any before row insert
- * or insert instead triggers (same as we checked above for the parent
- * table). Since the partition's resultRelInfos are initialized only
- * when we actually need to insert the first tuple into them, we must
- * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
- * flag that we must later determine if we can use bulk-inserts for
- * the partition being inserted into.
+ * if the partition is a foreign table that can't use batching or it
+ * has any before row insert or insert instead triggers (same as we
+ * checked above for the parent table). Since the partition's
+ * resultRelInfos are initialized only when we actually need to insert
+ * the first tuple into them, we must have the intermediate insert
+ * method of CIM_MULTI_CONDITIONAL to flag that we must later
+ * determine if we can use bulk-inserts for the partition being
+ * inserted into.
*/
if (proute)
insertMethod = CIM_MULTI_CONDITIONAL;
@@ -910,12 +1030,14 @@ CopyFrom(CopyFromState cstate)
/*
* Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
+ * OF triggers, or if the partition is a foreign table that
+ * can't use batching.
*/
leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
!has_before_insert_row_trig &&
!has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
+ (resultRelInfo->ri_FdwRoutine == NULL ||
+ resultRelInfo->ri_BatchSize > 1);
/* Set the multi-insert buffer to use for this partition. */
if (leafpart_use_multi_insert)
@@ -931,7 +1053,9 @@ CopyFrom(CopyFromState cstate)
* Flush pending inserts if this partition can't use
* batching, so rows are visible to triggers etc.
*/
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
}
if (bistate != NULL)
@@ -1067,7 +1191,17 @@ CopyFrom(CopyFromState cstate)
* buffers out to their tables.
*/
if (CopyMultiInsertInfoIsFull(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
+
+ /*
+ * We delay updating the row counter and progress of the
+ * COPY command until after writing the tuples stored in
+ * the buffer out to the table, as in single insert mode.
+ * See CopyMultiInsertBufferFlush().
+ */
+ continue; /* next tuple please */
}
else
{
@@ -1130,7 +1264,7 @@ CopyFrom(CopyFromState cstate)
if (insertMethod != CIM_SINGLE)
{
if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL, &processed);
}
/* Done, clean up */
@@ -1348,6 +1482,7 @@ BeginCopyFrom(ParseState *pstate,
cstate->cur_lineno = 0;
cstate->cur_attname = NULL;
cstate->cur_attval = NULL;
+ cstate->relname_only = false;
/*
* Allocate buffers for the input pipeline.
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index e37c6032ae..21e8b89baa 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -81,6 +81,7 @@ typedef struct CopyFromStateData
uint64 cur_lineno; /* line number for error messages */
const char *cur_attname; /* current att for error messages */
const char *cur_attval; /* current att value for error messages */
+ bool relname_only; /* don't output line number, att, etc. */
/*
* Working state
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-11 06:06 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-10-11 06:06 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; Zhihong Yu <zyu@yugabyte.com>; +Cc: Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 10/7/22 11:18, Etsuro Fujita wrote:
> On Tue, Sep 27, 2022 at 6:03 PM Etsuro Fujita <etsuro.fujita@gmail.com> wrote:
>> I will review the patch a bit more, but I feel that it is
>> in good shape.
>
> One thing I noticed is this bit added to CopyMultiInsertBufferFlush()
> to run triggers on the foreign table.
>
> + /* Run AFTER ROW INSERT triggers */
> + if (resultRelInfo->ri_TrigDesc != NULL &&
> + (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
> + resultRelInfo->ri_TrigDesc->trig_insert_new_table))
> + {
> + Oid relid =
> RelationGetRelid(resultRelInfo->ri_RelationDesc);
> +
> + for (i = 0; i < inserted; i++)
> + {
> + TupleTableSlot *slot = rslots[i];
> +
> + /*
> + * AFTER ROW Triggers might reference the tableoid column,
> + * so (re-)initialize tts_tableOid before evaluating them.
> + */
> + slot->tts_tableOid = relid;
> +
> + ExecARInsertTriggers(estate, resultRelInfo,
> + slot, NIL,
> + cstate->transition_capture);
> + }
> + }
>
> Since foreign tables cannot have transition tables, we have
> trig_insert_new_table=false. So I simplified the if test and added an
> assertion ensuring trig_insert_new_table=false. Attached is a new
> version of the patch. I tweaked some comments a bit as well. I think
> the patch is committable. So I plan on committing it next week if
> there are no objections.
I reviewed the patch one more time. Only one question: bistate and
ri_FdwRoutine are strongly bounded. Maybe to add some assertion on
(ri_FdwRoutine XOR bistate) ? Just to prevent possible errors in future.
--
Regards
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-12 02:56 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-10-12 02:56 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Zhihong Yu <zyu@yugabyte.com>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Tue, Oct 11, 2022 at 3:06 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> I reviewed the patch one more time. Only one question: bistate and
> ri_FdwRoutine are strongly bounded. Maybe to add some assertion on
> (ri_FdwRoutine XOR bistate) ? Just to prevent possible errors in future.
You mean the bistate member of CopyMultiInsertBuffer?
We do not use that member at all for foreign tables, so the patch
avoids initializing that member in CopyMultiInsertBufferInit() when
called for a foreign table. So we have bistate = NULL for foreign
tables (and bistate != NULL for plain tables), as you mentioned above.
I think it is a good idea to add such assertions. How about adding
them to CopyMultiInsertBufferFlush() and
CopyMultiInsertBufferCleanup() like the attached? In the attached I
updated comments a bit further as well.
Thanks for reviewing!
Best regards,
Etsuro Fujita
Attachments:
v4-0001-Implementation-of-a-Bulk-COPY-FROM-efujita-5.patch (application/octet-stream, 25.4K) download
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index cc9e39c4a5..9746998751 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8608,6 +8608,39 @@ select tableoid::regclass, * FROM remp1;
remp1 | 1 | bar
(2 rows)
+delete from ctrtest;
+-- Test copy tuple routing with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+copy ctrtest from stdin;
+select tableoid::regclass, * FROM ctrtest;
+ tableoid | a | b
+----------+---+-------
+ remp1 | 1 | foo
+ remp1 | 1 | bar
+ remp1 | 1 | test1
+ remp2 | 2 | baz
+ remp2 | 2 | qux
+ remp2 | 2 | test2
+(6 rows)
+
+select tableoid::regclass, * FROM remp1;
+ tableoid | a | b
+----------+---+-------
+ remp1 | 1 | foo
+ remp1 | 1 | bar
+ remp1 | 1 | test1
+(3 rows)
+
+select tableoid::regclass, * FROM remp2;
+ tableoid | b | a
+----------+-------+---
+ remp2 | baz | 2
+ remp2 | qux | 2
+ remp2 | test2 | 2
+(3 rows)
+
+delete from ctrtest;
+alter server loopback options (drop batch_size);
drop table ctrtest;
drop table loct1;
drop table loct2;
@@ -8771,6 +8804,78 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- Test COPY FROM with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+-- Test basic functionality
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+-----
+ 1 | foo
+ 2 | bar
+ 3 | baz
+(3 rows)
+
+delete from rem2;
+-- Test check constraints
+alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+-- check constraint is enforced on the remote side, not locally
+copy rem2 from stdin;
+copy rem2 from stdin; -- ERROR
+ERROR: new row for relation "loc2" violates check constraint "loc2_f1positive"
+DETAIL: Failing row contains (-1, xyzzy).
+CONTEXT: remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
+COPY rem2
+select * from rem2;
+ f1 | f2
+----+-----
+ 1 | foo
+ 2 | bar
+ 3 | baz
+(3 rows)
+
+alter foreign table rem2 drop constraint rem2_f1positive;
+alter table loc2 drop constraint loc2_f1positive;
+delete from rem2;
+-- Test remote triggers
+create trigger trig_row_before_insert before insert on loc2
+ for each row execute procedure trig_row_before_insupdate();
+-- The new values are concatenated with ' triggered !'
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+-----------------
+ 1 | foo triggered !
+ 2 | bar triggered !
+ 3 | baz triggered !
+(3 rows)
+
+drop trigger trig_row_before_insert on loc2;
+delete from rem2;
+create trigger trig_null before insert on loc2
+ for each row execute procedure trig_null();
+-- Nothing happens
+copy rem2 from stdin;
+select * from rem2;
+ f1 | f2
+----+----
+(0 rows)
+
+drop trigger trig_null on loc2;
+delete from rem2;
+-- Check with zero-column foreign table; batch insert will be disabled
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(3 rows)
+
+delete from rem2;
+alter server loopback options (drop batch_size);
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 8d013f5b1a..d98709e5e8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -2057,6 +2057,15 @@ postgresGetForeignModifyBatchSize(ResultRelInfo *resultRelInfo)
resultRelInfo->ri_TrigDesc->trig_insert_after_row)))
return 1;
+ /*
+ * If the foreign table has no columns, disable batching as the INSERT
+ * syntax doesn't allow batching multiple empty rows into a zero-column
+ * table in a single statement. This is needed for COPY FROM, in which
+ * case fmstate must be non-NULL.
+ */
+ if (fmstate && list_length(fmstate->target_attrs) == 0)
+ return 1;
+
/*
* Otherwise use the batch size specified for server/table. The number of
* parameters in a batch is limited to 65535 (uint16), so make sure we
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index e48ccd286b..1962051e54 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2373,6 +2373,28 @@ copy remp1 from stdin;
select tableoid::regclass, * FROM remp1;
+delete from ctrtest;
+
+-- Test copy tuple routing with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+
+copy ctrtest from stdin;
+1 foo
+1 bar
+2 baz
+2 qux
+1 test1
+2 test2
+\.
+
+select tableoid::regclass, * FROM ctrtest;
+select tableoid::regclass, * FROM remp1;
+select tableoid::regclass, * FROM remp2;
+
+delete from ctrtest;
+
+alter server loopback options (drop batch_size);
+
drop table ctrtest;
drop table loct1;
drop table loct2;
@@ -2527,6 +2549,86 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- Test COPY FROM with the batch_size option enabled
+alter server loopback options (add batch_size '2');
+
+-- Test basic functionality
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+delete from rem2;
+
+-- Test check constraints
+alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+
+-- check constraint is enforced on the remote side, not locally
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+copy rem2 from stdin; -- ERROR
+-1 xyzzy
+\.
+select * from rem2;
+
+alter foreign table rem2 drop constraint rem2_f1positive;
+alter table loc2 drop constraint loc2_f1positive;
+
+delete from rem2;
+
+-- Test remote triggers
+create trigger trig_row_before_insert before insert on loc2
+ for each row execute procedure trig_row_before_insupdate();
+
+-- The new values are concatenated with ' triggered !'
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+drop trigger trig_row_before_insert on loc2;
+
+delete from rem2;
+
+create trigger trig_null before insert on loc2
+ for each row execute procedure trig_null();
+
+-- Nothing happens
+copy rem2 from stdin;
+1 foo
+2 bar
+3 baz
+\.
+select * from rem2;
+
+drop trigger trig_null on loc2;
+
+delete from rem2;
+
+-- Check with zero-column foreign table; batch insert will be disabled
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+
+\.
+select * from rem2;
+
+delete from rem2;
+
+alter server loopback options (drop batch_size);
+
-- ===================================================================
-- test for TRUNCATE
-- ===================================================================
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index d0b5951019..94263c628f 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -665,7 +665,9 @@ ExecForeignBatchInsert(EState *estate,
<para>
Note that this function is also called when inserting routed tuples into
- a foreign-table partition. See the callback functions
+ a foreign-table partition or executing <command>COPY FROM</command> on
+ a foreign table, in which case it is called in a different way than it
+ is in the <command>INSERT</command> case. See the callback functions
described below that allow the FDW to support that.
</para>
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index bfd344cdc0..527f4deaaa 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -398,6 +398,10 @@ OPTIONS (ADD password_required 'false');
exceeds the limit, the <literal>batch_size</literal> will be adjusted to
avoid an error.
</para>
+
+ <para>
+ This option also applies when copying into foreign tables.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 175aa837f2..f5af46166a 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -78,7 +78,8 @@ typedef struct CopyMultiInsertBuffer
{
TupleTableSlot *slots[MAX_BUFFERED_TUPLES]; /* Array to store tuples */
ResultRelInfo *resultRelInfo; /* ResultRelInfo for 'relid' */
- BulkInsertState bistate; /* BulkInsertState for this rel */
+ BulkInsertState bistate; /* BulkInsertState for this rel if plain table
+ * or NULL if foreign table */
int nused; /* number of 'slots' containing tuples */
uint64 linenos[MAX_BUFFERED_TUPLES]; /* Line # of tuple in copy
* stream */
@@ -116,6 +117,12 @@ CopyFromErrorCallback(void *arg)
{
CopyFromState cstate = (CopyFromState) arg;
+ if (cstate->relname_only)
+ {
+ errcontext("COPY %s",
+ cstate->cur_relname);
+ return;
+ }
if (cstate->opts.binary)
{
/* can't usefully display the data */
@@ -222,7 +229,7 @@ CopyMultiInsertBufferInit(ResultRelInfo *rri)
buffer = (CopyMultiInsertBuffer *) palloc(sizeof(CopyMultiInsertBuffer));
memset(buffer->slots, 0, sizeof(TupleTableSlot *) * MAX_BUFFERED_TUPLES);
buffer->resultRelInfo = rri;
- buffer->bistate = GetBulkInsertState();
+ buffer->bistate = (rri->ri_FdwRoutine == NULL) ? GetBulkInsertState() : NULL;
buffer->nused = 0;
return buffer;
@@ -299,83 +306,171 @@ CopyMultiInsertInfoIsEmpty(CopyMultiInsertInfo *miinfo)
*/
static inline void
CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
- CopyMultiInsertBuffer *buffer)
+ CopyMultiInsertBuffer *buffer,
+ int64 *processed)
{
- MemoryContext oldcontext;
- int i;
- uint64 save_cur_lineno;
CopyFromState cstate = miinfo->cstate;
EState *estate = miinfo->estate;
- CommandId mycid = miinfo->mycid;
- int ti_options = miinfo->ti_options;
- bool line_buf_valid = cstate->line_buf_valid;
int nused = buffer->nused;
ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
TupleTableSlot **slots = buffer->slots;
+ int i;
- /*
- * Print error context information correctly, if one of the operations
- * below fails.
- */
- cstate->line_buf_valid = false;
- save_cur_lineno = cstate->cur_lineno;
+ if (resultRelInfo->ri_FdwRoutine)
+ {
+ int batch_size = resultRelInfo->ri_BatchSize;
+ int sent = 0;
- /*
- * table_multi_insert may leak memory, so switch to short-lived memory
- * context before calling it.
- */
- oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
- table_multi_insert(resultRelInfo->ri_RelationDesc,
- slots,
- nused,
- mycid,
- ti_options,
- buffer->bistate);
- MemoryContextSwitchTo(oldcontext);
+ Assert(buffer->bistate == NULL);
+
+ /* Ensure that the FDW supports batching and it's enabled */
+ Assert(resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert);
+ Assert(batch_size > 1);
- for (i = 0; i < nused; i++)
- {
/*
- * If there are any indexes, update them for all the inserted tuples,
- * and run AFTER ROW INSERT triggers.
+ * We suppress error context information other than the relation name,
+ * if one of the operations below fails.
*/
- if (resultRelInfo->ri_NumIndices > 0)
+ Assert(!cstate->relname_only);
+ cstate->relname_only = true;
+
+ while (sent < nused)
{
- List *recheckIndexes;
-
- cstate->cur_lineno = buffer->linenos[i];
- recheckIndexes =
- ExecInsertIndexTuples(resultRelInfo,
- buffer->slots[i], estate, false, false,
- NULL, NIL);
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], recheckIndexes,
- cstate->transition_capture);
- list_free(recheckIndexes);
+ int size = (batch_size < nused - sent) ? batch_size : (nused - sent);
+ int inserted = size;
+ TupleTableSlot **rslots;
+
+ /* insert into foreign table: let the FDW do it */
+ rslots =
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert(estate,
+ resultRelInfo,
+ &slots[sent],
+ NULL,
+ &inserted);
+
+ sent += size;
+
+ /* No need to do anything if there are no inserted rows */
+ if (inserted <= 0)
+ continue;
+
+ /* Triggers on foreign tables should not have transition tables */
+ Assert(resultRelInfo->ri_TrigDesc == NULL ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table == false);
+
+ /* Run AFTER ROW INSERT triggers */
+ if (resultRelInfo->ri_TrigDesc != NULL &&
+ resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+ {
+ Oid relid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
+
+ for (i = 0; i < inserted; i++)
+ {
+ TupleTableSlot *slot = rslots[i];
+
+ /*
+ * AFTER ROW Triggers might reference the tableoid column,
+ * so (re-)initialize tts_tableOid before evaluating them.
+ */
+ slot->tts_tableOid = relid;
+
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slot, NIL,
+ cstate->transition_capture);
+ }
+ }
+
+ /* Update the row counter and progress of the COPY command */
+ *processed += inserted;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
}
+ for (i = 0; i < nused; i++)
+ ExecClearTuple(slots[i]);
+
+ /* reset relname_only */
+ cstate->relname_only = false;
+ }
+ else
+ {
+ CommandId mycid = miinfo->mycid;
+ int ti_options = miinfo->ti_options;
+ bool line_buf_valid = cstate->line_buf_valid;
+ uint64 save_cur_lineno = cstate->cur_lineno;
+ MemoryContext oldcontext;
+
+ Assert(buffer->bistate != NULL);
+
/*
- * There's no indexes, but see if we need to run AFTER ROW INSERT
- * triggers anyway.
+ * Print error context information correctly, if one of the operations
+ * below fails.
*/
- else if (resultRelInfo->ri_TrigDesc != NULL &&
- (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
- resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ cstate->line_buf_valid = false;
+
+ /*
+ * table_multi_insert may leak memory, so switch to short-lived memory
+ * context before calling it.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ table_multi_insert(resultRelInfo->ri_RelationDesc,
+ slots,
+ nused,
+ mycid,
+ ti_options,
+ buffer->bistate);
+ MemoryContextSwitchTo(oldcontext);
+
+ for (i = 0; i < nused; i++)
{
- cstate->cur_lineno = buffer->linenos[i];
- ExecARInsertTriggers(estate, resultRelInfo,
- slots[i], NIL, cstate->transition_capture);
+ /*
+ * If there are any indexes, update them for all the inserted
+ * tuples, and run AFTER ROW INSERT triggers.
+ */
+ if (resultRelInfo->ri_NumIndices > 0)
+ {
+ List *recheckIndexes;
+
+ cstate->cur_lineno = buffer->linenos[i];
+ recheckIndexes =
+ ExecInsertIndexTuples(resultRelInfo,
+ buffer->slots[i], estate, false,
+ false, NULL, NIL);
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], recheckIndexes,
+ cstate->transition_capture);
+ list_free(recheckIndexes);
+ }
+
+ /*
+ * There's no indexes, but see if we need to run AFTER ROW INSERT
+ * triggers anyway.
+ */
+ else if (resultRelInfo->ri_TrigDesc != NULL &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+ {
+ cstate->cur_lineno = buffer->linenos[i];
+ ExecARInsertTriggers(estate, resultRelInfo,
+ slots[i], NIL,
+ cstate->transition_capture);
+ }
+
+ ExecClearTuple(slots[i]);
}
- ExecClearTuple(slots[i]);
+ /* Update the row counter and progress of the COPY command */
+ *processed += nused;
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED,
+ *processed);
+
+ /* reset cur_lineno and line_buf_valid to what they were */
+ cstate->line_buf_valid = line_buf_valid;
+ cstate->cur_lineno = save_cur_lineno;
}
/* Mark that all slots are free */
buffer->nused = 0;
-
- /* reset cur_lineno and line_buf_valid to what they were */
- cstate->line_buf_valid = line_buf_valid;
- cstate->cur_lineno = save_cur_lineno;
}
/*
@@ -387,22 +482,30 @@ static inline void
CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
CopyMultiInsertBuffer *buffer)
{
+ ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
int i;
/* Ensure buffer was flushed */
Assert(buffer->nused == 0);
/* Remove back-link to ourself */
- buffer->resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+ resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
- FreeBulkInsertState(buffer->bistate);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ {
+ Assert(buffer->bistate != NULL);
+ FreeBulkInsertState(buffer->bistate);
+ }
+ else
+ Assert(buffer->bistate == NULL);
/* Since we only create slots on demand, just drop the non-null ones. */
for (i = 0; i < MAX_BUFFERED_TUPLES && buffer->slots[i] != NULL; i++)
ExecDropSingleTupleTableSlot(buffer->slots[i]);
- table_finish_bulk_insert(buffer->resultRelInfo->ri_RelationDesc,
- miinfo->ti_options);
+ if (resultRelInfo->ri_FdwRoutine == NULL)
+ table_finish_bulk_insert(resultRelInfo->ri_RelationDesc,
+ miinfo->ti_options);
pfree(buffer);
}
@@ -418,7 +521,8 @@ CopyMultiInsertBufferCleanup(CopyMultiInsertInfo *miinfo,
* 'curr_rri'.
*/
static inline void
-CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
+CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri,
+ int64 *processed)
{
ListCell *lc;
@@ -426,7 +530,7 @@ CopyMultiInsertInfoFlush(CopyMultiInsertInfo *miinfo, ResultRelInfo *curr_rri)
{
CopyMultiInsertBuffer *buffer = (CopyMultiInsertBuffer *) lfirst(lc);
- CopyMultiInsertBufferFlush(miinfo, buffer);
+ CopyMultiInsertBufferFlush(miinfo, buffer, processed);
}
miinfo->bufferedTuples = 0;
@@ -679,6 +783,23 @@ CopyFrom(CopyFromState cstate)
resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
resultRelInfo);
+ /*
+ * Also, if the named relation is a foreign table, determine if the FDW
+ * supports batch insert and determine the batch size (a FDW may support
+ * batching, but it may be disabled for the server/table).
+ *
+ * If the FDW does not support batching, we set the batch size to 1.
+ */
+ if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
+ resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
+ resultRelInfo->ri_BatchSize =
+ resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+ else
+ resultRelInfo->ri_BatchSize = 1;
+
+ Assert(resultRelInfo->ri_BatchSize >= 1);
+
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -708,10 +829,11 @@ CopyFrom(CopyFromState cstate)
/*
* It's generally more efficient to prepare a bunch of tuples for
- * insertion, and insert them in one table_multi_insert() call, than call
- * table_tuple_insert() separately for every tuple. However, there are a
- * number of reasons why we might not be able to do this. These are
- * explained below.
+ * insertion, and insert them in one
+ * table_multi_insert()/ExecForeignBatchInsert() call, than call
+ * table_tuple_insert()/ExecForeignInsert() separately for every tuple.
+ * However, there are a number of reasons why we might not be able to do
+ * this. These are explained below.
*/
if (resultRelInfo->ri_TrigDesc != NULL &&
(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
@@ -725,6 +847,15 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
+ else if (resultRelInfo->ri_FdwRoutine != NULL &&
+ resultRelInfo->ri_BatchSize == 1)
+ {
+ /*
+ * Can't support multi-inserts to a foreign table if the FDW does not
+ * support batching, or it's disabled for the server or foreign table.
+ */
+ insertMethod = CIM_SINGLE;
+ }
else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
resultRelInfo->ri_TrigDesc->trig_insert_new_table)
{
@@ -737,14 +868,12 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
- else if (resultRelInfo->ri_FdwRoutine != NULL ||
- cstate->volatile_defexprs)
+ else if (cstate->volatile_defexprs)
{
/*
- * Can't support multi-inserts to foreign tables or if there are any
- * volatile default expressions in the table. Similarly to the
- * trigger case above, such expressions may query the table we're
- * inserting into.
+ * Can't support multi-inserts if there are any volatile default
+ * expressions in the table. Similarly to the trigger case above,
+ * such expressions may query the table we're inserting into.
*
* Note: It does not matter if any partitions have any volatile
* default expressions as we use the defaults from the target of the
@@ -767,13 +896,14 @@ CopyFrom(CopyFromState cstate)
* For partitioned tables, we may still be able to perform bulk
* inserts. However, the possibility of this depends on which types
* of triggers exist on the partition. We must disable bulk inserts
- * if the partition is a foreign table or it has any before row insert
- * or insert instead triggers (same as we checked above for the parent
- * table). Since the partition's resultRelInfos are initialized only
- * when we actually need to insert the first tuple into them, we must
- * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
- * flag that we must later determine if we can use bulk-inserts for
- * the partition being inserted into.
+ * if the partition is a foreign table that can't use batching or it
+ * has any before row insert or insert instead triggers (same as we
+ * checked above for the parent table). Since the partition's
+ * resultRelInfos are initialized only when we actually need to insert
+ * the first tuple into them, we must have the intermediate insert
+ * method of CIM_MULTI_CONDITIONAL to flag that we must later
+ * determine if we can use bulk-inserts for the partition being
+ * inserted into.
*/
if (proute)
insertMethod = CIM_MULTI_CONDITIONAL;
@@ -910,12 +1040,14 @@ CopyFrom(CopyFromState cstate)
/*
* Disable multi-inserts when the partition has BEFORE/INSTEAD
- * OF triggers, or if the partition is a foreign partition.
+ * OF triggers, or if the partition is a foreign table that
+ * can't use batching.
*/
leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
!has_before_insert_row_trig &&
!has_instead_insert_row_trig &&
- resultRelInfo->ri_FdwRoutine == NULL;
+ (resultRelInfo->ri_FdwRoutine == NULL ||
+ resultRelInfo->ri_BatchSize > 1);
/* Set the multi-insert buffer to use for this partition. */
if (leafpart_use_multi_insert)
@@ -931,7 +1063,9 @@ CopyFrom(CopyFromState cstate)
* Flush pending inserts if this partition can't use
* batching, so rows are visible to triggers etc.
*/
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
}
if (bistate != NULL)
@@ -1067,7 +1201,17 @@ CopyFrom(CopyFromState cstate)
* buffers out to their tables.
*/
if (CopyMultiInsertInfoIsFull(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, resultRelInfo);
+ CopyMultiInsertInfoFlush(&multiInsertInfo,
+ resultRelInfo,
+ &processed);
+
+ /*
+ * We delay updating the row counter and progress of the
+ * COPY command until after writing the tuples stored in
+ * the buffer out to the table, as in single insert mode.
+ * See CopyMultiInsertBufferFlush().
+ */
+ continue; /* next tuple please */
}
else
{
@@ -1130,7 +1274,7 @@ CopyFrom(CopyFromState cstate)
if (insertMethod != CIM_SINGLE)
{
if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
- CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
+ CopyMultiInsertInfoFlush(&multiInsertInfo, NULL, &processed);
}
/* Done, clean up */
@@ -1348,6 +1492,7 @@ BeginCopyFrom(ParseState *pstate,
cstate->cur_lineno = 0;
cstate->cur_attname = NULL;
cstate->cur_attval = NULL;
+ cstate->relname_only = false;
/*
* Allocate buffers for the input pipeline.
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index e37c6032ae..8d9cc5accd 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -40,13 +40,16 @@ typedef enum EolType
} EolType;
/*
- * Represents the heap insert method to be used during COPY FROM.
+ * Represents the insert method to be used during COPY FROM.
*/
typedef enum CopyInsertMethod
{
- CIM_SINGLE, /* use table_tuple_insert or fdw routine */
- CIM_MULTI, /* always use table_multi_insert */
- CIM_MULTI_CONDITIONAL /* use table_multi_insert only if valid */
+ CIM_SINGLE, /* use table_tuple_insert or
+ * ExecForeignInsert */
+ CIM_MULTI, /* always use table_multi_insert or
+ * ExecForeignBatchInsert */
+ CIM_MULTI_CONDITIONAL /* use table_multi_insert or
+ * ExecForeignBatchInsert only if valid */
} CopyInsertMethod;
/*
@@ -81,6 +84,7 @@ typedef struct CopyFromStateData
uint64 cur_lineno; /* line number for error messages */
const char *cur_attname; /* current att for error messages */
const char *cur_attval; /* current att value for error messages */
+ bool relname_only; /* don't output line number, att, etc. */
/*
* Working state
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-13 04:38 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-10-13 04:38 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Zhihong Yu <zyu@yugabyte.com>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 10/12/22 07:56, Etsuro Fujita wrote:
> On Tue, Oct 11, 2022 at 3:06 PM Andrey Lepikhov
> <a.lepikhov@postgrespro.ru> wrote:
>> I reviewed the patch one more time. Only one question: bistate and
>> ri_FdwRoutine are strongly bounded. Maybe to add some assertion on
>> (ri_FdwRoutine XOR bistate) ? Just to prevent possible errors in future.
>
> You mean the bistate member of CopyMultiInsertBuffer?
Yes
>
> We do not use that member at all for foreign tables, so the patch
> avoids initializing that member in CopyMultiInsertBufferInit() when
> called for a foreign table. So we have bistate = NULL for foreign
> tables (and bistate != NULL for plain tables), as you mentioned above.
> I think it is a good idea to add such assertions. How about adding
> them to CopyMultiInsertBufferFlush() and
> CopyMultiInsertBufferCleanup() like the attached? In the attached I
> updated comments a bit further as well.
Yes, quite enough.
--
Regards
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-13 09:58 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-10-13 09:58 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Zhihong Yu <zyu@yugabyte.com>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Thu, Oct 13, 2022 at 1:38 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> On 10/12/22 07:56, Etsuro Fujita wrote:
> > On Tue, Oct 11, 2022 at 3:06 PM Andrey Lepikhov
> > <a.lepikhov@postgrespro.ru> wrote:
> >> I reviewed the patch one more time. Only one question: bistate and
> >> ri_FdwRoutine are strongly bounded. Maybe to add some assertion on
> >> (ri_FdwRoutine XOR bistate) ? Just to prevent possible errors in future.
> >
> > You mean the bistate member of CopyMultiInsertBuffer?
> Yes
> >
> > We do not use that member at all for foreign tables, so the patch
> > avoids initializing that member in CopyMultiInsertBufferInit() when
> > called for a foreign table. So we have bistate = NULL for foreign
> > tables (and bistate != NULL for plain tables), as you mentioned above.
> > I think it is a good idea to add such assertions. How about adding
> > them to CopyMultiInsertBufferFlush() and
> > CopyMultiInsertBufferCleanup() like the attached? In the attached I
> > updated comments a bit further as well.
> Yes, quite enough.
I have committed the patch after tweaking comments a little bit further.
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-28 10:12 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Etsuro Fujita @ 2022-10-28 10:12 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Zhihong Yu <zyu@yugabyte.com>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Thu, Oct 13, 2022 at 6:58 PM Etsuro Fujita <etsuro.fujita@gmail.com> wrote:
> I have committed the patch after tweaking comments a little bit further.
I think there is another patch that improves performance of COPY FROM
for foreign tables using COPY FROM STDIN, but if Andrey (or anyone
else) want to work on it again, I think it would be better to create a
new CF entry for it (and start a new thread for it). So I plan to
close this in the November CF unless they think otherwise.
Anyway, thanks for the patch, Andrey! Thanks for reviewing, Ian and Zhihong!
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-28 10:53 Andrey Lepikhov <a.lepikhov@postgrespro.ru>
parent: Etsuro Fujita <etsuro.fujita@gmail.com>
0 siblings, 1 reply; 39+ messages in thread
From: Andrey Lepikhov @ 2022-10-28 10:53 UTC (permalink / raw)
To: Etsuro Fujita <etsuro.fujita@gmail.com>; +Cc: Zhihong Yu <zyu@yugabyte.com>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On 28/10/2022 16:12, Etsuro Fujita wrote:
> On Thu, Oct 13, 2022 at 6:58 PM Etsuro Fujita <etsuro.fujita@gmail.com> wrote:
>> I have committed the patch after tweaking comments a little bit further.
>
> I think there is another patch that improves performance of COPY FROM
> for foreign tables using COPY FROM STDIN, but if Andrey (or anyone
> else) want to work on it again, I think it would be better to create a
> new CF entry for it (and start a new thread for it). So I plan to
> close this in the November CF unless they think otherwise.
>
> Anyway, thanks for the patch, Andrey! Thanks for reviewing, Ian and Zhihong!
Thanks,
I studied performance of this code in comparison to bulk INSERTions.
This patch seems to improve speed of insertion by about 20%. Also, this
patch is very invasive. So, I don't have any plans to work on it now.
--
regards,
Andrey Lepikhov
Postgres Professional
^ permalink raw reply [nested|flat] 39+ messages in thread
* Re: Fast COPY FROM based on batch insert
@ 2022-10-31 08:50 Etsuro Fujita <etsuro.fujita@gmail.com>
parent: Andrey Lepikhov <a.lepikhov@postgrespro.ru>
0 siblings, 0 replies; 39+ messages in thread
From: Etsuro Fujita @ 2022-10-31 08:50 UTC (permalink / raw)
To: Andrey Lepikhov <a.lepikhov@postgrespro.ru>; +Cc: Zhihong Yu <zyu@yugabyte.com>; Justin Pryzby <pryzby@telsasoft.com>; Amit Langote <amitlangote09@gmail.com>; tanghy.fnst@fujitsu.com, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; pgsql-hackers; Tomas Vondra <tomas.vondra@enterprisedb.com>; houzj.fnst@fujitsu.com
On Fri, Oct 28, 2022 at 7:53 PM Andrey Lepikhov
<a.lepikhov@postgrespro.ru> wrote:
> On 28/10/2022 16:12, Etsuro Fujita wrote:
> > I think there is another patch that improves performance of COPY FROM
> > for foreign tables using COPY FROM STDIN, but if Andrey (or anyone
> > else) want to work on it again, I think it would be better to create a
> > new CF entry for it (and start a new thread for it). So I plan to
> > close this in the November CF unless they think otherwise.
> I studied performance of this code in comparison to bulk INSERTions.
> This patch seems to improve speed of insertion by about 20%. Also, this
> patch is very invasive. So, I don't have any plans to work on it now.
Ok, let's leave that for future work. I closed this entry in the November CF.
Best regards,
Etsuro Fujita
^ permalink raw reply [nested|flat] 39+ messages in thread
end of thread, other threads:[~2022-10-31 08:50 UTC | newest]
Thread overview: 39+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2021-06-04 08:26 Fast COPY FROM based on batch insert Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2021-06-04 08:45 ` tsunakawa.takay@fujitsu.com <tsunakawa.takay@fujitsu.com>
2021-06-07 06:36 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2021-06-07 11:16 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-03-21 23:58 ` Andres Freund <andres@anarazel.de>
2022-03-22 02:36 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-03-22 01:54 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-03-24 06:43 ` Andrey V. Lepikhov <a.lepikhov@postgrespro.ru>
2022-07-07 03:14 ` Ian Barwick <ian.barwick@enterprisedb.com>
2022-07-07 13:51 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-07-08 02:12 ` Ian Barwick <ian.barwick@enterprisedb.com>
2022-07-08 15:09 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-07-11 01:12 ` Ian Barwick <ian.barwick@enterprisedb.com>
2022-07-11 03:54 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-07-18 08:22 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-07-19 09:35 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-07-20 08:10 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-07-22 06:39 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-07-22 08:14 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-07-22 08:42 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-07-26 10:19 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-08-09 11:44 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-08-09 16:13 ` Zhihong Yu <zyu@yugabyte.com>
2022-08-10 08:30 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-09-27 09:03 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-10-07 06:18 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-10-11 06:06 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-10-12 02:56 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-10-13 04:38 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-10-13 09:58 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-10-28 10:12 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-10-28 10:53 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-10-31 08:50 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-08-15 05:29 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-08-22 08:44 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-08-23 05:58 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-09-27 08:47 ` Etsuro Fujita <etsuro.fujita@gmail.com>
2022-07-27 05:42 ` Andrey Lepikhov <a.lepikhov@postgrespro.ru>
2022-08-01 04:00 ` Etsuro Fujita <etsuro.fujita@gmail.com>
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox