Project

General

Profile

Support #4060

investigate converting extent fields to array columns

Added by Eric Faulhaber about 7 years ago. Updated 9 months ago.

Status:
WIP
Priority:
Normal
Target version:
-
Start date:
Due date:
% Done:

50%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
reviewer:
production:
No
env_name:
topics:

Related issues

Related to Database - Support #4058: consider denormalizing tables as the default approach New
Related to Database - Feature #6418: represent extent fields as arrays WIP

History

#1 Updated by Eric Faulhaber about 7 years ago

We have found on occasion that our default approach of normalizing extent fields of like extent into a secondary table and mapping these to an associated list in Hibernate can cause a performance problem with some queries. OTOH, denormalizing across the board can cause problems with table design (very wide tables when large extent fields are exploded into separate columns) and potentially cause their own performance problems. Also, the associated list approach has caused no end of lazy initialization problems with Hibernate over the years.

Rather than choosing between the normalized and denormalized approaches we use today, we should investigate the implications of converting extent fields in the most natural/analogous way: to array columns.

I had discarded this approach early on, because Hibernate has limitations in how it deals with array columns. I don't recall ATM what those limitations were (possibly related to managed flushes and dirty checking?), but it may be time to revisit this decision. Perhaps the limitations aren't really issues for us after all, given that we use these fields in very specific and well-defined ways.

#2 Updated by Eric Faulhaber about 7 years ago

Note that this task is created in the context of investigating changes that may improve performance, but I don't know that using array columns would be any more or less performant than our current approaches. This requires investigation.

Also note that there is quite a bit of complexity in how we convert where clauses and sorting and how we preprocess where clauses at runtime, which is hard-wired to the current normalized and denormalized approaches. Thus, changing this to support array columns may be a relatively complicated proposition. So, we need to understand whether it is worth the effort from a performance standpoint, before undertaking any changes.

DBAs and BI developers may prefer using array columns from a design perspective, though, in that they would represent the closest design analog to the current schema in Progress.

#3 Updated by Greg Shah over 3 years ago

  • Related to Support #4058: consider denormalizing tables as the default approach added

#4 Updated by Greg Shah over 3 years ago

  • Related to Feature #6418: represent extent fields as arrays added

#5 Updated by Greg Shah over 1 year ago

I believe our testing showed that array columns are faster than using "normalized" (secondary) array tables. We are already moving to "expanded extents" as our default. We can close this task, right?

#6 Updated by Ovidiu Maxiniuc over 1 year ago

I think this is dialect dependent. If all supported dialects had a fast native support for this, then the mapping would be perfect. But they don't, or use workarounds (like storing in a JSON format) which will degrade performance unnecessary (because of parsing and conversion to/from string).

With #7020, the full record 4GL is stored in a single SQL record so we can query it directly and fetched in a single SQL statement. So I think we can reject this idea now. At least until all our supported dialects cleanly and faster handle this natively, if ever.

#7 Updated by Greg Shah over 1 year ago

Recently we found out that:

  • Most databases have a limit on the number of columns in a table.
    • PostgreSQL: 1600
    • H2: no limit
    • MariaDB: 4096
    • SQLServer: 1024
  • Some customers (see #8982) have extext columns and blow past these limits.

With that in mind there is a real value to using native array column support. Even if it is only in the subset of dialects that support it (PostgreSQL does, H2 does, MariaDB does not, SQLServer does not).

Using the stupid JSON packing idea in a varchar is not going to happen because it would make the SQL ridiculous and it would kill performance. So the idea here is to implement native array support where possible but as an optional feature that can be enabled by customers that are willing too accept the following:

  • Only use databases that support the feature. OR
  • Run multiple conversions (for array column and non-array column cases).

This is only a limitation if the converted code is actually different in these cases. If the code is the same, then this is easily handled as a standard feature instead of an option.

#9 Updated by Stefanel Pezamosca over 1 year ago

I wanted to investigate this a little bit. 7020d branch makes it easier to implement this. Most places are already using array[index] syntax. For native array support when expand_extents=true is enough to just not replace array[index] with array_index. I tested the syntax in PostgreSQL and initially it seems to work well. SELECT and UPDATE should work ok. I haven't tested INSERTs yet. In DB the fields look something like this:

int_array(int[]) = {1,2,3,4,5,6,7,8,9,10, ...}
text_array(text[]) = {"text1","text2","","","","", ...} 
etc...
Greg, should I continue with this investigation and maybe also start testing some implementations?

#10 Updated by Eric Faulhaber 10 months ago

  • Assignee set to Stefanel Pezamosca

Stefanel Pezamosca wrote:

[...]
[...] Greg, should I continue with this investigation and maybe also start testing some implementations?

It is time to pick this investigation up again. We have at least one customer being held back by the column limit in PostgreSQL, so they are stuck on the normalized extent schema, which we are trying to deprecate.

The key things to consider are:

  • Does this data mapping limit legacy functionality in any way, compared to expanded extents?
  • Performance:
    • Runtime - all CRUD operations
    • Import
  • Conversion considerations:
    • DDL generation
    • CRUD syntax in the ORM. Code must convert to FQL the same way (conversion to SQL happens transparently at runtime).
  • Runtime ORM support:
    • Encapsulation. Can the implementation be neatly contained in the dialect hierarchy? We don't want dialect-sensitive, hard-to-maintain code sprinkled throughout the persistence layer.
  • Scope:
    • Is this data mapping applied across the entire database (preferred for consistency/simplicity), or only for tables which exceed column limits (not preferred, but may be needed)? To a large degree, the performance of the implementation will answer this question. Whether this becomes the default implementation also depends on performance.
  • Index impacts?
    • Shouldn't be a problem for non-word indices, since extent fields do not participate.
    • Shouldn't be a problem for word indices either, since they're completely refactored already.
  • How to migrate an existing database to the new schema safely and efficiently (including in production)?

#11 Updated by Stefanel Pezamosca 10 months ago

  • Status changed from New to WIP

#12 Updated by Stefanel Pezamosca 10 months ago

I am working on a prototype implementation for this, that I activated only for PostgreSQL dialect by adding a "useArrayField" flag. Some changes I encountered:

create table pt (
   recid int8 not null,
   f1_1 text,
   f1_2 text
   f1_3 text
   f1_4 text
   f1_5 text
   primary key (recid)
);
becomes:
reate table pt (
   recid int8 not null,
   f1 text[5],
   primary key (recid)
);
Insert statements can look like this:
INSERT INTO pt(f1[1], f2[2], f2[3], f2[4], f2[5]) VALUES ('a', 'b', 'c', 'd', 'e')
or
INSERT INTO pt(f1) VALUES (ARRAY['a', 'b', 'c', 'd', 'e'])

Update statements: UPDATE pt SET f1[1] = 'x'. So, Insert and updates should be easy to generate, f1_1 just replaced with f1[1].

Another big change related with DDLs are for Word Table support. Currently we have a trigger that looks like this for an individual field:

create trigger pt__f1_4__upd after
update of f1_1
   on
   pt for each row execute procedure pt__f1_4__trg();

create trigger pt__f1_4__upd after
update of f1_2
   on
   pt for each row execute procedure pt__f1_4__trg();
...
etc.
For array fields we cannot create a trigger for individual elements. So we have to make only one trigger over f1:
create trigger pt__f1__upd after
update of f1
   on
   pt for each row execute procedure pt__f1__trg();
Using this function:
create or replace function pt__f1__trg()
returns trigger
language plpgsql
as
$$
begin
   delete from pt__f1 where pt__f1.parent__id = new.recid;
   insert into pt__f1 select * from words(new.recid, 1, new.f1[1], true);
   insert into pt__f1 select * from words(new.recid, 2, new.f1[2], true);
   insert into pt__f1 select * from words(new.recid, 3, new.f1[3], true);
   insert into pt__f1 select * from words(new.recid, 4, new.f1[4], true);
   insert into pt__f1 select * from words(new.recid, 5, new.f1[5], true);
   return new;
end;
$$;
Otherwise, for all other queries, instead of generating f1_1, f1_2, and so on, we just need to generate or keep f1[1], f1[2], etc. Other than the changes mentioned above, there shouldn’t be any additional issues, but I’ll confirm that with further testing.

#13 Updated by Greg Shah 10 months ago

A very important test to do early: does this resolve the issue in #8982?

#14 Updated by Stefanel Pezamosca 10 months ago

Greg Shah wrote:

A very important test to do early: does this resolve the issue in #8982?

Yes, it should solve it. I did this:

CREATE TABLE random_letters (
    field1  text[],
    field2  text[],
    field3  text[],
    field4  text[],
    field5  text[],
    field6  text[],
    field7  text[],
    field8  text[],
    field9  text[],
    field10 text[],
    field11 text[],
    field12 text[],
    field13 text[],
    field14 text[],
    field15 text[],
    field16 text[],
    field17 text[],
    field18 text[],
    field19 text[],
    field20 text[]
);
And made an insert statement to insert 100 elements for each field. It works well.
I’ll keep running performance tests on a larger dataset and am also working on setting up a bigger project to compare real-world scenarios.

#15 Updated by Greg Shah 10 months ago

My fingers are crossed for a performance improvement!

#16 Updated by Stefanel Pezamosca 9 months ago

Performance doesn't seem to be a big issue, is similar to what we have now.
I could also improve performance for SELECT statements by retrieving the entire array at once:

SELECT field1 FROM public.test_array
ORDER BY recid ASC;
This is much more efficient than selecting individual elements as we currently do this:
SELECT field1[1], field1[2], field1[3], field1[4], field1[5],
field1[6], field1[7], field1[8], field1[9], field1[10],
field1[11], field1[12], field1[13], field1[14], field1[15]
FROM public.test_array
ORDER BY recid ASC;
This fix will hydrate the single field1 result as an array instead of hydrating each extent element separately.

#17 Updated by Greg Shah 9 months ago

That improvement is only possible in native array column mode, right? So there is a potential win here for the native mode.

#18 Updated by Stefanel Pezamosca 9 months ago

  • % Done changed from 0 to 50

I was working on implementing the optimization discussed in #4060-16. I have the base but at the moment I got an error that I'm not sure what's the cause of it. I think there are still places in the code that I need to adjust to work with arrays. I will keep looking.

Caused by: java.lang.AbstractMethodError: Receiver class com.goldencode.dataset.dmo.fwd.TestArray__Impl__ does not define or inherit an implementation of the resolved method 'abstract void setId(com.goldencode.p2j.util.NumberType)' of interface com.goldencode.dataset.dmo.fwd.TestArray.
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:569)
    at com.goldencode.p2j.util.Utils.invoke(Utils.java:1793)
    at com.goldencode.p2j.persist.RecordBuffer$Handler.invoke(RecordBuffer.java:13187)
    at com.goldencode.p2j.persist.$__Proxy5.setId(Unknown Source)
    at com.goldencode.dataset.Demo.lambda$createData$4(Demo.java:111)
    at com.goldencode.p2j.persist.RecordBuffer.batch(RecordBuffer.java:3466)
    at com.goldencode.dataset.Demo.lambda$createData$5(Demo.java:109)
    at com.goldencode.p2j.util.Block.body(Block.java:636)
    at com.goldencode.p2j.util.BlockManager.processBody(BlockManager.java:9665)
    at com.goldencode.p2j.util.BlockManager.coreLoop(BlockManager.java:11455)
    ... 48 more
Otherwise, the implementation even without this optimisation is still promising. I don't see a noticeable performance impact and it may be slightly better.
I will continue to investigate the above issue and to think about the migration of current databases from normalized / expanded extents to this array approach.

Also available in: Atom PDF