diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java
index 3365c79..2a31776 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java
@@ -15,8 +15,13 @@ package org.apache.jackrabbit.oak.query;
 
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import aQute.bnd.annotation.ProviderType;
+
 import org.apache.jackrabbit.oak.api.PropertyValue;
 import org.apache.jackrabbit.oak.api.Result;
 import org.apache.jackrabbit.oak.api.Tree;
@@ -118,4 +123,53 @@ public interface Query {
      * @return if sorted by index
      */
     boolean isSortedByIndex();
+    
+    /**
+     * Perform optimisation on the object itself. To avoid any potential error due to state
+     * variables perfom the optimisation before the {@link #init()}.
+     * 
+     * @return {@code this} if no optimisations are possible or a new instance of a {@link Query}.
+     *         Cannot return null.
+     */
+    @Nonnull
+    Query optimise();
+    
+    /**
+     * <p>
+     * returns a clone of the current object. Will throw an exception in case it's invoked in a non
+     * appropriate moment. For example the default {@link QueryImpl} cannot be cloned once the
+     * {@link #init()} has been executed.
+     * </p>
+     * 
+     * <p>
+     * <strong>May return null if not implemented.</strong>
+     * </p>
+     * @return a clone of self
+     * @throws IllegalStateException
+     */
+    @Nullable
+    Query copyOf() throws IllegalStateException;
+    
+    /**
+     * @return {@code true} if the query has been already initialised. {@code false} otherwise.
+     */
+    boolean isInit();
+    
+    /**
+     * @return {@code true} if the query is a result of optimisations. {@code false} if it's the
+     *         originally computed one.
+     */
+    boolean isOptimised();
+    
+    /**
+     * @return the original statement as it was used to construct the object. If not provided the
+     *         {@link #toString()} will be used instead.
+     */
+    String getStatement();
+    
+    /**
+     * 
+     * @return {@code true} if the current query is internal. {@code false} otherwise.
+     */
+    boolean isInternal();
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineImpl.java
index 0469637..12d65de 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineImpl.java
@@ -16,7 +16,9 @@
  */
 package org.apache.jackrabbit.oak.query;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.ImmutableSet.of;
+import static com.google.common.collect.Sets.newHashSet;
 import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM;
 import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_NODE_TYPES;
 
@@ -27,6 +29,8 @@ import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import javax.annotation.Nonnull;
+
 import org.apache.jackrabbit.oak.api.PropertyValue;
 import org.apache.jackrabbit.oak.api.QueryEngine;
 import org.apache.jackrabbit.oak.api.Result;
@@ -44,6 +48,27 @@ import org.slf4j.MDC;
  */
 public abstract class QueryEngineImpl implements QueryEngine {
     
+    /**
+     * used to instruct the {@link QueryEngineImpl} on how to act with respect of the SQL2
+     * optimisation.
+     */
+    public static enum ForceOptimised {
+        /**
+         * will force the original SQL2 query to be executed
+         */
+        ORIGINAL, 
+        
+        /**
+         * will force the computed optimised query to be executed. If available.
+         */
+        OPTIMISED, 
+        
+        /**
+         * will execute the cheapest.
+         */
+        CHEAPEST
+    }
+
     private static final AtomicInteger ID_COUNTER = new AtomicInteger();
     private static final String MDC_QUERY_ID = "oak.query.id";
     private static final String OAK_QUERY_ANALYZE = "oak.query.analyze";
@@ -70,6 +95,12 @@ public abstract class QueryEngineImpl implements QueryEngine {
     private boolean traversalEnabled = true;
     
     /**
+     * Whether the query engine should be forced to use the optimised version of the query if
+     * available.
+     */
+    private ForceOptimised forceOptimised = ForceOptimised.CHEAPEST;
+
+    /**
      * Get the execution context for a single query execution.
      * 
      * @return the context
@@ -94,11 +125,12 @@ public abstract class QueryEngineImpl implements QueryEngine {
     public List<String> getBindVariableNames(
             String statement, String language, Map<String, String> mappings)
             throws ParseException {
-        Query q = parseQuery(statement, language, getExecutionContext(), mappings);
-        return q.getBindVariableNames();
+        Set<Query> qs = parseQuery(statement, language, getExecutionContext(), mappings);
+        
+        return qs.iterator().next().getBindVariableNames();
     }
 
-    private static Query parseQuery(
+    private static Set<Query> parseQuery(
             String statement, String language, ExecutionContext context,
             Map<String, String> mappings) throws ParseException {
         
@@ -123,11 +155,16 @@ public abstract class QueryEngineImpl implements QueryEngine {
             parser.setAllowNumberLiterals(false);
             parser.setAllowTextLiterals(false);
         }
+        
+        Set<Query> queries = newHashSet();
+        
+        Query q;
+        
         if (SQL2.equals(language) || JQOM.equals(language)) {
-            return parser.parse(statement);
+            q = parser.parse(statement, false);
         } else if (SQL.equals(language)) {
             parser.setSupportSQL1(true);
-            return parser.parse(statement);
+            q = parser.parse(statement, false);
         } else if (XPATH.equals(language)) {
             XPathToSQL2Converter converter = new XPathToSQL2Converter();
             String sql2 = converter.convert(statement);
@@ -135,7 +172,7 @@ public abstract class QueryEngineImpl implements QueryEngine {
             try {
                 // OAK-874: No artificial XPath selector name in wildcards
                 parser.setIncludeSelectorNameInWildcardColumns(false);
-                return parser.parse(sql2);
+                q = parser.parse(sql2, false);
             } catch (ParseException e) {
                 ParseException e2 = new ParseException(
                         statement + " converted to SQL-2 " + e.getMessage(), 0);
@@ -145,6 +182,34 @@ public abstract class QueryEngineImpl implements QueryEngine {
         } else {
             throw new ParseException("Unsupported language: " + language, 0);
         }
+        
+        queries.add(q);
+        
+        if (settings.isSql2Optimisation()) {
+            if (q.isInternal()) {
+                LOG.trace("Skipping optimisation as internal query.");
+            } else {
+                LOG.trace("Attempting optimisation");
+                Query q2 = q.optimise();
+                if (q2 != q) {
+                    LOG.debug("Optimised query available. {}", q2);
+                    queries.add(q2);
+                }
+            }
+        }
+        
+        // initialising all the queries.
+        for (Query query : queries) {
+            try {
+                query.init();
+            } catch (Exception e) {
+                ParseException e2 = new ParseException(query.getStatement() + ": " + e.getMessage(), 0);
+                e2.initCause(e);
+                throw e2;
+            }
+        }
+
+        return queries;
     }
     
     @Override
@@ -176,7 +241,9 @@ public abstract class QueryEngineImpl implements QueryEngine {
         }
 
         ExecutionContext context = getExecutionContext();
-        Query q = parseQuery(statement, language, context, mappings);
+        Set<Query> queries = parseQuery(statement, language, context, mappings);
+        
+        for (Query q : queries) {
             q.setExecutionContext(context);
             q.setLimit(limit);
             q.setOffset(offset);
@@ -186,11 +253,13 @@ public abstract class QueryEngineImpl implements QueryEngine {
                 }
             }
             q.setTraversalEnabled(traversalEnabled);            
+        }
 
         boolean mdc = false;
         try {
-            mdc = setupMDC(q);
-            q.prepare();
+            MdcAndPrepared map = prepareAndGetCheapest(queries); 
+            mdc = map.mdc;
+            Query q = map.query;
             return q.executeQuery();
         } finally {
             if (mdc) {
@@ -199,6 +268,106 @@ public abstract class QueryEngineImpl implements QueryEngine {
         }
     }
 
+    /**
+     * POJO class used to return the cheapest prepared query from the set and related MDC status
+     */
+    private static class MdcAndPrepared {
+        private final boolean mdc;
+        private final Query query;
+        
+        public MdcAndPrepared(final boolean mdc, @Nonnull final Query q) {
+            this.mdc = mdc;
+            this.query = checkNotNull(q);
+        }
+    }
+    
+    /**
+     * will prepare all the available queries and by based on the {@link ForceOptimised} flag return
+     * the appropriate.
+     * 
+     * @param queries the list of queries to be executed. cannot be null
+     * @return
+     */
+    @Nonnull
+    private MdcAndPrepared prepareAndGetCheapest(@Nonnull final Set<Query> queries) {
+        MdcAndPrepared map = null;
+        double bestCost = Double.POSITIVE_INFINITY;
+        Query cheapest = null;
+        Query original = null;
+        
+        
+        // always prepare all of the queries and compute the cheapest as it's the default behaviour.
+        // It should trigger more errors during unit and integration testing. Changing
+        // `forceOptimised` flag should be in case used only during testing.
+        for (Query q : checkNotNull(queries)) {
+            LOG.debug("Preparing: {}", q);
+            q.prepare();
+            if (q.getEstimatedCost() < bestCost) {
+                bestCost = q.getEstimatedCost();
+                cheapest = q;
+            }
+            if (!q.isOptimised()) {
+                original = q;
+            }
+        }
+        
+        // if the optimised cost is the same as the original SQL2 query we prefer the original. As
+        // we deal with references the `cheapest!=original` should work.
+        if (original != null && bestCost == original.getEstimatedCost()
+            && cheapest != original) {
+            LOG.trace("Same cost for original and optimised. Forcing original");
+            cheapest = original;
+        }
+        
+        switch (forceOptimised) {
+        case ORIGINAL:
+            LOG.debug("Forcing the original SQL2 query to be executed by flag");
+            for (Query q  : checkNotNull(queries)) {
+                if (!q.isOptimised()) {
+                    map = new MdcAndPrepared(setupMDC(q), q);
+                }
+            }
+            break;
+
+        case OPTIMISED:
+            LOG.debug("Forcing the optimised SQL2 query to be executed by flag");
+            for (Query q  : checkNotNull(queries)) {
+                if (q.isOptimised()) {
+                    map = new MdcAndPrepared(setupMDC(q), q);
+                }
+            }
+            break;
+
+        // CHEAPEST is the default behaviour
+        case CHEAPEST:
+        default:
+            if (cheapest == null) {
+                // this should not really happen. Defensive coding.
+                LOG.debug("Cheapest is null. Returning the original SQL2 query.");
+                for (Query q  : checkNotNull(queries)) {
+                    if (!q.isOptimised()) {
+                        map = new MdcAndPrepared(setupMDC(q), q);
+                    }
+                }
+            } else {
+                LOG.debug("Cheapest cost: {} - query: {}", bestCost, cheapest);
+                map = new MdcAndPrepared(setupMDC(cheapest), cheapest);                
+            }
+        }
+        
+        if (map == null) {
+            // we should only get here in case of testing forcing weird conditions
+            LOG.trace("`MdcAndPrepared` is null. Falling back to the original query");
+            for (Query q  : checkNotNull(queries)) {
+                if (!q.isOptimised()) {
+                    map = new MdcAndPrepared(setupMDC(q), q);
+                }
+            }
+        }
+        
+        return map;
+    }
+    
     protected void setTraversalEnabled(boolean traversalEnabled) {
         this.traversalEnabled = traversalEnabled;
     }
@@ -222,4 +391,13 @@ public abstract class QueryEngineImpl implements QueryEngine {
         MDC.remove(OAK_QUERY_ANALYZE);
     }
 
+    /**
+     * Instruct the query engine on how to behave with regards to the SQL2 optimised query if
+     * available.
+     * 
+     * @param forceOptimised cannot be null
+     */
+    protected void setForceOptimised(@Nonnull ForceOptimised forceOptimised) {
+        this.forceOptimised = forceOptimised;
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
index 07560c3..a040aa9 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
@@ -41,6 +41,8 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean {
     private boolean fullTextComparisonWithoutIndex = 
             DEFAULT_FULL_TEXT_COMPARISON_WITHOUT_INDEX;
     
+    private boolean sql2Optimisation = Boolean.getBoolean("oak.query.sql2optimisation");
+    
     /**
      * Get the limit on how many nodes a query may read at most into memory, for
      * "order by" and "distinct" queries. If this limit is exceeded, the query
@@ -93,4 +95,7 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean {
         return fullTextComparisonWithoutIndex;
     }
     
+    public boolean isSql2Optimisation() {
+        return sql2Optimisation;
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java
index 8d47b8a..d58eb19 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java
@@ -13,6 +13,10 @@
  */
 package org.apache.jackrabbit.oak.query;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Lists.newArrayList;
+import static org.apache.jackrabbit.oak.query.ast.AstElementFactory.copyElementAndCheckReference;
+
 import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -24,6 +28,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.google.common.base.Strings;
 import com.google.common.collect.AbstractIterator;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -153,6 +161,16 @@ public class QueryImpl implements Query {
     private boolean prepared;
     private ExecutionContext context;
     
+    /**
+     * whether the object has been initialised or not
+     */
+    private boolean init;
+    
+    /**
+     * whether the query is a result of optimisation or original one.
+     */
+    private boolean optimised;
+
     private boolean isSortedByIndex;
 
     private final NamePathMapper namePathMapper;
@@ -167,12 +185,19 @@ public class QueryImpl implements Query {
 
     QueryImpl(String statement, SourceImpl source, ConstraintImpl constraint,
             ColumnImpl[] columns, NamePathMapper mapper, QueryEngineSettings settings) {
+        this(statement, source, constraint, columns, mapper, settings, false);
+    }
+
+    QueryImpl(String statement, SourceImpl source, ConstraintImpl constraint,
+        ColumnImpl[] columns, NamePathMapper mapper, QueryEngineSettings settings, 
+        final boolean optimised) {
         this.statement = statement;
         this.source = source;
         this.constraint = constraint;
         this.columns = columns;
         this.namePathMapper = mapper;
         this.settings = settings;
+        this.optimised = optimised;
     }
 
     @Override
@@ -408,6 +433,8 @@ public class QueryImpl implements Query {
             }
             distinctColumns[i] = distinct;
         }
+        
+        init = true;
     }
 
     @Override
@@ -1110,8 +1137,9 @@ public class QueryImpl implements Query {
         return Math.min(limit, source.getSize(precision, max));
     }
 
+    @Override
     public String getStatement() {
-        return statement;
+        return Strings.isNullOrEmpty(statement) ? toString() : statement;
     }
 
     public QueryEngineSettings getSettings() {
@@ -1142,4 +1170,128 @@ public class QueryImpl implements Query {
         return sum.min(max).max(min).longValue();
     }
 
+    @Override
+    public Query optimise() {
+        // optimising for UNION
+        Query optimised = this;
+        
+        if (constraint != null) {
+            List<ConstraintImpl> unionList = addToUnionList(constraint, null);
+            if (!unionList.isEmpty()) {
+                QueryImpl left = null;
+                Query right = null;
+                // we have something to do here.
+                for (ConstraintImpl c : unionList) {
+                    if (right != null) {
+                        right = newOptimisedUnionQuery(left, right);
+                    } else {
+                        // pulling left to the right
+                        if (left != null) {
+                            right = left;
+                        }
+                    }
+                    
+                    // cloning original query
+                    left = (QueryImpl) this.copyOf(true);
+                    
+                    // cloning the constraints and assigning to new query
+                    left.constraint = (ConstraintImpl) copyElementAndCheckReference(c);                     
+                }
+                
+                optimised = newOptimisedUnionQuery(left, right);
+            }
+        }
+        
+        return optimised;
+    }
+    
+    /**
+     * convenience method for creating a UnionQueryImpl with proper settings.
+     * 
+     * @param left
+     * @param right
+     * @return
+     */
+    private UnionQueryImpl newOptimisedUnionQuery(@Nonnull Query left, @Nonnull Query right) {
+        UnionQueryImpl u = new UnionQueryImpl(false, checkNotNull(left), checkNotNull(right),
+            this.settings, true);
+        u.setExplain(explain);
+        return u;
+    }
+    
+    /**
+     * parse the provided {@link ConstraintImpl} and will add it to the provided {@code unionList}.
+     * if {@code unionList} is null an empty one will be created.
+     * 
+     * @param constraint the constraint to analyse. Cannot be null.
+     * @param unionList the list to which adding the constraints for the union.
+     * @return a list with all the union constraints. An empty one if no constraint could have been
+     *         converted to union.
+     */
+    @Nonnull
+    private static List<ConstraintImpl> addToUnionList(@Nonnull ConstraintImpl constraint,
+                                                       @Nullable List<ConstraintImpl> unionList) {
+        checkNotNull(constraint);
+        List<ConstraintImpl> u = unionList;
+        
+        if (u == null) {
+            u = newArrayList();
+        }
+        
+        if (constraint.isUnion() && constraint.getConstraints() != null) {
+            for (ConstraintImpl c : constraint.getConstraints()) {
+                if (c.isUnion()) {
+                    u = addToUnionList(c, u);
+                } else {
+                    u.add(c);
+                }
+            }
+        }
+        
+        return u;
+    }
+
+    @Override
+    public Query copyOf() throws IllegalStateException {
+        return copyOf(false);
+    }
+    
+    private Query copyOf(final boolean optimised) {
+        if (isInit()) {
+            throw new IllegalStateException("QueryImpl cannot be cloned once initialised.");
+        }
+        
+        List<ColumnImpl> cols = newArrayList();
+        for (ColumnImpl c : columns) {
+            cols.add((ColumnImpl) copyElementAndCheckReference(c));
+        }
+                
+        QueryImpl copy = new QueryImpl(
+            this.statement, 
+            (SourceImpl) copyElementAndCheckReference(this.source),
+            this.constraint,
+            cols.toArray(new ColumnImpl[0]),
+            this.namePathMapper,
+            this.settings,
+            optimised);
+        copy.explain = this.explain;
+        copy.distinct = this.distinct;
+        
+        return copy;        
+    }
+
+    @Override
+    public boolean isInit() {
+        return init;
+    }
+
+    @Override
+    public boolean isOptimised() {
+        return optimised;
+    }
+
+    @Override
+    public boolean isInternal() {
+        return isInternal;
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java
index 6f3191a..52a5765 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java
@@ -125,10 +125,11 @@ public class SQL2Parser {
      * Parse the statement and return the query.
      *
      * @param query the query string
+     * @param initialise if performing the query init ({@code true}) or not ({@code false})
      * @return the query
      * @throws ParseException if parsing fails
      */
-    public Query parse(String query) throws ParseException {
+    public Query parse(final String query, final boolean initialise) throws ParseException {
         // TODO possibly support union,... as available at
         // http://docs.jboss.org/modeshape/latest/manuals/reference/html/jcr-query-and-search.html
 
@@ -163,17 +164,32 @@ public class SQL2Parser {
         q.setOrderings(orderings);
         q.setExplain(explain);
         q.setMeasure(measure);
+        q.setInternal(isInternal(query));
+
+        if (initialise) {
             try {
                 q.init();
             } catch (Exception e) {
-            ParseException e2 = new ParseException(query + ": " + e.getMessage(), 0);
+                ParseException e2 = new ParseException(statement + ": " + e.getMessage(), 0);
                 e2.initCause(e);
                 throw e2;
             }
-        q.setInternal(isInternal(query));
+        }
+
         return q;
     }
     
+    /**
+     * as {@link #parse(String, boolean)} by providing {@code true} to the initialisation flag.
+     * 
+     * @param query
+     * @return
+     * @throws ParseException
+     */
+    public Query parse(final String query) throws ParseException {
+        return parse(query, true);
+    }
+    
     private QueryImpl parseSelect() throws ParseException {
         read("SELECT");
         boolean distinct = readIf("DISTINCT");
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/UnionQueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/UnionQueryImpl.java
index 8884e96..320eac3 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/UnionQueryImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/UnionQueryImpl.java
@@ -56,11 +56,22 @@ public class UnionQueryImpl implements Query {
     private final QueryEngineSettings settings;
     private boolean isInternal;
     
+    /**
+     * whether the query is a result of optimisation or not
+     */
+    private boolean optimised;
+    
     UnionQueryImpl(boolean unionAll, Query left, Query right, QueryEngineSettings settings) {
+        this(unionAll, left, right, settings, false);
+    }
+
+    UnionQueryImpl(final boolean unionAll, final Query left, final Query right,
+                   final QueryEngineSettings settings, final boolean optimised) {
         this.unionAll = unionAll;
         this.left = left;
         this.right = right;
         this.settings = settings;
+        this.optimised = optimised;
     }
 
     @Override
@@ -344,4 +355,34 @@ public class UnionQueryImpl implements Query {
     public boolean isSortedByIndex() {
         return left.isSortedByIndex() && right.isSortedByIndex();
     }
+
+    @Override
+    public Query optimise() {
+        return this;
+    }
+
+    @Override
+    public Query copyOf() throws IllegalStateException {
+        return null;
+    }
+
+    @Override
+    public boolean isInit() {
+        return left.isInit() || right.isInit();
+    }
+
+    @Override
+    public boolean isOptimised() {
+        return optimised;
+    }
+
+    @Override
+    public String getStatement() {
+        return toString();
+    }
+
+    @Override
+    public boolean isInternal() {
+        return left.isInternal() || right.isInternal();
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java
index bf4f0dc..081d9e2 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java
@@ -22,7 +22,9 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Sets.newHashSet;
 import static com.google.common.collect.Sets.newLinkedHashSet;
+import static org.apache.jackrabbit.oak.query.ast.AstElementFactory.copyElementAndCheckReference;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -48,6 +50,7 @@ public class AndImpl extends ConstraintImpl {
         this(Arrays.asList(constraint1, constraint2));
     }
 
+    @Override
     public List<ConstraintImpl> getConstraints() {
         return constraints;
     }
@@ -195,4 +198,13 @@ public class AndImpl extends ConstraintImpl {
         return constraints.hashCode();
     }
 
+    @Override
+    public AstElement copyOf() {
+        List<ConstraintImpl> clone = new ArrayList<ConstraintImpl>(constraints.size());
+        for (ConstraintImpl c : constraints) {
+            clone.add((ConstraintImpl) copyElementAndCheckReference(c));
+        }
+        return new AndImpl(clone);
+    }
+
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElement.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElement.java
index 6e6d81e..6da40dd2 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElement.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElement.java
@@ -18,6 +18,8 @@
  */
 package org.apache.jackrabbit.oak.query.ast;
 
+import javax.annotation.Nonnull;
+
 import org.apache.jackrabbit.oak.api.PropertyValue;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.query.QueryImpl;
@@ -27,7 +29,6 @@ import org.apache.jackrabbit.oak.spi.query.PropertyValues;
  * The base class for all abstract syntax tree nodes.
  */
 abstract class AstElement {
-
     protected QueryImpl query;
 
     abstract boolean accept(AstVisitor v);
@@ -143,5 +144,13 @@ abstract class AstElement {
         return path;
     }
 
+    /**
+     * @return a clone of self. Default implementation in {@link AstElement} returns same reference
+     *         to {@code this}.
+     */
+    @Nonnull
+    public AstElement copyOf() {
+        return this;
+    }    
 }
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
index e2ec02f..ea2522e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
@@ -13,15 +13,22 @@
  */
 package org.apache.jackrabbit.oak.query.ast;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import java.util.ArrayList;
 
+import javax.annotation.Nonnull;
+
 import org.apache.jackrabbit.oak.api.PropertyValue;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A factory for syntax tree elements.
  */
 public class AstElementFactory {
+    private static final Logger LOG = LoggerFactory.getLogger(AstElementFactory.class);
 
     public AndImpl and(ConstraintImpl constraint1, ConstraintImpl constraint2) {
         return new AndImpl(constraint1, constraint2);
@@ -164,4 +171,28 @@ public class AstElementFactory {
     public ConstraintImpl suggest(String selectorName, StaticOperandImpl expression) {
         return new SuggestImpl(selectorName, expression);
     }
+    
+    /**
+     * <p>
+     * as the {@link AstElement#copyOf()} can return {@code this} is the cloning is not implemented
+     * by the subclass, this method add some spice around it by checking for this case and tracking
+     * a DEBUG message in the logs.
+     * </p>
+     * 
+     * @param e the element to be cloned. Cannot be null.
+     * @return same as {@link AstElement#copyOf()}
+     */
+    @Nonnull
+    public static AstElement copyElementAndCheckReference(@Nonnull final AstElement e) {
+        AstElement clone = checkNotNull(e).copyOf();
+        
+        if (clone == e && LOG.isDebugEnabled()) {
+            LOG.debug(
+                "Failed to clone the AstElement. Returning same reference; the client may fail. {} - {}",
+                e.getClass().getName(), e);
+        }
+        
+        return clone;
+    }
+
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeImpl.java
index a053ada..385a408 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeImpl.java
@@ -95,4 +95,8 @@ public class ChildNodeImpl extends ConstraintImpl {
         }
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new ChildNodeImpl(selectorName, parentPath);
+    }
 }
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeJoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeJoinConditionImpl.java
index 2994549..b91c484 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeJoinConditionImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ChildNodeJoinConditionImpl.java
@@ -103,4 +103,8 @@ public class ChildNodeJoinConditionImpl extends JoinConditionImpl {
         return available.contains(childSelector) && available.contains(parentSelector);
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new ChildNodeJoinConditionImpl(childSelectorName, parentSelectorName);
+    }
 }
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ColumnImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ColumnImpl.java
index ed806a8..e60736a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ColumnImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ColumnImpl.java
@@ -67,4 +67,8 @@ public class ColumnImpl extends AstElement {
         return selector;
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new ColumnImpl(selectorName, propertyName, columnName);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java
index 51531a3..90ee031 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java
@@ -30,11 +30,14 @@ import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
 import org.apache.jackrabbit.oak.query.fulltext.LikePattern;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 import org.apache.jackrabbit.oak.spi.query.PropertyValues;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A comparison operation (including "like").
  */
 public class ComparisonImpl extends ConstraintImpl {
+    private static final Logger LOG = LoggerFactory.getLogger(ComparisonImpl.class);
     
     private final DynamicOperandImpl operand1;
     private final Operator operator;
@@ -195,4 +198,8 @@ public class ComparisonImpl extends ConstraintImpl {
         }
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new ComparisonImpl(operand1.createCopy(), operator, operand2);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java
index 916f8c8..f20f13c 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java
@@ -16,8 +16,11 @@
  */
 package org.apache.jackrabbit.oak.query.ast;
 
+import java.util.List;
 import java.util.Set;
 
+import javax.annotation.Nullable;
+
 import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 
@@ -126,4 +129,23 @@ public abstract class ConstraintImpl extends AstElement {
         return toString().hashCode();
     }
 
+    /**
+     * Tells whether the current constraint can be optimised as union query or not. The base
+     * implementation {@link ConstraintImpl#isUnion()} always return false.
+     * 
+     * @return {@code true} if can be converted to union, {@code false} otherwise.
+     */
+    public boolean isUnion() {
+        return false;
+    }
+    
+    /**
+     * 
+     * @return the list of {@link ConstraintImpl} that the current constraint could hold. Default
+     *         implementation returns {@code null}.
+     */
+    @Nullable
+    public List<ConstraintImpl> getConstraints() {
+        return null;
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeImpl.java
index 85aa73b..c2f7098 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeImpl.java
@@ -92,4 +92,8 @@ public class DescendantNodeImpl extends ConstraintImpl {
         }
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new DescendantNodeImpl(selectorName, ancestorPath);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeJoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeJoinConditionImpl.java
index d0f7e0e..e8a5f66 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeJoinConditionImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/DescendantNodeJoinConditionImpl.java
@@ -97,4 +97,9 @@ public class DescendantNodeJoinConditionImpl extends JoinConditionImpl {
         return available.contains(descendantSelector) && available.contains(ancestorSelector);
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new DescendantNodeJoinConditionImpl(descendantSelectorName, ancestorSelectorName);
+    }
+
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/EquiJoinConditionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/EquiJoinConditionImpl.java
index 1e26c73..6908019 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/EquiJoinConditionImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/EquiJoinConditionImpl.java
@@ -172,4 +172,8 @@ public class EquiJoinConditionImpl extends JoinConditionImpl {
         return available.contains(selector1) && available.contains(selector2);
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new EquiJoinConditionImpl(selector1Name, property1Name, selector2Name, property2Name);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
index d1dc40f..50eb883 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
@@ -272,4 +272,8 @@ public class FullTextSearchImpl extends ConstraintImpl {
         }
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new FullTextSearchImpl(selectorName, propertyName, fullTextSearchExpression);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/InImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/InImpl.java
index d306d92..cea92e4 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/InImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/InImpl.java
@@ -179,4 +179,8 @@ public class InImpl extends ConstraintImpl {
         return operand1.hashCode();
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new InImpl(operand1.createCopy(), operand2);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinImpl.java
index c905ab9..8a004c9 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/JoinImpl.java
@@ -13,6 +13,8 @@
  */
 package org.apache.jackrabbit.oak.query.ast;
 
+import static org.apache.jackrabbit.oak.query.ast.AstElementFactory.copyElementAndCheckReference;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -27,7 +29,6 @@ import org.apache.jackrabbit.oak.spi.state.NodeState;
  * source, the join type, and the join condition.
  */
 public class JoinImpl extends SourceImpl {
-
     private final JoinConditionImpl joinCondition;
     private JoinType joinType;
     private SourceImpl left;
@@ -273,4 +274,13 @@ public class JoinImpl extends SourceImpl {
         return -1;
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new JoinImpl(
+            (SourceImpl) copyElementAndCheckReference(left),
+            (SourceImpl) copyElementAndCheckReference(right),
+            joinType,
+            (JoinConditionImpl) copyElementAndCheckReference(joinCondition)
+            );
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NativeFunctionImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NativeFunctionImpl.java
index 083d7ec..303f139 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NativeFunctionImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NativeFunctionImpl.java
@@ -107,4 +107,8 @@ public class NativeFunctionImpl extends ConstraintImpl {
         return nativeSearchExpression;
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new NativeFunctionImpl(selectorName, language, nativeSearchExpression);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotImpl.java
index b334701..440b10a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotImpl.java
@@ -19,6 +19,7 @@
 package org.apache.jackrabbit.oak.query.ast;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static org.apache.jackrabbit.oak.query.ast.AstElementFactory.copyElementAndCheckReference;
 
 import java.util.Collections;
 import java.util.List;
@@ -124,4 +125,8 @@ public class NotImpl extends ConstraintImpl {
         // TODO convert NOT conditions
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new NotImpl((ConstraintImpl) copyElementAndCheckReference(constraint));
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java
index e2a8d04..7ff6f8f 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java
@@ -23,6 +23,7 @@ import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Maps.newLinkedHashMap;
 import static com.google.common.collect.Sets.newHashSet;
 import static com.google.common.collect.Sets.newLinkedHashSet;
+import static org.apache.jackrabbit.oak.query.ast.AstElementFactory.copyElementAndCheckReference;
 import static org.apache.jackrabbit.oak.query.ast.Operator.EQUAL;
 
 import java.util.Arrays;
@@ -37,6 +38,8 @@ import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.query.fulltext.FullTextOr;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 
+import com.google.common.collect.Lists;
+
 /**
  * An "or" condition.
  */
@@ -53,6 +56,7 @@ public class OrImpl extends ConstraintImpl {
         this(Arrays.asList(constraint1, constraint2));
     }
 
+    @Override
     public List<ConstraintImpl> getConstraints() {
         return constraints;
     }
@@ -335,4 +339,17 @@ public class OrImpl extends ConstraintImpl {
         return constraints.hashCode();
     }
 
+    @Override
+    public boolean isUnion() {
+        return true;
+    }
+
+    @Override
+    public AstElement copyOf() {
+        List<ConstraintImpl> clone = newArrayList();
+        for (ConstraintImpl c : constraints) {
+            clone.add((ConstraintImpl) copyElementAndCheckReference(c));
+        }
+        return new OrImpl(clone);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyExistenceImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyExistenceImpl.java
index 8dbd367..ca17395 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyExistenceImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyExistenceImpl.java
@@ -114,4 +114,8 @@ public class PropertyExistenceImpl extends ConstraintImpl {
         return a == null || b == null ? a == b : a.equals(b);
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new PropertyExistenceImpl(selectorName, propertyName);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyInexistenceImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyInexistenceImpl.java
index 259c386..6ad156b 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyInexistenceImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyInexistenceImpl.java
@@ -165,4 +165,8 @@ public class PropertyInexistenceImpl extends ConstraintImpl {
         return a == null || b == null ? a == b : a.equals(b);
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new PropertyInexistenceImpl(selectorName, propertyName);
+    }
 }
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyValueImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyValueImpl.java
index 0c30068..1f365d3 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyValueImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PropertyValueImpl.java
@@ -155,5 +155,4 @@ public class PropertyValueImpl extends DynamicOperandImpl {
     public PropertyValueImpl createCopy() {
         return new PropertyValueImpl(selectorName, propertyName);
     }
-
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeImpl.java
index 57995e3..c84686a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SameNodeImpl.java
@@ -84,4 +84,8 @@ public class SameNodeImpl extends ConstraintImpl {
         }
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new SameNodeImpl(selectorName, path);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
index 65179e8..affc48b 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
@@ -768,4 +768,8 @@ public class SelectorImpl extends SourceImpl {
         return cursor.getSize(precision, max);
     }
 
+    @Override
+    public SourceImpl copyOf() {
+        return new SelectorImpl(nodeType, selectorName);
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SimilarImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SimilarImpl.java
index ada24fd..70af239 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SimilarImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SimilarImpl.java
@@ -127,4 +127,9 @@ public class SimilarImpl extends ConstraintImpl {
         return pathExpression;
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new SimilarImpl(selectorName, propertyName, pathExpression);
+    }
+
 }
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SourceImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SourceImpl.java
index 56d23ea..f54f8ba 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SourceImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SourceImpl.java
@@ -162,5 +162,4 @@ public abstract class SourceImpl extends AstElement {
      * @return the size, or -1 if unknown
      */
     public abstract long getSize(SizePrecision precision, long max);
-
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java
index c120c76..4a3e8cc 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java
@@ -114,4 +114,9 @@ public class SpellcheckImpl extends ConstraintImpl {
         return expression;
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new SpellcheckImpl(selectorName, expression);
+    }
+
 }
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SuggestImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SuggestImpl.java
index 033dd9e..f3211f3 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SuggestImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SuggestImpl.java
@@ -114,4 +114,8 @@ public class SuggestImpl extends ConstraintImpl {
         return expression;
     }
 
+    @Override
+    public AstElement copyOf() {
+        return new SuggestImpl(selectorName, expression);
+    }
 }
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/package-info.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/package-info.java
index e4806c3..5354d47 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/package-info.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/package-info.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@Version("2.2")
+@Version("2.3")
 @Export(optional = "provide:=true")
 package org.apache.jackrabbit.oak.query;
 
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/MultiPropertyOrTestOptimisation.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/MultiPropertyOrTestOptimisation.java
new file mode 100644
index 0000000..c90fba2
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/MultiPropertyOrTestOptimisation.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.plugins.index.property;
+
+import static org.apache.jackrabbit.oak.query.QueryEngineImpl.ForceOptimised.OPTIMISED;
+
+import org.junit.Before;
+
+/**
+ * should be executing the {@link MultiPropertyOrTest} by forcing the optimisation in place.
+ */
+public class MultiPropertyOrTestOptimisation extends MultiPropertyOrTest {
+    
+    @Override
+    @Before
+    public void before() throws Exception {
+        super.before();
+        setForceOptimised(OPTIMISED);
+        setTraversalEnabled(false);
+    }    
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
index fe5f1d1..aeb1f11 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
@@ -32,6 +32,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 
+import javax.annotation.Nonnull;
 import javax.jcr.PropertyType;
 
 import com.google.common.collect.Lists;
@@ -55,9 +56,11 @@ import org.apache.jackrabbit.oak.json.TypeCodes;
 import org.apache.jackrabbit.oak.plugins.memory.BooleanPropertyState;
 import org.apache.jackrabbit.oak.plugins.memory.StringPropertyState;
 import org.apache.jackrabbit.oak.plugins.value.Conversions;
+import org.apache.jackrabbit.oak.query.QueryEngineImpl.ForceOptimised;
 import org.apache.jackrabbit.oak.query.xpath.XPathToSQL2Converter;
 import org.junit.Before;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static org.apache.jackrabbit.oak.api.QueryEngine.NO_BINDINGS;
 import static org.apache.jackrabbit.oak.api.QueryEngine.NO_MAPPINGS;
 import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
@@ -308,19 +311,28 @@ public abstract class AbstractQueryTest {
     protected List<String> assertQuery(String sql, String language,
                                        List<String> expected, boolean skipSort) {
         List<String> paths = executeQuery(sql, language, true, skipSort);
-        for (String p : expected) {
-            assertTrue("Expected path " + p + " not found, got " + paths, paths.contains(p));
-        }
-        assertEquals("Result set size is different", expected.size(),
-                paths.size());
+        assertResult(expected, paths);
         return paths;
 
     }
     
+    protected static void assertResult(@Nonnull List<String> expected, @Nonnull List<String> actual) {
+        for (String p : checkNotNull(expected)) {
+            assertTrue("Expected path " + p + " not found, got " + actual, checkNotNull(actual)
+                .contains(p));
+        }
+        assertEquals("Result set size is different", expected.size(),
+                actual.size());
+    }
+
     protected void setTraversalEnabled(boolean traversalEnabled) {
         ((QueryEngineImpl) qe).setTraversalEnabled(traversalEnabled);
     }
     
+    protected void setForceOptimised(@Nonnull ForceOptimised forceOptimised) {
+        ((QueryEngineImpl) qe).setForceOptimised(checkNotNull(forceOptimised));
+    }
+
     protected static String readRow(ResultRow row, boolean pathOnly) {
         if (pathOnly) {
             return row.getValue(QueryImpl.JCR_PATH).getValue(Type.STRING);
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2OptimiseQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2OptimiseQueryTest.java
new file mode 100644
index 0000000..ab7cb08
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2OptimiseQueryTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.query;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.of;
+import static javax.jcr.query.Query.JCR_SQL2;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NT_OAK_UNSTRUCTURED;
+import static org.apache.jackrabbit.oak.query.QueryEngineImpl.ForceOptimised.CHEAPEST;
+import static org.apache.jackrabbit.oak.query.QueryEngineImpl.ForceOptimised.OPTIMISED;
+import static org.apache.jackrabbit.oak.query.QueryEngineImpl.ForceOptimised.ORIGINAL;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+import org.junit.Test;
+
+/**
+ * aim to cover the various aspects of Query.optimise()
+ */
+public class SQL2OptimiseQueryTest extends  AbstractQueryTest {
+
+    @Test
+    public void orToUnions() throws RepositoryException, CommitFailedException {
+        Tree test, t;
+        List<String> original, optimised, cheapest, expected;
+        String statement;
+        
+        test = root.getTree("/").addChild("test");
+        test.setProperty(JCR_PRIMARYTYPE, NT_OAK_UNSTRUCTURED, NAME);
+        t = addChildWithProperty(test, "a", "p", "a");
+        t.setProperty("p1", "a1");
+        t = addChildWithProperty(test, "b", "p", "b");
+        t.setProperty("p1", "b1");
+        addChildWithProperty(test, "c", "p", "c");
+        addChildWithProperty(test, "d", "p", "d");
+        addChildWithProperty(test, "e", "p", "e");
+        root.commit();
+        
+        statement = String.format("SELECT * FROM [%s] WHERE p = 'a' OR p = 'b'",
+            NT_OAK_UNSTRUCTURED);
+        expected = of("/test/a", "/test/b");
+        setForceOptimised(ORIGINAL);
+        original = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(OPTIMISED);
+        optimised = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(CHEAPEST);
+        cheapest = executeQuery(statement, JCR_SQL2, true);
+        assertOrToUnionResults(expected, original, optimised, cheapest);
+        
+        statement = String.format(
+            "SELECT * FROM [%s] WHERE p = 'a' OR p = 'b' OR p = 'c' OR p = 'd' OR p = 'e' ",
+            NT_OAK_UNSTRUCTURED);
+        expected = of("/test/a", "/test/b", "/test/c", "/test/d", "/test/e");
+        setForceOptimised(ORIGINAL);
+        original = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(OPTIMISED);
+        optimised = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(CHEAPEST);
+        cheapest = executeQuery(statement, JCR_SQL2, true);
+        assertOrToUnionResults(expected, original, optimised, cheapest);
+
+        statement = String.format(
+            "SELECT * FROM [%s] WHERE (p = 'a' OR p = 'b') AND (p1 = 'a1' OR p1 = 'b1')",
+            NT_OAK_UNSTRUCTURED);
+        expected = of("/test/a", "/test/b");
+        setForceOptimised(ORIGINAL);
+        original = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(OPTIMISED);
+        optimised = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(CHEAPEST);
+        cheapest = executeQuery(statement, JCR_SQL2, true);
+        assertOrToUnionResults(expected, original, optimised, cheapest);
+
+        statement = String.format(
+            "SELECT * FROM [%s] WHERE (p = 'a' AND p1 = 'a1') OR (p = 'b' AND p1 = 'b1')",
+            NT_OAK_UNSTRUCTURED);
+        expected = of("/test/a", "/test/b");
+        setForceOptimised(ORIGINAL);
+        original = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(OPTIMISED);
+        optimised = executeQuery(statement, JCR_SQL2, true);
+        setForceOptimised(CHEAPEST);
+        cheapest = executeQuery(statement, JCR_SQL2, true);
+        assertOrToUnionResults(expected, original, optimised, cheapest);
+    }
+    
+    private static void assertOrToUnionResults(@Nonnull List<String> expected, 
+                                               @Nonnull List<String> original,
+                                               @Nonnull List<String> optimised,
+                                               @Nonnull List<String> cheapest) {
+        // checks that all the three list are the expected content
+        assertThat(checkNotNull(original), is(checkNotNull(expected)));        
+        assertThat(checkNotNull(optimised), is(expected));
+        assertThat(checkNotNull(cheapest), is(expected));
+        
+        // check that all the three lists contains the same. Paranoid but still a fast check
+        assertThat(original, is(optimised));
+        assertThat(optimised, is(cheapest));
+        assertThat(cheapest, is(original));
+    }
+
+    private static Tree addChildWithProperty(@Nonnull Tree father, @Nonnull String name,
+                                             @Nonnull String propName, @Nonnull String propValue) {
+        Tree t = checkNotNull(father).addChild(checkNotNull(name));
+        t.setProperty(JCR_PRIMARYTYPE, NT_OAK_UNSTRUCTURED, NAME);
+        t.setProperty(checkNotNull(propName), checkNotNull(propValue));
+        return t;
+    }
+    
+    @Override
+    protected ContentRepository createRepository() {
+        return new Oak()
+        .with(new OpenSecurityProvider())
+        .with(new InitialContent())
+        .with(new QueryEngineSettings() {
+            @Override
+            public boolean isSql2Optimisation() {
+                return true;
+            }
+        })
+        .createContentRepository();
+    }
+}
diff --git a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java
index 5b12eb8..77fde19 100644
--- a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java
+++ b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java
@@ -67,6 +67,8 @@ public class LuceneIndexQueryTest extends AbstractQueryTest {
         props.getParent().setProperty(LuceneIndexConstants.INDEX_NODE_NAME, true);
         TestUtil.enablePropertyIndex(props, "c1/p", false);
         TestUtil.enableForFullText(props, LuceneIndexConstants.REGEX_ALL_PROPS, true);
+        TestUtil.enablePropertyIndex(props, "a/name", false);
+        TestUtil.enablePropertyIndex(props, "b/name", false);
         
         root.commit();                
     }
diff --git a/oak-solr-core/pom.xml b/oak-solr-core/pom.xml
index c5f0f5b..d8c6209 100644
--- a/oak-solr-core/pom.xml
+++ b/oak-solr-core/pom.xml
@@ -60,6 +60,8 @@
             org.apache.jackrabbit.core.query.ExcerptTest#testPunctuationStartsFragmentEndsWithDots         <!-- OAK-318 -->
             org.apache.jackrabbit.core.query.ExcerptTest#testPreferPhrase                                  <!-- OAK-318 -->
             org.apache.jackrabbit.oak.jcr.query.SpellcheckTest#testSpellcheckMultipleWords                 <!-- FIXME OAK-3355 -->
+            
+            org.apache.jackrabbit.core.query.JoinTest#testJoinWithOR5                                      <!-- OAK-1617 -->
         </known.issues>
     </properties>
 
