Bug #9317
FIRST/LAST clauses can avoid generating sub-selects if the WHERE clause matches a unique index and at least one field is MANDATORY
100%
History
#1 Updated by Alexandru Lungu over 1 year ago
- Assignee set to Ioana-Cristina Prioteasa
For optimized compound queries that have FIRST/LAST components, FWD emits something like:
select * from table1 cross join table2 where table2.recid = (select recid from _table2 where <where-clause> order by <order-by> LIMIT 1) order by ....
But if we can detect that the where-clause guarantees zero or one row from table2 anyway, there is no need for a sub-select and this can be transformed to:
select * from table1 cross join table2 where <where-clause> order by ....
The sub-select was intended to guarantee that this works as a FIRST/LAST instead of EACH. However, some customer applications have the <where-clause> like table1.pk = table2.fk, fk is MANDATORY and uniquely indexed. Thus, it guarantees that there will be at most one correct row found in the second table.
This optimization may prove especially useful in OUTER-JOIN operations.
#3 Updated by Ioana-Cristina Prioteasa over 1 year ago
- Status changed from New to WIP
Testcase¶
The schema consists of two persistent tables,table1 and table2, with the following characteristics:
table1- one field:- pk - primary
table2- 2 fields:- pk - primary
- fk - mandatory field with unique index.
This is what is needed in the data definition file (fwd.df).
ADD TABLE "table1" DUMP-NAME "table1" ADD FIELD "pk" OF "table1" AS integer FORMAT "->>,>>9" INITIAL "0" POSITION 2 MAX-WIDTH 4 ORDER 10 ADD INDEX "pk_index" ON "table1" PRIMARY INDEX-FIELD "pk" ASCENDING ADD TABLE "table2" DUMP-NAME "table2" ADD FIELD "fk" OF "table2" AS integer FORMAT "->>,>>9" INITIAL "0" POSITION 2 MAX-WIDTH 4 ORDER 10 MANDATORY ADD FIELD "pk" OF "table2" AS integer FORMAT "->>,>>9" INITIAL "0" POSITION 3 MAX-WIDTH 4 ORDER 20 ADD INDEX "pk_index" ON "table2" PRIMARY INDEX-FIELD "pk" ASCENDING ADD INDEX "fk_index" ON "table2" UNIQUE INDEX-FIELD "fk" ASCENDING
The progress code used for the testcase:
DEFINE QUERY q1 FOR table1, table2.
OPEN QUERY q1 FOR EACH table1,
FIRST table2 WHERE table1.pk = table2.fk NO-LOCK OUTER-JOIN.
GET FIRST q1.
IF AVAILABLE table1 THEN
DISPLAY table1.pk table2.fk WITH FRAME f1.
CLOSE QUERY q1.
Currently, with the outer join optimization enabled this is the query that gets executed:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.fk as fk2_0_, table2__im1_.pk as pk3_0_
from table1 table1__im0_
left outer join table2 table2__im1_
on (table2__im1_.recid = ( select table2__im2_.recid as col4_1_
from table2 table2__im2_
where table1__im0_.pk = table2__im2_.fk
order by table2__im2_.fk asc, table2__im2_.recid asc limit 1))
order by table1__im0_.pk asc, table1__im0_.recid asc
limit 1
In this case the where clause table1__im0_.pk = table2__im2_.fk is guaranteed to find either one or 0 rows because pk is primary key in table1 and fk is mandatory and unique in table2. So, the above query can be optimized to:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.fk as fk2_0_, table2__im1_.pk as pk3_0_
from table1 table1__im0_
left outer join table2 table2__im1_
on table1__im0_.pk = table2__im2_.fk
order by table1__im0_.pk asc, table1__im0_.recid asc
limit 1
#4 Updated by Alexandru Lungu over 1 year ago
This is right! However, the optimization shouldn't be only on OUTER-JOIN; it can be used for non-outer join queries. Also, it can have a bigger join keys (not only fk, but fk1 = pk1 and f2 = pk2. Lastly, as you noted, the index over fk or (fk1, fk2) should be unique and all components MANDATORY.
#5 Updated by Ioana-Cristina Prioteasa over 1 year ago
QueryComponent.getWhere():FQLExpression baseWhere = getFQLPreprocessor().getFQL(); if ((iteration != FIRST && iteration != LAST) || top) { return baseWhere; } //construct subselect and return it- In
QueryComponent.prepareFQLPreprocessor():boolean subselect = !top && (iteration == FIRST || iteration == LAST); RecordBuffer buffer = getBuffer(); String dropAlias = subselect ? buffer.getDMOAlias() : null; String replacementAlias = dropAlias != null ? DBUtils.getSubselectAlias(dropAlias) : null; return FQLPreprocessor.get(dfWhere, buffer.getDatabase(), buffer.getDialect(), getReferencedBuffers(), dfArgs, referenceSubs, true, dropAlias, replacementAlias, false, null);
I want to introduce a dedicated method isSubselectNeeded(FQLExpression baseWhere) to consolidate the logic for determining subselect necessity. This method would also check for index-based optimization criteria:
private boolean isSubselectNeeded(FQLExpression baseWhere)
throws PersistenceException
{
if ((iteration != FIRST && iteration != LAST) || top)
{
return false; // Subselect not needed
}
String entity = buffer.getEntityName();
IndexHelper indexHelper = IndexHelper.get(buffer.getDatabase());
List<String> indexes = indexHelper.getIndexesForEntity(entity, true); // Get unique indexes
if (isIndexOptimizable(baseWhere, indexes))
{
return false;
}
return true; // Subselect is needed if no optimization criteria are met
}
Right now I have a very basic check in the isIndexOptimizable(), there is still work to be done here.
Calling FQLExpression baseWhere = getFQLPreprocessor().getFQL(); within prepareFQLPreprocessor() or isSubselectNeeded() causes a circular dependency since getFQLPreprocessor() invokes prepareFQLPreprocessor(). Resolving this dependency is the next step before proceeding to refine the isIndexOptimizable() method.
#6 Updated by Alexandru Lungu over 1 year ago
dropAlias and replacementAlias are relevant and why we need to computed them before-hand. Lets presume that the isIndexOptimizable is identifiable only after the FQL is parsed. So at that point, we can't precompute anything. Thus, we need to know if we can detect inside FqlPreprocessor if the sub-select is required and if it is required, compute the @dropAlias and replacementAlias elements on spot:
buffer.getDMOAlias()can be computed insideFqlPreprocessoras we have access to the buffer.DBUtils.getSubselectAlias(dropAlias)is stateless, so it doesn't pose any problem.
#7 Updated by Alexandru Lungu over 1 year ago
PS: Mind that FqlPreprocessor.preprocess does the whole work. It iterates the AST multiple times, so there are several phases of information extraction. If the information needed to check if the sub-select is required or not can be done in an earlier phase that the one that requires the dropAlias and replacementAlias.
For the record, emit, where dropAlias and replacementAlias are actually used, is the very last step. checkUniqueFind is used before emit, so you have the knowledge before actually using emit. Thus, you can always pass down the right dropAlias and replacementAlias. If you detect in checkUniqueFind that the sub-select is not required, simply make both dropAlias and replacementAlias null. dropAlias and replacementAlias are used in the cache key, but that will only mean that the FQL preprocess will be used less. Functionally, it should work, but it may affect performance of the cache - we will analyze this last. I don't know about bufferMap ... extra investigation should be conducted.
#8 Updated by Ioana-Cristina Prioteasa over 1 year ago
- % Done changed from 0 to 50
Successfully refactored the logic to detect the need for a subselect in FQLPreprocessor.
When the preprocessor is computed, dropAlias and replaceAlias are reset to null if a subselect is determined to be unnecessary before calling emit(). Additionally, the PropertyMatches logic and its collection process have been enhanced to support more complex clauses involving two properties and two aliases.
Next Steps: Utilize propertyMatches to determine whether a subselect is required or not.
Committed on 9317a, no review needed for now.
#9 Updated by Ioana-Cristina Prioteasa over 1 year ago
Successfully integrated propertyMatches into the subselect decision logic and the above testcase generates the simplified query.
However, my current implementation is causing regressions in a large customer application. The issue arises in a scenario involving record insertions and deletions. While the test passes successfully on the first run, subsequent runs fail due to a "record already exists" error during creation. Restarting the server did not resolve the issue, suggesting that the problem lies in the persistent database rather than the cache.
Despite debugging efforts, I have not yet pinpointed why the record is not being deleted and how my changes are contributing to this behavior. My next step will be to replicate the issue in a smaller test case for further investigation.
#10 Updated by Ioana-Cristina Prioteasa over 1 year ago
The problem was caused by another function detecting the need for the subselect which I had initially overlooked and was not considering the new conditions and also treating wrong the case where the baseWhere was null. With these fixes the regression is solved.
Unfortunately, while the test now runs, it appears that no queries are actually being optimized. I need to identify a query that should be optimized and investigate where the detection process is failing. I suspect that my changes in PropertyMatches might be the cause, as they were only tested with the small test case. It's possible that for more complex cases, the matches get computed to null and so preventing the optimization from being applied.
#11 Updated by Ioana-Cristina Prioteasa over 1 year ago
- Status changed from WIP to Review
- reviewer Alexandru Lungu, Eric Faulhaber added
Committed on 9317a.
Alex/Eric, please review 9317a rev. 15624 - 15627.
Local performance tests were inconclusive. The test results showed minimal variance across the tested scenarios (Compound query optimization disabled, Compound query optimization enabled, Compound query optimization enabled with 9317 changes). I plan to run an extended performance test on the bare metal to gather more conclusive data.
Additionally, I want to enable query logging to identify queries that still include subselects but may not need them. My concern is that the current implementation of isUnique() may be overly restrictive. The method checks for unique constraints containing only one field and matches it with the property.
Potential issue: Composite unique constraints that map to different primary key components could also be valid for optimization. However, the current restriction excludes these cases, potentially missing opportunities for optimization.
#12 Updated by Alexandru Lungu over 1 year ago
The method checks for unique constraints containing only one field and matches it with the property.
I think this should be assessed now. Most of the PK/FK are groups of more than 1 field in #8616 application.
#13 Updated by Ioana-Cristina Prioteasa over 1 year ago
Alexandru Lungu wrote:
I think this should be assessed now. Most of the PK/FK are groups of more than 1 field in #8616 application.
This has been addressed in the latest commit on 9317a, revision 15628.
The implementation now checks whether the properties of the join buffer appearing in the WHERE clause form a unique index and whether the corresponding properties of the other buffer involved are mandatory.
With this change, the number of optimized queries in the test has increased to 260 and local performance tests show promising results. Yesterday, the local tests duration varied between 9.5 and 10 seconds and now the average has improved to 8.8 seconds. A full performance test has been started on the bare metal.
Alex/Eric, please review 9317a, revisions 15624–15628.
#14 Updated by Alexandru Lungu over 1 year ago
Overall, I would want to refactor a bit the javadoc/parameter and method names to reflect the join with a FIRST/LAST scenario. We used the "subselect" term to reflect that such construct will convert in an SQL subselect. However, this is confusing as it can also refer to CAN-FIND that may be embedded as a sub-select. Lets find another keyword for it - lets say "joinWithSubselect".
First review iteration:QueryComponent- rename
hasSubselecttojoinWithSubselectand getter toisJoinWithSubselect. Edit the javadoc. - I must admit that I am concerned that
hasSubselectis a mutable flag that is changed both inprepareFQLPreprocessorandgetWhereand is queried bygetWhereandgetHasSubselect. Thus, the computation of this flag is highly dependent on the order in which these set/get methods are called. The state automata is too complex. If one callsgetHasSubselectsomewhere in before/after thegetWhere/prepareFQLPreprocessorcode, then the caller will get stale data. Lets make the code a bit more robust and proof to future changes.- make
getHasSubselectreturngetFQLPreprocessor().isSubselectNeeded - change
getWhereto simply checkif (!proc.isSubselectNeeded()) return baseWhere; - restore the
subselectflag inprepareFQLPreprocessor - overall,
isSubselectNeededis already cached inFQLPreprocessor, so it doesn't need another caching inQueryComponent
- make
- rename
PreselectQuery- I don't understand the change here. At first sight, I would have opted for a
(iteration != FIRST && iteration != LAST) || !comp.getHasSubselect()(note the use of || instead of &&). Please make some tests for this decision. I think you need something likefor each a, first b on a,for each a, last b on a,for each a, each b on a(note that a and b should share some common fields with the same name and unique indexed). Do you have a test-case that motivates this change?
- I don't understand the change here. At first sight, I would have opted for a
FQLPreprocessorcollectPropertyMatchesis changing the state of the preprocessor. As you call it once again, you may affect the output of the firstcollectPropertyMatches.- Does it make sense to run
collectPropertyMatchesonly once (where was called at the first time) and simply decide whether it should be with true or false to simple? This way we avoid two calls tocollectPropertyMatches.
- Does it make sense to run
- The code of
isOptimizableandareCorrespondingPropertiesMandatoryare good!
- Other
- To avoid the bloat of the
falseflag anywhere, can you overload the method to make the last flagfalse?
- To avoid the bloat of the
- Some places like
if(that should be rewritten asif ( - Update copyright year
#15 Updated by Ioana-Cristina Prioteasa over 1 year ago
Committed on 9317a rev.15629 the changes needed based on the code review.
For the PreselectQuery change, Alex is right. I included all the logic in hasJoinWithSubselect and used that in PreselectQuery and also in QueryComponent.getWhere.
I also tested the changes with the suggested testcases and on a bigger customer application and it works as expected.
Alex, please review. Thank you!
#16 Updated by Ioana-Cristina Prioteasa over 1 year ago
I went ahead and started testing the changes, on a big customer application I tested regression tests and the results are ok. I will continue with unit tests.
#17 Updated by Alexandru Lungu over 1 year ago
Overall, I would want to refactor a bit the javadoc/parameter and method names to reflect the join with a FIRST/LAST scenario. We used the "subselect" term to reflect that such construct will convert in an SQL subselect. However, this is confusing as it can also refer to CAN-FIND that may be embedded as a sub-select. Lets find another keyword for it - lets say "joinWithSubselect".
This should still be assessed
hasJoin = !comp.isJoinWithSubselect();
This is the only bit of code I am uncertain ATM. I think it should be tested more in depth with some debugging - maybe an example with OF keyword to trigger a join?
Otherwise, the changes are good, but should be thoroughly tested.
#18 Updated by Ioana-Cristina Prioteasa over 1 year ago
Trying to create a test using the OF keyword, I stumbled upon another bug that might need fixing.
With the schema detailed in #9317-3, for this testcase:
DEFINE QUERY q1 FOR table1, table2.
OPEN QUERY q1 FOR EACH table2 OF table1 WHERE table1.pk = table2.fk NO-LOCK.
GET FIRST q1.
IF AVAILABLE table1 THEN
DISPLAY table1.pk table2.fk WITH FRAME f1.
CLOSE QUERY q1.
In progress the shown error is:
Index fields of table1 must be fields in table2. In FWD we show:
Unknown database name., I traced the issue in the constructor of AbstractJoin.#19 Updated by Alexandru Lungu over 1 year ago
I think 4GL shows a compile error, right? In that case, FWD shouldn't be fixed as it presumes correct 4GL input.
For OF, you need for each table1, each table2 ....
#20 Updated by Ioana-Cristina Prioteasa over 1 year ago
I build the following testcase:
DEFINE QUERY q1 FOR table1, table2.
OPEN QUERY q1 FOR EACH table1,
FIRST table2 OF table1 WHERE table1.pk = table2.fk NO-LOCK OUTER-JOIN.
GET FIRST q1.
IF AVAILABLE table1 THEN
DISPLAY table1.pk table2.pk WITH FRAME f1.
CLOSE QUERY q1.
Initially, this query was not being optimized into an outer join, even though it should have been. I identified the issue, found a solution, and committed the fix to 9317a. Now, the query is optimized with both the outer join and subselect optimizations.
Now the testcase generates the following SQL:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_
from table1 table1__im0_
left outer join table2 table2__im1_ on (table2__im1_.pk = table1__im0_.pk and table1__im0_.pk = table2__im1_.fk)
order by table1__im0_.pk asc, table1__im0_.recid asc
limit ?
The changes in PreselectQuery are triggered and the SQL query is generated correctly. I simulated additional cases and all seem to work as expected. I think the PreselectQuery changes are good.
In addition, I ran unit tests for a large customer application, and no regressions were found. However, this testing was done before the last commit, so the most recent changes have not yet been validated with unit tests.
Alex, please review 9137a rev.15630-15632:- 15630: esthetic changes
- 15631: changes for the outer join optimization to take effect
- 15632: history entries
#21 Updated by Alexandru Lungu over 1 year ago
Review of 9317a:
if (joinRef != null && !joinRef.equals(ref))this makes the query matching a bit more relaxed, but mind thatFIRST table2 OF table1 WHERE table1.pk = table2.fk NO-LOCK OUTER-JOIN.is a very rare construct. Usually,FIRST table2 OF table1is enough without "duplicating" the implicit condition. The OF will automatically dott1.f1 = tt2.f1. I understand that using a WHERE will create two conditions overpk. But I would avoid making this more relaxed as the code that follows doesn't allow multiple logical "joinRef".- for instance, the upcoming
for (int i = 0; i < parms.length; i++)will search theparms[i]that will match thejoinRef. With your changes, there will be 2 theoretical suchjoinRefs, so two possiblerefSubstIndex. The code will simply pick the first one as the for-loop is ascending. Making the for-loop descending will yield a different output. This makes the solution buggy asa = b && a = cwill behave different thana = c && a = b, although they are theoretically the same. - if you need to optimize this now, then more extensive changes are needed. ATM, I prefer to opt out optimizing this now as there is no urge to do so. Please rewrite your test to dismiss the where clause when testing
OF. - I think this makes the
FieldReferencechange redundant (?).
- for instance, the upcoming
- Can you provide an example in which
hasJoin = !comp.isJoinWithSubselect();is mandatory?- What happens if we don't do this change?
- Debugging an example with OF, will
hasJoinbe true or false? - How does this affect the FQL?
#22 Updated by Ioana-Cristina Prioteasa over 1 year ago
--¶
OPEN QUERY q1 FOR EACH table1,
EACH table2 OF table1 NO-LOCK.
In PreselectQuery hasJoin is initially true and enters the if, but the behavior is the same with or without the change in PreselectQuery because:
hasJoin = (iteration != FIRST && iteration != LAST);is true - iteration is NEXT!comp.isJoinWithSubselect();is also true (the same reason, the iteration).
This generates:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_ from table1 table1__im0_ cross join table2 table2__im1_ where table2__im1_.pk = table1__im0_.pk order by table1__im0_.pk asc, table1__im0_.recid asc, table2__im1_.pk asc, table2__im1_.recid asc limit 1
--¶
OPEN QUERY q1 FOR EACH table1,
FIRST table2 OF table1 NO-LOCK.
Here, hasJoin is again initially true and enters the if, but the behavior is again the same with or without the change in PreselectQuery because:
hasJoin = (iteration != FIRST && iteration != LAST);is false - iteration is FIRST!comp.isJoinWithSubselect();is also false becausegetFQLPreprocessor().isSubselectNeeded();will return true (baseWhere is null so the subselect is needed) ? this requires investigation.
InFQLPreprocessor.isSubselectNeeded():if (fql == null) { return true; }
This generates:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_
from table1 table1__im0_
cross join table2 table2__im1_ where table2__im1_.recid = ( select table2__im2_.recid as col4_1_
from table2 table2__im2_
where table2__im2_.pk = table1__im0_.pk
order by table2__im2_.pk asc, table2__im2_.recid asc
limit 1)
order by table1__im0_.pk asc, table1__im0_.recid asc
limit 1
Here the subselect is unnecessary. I need to look into this case.
--¶
As is the code now, we need the where explicit to see a difference in behaviour with and without the change in PreselectQuery. This is why the previous testcase was built like that.
OPEN QUERY q1 FOR EACH table1,
FIRST table2 OF table1 WHERE table1.pk = table2.fk NO-LOCK.
Here, hasJoin is again initially true and enters the if, but the behavior is different now with or without the change in PreselectQuery because:
hasJoin = (iteration != FIRST && iteration != LAST);becomes false - iteration is FIRST
This generates:select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_ from table1 table1__im0_ cross join table2 table2__im1_ where table1__im0_.pk = table2__im1_.fk order by table1__im0_.pk asc, table1__im0_.recid asc limit 1
hasJoin = !comp.isJoinWithSubselect();is true becausegetFQLPreprocessor().isSubselectNeeded();will return false.
This generates:select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_ from table1 table1__im0_ cross join table2 table2__im1_ where table2__im1_.pk = table1__im0_.pk and table1__im0_.pk = table2__im1_.fk order by table1__im0_.pk asc, table1__im0_.recid asc limit 1
So here the emitted sql differs and without the changes the where clause coming from the OF clause is not included.
I will test now with this in FQLPreprocessor.isSubselectNeeded():
if (fql == null)
{
return false;
}
#23 Updated by Ioana-Cristina Prioteasa over 1 year ago
For the case baseWhere == null the subselect is not to be generated. This was overlooked. And now it becomes clear that we need the changes in PreselectQuery.
For this:
OPEN QUERY q1 FOR EACH table1,
FIRST table2 OF table1 NO-LOCK.
- without the change this is the generated sql:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_ from table1 table1__im0_ cross join table2 table2__im1_ order by table1__im0_.pk asc, table1__im0_.recid asc limit 1
- with the change this is the generated sql:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_ from table1 table1__im0_ cross join table2 table2__im1_ where table2__im1_.pk = table1__im0_.pk order by table1__im0_.pk asc, table1__im0_.recid asc limit 1
I committed the change in
FQLPreprocessor.isSubselectNeeded with the change for the case where the baseWhere is null and reverted the changes for the relaxed outer join optimization in 9317a rev. 15633. Alex, please review.#24 Updated by Alexandru Lungu over 1 year ago
- Status changed from Review to Internal Test
So here the emitted sql differs and without the changes the where clause coming from the OF clause is not included.
- So this means that there was a bug in trunk regarding FIRST, OF and WHERE?
- Can you confirm that 4GL is indeed "appending" the OF and that the apparent bug in FWD trunk is not in fact a quirk replication?
- Can you do a test with
FOR EACH tt, EACH tt2 OF tt WHERE ...(aka use EACH instead of FIRST, but still keep the OF and WHERE).
!comp.isJoinWithSubselect(); is also false because getFQLPreprocessor().isSubselectNeeded(); will return true (baseWhere is null so the subselect is needed) ? this requires investigation.
Is this something fixed in 9317a or is a remaining TODO? From my understanding is fixed (especially expanded in #9317-23) and seems to be a quite a common pattern, but please re-confirm.
I am technically OK with 9317a in this state, but I recommend serious testing. As there are not that many different customer applications that follow this specific pattern, I suggest building a unit test suite to cover the examples in #9317 (especially #9317-23 and #9317-22). The unit tests shouldn't test whether the optimization kicks in or not, but in a context in which it does, the resulting query is functionally good.
#25 Updated by Ioana-Cristina Prioteasa over 1 year ago
..¶
Alexandru Lungu wrote:
So here the emitted sql differs and without the changes the where clause coming from the OF clause is not included.
- So this means that there was a bug in trunk regarding FIRST, OF and WHERE?
- Can you confirm that 4GL is indeed "appending" the OF and that the apparent bug in FWD trunk is not in fact a quirk replication?
To clarify my previous response, all the examples referenced were tested with the 9317a changes (where subselect optimization is active) and only differ by whether the modification in PreselectQuery is applied. I was addressing the question from the last review: Can you provide an example where hasJoin = !comp.isJoinWithSubselect(); is mandatory?
With a clean trunk, the subselect is generated and the query is correct, there is no bug in trunk. The issue occurs in 9317a when the change in PreselectQuery is missing.
For this:
OPEN QUERY q1 FOR EACH table1,
FIRST table2 OF table1 NO-LOCK.
Trunk generates this query:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_
from table1 table1__im0_
cross join table2 table2__im1_ where table2__im1_.recid = ( select table2__im2_.recid as col4_1_
from table2 table2__im2_
where table2__im2_.pk = table1__im0_.pk
order by table2__im2_.pk asc, table2__im2_.recid asc
limit 1)
order by table1__im0_.pk asc, table1__im0_.recid asc
limit 1
And now with 9317a latest, the query becomes:
select table1__im0_.recid as col0_0_, table2__im1_.recid as id1_, table2__im1_.pk as pk2_0_, table2__im1_.fk as fk3_0_ from table1 table1__im0_ cross join table2 table2__im1_ where table2__im1_.pk = table1__im0_.pk order by table1__im0_.pk asc, table1__im0_.recid asc limit 1
Both queries produce the same result and are functionally equivalent.
..¶
Alexandru Lungu wrote:
- Can you do a test with
FOR EACH tt, EACH tt2 OF tt WHERE ...(aka use EACH instead of FIRST, but still keep the OF and WHERE).
This case does not get optimized, neither with the outer join nor with the subselect optimization, in both trunk and 9317a.
..¶
Alexandru Lungu wrote:
!comp.isJoinWithSubselect(); is also false because getFQLPreprocessor().isSubselectNeeded(); will return true (baseWhere is null so the subselect is needed) ? this requires investigation.
Is this something fixed in 9317a or is a remaining TODO? From my understanding is fixed (especially expanded in #9317-23) and seems to be a quite a common pattern, but please re-confirm.
This issue has been fully addressed and committed in the latest 9317a revision.
..¶
Alexandru Lungu wrote:
I am technically OK with 9317a in this state, but I recommend serious testing. As there are not that many different customer applications that follow this specific pattern, I suggest building a unit test suite to cover the examples in #9317 (especially #9317-23 and #9317-22). The unit tests shouldn't test whether the optimization kicks in or not, but in a context in which it does, the resulting query is functionally good.
I will put toghether a test plan and start working on unit tests.
#26 Updated by Ioana-Cristina Prioteasa over 1 year ago
- Ioana[icp]: large customer application unit tests and regression tests - DONE
- Andrei[ai]: large customer Gui application regression tests and smoke test - DONE
- Lorian[ls]/Ioana[icp]: large customer application harness tests - unavailable ATM
- Lorian[ls]/Andrei[ai]/Ioana[icp]: Chui regression tests - DONE
- Lorian[ls]: ETF tests - DONE
- Ioana[icp]: create, convert and test new 4GL unit tests - DONE
- Ioana[icp]: performance tests - DONE
I am rebasing 9317a as we speak, will update when that is done.
#27 Updated by Ioana-Cristina Prioteasa over 1 year ago
Rebasing 9317 cause some issues because of trunk rev. 15656:
revno: 15656 [merge] author: Chichirau Razvan <rnc@goldencode.com> committer: Radu-Andrei Apetrii <raa@goldencode.com> branch nick: trunk timestamp: Wed 2025-01-22 06:32:01 -0500 message: Added logic to honor comparisons between fields and unknown value while using indexes in FIND queries. (Refs: #9337)
Some extra logic was added regarding
collectPropertyMatches in FQLPreprocessor. In 9317 a new PropertyMatch constructor was added for the case where we need to compute a complex property match (table1.col1 = table2.col2). In trunk PropertyMatch has now 2 extra fields:
/** Flag indicating if the alias is the first or second operand. */
final boolean isAliasFirst;
/** The index of the SUBST node relative to the 'where' clause parameters. */
final long substIndex;
In my (9317a) construnctor, I defaulted those to:
this.isAliasFirst = false; this.substIndex = -1;
These should not interfere with each other, as my changes take effect only when
collectPropertyMatch is called with the simple=false parameter.Razvan, does this look ok?
#28 Updated by Razvan-Nicolae Chichirau over 1 year ago
In the trunk version, collectPropertyMatches defaults them to false and -1 when creating the AstWalkListener, so there shouldn't be any problem.
#29 Updated by Ioana-Cristina Prioteasa over 1 year ago
9317a is rebased, the revisions that need testing: 15679 - 15689.
#30 Updated by Andrei Iacob over 1 year ago
Ioana-Cristina Prioteasa wrote:
Testing plan for 9317a:
- Andrei[ai]: large customer Gui application regression tests and smoke test
All good.
#31 Updated by Ioana-Cristina Prioteasa over 1 year ago
I enabled SQL logging during a performance test with a threshold of 0, ensuring all queries were logged. The test was conducted across three different scenarios. For each, I performed three warm-up runs and then recorded the queries executed during the fourth test, along with their total execution time.
| Scenario | Total execution time (s) |
|---|---|
| Outer join optimization disabled | 1874 |
| Outer join optimization enabled | 2132 |
| Outer join optimization enabled + 9317a changes | 2013 |
From these results, I can conclude that 9317a introduces a performance improvement, but it does not fully resolve all the existing outer join optimization issues. Further investigation is needed on #8616 to identify and address the remaining inefficiencies.
#32 Updated by Alexandru Lungu over 1 year ago
What is the timing reflect? The total time for a specific query? What was the query analyzed? Or do you mean all SQL queries in PG?
#33 Updated by Ioana-Cristina Prioteasa over 1 year ago
The time is the total execution time of all queries executed during the test.
#34 Updated by Lorian Sandu over 1 year ago
Ioana-Cristina Prioteasa wrote:
Testing plan for 9317a:
- Lorian[ls]/Andrei[ai]/Ioana[icp]: Chui regression tests
Chui regression tests passed ✅
#35 Updated by Ioana-Cristina Prioteasa over 1 year ago
Tested 150 performance tests and the average time for a run stands for what I observed in #9317-31.
| Scenario | Average time for a run (ms) |
|---|---|
| Outer join optimization disabled | 38473 |
| Outer join optimization enabled | 39968 |
| Outer join optimization enabled + 9317a changes | 39386 |
In a big customer application I have 2 extra passing unit tests. As these changes should not introduce functional difference, I am actively investigating those.
I also wrote 10 new 4gl unit tests that verify the number of results returned by a query and the order of those results. These pass without and with 9317a changes.
I updated the testing plan in #9317-26 to reflect the current status.
#36 Updated by Lorian Sandu over 1 year ago
ETF passed .
#37 Updated by Ioana-Cristina Prioteasa over 1 year ago
The two additional tests passing are not a direct result of 9317a. The failures occur when the outer join optimization is disabled, while the tests pass in both scenarios where outer join optimization is enabled, with and without 9317a. This suggests that the root cause of these test results lies in #8616 rather than #9317 and further investigation should be conducted there.
The testing plan is now complete. While harness tests are currently unavailable, all other testing steps showed no regressions.
In my opinion, 9317 is ready to be merged.
#38 Updated by Eric Faulhaber over 1 year ago
Ioana-Cristina Prioteasa wrote:
[...]
The testing plan is now complete. While harness tests are currently unavailable, all other testing steps showed no regressions.
Ovidiu is close to making the harness work. Ovidiu, please regression test 9317a with the harness in multi-tenant mode, when you have it working, and report success/failure here.
#39 Updated by Ioana-Cristina Prioteasa over 1 year ago
#40 Updated by Ovidiu Maxiniuc over 1 year ago
9317a/15689 passed the MT harness test ✅.
#41 Updated by Alexandru Lungu over 1 year ago
- Status changed from Internal Test to Merge Pending
- % Done changed from 50 to 100
Please merge 9317a to trunk now.
#42 Updated by Ioana-Cristina Prioteasa over 1 year ago
- % Done changed from 100 to 50
9317a is merged to trunk as rev. 15708 and archived.
#43 Updated by Ioana-Cristina Prioteasa over 1 year ago
- % Done changed from 50 to 100
#44 Updated by Alexandru Lungu over 1 year ago
- Status changed from Merge Pending to Test
Ioana, feel free to also merge 9317a to 7156c and 7156d.
#45 Updated by Alexandru Lungu about 1 year ago
- Status changed from Test to Closed
This can be closed.