Feature #11467
Improve ImportWorker performance
100%
History
#1 Updated by Teodor Gorghe 2 months ago
ImportWorker is responsible for creating the schema from database, creating the indexes, copy the data from the progress dump into the database, etc (ant import.db task).
The implementation for copy of data from the dump into the database uses separate INSERT statements with executeBatch API from JDBC.
When working on #11399, I have found that there is a specific PostgresSQL statement, called COPY, which is faster than SQL INSERT, optimized for bulk insert. There are also equivalents for this on MariaDB (LOAD DATA INFILE) and SQLServer (BULK INSERT).
Initial experiments for PSQL showed a throughput which has almost doubled (old: 65502 records/sec new: 114942 records/sec).
#3 Updated by Teodor Gorghe 2 months ago
Created task branch 11467a and added an initial implementation as revision 16562.
Please note that it is unfinished (code-style and javadoc are not right), but the import has succeeded for the projects that I have managed to test.
#4 Updated by Constantin Asofiei 2 months ago
- use bulk-copy if there is no word-index for the current table, but the dialect supports it (please explain this limitation for word indexes)
- the 'recovery' we do for batch inserts: we 'rollback' only the offending record, not the full record (like we encountered with SQLServer and mariadb I think, where a field's string value does not fit in the fixed-width char columns).
- blob/clob types which I'm not sure how they are supposed to work in 11467a
- there is no 'batching' of the records in the .d file - a .d file can be 10s of GB or more. We can not keep this in memory entirely
#5 Updated by Teodor Gorghe 2 months ago
Constantin Asofiei wrote:
The main issues we need to solve are:
- use bulk-copy if there is no word-index for the current table, but the dialect supports it (please explain this limitation for word indexes)
Ok, I need to check this, but for normalized extends schema and for any other non-flat schema, there is a check to fallback to the old approach.
boolean doBulkCopy = dialect.supportsBulkCopy() && (!dialect.useWordTables() || wordTables.isEmpty());
Constantin Asofiei wrote:
- the 'recovery' we do for batch inserts: we 'rollback' only the offending record, not the full record (like we encountered with SQLServer and mariadb I think, where a field's string value does not fit in the fixed-width char columns).
Currently, when a record insertion fails (due to a UNIQUE constraint violation for example), it rolls back all the inserted records from that COPY statement.
In PSQL 17, there is ON-ERROR "ignore", but PSQL 14-16 does not support it.
Constantin Asofiei wrote:
- blob/clob types which I'm not sure how they are supposed to work in 11467a
There is a check for this, to fall back to the old approach. I have encountered this issue for one project.
// ImportWorker line 1676
catch (UnsupportedOperationException uoe)
{
LOG.log(Level.INFO, String.format("%s: Bulk copy not supported for table %s: %s. Falling back to batch insert.", fileName, table, uoe.getMessage()));
...
}
Constantin Asofiei wrote:
- there is no 'batching' of the records in the .d file - a .d file can be 10s of GB or more. We can not keep this in memory entirely
I don't know how COPY handles transactions internally, but they are supported.
What does the current approach does:
- creates an iterator which iterates through the loader instance (which loads the next record from .d) (
hasNextcall). - with that iterator, we use the next record (DMO), serialize into CSV and write it to stdin stream of COPY statement.
- this stream is through that JDBC connection channel, being implemented by the PSQL JDBC driver.
Of course, these changes are not 100% right (are being generated as for POC), which will be revisited once we fix the tasks which are higher as priority.
#6 Updated by Teodor Gorghe 5 days ago
- Assignee set to Teodor Gorghe
- % Done changed from 0 to 100
- reviewer Ovidiu Maxiniuc added
- topics Word Indexes added
I have done a couple of fixes, most of them were style fixes, support of COPY for word index tables, tuned ImportWorker to use asynchronous_commit and maintenance_work_mem.
Ovidiu, can you review 11467a/r16565. Thanks!
#7 Updated by Teodor Gorghe 5 days ago
- Status changed from New to WIP
#8 Updated by Teodor Gorghe 5 days ago
- Status changed from WIP to Review
#9 Updated by Teodor Gorghe 5 days ago
Some measurements done on two projects, one with a smaller dump and another one, with way larger dump.
Project 1:- Baseline: 5 minutes 9 seconds
- 11467/r16565: 1 minute 14 seconds
- Baseline: 41 minutes 3 seconds
- 11467/r16565: 30 minutes 15 seconds
#10 Updated by Constantin Asofiei 5 days ago
Teodor Gorghe wrote:
I have done a couple of fixes, most of them were style fixes, support of COPY for word index tables, tuned ImportWorker to use
asynchronous_commitandmaintenance_work_mem.
So, in COPY mode, the triggers are not being executed, to populate the word-tables (these are only for INSERT); so you rebuild the word-tables explicitly, right?
I would have expected a greater improvement than just ~25%.
Also:Numeric values (e.g. BigDecimal) never contain a CSV metacharacter- this is valid only if the import worker can't set custom separators for decimal or grouping.- how are LOBs managed?
#11 Updated by Teodor Gorghe 5 days ago
When running ant import.db, clean.db is being executed, which drops the database.
Word-index triggers are being created in a later stage, after the data being imported to database.
CLOB data type is using COPY statement, there is no issue regarding that. I have gated BLOB data type to fallback to old batched INSERT. See the BulkCopyCsvWriter which gates the expanded extents case and the tables which has columns with BLOB data type.
The BigNumber or any Number toString implementation does not use comma as string separator (for decimal separator uses . and there is no comma grouping of three digits).
#12 Updated by Constantin Asofiei 5 days ago
Teodor Gorghe wrote:
CLOB data type is using
COPYstatement, there is no issue regarding that. I have gated BLOB data type to fallback to old batched INSERT. See theBulkCopyCsvWriterwhich gates the expanded extents case and the tables which has columns with BLOB data type.
OK, I understand now, ImportWorker falls back to normal import if bulk fails for any reason.
The
BigNumberor anyNumbertoStringimplementation does not use comma as string separator (for decimal separator uses.and there is no comma grouping of three digits.
Thanks.
#13 Updated by Teodor Gorghe 5 days ago
Constantin Asofiei wrote:
I would have expected a greater improvement than just ~25%.
I think I have a high speed SSD on my system. I see that the system runs smoother when the import process is going on.
I have a system on the office which has an HDD, it may be worth doing some comparison.
#14 Updated by Constantin Asofiei 5 days ago
- 8755d import
100.00000% complete: 95208995 recs in 0:33:56.810 (46744/sec, 2804650/min, 168279015/hr)
- 8755d + 11467a import
100.00000% complete: 95208995 recs in 0:12:54.718 (122895/sec, 7373702/min, 442422122/hr)
So it looks really promising, on 'good data' or with just a few blob tables.
Now, do we need to validate the import (i.e. all records are imported with their correct values)?
#15 Updated by Teodor Gorghe 5 days ago
I am finding a way to validate the import on a 41GB dump.
#17 Updated by Ovidiu Maxiniuc 5 days ago
Teodor Gorghe wrote:
Ovidiu, can you review 11467a/r16565. Thanks!
Review of 11467a / r16565
I think the update is safe. I did not spot any severe issue.
BulkCopyCsvWriter.java:- lines 66-70: the full package import (
.*syntax) should be used instead of individual classes; - lines 119/126/128: why passing the
firstRecordinstead of only itsrecordMetadirectly?
- lines 66-70: the full package import (
CsvRecordReader.java:- line 75:
extendsdeclaration should be on next line; - line 102: a bit strange to pass the 1st element from the 'stream' and the
Iterator. The 'head' can be extracted in constructor usingnext(). Just noticing. No need to adjust; - line 129:
throwsdeclaration should be on next line;
- line 75:
P2JPostgreSQLDialect.java:- line 2884-2996: a more intuitive way to unwrap the connection would be:
if (conn instanceof PGConnection) return (PGConnection) conn; else if (conn.isWrapperFor(PGConnection.class)) return conn.unwrap(PGConnection.class); else throw new SQLException("failed");
- line 2884-2996: a more intuitive way to unwrap the connection would be:
BaseRecord.java:- lines 172-181: the original convention Eric and I agreed upon when the
BaseRecordwas first created is to keep thedataas encapsulated as possible and never allow direct access to it. Meanwhile, there are other 2 method that break the same rule but add some constraints. Can we usegetData(PropertyMapper)instead since it is also used in import process? I really miss C++friendclasses. - lines 1099-1108: a similar method already exists in
Recordclass. To use that, theImportWorker$ImportRecordIteratorshould iterate onRecordinstead ofBaseRecord;
- lines 172-181: the original convention Eric and I agreed upon when the
Persister.java:- line 488: I think passing the
Dialectas parameter is a better solution than lookil it up, locally. (Requires adding it as parameter toSession.bulkCopy()method, as well);
- line 488: I think passing the
ImportWorker.java- line 1715: the
startPosshould be always 0, but having more flexibility will not hurt; - line 2159-2160: The parameter list should be chopped with one parameter on each line if the method declaration line is longer than 110 chars;
- line 4303 &4363: missing method javadoc.
- line 1715: the
#18 Updated by Ovidiu Maxiniuc 5 days ago
- Status changed from Review to Internal Test
#19 Updated by Teodor Gorghe 5 days ago
Greg Shah wrote:
Use our export tool to dump the content to
.dand then compare the.dfiles to the originals.
I have imported and then exported the dump.
I see just one difference, the exported dump is encoded in a different charset.
#21 Updated by Ovidiu Maxiniuc 5 days ago
I think that is a pass.
However, if you want to do a cross-check you can do a SQL database dump (backup) in text (SQL) format and compare the result for a database imported with the trunk and same database imported with 11467a. These dumps show the full SQL database content, including schema and the UDFs.
#22 Updated by Teodor Gorghe 4 days ago
SQL dump gives output difference every time when you import from database, due to the fact how data is being stored in the PG heap. Recid is also different.
I have done a comparison by stripping recids and sorting the records inside COPY blocks, and I don't see any difference (word tables are also populated right).
#23 Updated by Teodor Gorghe 4 days ago
Addressed code review #11467-17 in 11467a/r16566.
#24 Updated by Constantin Asofiei 4 days ago
Teodor, please also test a (small) import with 10614_main.
#25 Updated by Teodor Gorghe 4 days ago
Constantin Asofiei wrote:
Teodor, please also test a (small) import with 10614_main.
On that app, we don't have the .d files, do I you mean to extract .d files and then reimport?
#26 Updated by Constantin Asofiei 4 days ago
Teodor Gorghe wrote:
Constantin Asofiei wrote:
Teodor, please also test a (small) import with 10614_main.
On that app, we don't have the .d files, do I you mean to extract .d files and then reimport?
That, or use 10614_main to convert the .df schema for another app and import with it (you don't have to fully convert or parse the code).
#27 Updated by Constantin Asofiei 4 days ago
Let me know if and when the branch is ready to merge.
#28 Updated by Teodor Gorghe 4 days ago
Currently working on #7143-1977, but I think it will be ready EOD.
#29 Updated by Teodor Gorghe 4 days ago
Tested 10614_main to import the database. Import time has reduced with nearly 50%.
Committed revision 16567 on task branch 11467a:- Fixed the NPE when COPY fails and rollbacks to batch insert.
#30 Updated by Constantin Asofiei 4 days ago
I see this from claude, it may be a valid point:
- [MINOR] functional
ImportWorker.importTable(bulk path): Column-less ("empty") tables — a DMO with only the surrogate PK, a first-class case thatRecordLoader.isRecordEmpty()explicitly supports — with a PSC-footer count > 0 crash the bulk path.iterator.hasNext()returnstrue(empty records are non-null), sonew BulkCopyCsvWriter(loader.getMetadata(), loader.getProperties().get(0))runs and.get(0)on the empty properties list throwsIndexOutOfBoundsException. It bypasses thecatch (UnsupportedOperationException)and lands incatch (Exception), logged at INFO as a misleading "Bulk copy failed", so every such table silently loses the COPY optimization. No data loss (batch fallback handles it correctly). Fix: guard withif (!loader.getProperties().isEmpty())(equivalently!loader.isRecordEmpty()) and let the batch path handle column-less tables — note a lazy-witness fix does not work, as such a table has noPropertyMapperandappendRowcallsdmo.getData(witness)which requires a non-null mapper.
#31 Updated by Teodor Gorghe 4 days ago
Done in 11467a/r16568.
#32 Updated by Constantin Asofiei 4 days ago
Teodor Gorghe wrote:
Done in 11467a/r16568.
Ovidiu, please review again.
#33 Updated by Eric Faulhaber 4 days ago
Teodor Gorghe wrote:
SQL dump gives output difference every time when you import from database, due to the fact how data is being stored in the PG heap. Recid is also different.
I haven't reviewed the code, so this may not be a relevant inquiry, but please confirm:
- How are primary keys being generated/assigned now?
- Are primary keys still globally unique across all tables?
- At the end of the import, whether tables have used the COPY mode or batched INSERT mode, is the
p2j_id_generator_sequencesequence still being properly updated to reflect the highest assigned primary key? We rely on this at runtime when creating new records.
#34 Updated by Teodor Gorghe 3 days ago
Eric Faulhaber wrote:
I haven't reviewed the code, so this may not be a relevant inquiry, but please confirm:
- How are primary keys being generated/assigned now?
Using the same IDContext instance like how to batch insert path works.
Relevant sections:- Obtaining current id and passing to the iterator: code
- ImportRecordIterator hasNext(): code
- When an exception happens, calls
discardBulkCopyAttemptwhich resets the context: section 1 discardBulkCopyAttempt
- Are primary keys still globally unique across all tables?
- Yes, we use the same
IDContext/TenantIDContext, which handles the primary key in brackets, across multiple threads. - Please take in consideration that the database rollback in PSQL does not restore the sequence value (is non-transactional). This may be reason why we handle the primary key in memory and we create the sequence at the end.
- At the end of the import, whether tables have used the COPY mode or batched INSERT mode, is the
p2j_id_generator_sequencesequence still being properly updated to reflect the highest assigned primary key? We rely on this at runtime when creating new records.
- Yes, of course. It still uses last id of
IDContextto create the sequence: code
#35 Updated by Teodor Gorghe 3 days ago
- table has records more than the number of
IDContextbracket size and the import process is multi-threaded. - when a rollback happens and there is a need to restore the
IDContext, I have added a safe-guard to restore theIDContextonly when they were inserted records which does not exceed the first current bracket. - when the first current bracket is exceeded,
IDContext.resetTois essentially a NO-OP. - why works in this way: I thought that it is better to leave a gap between recids, than the unnecessary work to restore the entire
IDContext.
#36 Updated by Constantin Asofiei about 9 hours ago
- Status changed from Internal Test to Review
Eric, any other concerns?
Ovidiu - please take at the changes again (moved to review for better visibility).
#37 Updated by Eric Faulhaber about 1 hour ago
Constantin Asofiei wrote:
Eric, any other concerns?
Let's be sure to include multi-tenant in the testing.
Other than that, I just wanted to make sure we were good on the primary keys and it seems we are. Thanks for the answers, Teodor.
Let's try to get this into trunk ASAP after Ovidiu's (hopefully) final review and any other testing that needs to be done.