You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asterixdb.apache.org by gg...@apache.org on 2021/08/28 19:00:11 UTC

[asterixdb] branch master updated: [ASTERIXDB-2829][IDX] Adding support for composite atomic-array indexes

This is an automated email from the ASF dual-hosted git repository.

ggalvizo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/asterixdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 36fd16e  [ASTERIXDB-2829][IDX] Adding support for composite atomic-array indexes
36fd16e is described below

commit 36fd16eefd367ef7d63e2158beeadeefd1760529
Author: ggalvizo <gg...@uci.edu>
AuthorDate: Wed Aug 25 09:34:32 2021 -0700

    [ASTERIXDB-2829][IDX] Adding support for composite atomic-array indexes
    
    - user mode changes: no
    - storage format changes: no
    - interface changes: no
    
    Adding support for composite atomic-array indexes. The only prefix
    queries supported must involved the array component on closed fields.
    Also expanded the test coverage to explore more paths (w.r.t the
    optimizer).
    
    Change-Id: I2579a9da8dcd8b8c808b20883f356aab3e1a7095
    Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/12764
    Integration-Tests: Jenkins <je...@fulliautomatix.ics.uci.edu>
    Tested-by: Jenkins <je...@fulliautomatix.ics.uci.edu>
    Reviewed-by: Dmitry Lychagin <dm...@couchbase.com>
---
 .../IntroduceSecondaryIndexInsertDeleteRule.java   |  51 +--
 .../am/AbstractIntroduceAccessMethodRule.java      |  19 +
 .../rules/am/AccessMethodAnalysisContext.java      |   1 +
 .../optimizer/rules/am/ArrayBTreeAccessMethod.java |   4 +-
 .../rules/am/IntroduceJoinAccessMethodRule.java    |  53 +--
 .../rules/am/IntroduceSelectAccessMethodRule.java  |  52 ++-
 .../array/AbstractOperatorFromSubplanRewrite.java} | 424 ++++++++++-----------
 .../am/{ => array}/ArrayIndexStructureMatcher.java |   5 +-
 .../IIntroduceAccessMethodRuleLocalRewrite.java    |  46 +++
 .../array/JoinFromSubplanRewrite.java}             |  88 ++++-
 .../rules/am/array/MergedSelectRewrite.java        | 155 ++++++++
 .../array/SelectFromSubplanRewrite.java}           |   6 +-
 .../asterix/app/translator/QueryTranslator.java    |   5 -
 .../query1.sqlpp}                                  |  37 +-
 .../query2.sqlpp}                                  |  39 +-
 .../query3.sqlpp                                   |  41 +-
 .../query4.sqlpp}                                  |  37 +-
 .../atomic-and-array-queries/query5.sqlpp          |  51 +++
 .../atomic-and-array-queries/query6.sqlpp          |  53 +++
 .../atomic-and-array-queries/query7.sqlpp          |  55 +++
 .../atomic-and-array-queries/query8.sqlpp          |  50 +++
 .../atomic-and-array-queries/query9.sqlpp          |  54 +++
 .../multiple-quantifiers/query9.sqlpp              |  51 +++
 .../use-case-1/query3.sqlpp                        |   2 +-
 .../use-case-1/{query3.sqlpp => query5.sqlpp}      |  28 +-
 .../query3.sqlpp => use-case-2/query4.sqlpp}       |  31 +-
 .../query3.sqlpp => use-case-2/query5.sqlpp}       |  31 +-
 .../query3.sqlpp => use-case-3/query4.sqlpp}       |  31 +-
 .../use-case-4/query7.sqlpp}                       |  23 +-
 .../closed/with-composite-sk}/query3.sqlpp         |  32 +-
 .../atomic-and-array-queries/query1.plan           |  23 ++
 .../atomic-and-array-queries/query2.plan           |  23 ++
 .../atomic-and-array-queries/query3.plan           |  27 ++
 .../atomic-and-array-queries/query4.plan           |  31 ++
 .../atomic-and-array-queries/query5.plan           |  40 ++
 .../atomic-and-array-queries/query6.plan           |  31 ++
 .../atomic-and-array-queries/query7.plan           |  31 ++
 .../atomic-and-array-queries/query8.plan           |  35 ++
 .../atomic-and-array-queries/query9.plan           |  44 +++
 .../multiple-quantifiers/query9.plan               |  40 ++
 .../use-case-1/query5.plan                         |  24 ++
 .../use-case-2/query4.plan                         |  22 ++
 .../use-case-2/query5.plan                         |  22 ++
 .../use-case-3/query4.plan                         |  21 +
 .../use-case-4/query7.plan                         |  39 ++
 .../closed/with-composite-sk/query3.plan           |  19 +
 .../composite-index-queries.1.ddl.sqlpp}           |  43 ++-
 .../composite-index-queries.10.query.sqlpp         |  34 ++
 .../composite-index-queries.2.update.sqlpp         |  36 ++
 .../composite-index-queries.3.query.sqlpp}         |  25 +-
 .../composite-index-queries.4.query.sqlpp}         |  32 +-
 .../composite-index-queries.5.query.sqlpp}         |  25 +-
 .../composite-index-queries.6.query.sqlpp}         |  33 +-
 .../composite-index-queries.7.query.sqlpp}         |  34 +-
 .../composite-index-queries.8.query.sqlpp          |  35 ++
 .../composite-index-queries.9.query.sqlpp}         |  31 +-
 .../with-composite-sk.1.ddl.sqlpp                  |  22 +-
 .../with-composite-sk.2.query.sqlpp                |   7 +-
 .../composite-index-queries.1.adm                  |   3 +
 .../composite-index-queries.2.adm                  |   2 +
 .../composite-index-queries.3.adm                  |   3 +
 .../composite-index-queries.4.adm                  |   2 +
 .../composite-index-queries.5.adm                  |   7 +
 .../composite-index-queries.6.adm                  |   4 +
 .../composite-index-queries.7.adm                  |   7 +
 .../composite-index-queries.8.adm                  |   2 +
 .../open/with-composite-sk/with-composite-sk.1.adm |   5 +-
 .../test/resources/runtimets/testsuite_sqlpp.xml   |  34 +-
 68 files changed, 1847 insertions(+), 606 deletions(-)

diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/IntroduceSecondaryIndexInsertDeleteRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/IntroduceSecondaryIndexInsertDeleteRule.java
index 98c3edc..9c784ad 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/IntroduceSecondaryIndexInsertDeleteRule.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/IntroduceSecondaryIndexInsertDeleteRule.java
@@ -18,12 +18,10 @@
  */
 package org.apache.asterix.optimizer.rules;
 
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Deque;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -678,31 +676,14 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
             boolean hasMetaPart) throws AlgebricksException {
         Index.ArrayIndexDetails arrayIndexDetails = (Index.ArrayIndexDetails) index.getIndexDetails();
 
-        // First, locate a field having the required UNNEST path. Queue this first, and all other keys will follow.
-        Deque<Integer> keyPositionQueue = new ArrayDeque<>();
-        for (int i = 0; i < arrayIndexDetails.getElementList().size(); i++) {
-            Index.ArrayIndexElement e = arrayIndexDetails.getElementList().get(i);
-            if (e.getUnnestList().isEmpty()) {
-                keyPositionQueue.addLast(i);
-            } else {
-                keyPositionQueue.addFirst(i);
-            }
-        }
-
         // Get the record variable associated with our record path.
-        Index.ArrayIndexElement workingElement = arrayIndexDetails.getElementList().get(keyPositionQueue.getFirst());
-        int sourceIndicatorForBaseRecord = workingElement.getSourceIndicator();
+        int sourceIndicatorForBaseRecord = arrayIndexDetails.getElementList().get(0).getSourceIndicator();
         LogicalVariable sourceVarForBaseRecord = hasMetaPart
                 ? ((sourceIndicatorForBaseRecord == Index.RECORD_INDICATOR) ? recordVar : metaVar) : recordVar;
         UnnestBranchCreator branchCreator = new UnnestBranchCreator(sourceVarForBaseRecord, unnestSourceOp);
 
-        int initialKeyPositionQueueSize = keyPositionQueue.size();
-        Set<LogicalVariable> secondaryKeyVars = new HashSet<>();
-        for (int i = 0; i < initialKeyPositionQueueSize; i++) {
-
-            // Poll from our queue, and get a key position.
-            int workingKeyPos = keyPositionQueue.pollFirst();
-            workingElement = arrayIndexDetails.getElementList().get(workingKeyPos);
+        Set<LogicalVariable> secondaryKeyVars = new LinkedHashSet<>();
+        for (Index.ArrayIndexElement workingElement : arrayIndexDetails.getElementList()) {
             int sourceIndicator = workingElement.getSourceIndicator();
             ARecordType recordType =
                     hasMetaPart ? ((sourceIndicator == Index.RECORD_INDICATOR) ? recType : metaType) : recType;
@@ -714,7 +695,6 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
                 isOpenOrNestedField =
                         (atomicFieldName.size() != 1) || !recordType.isClosedField(atomicFieldName.get(0));
 
-                // The UNNEST path has already been created (we queued this first), so we look at the current top.
                 LogicalVariable newVar = context.newVar();
                 VariableReferenceExpression varRef = new VariableReferenceExpression(sourceVarForBaseRecord);
                 varRef.setSourceLocation(sourceLoc);
@@ -723,10 +703,14 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
                                 recordType.getFieldIndex(atomicFieldName.get(0)), atomicFieldName)
                         : getFieldAccessFunction(new MutableObject<>(varRef), -1, atomicFieldName);
 
+                // Add an assign on top to extract the atomic element.
                 AssignOperator newAssignOp = new AssignOperator(newVar, new MutableObject<>(newVarRef));
                 newAssignOp.setSourceLocation(sourceLoc);
                 branchCreator.currentTop = introduceNewOp(branchCreator.currentTop, newAssignOp, true);
                 secondaryKeyVars.add(newVar);
+                if (branchCreator.currentBottom == null) {
+                    branchCreator.currentBottom = branchCreator.currentTop;
+                }
 
             } else {
                 // We have an array element.  Walk the array path.
@@ -735,6 +719,7 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
                 List<Boolean> firstUnnestFlags = ArrayIndexUtil.getUnnestFlags(workingElement.getUnnestList(),
                         workingElement.getProjectList().get(0));
                 ArrayIndexUtil.walkArrayPath(recordType, flatFirstFieldName, firstUnnestFlags, branchCreator);
+                secondaryKeyVars.add(branchCreator.lastFieldVars.get(0));
 
                 // For all other elements in the PROJECT list, add an assign.
                 for (int j = 1; j < workingElement.getProjectList().size(); j++) {
@@ -749,15 +734,11 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
                     secondaryKeyVars.add(newVar);
                 }
             }
-
-            branchCreator.lowerIsFirstWalkFlag();
-            secondaryKeyVars.addAll(branchCreator.lastFieldVars);
         }
 
         // Update the variables we are to use for the head operators.
         branchCreator.lastFieldVars.clear();
         branchCreator.lastFieldVars.addAll(secondaryKeyVars);
-
         return branchCreator;
     }
 
@@ -1022,8 +1003,7 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
     private class UnnestBranchCreator implements ArrayIndexUtil.TypeTrackerCommandExecutor {
         private final List<LogicalVariable> lastFieldVars;
         private LogicalVariable lastRecordVar;
-        private ILogicalOperator currentTop, currentBottom;
-        private boolean isFirstWalk = true;
+        private ILogicalOperator currentTop, currentBottom = null;
 
         public UnnestBranchCreator(LogicalVariable recordVar, ILogicalOperator sourceOperator) {
             this.lastRecordVar = recordVar;
@@ -1035,10 +1015,6 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
             return new ALogicalPlanImpl(new MutableObject<>(currentTop));
         }
 
-        public void lowerIsFirstWalkFlag() {
-            isFirstWalk = false;
-        }
-
         public VariableReferenceExpression createLastRecordVarRef() {
             VariableReferenceExpression varRef = new VariableReferenceExpression(lastRecordVar);
             varRef.setSourceLocation(sourceLoc);
@@ -1098,11 +1074,6 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
         public void executeActionOnEachArrayStep(ARecordType startingStepRecordType, IAType workingType,
                 List<String> fieldName, boolean isFirstArrayStep, boolean isLastUnnestInIntermediateStep)
                 throws AlgebricksException {
-            if (!isFirstWalk) {
-                // We have already built the UNNEST path, do not build again.
-                return;
-            }
-
             // Get the field we want to UNNEST from our record.
             ILogicalExpression accessToUnnestVar;
             accessToUnnestVar = (startingStepRecordType != null)
@@ -1120,7 +1091,7 @@ public class IntroduceSecondaryIndexInsertDeleteRule implements IAlgebraicRewrit
             UnnestOperator unnestOp = new UnnestOperator(unnestVar, new MutableObject<>(scanCollection));
             unnestOp.setSourceLocation(sourceLoc);
             this.currentTop = introduceNewOp(currentTop, unnestOp, true);
-            if (isFirstArrayStep) {
+            if (isFirstArrayStep && this.currentBottom == null) {
                 this.currentBottom = unnestOp;
             }
 
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AbstractIntroduceAccessMethodRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AbstractIntroduceAccessMethodRule.java
index 8458fde..37109ab 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AbstractIntroduceAccessMethodRule.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AbstractIntroduceAccessMethodRule.java
@@ -487,6 +487,25 @@ public abstract class AbstractIntroduceAccessMethodRule implements IAlgebraicRew
                 if (lastFieldMatched < 0) {
                     indexExprAndVarIt.remove();
                     continue;
+
+                } else if (Index.IndexCategory.of(indexType).equals(Index.IndexCategory.ARRAY)) {
+                    // For array indexes, we cannot make the decision to apply the prefix until we see a conjunct
+                    // conditioning on an array. We should improve using array indexes for queries that don't involve
+                    // the array component in the future.
+                    Index.ArrayIndexDetails arrayIndexDetails = (Index.ArrayIndexDetails) index.getIndexDetails();
+                    int indexOfFirstArrayField = 0;
+                    for (Index.ArrayIndexElement e : arrayIndexDetails.getElementList()) {
+                        if (!e.getUnnestList().isEmpty()) {
+                            break;
+                        }
+                        for (List<String> ignored : e.getProjectList()) {
+                            indexOfFirstArrayField++;
+                        }
+                    }
+                    if (lastFieldMatched < indexOfFirstArrayField) {
+                        indexExprAndVarIt.remove();
+                        continue;
+                    }
                 }
             }
             analysisCtx.putNumberOfMatchedKeys(index, numMatchedKeys);
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AccessMethodAnalysisContext.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AccessMethodAnalysisContext.java
index 7fa6de1..b066339 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AccessMethodAnalysisContext.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AccessMethodAnalysisContext.java
@@ -26,6 +26,7 @@ import java.util.TreeMap;
 
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.Index;
+import org.apache.asterix.optimizer.rules.am.array.ArrayIndexStructureMatcher;
 import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.hyracks.algebricks.common.utils.Pair;
 import org.apache.hyracks.algebricks.common.utils.Quadruple;
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/ArrayBTreeAccessMethod.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/ArrayBTreeAccessMethod.java
index d364b53..a482dc1 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/ArrayBTreeAccessMethod.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/ArrayBTreeAccessMethod.java
@@ -51,8 +51,8 @@ public class ArrayBTreeAccessMethod extends BTreeAccessMethod {
 
     @Override
     public boolean matchAllIndexExprs(Index index) {
-        // Similar to BTree "matchAllIndexExprs", we only require all expressions to be matched if this is a composite
-        // key index with an unknowable field.
+        // We only require all expressions to be matched if this is a composite key index with an unknowable field.
+        // TODO (GLENN): When nulls become stored in array indexes, this should return false.
         return ((Index.ArrayIndexDetails) index.getIndexDetails()).getElementList().stream()
                 .map(e -> e.getProjectList().size()).reduce(0, Integer::sum) > 1 && hasUnknownableField(index);
     }
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java
index f74c0c8..4e2eba5 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java
@@ -27,7 +27,8 @@ import java.util.Map;
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.Index;
 import org.apache.asterix.om.functions.BuiltinFunctions;
-import org.apache.asterix.optimizer.rules.subplan.JoinFromSubplanCreator;
+import org.apache.asterix.optimizer.rules.am.array.IIntroduceAccessMethodRuleLocalRewrite;
+import org.apache.asterix.optimizer.rules.am.array.JoinFromSubplanRewrite;
 import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.commons.lang3.mutable.MutableObject;
 import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
@@ -92,7 +93,9 @@ public class IntroduceJoinAccessMethodRule extends AbstractIntroduceAccessMethod
     protected final OptimizableOperatorSubTree rightSubTree = new OptimizableOperatorSubTree();
     protected IVariableTypeEnvironment typeEnvironment = null;
     protected List<Mutable<ILogicalOperator>> afterJoinRefs = null;
-    private final JoinFromSubplanCreator joinFromSubplanCreator = new JoinFromSubplanCreator();
+
+    // For plan rewriting to recognize applicable array indexes.
+    private final JoinFromSubplanRewrite joinFromSubplanRewrite = new JoinFromSubplanRewrite();
 
     // Registers access methods.
     protected static Map<FunctionIdentifier, List<IAccessMethod>> accessMethods = new HashMap<>();
@@ -102,7 +105,7 @@ public class IntroduceJoinAccessMethodRule extends AbstractIntroduceAccessMethod
         registerAccessMethod(BTreeAccessMethod.INSTANCE, accessMethods);
         registerAccessMethod(RTreeAccessMethod.INSTANCE, accessMethods);
         registerAccessMethod(InvertedIndexAccessMethod.INSTANCE, accessMethods);
-        JoinFromSubplanCreator.addOptimizableFunction(BuiltinFunctions.EQ);
+        JoinFromSubplanRewrite.addOptimizableFunction(BuiltinFunctions.EQ);
     }
 
     /**
@@ -299,28 +302,16 @@ public class IntroduceJoinAccessMethodRule extends AbstractIntroduceAccessMethod
                 analyzedAMs = new HashMap<>();
             }
 
-            // If there exists a SUBPLAN in our plan, and we are conditioning on a variable,
-            // attempt to rewrite this subplan to allow an array-index AM to be introduced.
-            // If successful, this rewrite will transform into an index-nested-loop-join.
-            // This rewrite is to be used for pushing the UNNESTs and ASSIGNs from the subplan into
-            // the index branch and giving the join a condition for this rule to optimize. *No nodes*
-            // from this rewrite will be used beyond this point.
             if (continueCheck && context.getPhysicalOptimizationConfig().isArrayIndexEnabled()) {
-                joinFromSubplanCreator.findAfterSubplanSelectOperator(afterJoinRefs);
-                AbstractBinaryJoinOperator joinRewrite = joinFromSubplanCreator.createOperator(joinOp, context);
-                boolean transformationResult = false;
-                if (joinRewrite != null) {
-                    Mutable<ILogicalOperator> joinRuleInput = new MutableObject<>(joinRewrite);
-                    transformationResult = checkAndApplyJoinTransformation(joinRuleInput, context);
-                }
-
-                // Restore our state, so we can look for more INLJ optimizations if this transformation failed.
-                joinOp = joinFromSubplanCreator.restoreBeforeRewrite(afterJoinRefs, context);
-                joinRef = joinRefFromThisOp;
-
-                if (transformationResult) {
-                    // Join rewrite was successful. Connect the after-join operators to the index subtree root before
-                    // this rewrite. This also avoids performing the secondary index validation step twice.
+                // If there exists a SUBPLAN in our plan, and we are conditioning on a variable, attempt to rewrite
+                // this subplan to allow an array-index AM to be introduced. If successful, this rewrite will transform
+                // into an index-nested-loop-join. This rewrite is to be used for pushing the UNNESTs and ASSIGNs from
+                // the subplan into the index branch and giving the join a condition for this rule to optimize.
+                // *No nodes* from this rewrite will be used beyond this point.
+                joinFromSubplanRewrite.findAfterSubplanSelectOperator(afterJoinRefs);
+                if (rewriteLocallyAndTransform(joinRef, context, joinFromSubplanRewrite)) {
+                    // Connect the after-join operators to the index subtree root before this rewrite. This also avoids
+                    // performing the secondary index validation step twice.
                     ILogicalOperator lastAfterJoinOp = afterJoinRefs.get(afterJoinRefs.size() - 1).getValue();
                     OperatorManipulationUtil.substituteOpInInput(lastAfterJoinOp, joinOp, joinOp.getInputs().get(1));
                     context.computeAndSetTypeEnvironmentForOperator(lastAfterJoinOp);
@@ -486,4 +477,18 @@ public class IntroduceJoinAccessMethodRule extends AbstractIntroduceAccessMethod
         return false;
     }
 
+    private boolean rewriteLocallyAndTransform(Mutable<ILogicalOperator> opRef, IOptimizationContext context,
+            IIntroduceAccessMethodRuleLocalRewrite<AbstractBinaryJoinOperator> rewriter) throws AlgebricksException {
+        AbstractBinaryJoinOperator joinRewrite = rewriter.createOperator(joinOp, context);
+        boolean transformationResult = false;
+        if (joinRewrite != null) {
+            Mutable<ILogicalOperator> joinRuleInput = new MutableObject<>(joinRewrite);
+            transformationResult = checkAndApplyJoinTransformation(joinRuleInput, context);
+        }
+
+        // Restore our state, so we can look for more optimizations if this transformation failed.
+        joinOp = rewriter.restoreBeforeRewrite(afterJoinRefs, context);
+        joinRef = opRef;
+        return transformationResult;
+    }
 }
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java
index faabaef..3948726 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java
@@ -30,7 +30,9 @@ import org.apache.asterix.common.exceptions.CompilationException;
 import org.apache.asterix.common.exceptions.ErrorCode;
 import org.apache.asterix.metadata.declared.MetadataProvider;
 import org.apache.asterix.metadata.entities.Index;
-import org.apache.asterix.optimizer.rules.subplan.SelectFromSubplanCreator;
+import org.apache.asterix.optimizer.rules.am.array.IIntroduceAccessMethodRuleLocalRewrite;
+import org.apache.asterix.optimizer.rules.am.array.MergedSelectRewrite;
+import org.apache.asterix.optimizer.rules.am.array.SelectFromSubplanRewrite;
 import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.commons.lang3.mutable.MutableObject;
 import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
@@ -123,7 +125,10 @@ public class IntroduceSelectAccessMethodRule extends AbstractIntroduceAccessMeth
     protected IVariableTypeEnvironment typeEnvironment = null;
     protected final OptimizableOperatorSubTree subTree = new OptimizableOperatorSubTree();
     protected List<Mutable<ILogicalOperator>> afterSelectRefs = null;
-    private final SelectFromSubplanCreator selectFromSubplanCreator = new SelectFromSubplanCreator();
+
+    // For plan rewriting to recognize applicable array indexes.
+    private final SelectFromSubplanRewrite selectFromSubplanRewrite = new SelectFromSubplanRewrite();
+    private final MergedSelectRewrite mergedSelectRewrite = new MergedSelectRewrite();
 
     // Register access methods.
     protected static Map<FunctionIdentifier, List<IAccessMethod>> accessMethods = new HashMap<>();
@@ -134,7 +139,7 @@ public class IntroduceSelectAccessMethodRule extends AbstractIntroduceAccessMeth
         registerAccessMethod(InvertedIndexAccessMethod.INSTANCE, accessMethods);
         registerAccessMethod(ArrayBTreeAccessMethod.INSTANCE, accessMethods);
         for (Pair<FunctionIdentifier, Boolean> f : ArrayBTreeAccessMethod.INSTANCE.getOptimizableFunctions()) {
-            SelectFromSubplanCreator.addOptimizableFunction(f.first);
+            SelectFromSubplanRewrite.addOptimizableFunction(f.first);
         }
     }
 
@@ -376,26 +381,18 @@ public class IntroduceSelectAccessMethodRule extends AbstractIntroduceAccessMeth
                 analyzedAMs = new TreeMap<>();
             }
 
-            // If there exists a SUBPLAN in our plan, and we are conditioning on a variable,
-            // attempt to rewrite this subplan to allow an array-index AM to be introduced.
-            // This rewrite is to be used **solely** for the purpose of changing a DATA-SCAN into a
-            // non-index-only plan branch. No nodes from this rewrite will be used beyond this point. 
-            // If successful, this will create a non-index only plan that replaces the subplan's
-            // DATA-SCAN with a PIDX SEARCH <- DISTINCT <- ORDER <- SIDX SEARCH.
             if (continueCheck && context.getPhysicalOptimizationConfig().isArrayIndexEnabled()) {
-                SelectOperator selectRewrite = selectFromSubplanCreator.createOperator(selectOp, context);
-                boolean transformationResult = false;
-                if (selectRewrite != null) {
-                    Mutable<ILogicalOperator> selectRuleInput = new MutableObject<>(selectRewrite);
-                    transformationResult = checkAndApplyTheSelectTransformation(selectRuleInput, context);
+                // If there exists a composite atomic-array index, our conjuncts will be split across multiple
+                // SELECTs. This rewrite is to be used **solely** for the purpose of changing a DATA-SCAN into a
+                // non-index-only plan branch. No nodes introduced from this rewrite will be used beyond this point.
+                if (rewriteLocallyAndTransform(selectRef, context, mergedSelectRewrite)) {
+                    return true;
                 }
 
-                // Restore our state, so we can look for more optimizations if this transformation failed.
-                selectOp = selectFromSubplanCreator.restoreBeforeRewrite(null, null);
-                selectRef = selectRefFromThisOp;
-
-                if (transformationResult) {
-                    // Rewrite was successful. Exit early.
+                // If there exists a SUBPLAN in our plan, and we are conditioning on a variable, attempt to rewrite
+                // this subplan to allow an array-index AM to be introduced. Again, this rewrite is to be used
+                // **solely** for the purpose of changing a DATA-SCAN into a non-index-only plan branch.
+                if (rewriteLocallyAndTransform(selectRef, context, selectFromSubplanRewrite)) {
                     return true;
                 }
             }
@@ -493,6 +490,21 @@ public class IntroduceSelectAccessMethodRule extends AbstractIntroduceAccessMeth
         return accessMethods;
     }
 
+    private boolean rewriteLocallyAndTransform(Mutable<ILogicalOperator> opRef, IOptimizationContext context,
+            IIntroduceAccessMethodRuleLocalRewrite<SelectOperator> rewriter) throws AlgebricksException {
+        SelectOperator selectRewrite = rewriter.createOperator(selectOp, context);
+        boolean transformationResult = false;
+        if (selectRewrite != null) {
+            Mutable<ILogicalOperator> selectRuleInput = new MutableObject<>(selectRewrite);
+            transformationResult = checkAndApplyTheSelectTransformation(selectRuleInput, context);
+        }
+
+        // Restore our state, so we can look for more optimizations if this transformation failed.
+        selectOp = rewriter.restoreBeforeRewrite(null, null);
+        selectRef = opRef;
+        return transformationResult;
+    }
+
     private void clear() {
         afterSelectRefs = null;
         selectRef = null;
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/AbstractOperatorFromSubplanCreator.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/AbstractOperatorFromSubplanRewrite.java
similarity index 56%
rename from asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/AbstractOperatorFromSubplanCreator.java
rename to asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/AbstractOperatorFromSubplanRewrite.java
index 8fa7981..4decd9d 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/AbstractOperatorFromSubplanCreator.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/AbstractOperatorFromSubplanRewrite.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.asterix.optimizer.rules.subplan;
+package org.apache.asterix.optimizer.rules.am.array;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -49,16 +49,16 @@ import org.apache.hyracks.algebricks.core.algebra.expressions.ScalarFunctionCall
 import org.apache.hyracks.algebricks.core.algebra.expressions.UnnestingFunctionCallExpression;
 import org.apache.hyracks.algebricks.core.algebra.expressions.VariableReferenceExpression;
 import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
+import org.apache.hyracks.algebricks.core.algebra.operators.logical.AbstractBinaryJoinOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.AggregateOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.AssignOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.SelectOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.SubplanOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.UnnestOperator;
-import org.apache.hyracks.algebricks.core.algebra.operators.logical.visitors.VariableUtilities;
 import org.apache.hyracks.algebricks.core.algebra.util.OperatorManipulationUtil;
 import org.apache.hyracks.api.exceptions.SourceLocation;
 
-abstract public class AbstractOperatorFromSubplanCreator<T> {
+abstract public class AbstractOperatorFromSubplanRewrite<T> implements IIntroduceAccessMethodRuleLocalRewrite<T> {
     private final static List<IAlgebricksConstantValue> ZEROS_AS_ASTERIX_CONSTANTS =
             Arrays.asList(new IAlgebricksConstantValue[] { new AsterixConstantValue(new AInt64(0)),
                     new AsterixConstantValue(new AInt32(0)), new AsterixConstantValue(new AInt16((short) 0)),
@@ -68,11 +68,6 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
     private IOptimizationContext context;
     private SourceLocation sourceLocation;
 
-    abstract public T createOperator(T originalOperatorRef, IOptimizationContext context) throws AlgebricksException;
-
-    abstract public T restoreBeforeRewrite(List<Mutable<ILogicalOperator>> afterOperatorRefs,
-            IOptimizationContext context) throws AlgebricksException;
-
     protected void reset(SourceLocation sourceLocation, IOptimizationContext context,
             Set<FunctionIdentifier> optimizableFunctions) {
         this.optimizableFunctions = optimizableFunctions;
@@ -97,20 +92,11 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
     }
 
     protected Pair<SelectOperator, UnnestOperator> traverseSubplanBranch(SubplanOperator subplanOperator,
-            ILogicalOperator parentInput) throws AlgebricksException {
-        // We only expect one plan, and one root.
-        if (subplanOperator.getNestedPlans().size() > 1
-                || subplanOperator.getNestedPlans().get(0).getRoots().size() > 1) {
-            return null;
-        }
-
-        // This root of our "subplan" should always be an aggregate.
-        ILogicalOperator workingSubplanRoot = subplanOperator.getNestedPlans().get(0).getRoots().get(0).getValue();
-        AggregateOperator workingSubplanRootAsAggregate;
-        if (!workingSubplanRoot.getOperatorTag().equals(LogicalOperatorTag.AGGREGATE)) {
+            ILogicalOperator parentInput, boolean isConnectInput) throws AlgebricksException {
+        AggregateOperator workingSubplanRootAsAggregate = getAggregateFromSubplan(subplanOperator);
+        if (workingSubplanRootAsAggregate == null) {
             return null;
         }
-        workingSubplanRootAsAggregate = (AggregateOperator) workingSubplanRoot;
 
         // Find (or create, in the SOME AND EVERY case) a SELECT that we can potentially optimize.
         SelectOperator optimizableSelect = getSelectFromPlan(workingSubplanRootAsAggregate);
@@ -118,15 +104,14 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
             return null;
         }
 
-        // We have found a SELECT with a variable. Create a copy, and set this to our rewrite root.
-        SelectOperator rewriteRootSelect = new SelectOperator(optimizableSelect.getCondition(),
-                optimizableSelect.getRetainMissing(), optimizableSelect.getMissingPlaceholderVariable());
-
         // Ensure that this SELECT represents a predicate for an existential query, and is a query we can optimize.
-        rewriteRootSelect = normalizeSelectCondition(workingSubplanRootAsAggregate, rewriteRootSelect);
-        if (rewriteRootSelect == null) {
-            return null;
-        }
+        ILogicalExpression normalizedSelectCondition =
+                normalizeCondition(workingSubplanRootAsAggregate, optimizableSelect.getCondition().getValue());
+        normalizedSelectCondition = keepOptimizableFunctions(normalizedSelectCondition);
+
+        // Create a copy of this SELECT, and set this to our rewrite root.
+        SelectOperator rewriteRootSelect = new SelectOperator(new MutableObject<>(normalizedSelectCondition),
+                optimizableSelect.getRetainMissing(), optimizableSelect.getMissingPlaceholderVariable());
         rewriteRootSelect.setSourceLocation(sourceLocation);
         rewriteRootSelect.setExecutionMode(optimizableSelect.getExecutionMode());
 
@@ -137,10 +122,12 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
         while (!workingOriginalOperator.getOperatorTag().equals(LogicalOperatorTag.NESTEDTUPLESOURCE)) {
             if (workingOriginalOperator.getInputs().isEmpty()) {
                 throw new CompilationException(ErrorCode.COMPILATION_ILLEGAL_STATE,
-                        workingSubplanRoot.getSourceLocation(),
+                        workingSubplanRootAsAggregate.getSourceLocation(),
                         "NESTED-TUPLE-SOURCE expected in nested plan branch, but not found.");
             }
 
+            ScalarFunctionCallExpression updatedSelectCond;
+            SelectOperator updatedSelectOperator;
             switch (workingOriginalOperator.getOperatorTag()) {
                 case UNNEST:
                     UnnestOperator originalUnnest = (UnnestOperator) workingOriginalOperator;
@@ -164,25 +151,46 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
                 case SELECT:
                     // If we encounter another SELECT, then we have multiple quantifiers. Transform our new SELECT to
                     // include this condition.
-                    List<Mutable<ILogicalExpression>> selectArguments = new ArrayList<>();
-                    if (!rewriteRootSelect.getCondition().getValue().splitIntoConjuncts(selectArguments)) {
-                        selectArguments.add(rewriteRootSelect.getCondition());
-                    }
-                    if (!((SelectOperator) workingOriginalOperator).getCondition().getValue()
-                            .splitIntoConjuncts(selectArguments)) {
-                        selectArguments.add(((SelectOperator) workingOriginalOperator).getCondition());
-                    }
-                    ScalarFunctionCallExpression andCond = new ScalarFunctionCallExpression(
-                            BuiltinFunctions.getBuiltinFunctionInfo(BuiltinFunctions.AND), selectArguments);
-                    SelectOperator updatedSelectOperator = new SelectOperator(new MutableObject<>(andCond),
+                    updatedSelectCond = coalesceConditions(rewriteRootSelect, workingOriginalOperator);
+                    updatedSelectOperator = new SelectOperator(new MutableObject<>(updatedSelectCond),
                             rewriteRootSelect.getRetainMissing(), rewriteRootSelect.getMissingPlaceholderVariable());
                     updatedSelectOperator.setSourceLocation(sourceLocation);
                     updatedSelectOperator.getInputs().addAll(rewriteRootSelect.getInputs());
                     rewriteRootSelect = updatedSelectOperator;
+                    if (workingNewOperator.getOperatorTag().equals(LogicalOperatorTag.SELECT)) {
+                        workingNewOperator = rewriteRootSelect;
+                    }
                     break;
 
-                case AGGREGATE:
-                    break;
+                case SUBPLAN:
+                    // If we encounter another subplan, then we must look to include any SELECTs from here as well.
+                    Pair<SelectOperator, UnnestOperator> traversalOutput =
+                            traverseSubplanBranch((SubplanOperator) workingOriginalOperator, optimizableSelect, false);
+                    if (traversalOutput != null) {
+                        updatedSelectCond = coalesceConditions(rewriteRootSelect, traversalOutput.first);
+                        updatedSelectOperator = new SelectOperator(new MutableObject<>(updatedSelectCond),
+                                rewriteRootSelect.getRetainMissing(),
+                                rewriteRootSelect.getMissingPlaceholderVariable());
+                        updatedSelectOperator.setSourceLocation(sourceLocation);
+                        updatedSelectOperator.getInputs().addAll(rewriteRootSelect.getInputs());
+                        rewriteRootSelect = updatedSelectOperator;
+                        if (workingNewOperator.getOperatorTag().equals(LogicalOperatorTag.SELECT)) {
+                            workingNewOperator = rewriteRootSelect;
+                        }
+
+                        // Add the inputs from our subplan.
+                        Mutable<ILogicalOperator> traversalOperator = traversalOutput.first.getInputs().get(0);
+                        while (traversalOperator != null) {
+                            workingNewOperator.getInputs().add(new MutableObject<>(
+                                    OperatorManipulationUtil.deepCopy(traversalOperator.getValue())));
+                            workingNewOperator = workingNewOperator.getInputs().get(0).getValue();
+                            traversalOperator = (traversalOperator.getValue().getInputs().isEmpty()) ? null
+                                    : traversalOperator.getValue().getInputs().get(0);
+                        }
+                        workingNewOperator.getInputs().clear();
+                        bottommostNewUnnest = traversalOutput.second;
+                        break;
+                    }
 
                 default:
                     return null;
@@ -193,11 +201,12 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
 
         // Sanity check: we should always be working with an UNNEST at this stage.
         if (bottommostNewUnnest == null) {
-            throw new CompilationException(ErrorCode.COMPILATION_ERROR, workingSubplanRoot.getSourceLocation(),
+            throw new CompilationException(ErrorCode.COMPILATION_ERROR,
+                    workingSubplanRootAsAggregate.getSourceLocation(),
                     "UNNEST expected in nested plan branch, but not found.");
         }
 
-        // If we are working with strict universal quantification, then we must also check whether or not we have a
+        // If we are working with strict universal quantification, then we must also check whether we have a
         // conjunct that asserts that the array should also be non-empty.
         if (isStrictUniversalQuantification(workingSubplanRootAsAggregate)
                 && isArrayNonEmptyConjunctMissing(bottommostNewUnnest, subplanOperator.getInputs().get(0).getValue())
@@ -207,73 +216,154 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
 
         // We have added everything we need in our nested-plan branch. Now, connect the input of our SUBPLAN to our
         // current working branch.
-        bottommostNewUnnest.getInputs().addAll(subplanOperator.getInputs());
-        OperatorManipulationUtil.computeTypeEnvironmentBottomUp(rewriteRootSelect, context);
+        if (isConnectInput) {
+            bottommostNewUnnest.getInputs().addAll(subplanOperator.getInputs());
+            OperatorManipulationUtil.computeTypeEnvironmentBottomUp(rewriteRootSelect, context);
+        }
 
         return new Pair<>(rewriteRootSelect, bottommostNewUnnest);
     }
 
-    protected SelectOperator getSelectFromPlan(AggregateOperator subplanRoot) throws AlgebricksException {
-        ILogicalOperator subplanRootInput = subplanRoot.getInputs().get(0).getValue();
-        ILogicalOperator subplanOrSelect = findSubplanOrOptimizableSelect(subplanRootInput);
+    protected ScalarFunctionCallExpression coalesceConditions(SelectOperator selectOp, ILogicalOperator auxOp) {
+        ScalarFunctionCallExpression combinedCondition =
+                new ScalarFunctionCallExpression(BuiltinFunctions.getBuiltinFunctionInfo(BuiltinFunctions.AND));
+        combinedCondition.setSourceLocation(selectOp.getSourceLocation());
 
-        if (subplanOrSelect != null && subplanOrSelect.getOperatorTag().equals(LogicalOperatorTag.SUBPLAN)) {
-            // We have found a SUBPLAN. Recurse by calling our caller.
-            Pair<SelectOperator, UnnestOperator> traversalOutput =
-                    traverseSubplanBranch((SubplanOperator) subplanOrSelect, subplanRootInput);
-            return (traversalOutput != null) ? traversalOutput.first : null;
+        List<Mutable<ILogicalExpression>> conjuncts = new ArrayList<>();
+        if (selectOp.getCondition().getValue().splitIntoConjuncts(conjuncts)) {
+            combinedCondition.getArguments().addAll(conjuncts);
+            conjuncts.clear();
+        } else {
+            combinedCondition.getArguments().add(selectOp.getCondition());
+        }
 
-        } else if (subplanOrSelect != null && subplanOrSelect.getOperatorTag().equals(LogicalOperatorTag.SELECT)) {
-            // We have found a SELECT. Return this to our caller.
-            return (SelectOperator) subplanOrSelect;
+        switch (auxOp.getOperatorTag()) {
+            case LEFTOUTERJOIN:
+            case INNERJOIN:
+                AbstractBinaryJoinOperator joinOp = (AbstractBinaryJoinOperator) auxOp;
+                if (joinOp.getCondition().getValue().splitIntoConjuncts(conjuncts)) {
+                    combinedCondition.getArguments().addAll(conjuncts);
+                } else {
+                    combinedCondition.getArguments().add(joinOp.getCondition());
+                }
+                break;
+
+            case SELECT:
+                SelectOperator selectOp2 = (SelectOperator) auxOp;
+                if (selectOp2.getCondition().getValue().splitIntoConjuncts(conjuncts)) {
+                    combinedCondition.getArguments().addAll(conjuncts);
+                } else {
+                    combinedCondition.getArguments().add(selectOp2.getCondition());
+                }
+                break;
+        }
 
-        } else {
-            // We were not able to find a SELECT or a SUBPLAN. Try to find an expression in our aggregate that we
-            // can optimize (i.e. handle SOME AND EVERY case).
-            AbstractFunctionCallExpression optimizableCondition = null;
-            boolean isNonEmptyStream = false;
-            for (Mutable<ILogicalExpression> expression : subplanRoot.getExpressions()) {
-                AggregateFunctionCallExpression aggExpression = (AggregateFunctionCallExpression) expression.getValue();
-                if (aggExpression.getFunctionIdentifier().equals(BuiltinFunctions.NON_EMPTY_STREAM)) {
-                    isNonEmptyStream = true;
-
-                } else if (aggExpression.isTwoStep()
-                        && aggExpression.getStepOneAggregate().getFunctionIdentifier()
-                                .equals(BuiltinFunctions.SQL_COUNT)
-                        && aggExpression.getStepTwoAggregate().getFunctionIdentifier().equals(BuiltinFunctions.SQL_SUM)
-                        && aggExpression.getArguments().get(0).getValue().getExpressionTag()
-                                .equals(LogicalExpressionTag.FUNCTION_CALL)) {
-                    AbstractFunctionCallExpression switchExpression =
-                            (AbstractFunctionCallExpression) aggExpression.getArguments().get(0).getValue();
-                    if (!switchExpression.getArguments().get(0).getValue().getExpressionTag()
+        return combinedCondition;
+    }
+
+    private SelectOperator getSelectFromPlan(AggregateOperator subplanRoot) {
+        ILogicalExpression aggregateCondition = null;
+        boolean isNonEmptyStream = false;
+        for (Mutable<ILogicalExpression> expression : subplanRoot.getExpressions()) {
+            AggregateFunctionCallExpression aggExpression = (AggregateFunctionCallExpression) expression.getValue();
+            if (aggExpression.getFunctionIdentifier().equals(BuiltinFunctions.NON_EMPTY_STREAM)) {
+                isNonEmptyStream = true;
+
+            } else if (aggExpression.isTwoStep()
+                    && aggExpression.getStepOneAggregate().getFunctionIdentifier().equals(BuiltinFunctions.SQL_COUNT)
+                    && aggExpression.getStepTwoAggregate().getFunctionIdentifier().equals(BuiltinFunctions.SQL_SUM)
+                    && aggExpression.getArguments().get(0).getValue().getExpressionTag()
                             .equals(LogicalExpressionTag.FUNCTION_CALL)) {
-                        continue;
-                    }
-                    AbstractFunctionCallExpression switchCondition =
-                            (AbstractFunctionCallExpression) switchExpression.getArguments().get(0).getValue();
-
-                    ILogicalExpression arg2 = switchExpression.getArguments().get(1).getValue();
-                    ILogicalExpression arg3 = switchExpression.getArguments().get(2).getValue();
-                    ILogicalExpression arg4 = switchExpression.getArguments().get(3).getValue();
-                    if (arg2.getExpressionTag().equals(LogicalExpressionTag.CONSTANT)
-                            && arg3.getExpressionTag().equals(LogicalExpressionTag.CONSTANT)
-                            && arg4.getExpressionTag().equals(LogicalExpressionTag.CONSTANT)
-                            && ((ConstantExpression) arg2).getValue().isTrue()
-                            && ((ConstantExpression) arg3).getValue().isNull()
-                            && ((ConstantExpression) arg4).getValue().isTrue()) {
-                        optimizableCondition = switchCondition;
-                    }
+                AbstractFunctionCallExpression switchExpression =
+                        (AbstractFunctionCallExpression) aggExpression.getArguments().get(0).getValue();
+
+                ILogicalExpression arg1 = switchExpression.getArguments().get(0).getValue();
+                ILogicalExpression arg2 = switchExpression.getArguments().get(1).getValue();
+                ILogicalExpression arg3 = switchExpression.getArguments().get(2).getValue();
+                ILogicalExpression arg4 = switchExpression.getArguments().get(3).getValue();
+                if (arg2.getExpressionTag().equals(LogicalExpressionTag.CONSTANT)
+                        && arg3.getExpressionTag().equals(LogicalExpressionTag.CONSTANT)
+                        && arg4.getExpressionTag().equals(LogicalExpressionTag.CONSTANT)
+                        && ((ConstantExpression) arg2).getValue().isTrue()
+                        && ((ConstantExpression) arg3).getValue().isNull()
+                        && ((ConstantExpression) arg4).getValue().isTrue()) {
+                    aggregateCondition = arg1;
                 }
             }
-            if (isNonEmptyStream && optimizableCondition != null) {
-                SelectOperator newSelectFromAggregate =
-                        new SelectOperator(new MutableObject<>(optimizableCondition), false, null);
-                newSelectFromAggregate.getInputs().addAll(subplanRoot.getInputs());
-                newSelectFromAggregate.setSourceLocation(sourceLocation);
-                return newSelectFromAggregate;
+        }
+
+        // First, try to create a SELECT from the aggregate itself (i.e. handle the SOME AND EVERY case).
+        if (isNonEmptyStream && aggregateCondition != null) {
+            SelectOperator selectFromAgg = new SelectOperator(new MutableObject<>(aggregateCondition), false, null);
+            selectFromAgg.getInputs().addAll(subplanRoot.getInputs());
+            selectFromAgg.setSourceLocation(sourceLocation);
+            return selectFromAgg;
+        }
+
+        // If we could not create a SELECT from the aggregate, try to find a SELECT inside the subplan itself.
+        ILogicalOperator workingOperator = subplanRoot.getInputs().get(0).getValue();
+        while (workingOperator != null) {
+            if (workingOperator.getOperatorTag().equals(LogicalOperatorTag.SELECT)) {
+                return (SelectOperator) workingOperator;
+            }
+            workingOperator =
+                    (workingOperator.getInputs().isEmpty()) ? null : workingOperator.getInputs().get(0).getValue();
+        }
+
+        // We could not find a SELECT.
+        return null;
+    }
+
+    private ILogicalExpression keepOptimizableFunctions(ILogicalExpression cond) {
+        if (cond.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)) {
+            AbstractFunctionCallExpression func = (AbstractFunctionCallExpression) cond;
+            List<Mutable<ILogicalExpression>> conjuncts = new ArrayList<>();
+            if (func.splitIntoConjuncts(conjuncts)) {
+                List<Mutable<ILogicalExpression>> optimizableConjuncts = new ArrayList<>();
+                for (Mutable<ILogicalExpression> conjunct : conjuncts) {
+                    if (conjunct.getValue().getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)
+                            && optimizableFunctions.contains(
+                                    ((AbstractFunctionCallExpression) conjunct.getValue()).getFunctionIdentifier())) {
+                        optimizableConjuncts.add(conjunct);
+                    }
+                }
+
+                if (optimizableConjuncts.size() == 1) {
+                    return optimizableConjuncts.get(0).getValue();
+
+                } else if (optimizableConjuncts.size() > 1) {
+                    ScalarFunctionCallExpression andCond = new ScalarFunctionCallExpression(
+                            BuiltinFunctions.getBuiltinFunctionInfo(BuiltinFunctions.AND));
+                    andCond.setSourceLocation(cond.getSourceLocation());
+                    andCond.getArguments().addAll(optimizableConjuncts);
+                    return andCond;
+                }
+
+            } else if (func.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)
+                    && optimizableFunctions.contains(func.getFunctionIdentifier())) {
+                return cond;
+
             }
+        }
+
+        return ConstantExpression.TRUE;
+    }
+
+    private AggregateOperator getAggregateFromSubplan(SubplanOperator subplanOperator) {
+        // We only expect one plan, and one root.
+        if (subplanOperator.getNestedPlans().size() > 1
+                || subplanOperator.getNestedPlans().get(0).getRoots().size() > 1) {
+            return null;
+        }
+
+        // This root of our "subplan" should always be an aggregate.
+        ILogicalOperator workingSubplanRoot = subplanOperator.getNestedPlans().get(0).getRoots().get(0).getValue();
+        AggregateOperator workingSubplanRootAsAggregate;
+        if (!workingSubplanRoot.getOperatorTag().equals(LogicalOperatorTag.AGGREGATE)) {
             return null;
         }
+        workingSubplanRootAsAggregate = (AggregateOperator) workingSubplanRoot;
+        return workingSubplanRootAsAggregate;
     }
 
     private boolean isStrictUniversalQuantification(AggregateOperator workingSubplanRoot) {
@@ -286,8 +376,7 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
         return false;
     }
 
-    private boolean isArrayNonEmptyConjunctMissing(UnnestOperator firstUnnestInNTS, ILogicalOperator subplanInput)
-            throws AlgebricksException {
+    private boolean isArrayNonEmptyConjunctMissing(UnnestOperator firstUnnestInNTS, ILogicalOperator subplanInput) {
         UnnestingFunctionCallExpression unnestFunction =
                 (UnnestingFunctionCallExpression) firstUnnestInNTS.getExpressionRef().getValue();
         VariableReferenceExpression unnestVarExpr =
@@ -297,8 +386,9 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
         if (!subplanInput.getOperatorTag().equals(LogicalOperatorTag.SELECT)) {
             return true;
         }
-        SelectOperator subplanInputAsSelect = normalizeSelectCondition(null, (SelectOperator) subplanInput);
-        ILogicalExpression selectCondExpr = subplanInputAsSelect.getCondition().getValue();
+
+        ILogicalExpression selectCondExpr =
+                normalizeCondition(null, ((SelectOperator) subplanInput).getCondition().getValue());
         List<Mutable<ILogicalExpression>> conjunctsFromSelect = new ArrayList<>();
         if (selectCondExpr.splitIntoConjuncts(conjunctsFromSelect)) {
             // We have a collection of conjuncts. Analyze each conjunct w/ a function.
@@ -361,144 +451,28 @@ abstract public class AbstractOperatorFromSubplanCreator<T> {
         return false;
     }
 
-    private SelectOperator normalizeSelectCondition(AggregateOperator aggregateOperator, SelectOperator selectOperator)
-            throws AlgebricksException {
+    private ILogicalExpression normalizeCondition(AggregateOperator aggregateOperator, ILogicalExpression expr) {
         // The purpose of this function is to remove the NOT(IF-MISSING-OR-NULL(...)) functions for a strict universal
         // quantification query. The {@code ArrayBTreeAccessMethod} does not recognize the former as optimizable
         // functions, so we remove them here. This SELECT will never make it to the final query plan (after the
         // {@code IntroduceSelectAccessMethodRule}), which allows us to get away with this logically incorrect branch.
         if (aggregateOperator != null && !isStrictUniversalQuantification(aggregateOperator)) {
             // We are working with an existential quantification OR an EACH AND EVERY query. Do not modify the SELECT.
-            return selectOperator;
+            return expr;
 
         } else {
             // We are working with a strict universal quantification query.
-            ScalarFunctionCallExpression notFunction =
-                    (ScalarFunctionCallExpression) selectOperator.getCondition().getValue();
+            ScalarFunctionCallExpression notFunction = (ScalarFunctionCallExpression) expr;
             if (!notFunction.getFunctionIdentifier().equals(BuiltinFunctions.NOT)) {
-                return selectOperator;
+                return expr;
             }
 
             ScalarFunctionCallExpression ifMissingOrNullFunction =
                     (ScalarFunctionCallExpression) notFunction.getArguments().get(0).getValue();
             if (!ifMissingOrNullFunction.getFunctionIdentifier().equals(BuiltinFunctions.IF_MISSING_OR_NULL)) {
-                return selectOperator;
-            }
-
-            Mutable<ILogicalExpression> newSelectCondition =
-                    new MutableObject<>(ifMissingOrNullFunction.getArguments().get(0).getValue().cloneExpression());
-            return new SelectOperator(newSelectCondition, selectOperator.getRetainMissing(),
-                    selectOperator.getMissingPlaceholderVariable());
-
-        }
-    }
-
-    private ILogicalOperator findSubplanOrOptimizableSelect(ILogicalOperator operator) throws AlgebricksException {
-        // We are trying to find a SELECT operator with an optimizable function call.
-        if (operator.getOperatorTag().equals(LogicalOperatorTag.SELECT)) {
-            SelectOperator selectOperator = (SelectOperator) operator;
-            ILogicalExpression selectCondExpr = selectOperator.getCondition().getValue();
-            boolean containsValidVar = isAnyVarFromUnnestOrAssign(operator);
-            if (containsValidVar && selectCondExpr.getExpressionTag() == LogicalExpressionTag.FUNCTION_CALL) {
-
-                // We have a NOT function call. Determine if this follows the NOT(IF-MISSING-OR-NULL(...)) pattern.
-                ScalarFunctionCallExpression notExpr = (ScalarFunctionCallExpression) selectCondExpr;
-                if (notExpr.getFunctionIdentifier().equals(BuiltinFunctions.NOT)) {
-
-                    // This does not follow the NOT(IF-MISSING-OR-NULL(...)) pattern, but NOT is an optimizable
-                    // function call. Return this.
-                    ILogicalExpression notCondExpr = notExpr.getArguments().get(0).getValue();
-                    if (!notCondExpr.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)
-                            && optimizableFunctions.contains(BuiltinFunctions.NOT)) {
-                        return selectOperator;
-                    }
-
-                    // Inside the NOT(IF-MISSING-OR-NULL(...)) is an optimizable function. Return this.
-                    ScalarFunctionCallExpression ifMissingOrNullExpr = (ScalarFunctionCallExpression) notCondExpr;
-                    ILogicalExpression finalExpr = ifMissingOrNullExpr.getArguments().get(0).getValue();
-                    if (doesExpressionContainOptimizableFunction(finalExpr)) {
-                        return selectOperator;
-                    }
-
-                } else if (doesExpressionContainOptimizableFunction(selectCondExpr)) {
-                    // We have an optimizable function. Return this.
-                    return selectOperator;
-
-                }
-            }
-        } else if (operator.getOperatorTag().equals(LogicalOperatorTag.SUBPLAN)) {
-            // We have found an additional SUBPLAN branch to explore. Recurse w/ caller function.
-            return operator;
-        }
-
-        // No matching operator found. Recurse on current operator input.
-        return (operator.getInputs().isEmpty()) ? null
-                : findSubplanOrOptimizableSelect(operator.getInputs().get(0).getValue());
-    }
-
-    private boolean doesExpressionContainOptimizableFunction(ILogicalExpression inputExpr) {
-        if (!inputExpr.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)) {
-            return false;
-        }
-
-        // Check if the input expression itself is an optimizable function.
-        ScalarFunctionCallExpression inputExprAsFunc = (ScalarFunctionCallExpression) inputExpr;
-        if (isFunctionOptimizable(inputExprAsFunc)) {
-            return true;
-        }
-
-        // We have a collection of conjuncts. Return true if any of these conjuncts are optimizable.
-        List<Mutable<ILogicalExpression>> conjuncts = new ArrayList<>();
-        if (inputExprAsFunc.splitIntoConjuncts(conjuncts)) {
-            for (Mutable<ILogicalExpression> mutableConjunct : conjuncts) {
-                ILogicalExpression workingConjunct = mutableConjunct.getValue();
-                if (workingConjunct.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)
-                        && isFunctionOptimizable((ScalarFunctionCallExpression) workingConjunct)) {
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-    private boolean isFunctionOptimizable(ScalarFunctionCallExpression inputExpr) {
-        if (inputExpr.getFunctionIdentifier().equals(BuiltinFunctions.GT)) {
-            // Avoid the GT(LEN(array-field), 0) function.
-            ILogicalExpression gtExpr = inputExpr.getArguments().get(0).getValue();
-            return ((!gtExpr.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL))
-                    || !((ScalarFunctionCallExpression) gtExpr).getFunctionIdentifier().equals(BuiltinFunctions.LEN))
-                    && optimizableFunctions.contains(BuiltinFunctions.GT);
-
-        } else if (inputExpr.getFunctionIdentifier().equals(BuiltinFunctions.LT)) {
-            // Avoid the LT(0, LEN(array-field)) function.
-            ILogicalExpression ltExpr = inputExpr.getArguments().get(1).getValue();
-            return ((!ltExpr.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL))
-                    || !((ScalarFunctionCallExpression) ltExpr).getFunctionIdentifier().equals(BuiltinFunctions.LEN))
-                    && optimizableFunctions.contains(BuiltinFunctions.LT);
-
-        }
-
-        // Otherwise, check if the function itself is optimizable.
-        return (optimizableFunctions.contains(inputExpr.getFunctionIdentifier()));
-    }
-
-    private boolean isAnyVarFromUnnestOrAssign(ILogicalOperator op) throws AlgebricksException {
-        List<LogicalVariable> opUsedVars = new ArrayList<>(), relevantVars = new ArrayList<>();
-        VariableUtilities.getUsedVariables(op, opUsedVars);
-        ILogicalOperator workingOp = op;
-        boolean isMatchFound = false;
-        while (workingOp != null) {
-            if (workingOp.getOperatorTag().equals(LogicalOperatorTag.UNNEST)
-                    || workingOp.getOperatorTag().equals(LogicalOperatorTag.ASSIGN)) {
-                VariableUtilities.getProducedVariables(workingOp, relevantVars);
-                if (opUsedVars.stream().anyMatch(relevantVars::contains)) {
-                    isMatchFound = true;
-                    break;
-                }
+                return expr;
             }
-            workingOp = (workingOp.getInputs().isEmpty()) ? null : workingOp.getInputs().get(0).getValue();
+            return ifMissingOrNullFunction.getArguments().get(0).getValue().cloneExpression();
         }
-        return isMatchFound;
     }
-}
+}
\ No newline at end of file
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/ArrayIndexStructureMatcher.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/ArrayIndexStructureMatcher.java
similarity index 98%
rename from asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/ArrayIndexStructureMatcher.java
rename to asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/ArrayIndexStructureMatcher.java
index 2e7f814..62b266a 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/ArrayIndexStructureMatcher.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/ArrayIndexStructureMatcher.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.asterix.optimizer.rules.am;
+package org.apache.asterix.optimizer.rules.am.array;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -31,6 +31,7 @@ import org.apache.asterix.om.constants.AsterixConstantValue;
 import org.apache.asterix.om.functions.BuiltinFunctions;
 import org.apache.asterix.om.types.ARecordType;
 import org.apache.asterix.om.types.IAType;
+import org.apache.asterix.optimizer.rules.am.OptimizableOperatorSubTree;
 import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
 import org.apache.hyracks.algebricks.core.algebra.base.ILogicalOperator;
 import org.apache.hyracks.algebricks.core.algebra.base.LogicalExpressionTag;
@@ -126,8 +127,6 @@ public class ArrayIndexStructureMatcher implements ArrayIndexUtil.TypeTrackerCom
                     lastMatchedPosition = searchPosition;
                     isStructureMatchedFoundForThisStep = true;
 
-                } else {
-                    isStructureMatchedFoundForThisStep = false;
                 }
             }
             searchPosition++;
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/IIntroduceAccessMethodRuleLocalRewrite.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/IIntroduceAccessMethodRuleLocalRewrite.java
new file mode 100644
index 0000000..9942bbe
--- /dev/null
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/IIntroduceAccessMethodRuleLocalRewrite.java
@@ -0,0 +1,46 @@
+/*
+ * 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.asterix.optimizer.rules.am.array;
+
+import java.util.List;
+
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
+import org.apache.hyracks.algebricks.core.algebra.base.ILogicalOperator;
+import org.apache.hyracks.algebricks.core.algebra.base.IOptimizationContext;
+
+public interface IIntroduceAccessMethodRuleLocalRewrite<T> {
+    /**
+     * @param originalOperator Original operator before rewrite. Should be returned by {@code restoreBeforeRewrite}.
+     * @param context Optimization context.
+     * @return Null if no rewrite has occurred. Otherwise, a new plan from the given operator.
+     * @throws AlgebricksException
+     */
+    T createOperator(T originalOperator, IOptimizationContext context) throws AlgebricksException;
+
+    /**
+     * @param afterOperatorRefs Operators after the original operator that should be restored after the rewrite.
+     * @param context Optimization context.
+     * @return The original operator given at {@code createOperator} time.
+     * @throws AlgebricksException
+     */
+    T restoreBeforeRewrite(List<Mutable<ILogicalOperator>> afterOperatorRefs, IOptimizationContext context)
+            throws AlgebricksException;
+}
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/JoinFromSubplanCreator.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/JoinFromSubplanRewrite.java
similarity index 69%
rename from asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/JoinFromSubplanCreator.java
rename to asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/JoinFromSubplanRewrite.java
index 9a11b98..c6fb5fd 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/JoinFromSubplanCreator.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/JoinFromSubplanRewrite.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.asterix.optimizer.rules.subplan;
+package org.apache.asterix.optimizer.rules.am.array;
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -29,16 +29,24 @@ import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.commons.lang3.mutable.MutableObject;
 import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
 import org.apache.hyracks.algebricks.common.utils.Pair;
+import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
 import org.apache.hyracks.algebricks.core.algebra.base.ILogicalOperator;
 import org.apache.hyracks.algebricks.core.algebra.base.IOptimizationContext;
+import org.apache.hyracks.algebricks.core.algebra.base.LogicalExpressionTag;
 import org.apache.hyracks.algebricks.core.algebra.base.LogicalOperatorTag;
+import org.apache.hyracks.algebricks.core.algebra.base.LogicalVariable;
+import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
+import org.apache.hyracks.algebricks.core.algebra.expressions.ScalarFunctionCallExpression;
+import org.apache.hyracks.algebricks.core.algebra.expressions.VariableReferenceExpression;
+import org.apache.hyracks.algebricks.core.algebra.functions.AlgebricksBuiltinFunctions;
 import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.AbstractBinaryJoinOperator;
+import org.apache.hyracks.algebricks.core.algebra.operators.logical.AssignOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.InnerJoinOperator;
-import org.apache.hyracks.algebricks.core.algebra.operators.logical.LeftOuterJoinOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.SelectOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.SubplanOperator;
 import org.apache.hyracks.algebricks.core.algebra.operators.logical.UnnestOperator;
+import org.apache.hyracks.algebricks.core.algebra.operators.logical.visitors.VariableUtilities;
 import org.apache.hyracks.algebricks.core.algebra.util.OperatorManipulationUtil;
 
 /**
@@ -91,7 +99,7 @@ import org.apache.hyracks.algebricks.core.algebra.util.OperatorManipulationUtil;
  *
  * In the case of nested-subplans, we return a copy of the innermost SELECT followed by all relevant UNNEST/ASSIGNs.
  */
-public class JoinFromSubplanCreator extends AbstractOperatorFromSubplanCreator<AbstractBinaryJoinOperator> {
+public class JoinFromSubplanRewrite extends AbstractOperatorFromSubplanRewrite<AbstractBinaryJoinOperator> {
     private final static Set<FunctionIdentifier> optimizableFunctions = new HashSet<>();
     private final Deque<JoinFromSubplanContext> contextStack = new ArrayDeque<>();
 
@@ -174,27 +182,35 @@ public class JoinFromSubplanCreator extends AbstractOperatorFromSubplanCreator<A
         SubplanOperator subplanOperator =
                 (SubplanOperator) joinContext.selectAfterSubplan.getInputs().get(0).getValue();
         Pair<SelectOperator, UnnestOperator> traversalOutput =
-                traverseSubplanBranch(subplanOperator, originalOperator.getInputs().get(1).getValue());
+                traverseSubplanBranch(subplanOperator, originalOperator.getInputs().get(1).getValue(), true);
         if (traversalOutput == null) {
             return null;
         }
 
         // We have successfully generated a SELECT branch. Create the new JOIN operator.
-        if (originalOperator.getOperatorTag().equals(LogicalOperatorTag.INNERJOIN)) {
-            joinContext.newJoinRoot = new InnerJoinOperator(traversalOutput.first.getCondition());
-
-        } else { // originalOperator.getOperatorTag().equals(LogicalOperatorTag.LEFTOUTERJOIN)
-            joinContext.newJoinRoot = new LeftOuterJoinOperator(traversalOutput.first.getCondition());
-        }
+        ScalarFunctionCallExpression newCond = coalesceConditions(traversalOutput.first, joinContext.originalJoinRoot);
+        joinContext.newJoinRoot = new InnerJoinOperator(new MutableObject<>(newCond));
         joinContext.newJoinRoot.getInputs().add(0, originalOperator.getInputs().get(0));
 
-        // Create the index join branch.
+        // Connect the join branches together.
         traversalOutput.second.getInputs().clear();
         traversalOutput.second.getInputs().add(originalOperator.getInputs().get(1));
         context.computeAndSetTypeEnvironmentForOperator(traversalOutput.second);
         joinContext.newJoinRoot.getInputs().add(1, traversalOutput.first.getInputs().get(0));
         context.computeAndSetTypeEnvironmentForOperator(joinContext.newJoinRoot);
 
+        // To support type casting that is performed on the index subtree and still make this expression recognizable,
+        // push all function calls to a lower ASSIGN.
+        List<Mutable<ILogicalExpression>> conjuncts = new ArrayList<>();
+        if (newCond.splitIntoConjuncts(conjuncts)) {
+            for (Mutable<ILogicalExpression> conjunct : conjuncts) {
+                extractFunctionCallToAssign(joinContext.newJoinRoot, context, conjunct.getValue());
+            }
+
+        } else {
+            extractFunctionCallToAssign(joinContext.newJoinRoot, context, newCond);
+        }
+
         // Reconnect our after-join operator to our new join.
         OperatorManipulationUtil.substituteOpInInput(joinContext.afterJoinOpForRewrite,
                 joinContext.removedAfterJoinOperators.get(0).getValue(), new MutableObject<>(joinContext.newJoinRoot));
@@ -217,6 +233,56 @@ public class JoinFromSubplanCreator extends AbstractOperatorFromSubplanCreator<A
         return joinContext.originalJoinRoot;
     }
 
+    private void extractFunctionCallToAssign(AbstractBinaryJoinOperator joinOp, IOptimizationContext context,
+            ILogicalExpression condition) throws AlgebricksException {
+        if (!condition.getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)) {
+            return;
+        }
+        AbstractFunctionCallExpression conditionAsFuncCall = (AbstractFunctionCallExpression) condition;
+        if (!AlgebricksBuiltinFunctions.isComparisonFunction(conditionAsFuncCall.getFunctionIdentifier())) {
+            return;
+        }
+
+        for (Mutable<ILogicalExpression> arg : conditionAsFuncCall.getArguments()) {
+            if (!arg.getValue().getExpressionTag().equals(LogicalExpressionTag.FUNCTION_CALL)) {
+                continue;
+            }
+
+            LogicalVariable newVar = context.newVar();
+            VariableReferenceExpression newVarRef = new VariableReferenceExpression(newVar);
+            newVarRef.setSourceLocation(joinOp.getSourceLocation());
+            AssignOperator newAssign =
+                    new AssignOperator(newVar, new MutableObject<>(arg.getValue().cloneExpression()));
+            newAssign.setSourceLocation(arg.getValue().getSourceLocation());
+            newAssign.setExecutionMode(joinOp.getExecutionMode());
+
+            // Place the new ASSIGN in the appropriate join branch.
+            ILogicalOperator leftBranchRoot = joinOp.getInputs().get(0).getValue();
+            ILogicalOperator rightBranchRoot = joinOp.getInputs().get(1).getValue();
+            List<LogicalVariable> usedVarsFromFunc = new ArrayList<>();
+            List<LogicalVariable> varsFromLeftBranch = new ArrayList<>();
+            List<LogicalVariable> varsFromRightBranch = new ArrayList<>();
+            VariableUtilities.getUsedVariables(newAssign, usedVarsFromFunc);
+            VariableUtilities.getProducedVariablesInDescendantsAndSelf(leftBranchRoot, varsFromLeftBranch);
+            VariableUtilities.getProducedVariablesInDescendantsAndSelf(rightBranchRoot, varsFromRightBranch);
+            if (varsFromLeftBranch.containsAll(usedVarsFromFunc)) {
+                newAssign.getInputs().add(new MutableObject<>(leftBranchRoot));
+                context.computeAndSetTypeEnvironmentForOperator(newAssign);
+                joinOp.getInputs().get(0).setValue(newAssign);
+                context.computeAndSetTypeEnvironmentForOperator(joinOp);
+                arg.setValue(newVarRef);
+
+            } else if (varsFromRightBranch.containsAll(usedVarsFromFunc)) {
+                newAssign.getInputs().add(new MutableObject<>(rightBranchRoot));
+                context.computeAndSetTypeEnvironmentForOperator(newAssign);
+                joinOp.getInputs().get(1).setValue(newAssign);
+                context.computeAndSetTypeEnvironmentForOperator(joinOp);
+                arg.setValue(newVarRef);
+
+            }
+        }
+    }
+
     /**
      * All state associated with a single call of {@code createOperator}.
      */
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/MergedSelectRewrite.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/MergedSelectRewrite.java
new file mode 100644
index 0000000..c9705ab
--- /dev/null
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/MergedSelectRewrite.java
@@ -0,0 +1,155 @@
+/*
+ * 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.asterix.optimizer.rules.am.array;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
+import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
+import org.apache.hyracks.algebricks.core.algebra.base.ILogicalOperator;
+import org.apache.hyracks.algebricks.core.algebra.base.ILogicalPlan;
+import org.apache.hyracks.algebricks.core.algebra.base.IOptimizationContext;
+import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
+import org.apache.hyracks.algebricks.core.algebra.expressions.ScalarFunctionCallExpression;
+import org.apache.hyracks.algebricks.core.algebra.functions.AlgebricksBuiltinFunctions;
+import org.apache.hyracks.algebricks.core.algebra.operators.logical.SelectOperator;
+import org.apache.hyracks.algebricks.core.algebra.plan.ALogicalPlanImpl;
+import org.apache.hyracks.algebricks.core.algebra.util.OperatorManipulationUtil;
+
+/**
+ * For all expressions that pertain to a single dataset but are spread across various SELECTs due to requiring an
+ * intermediate UNNEST, "push up" the lower SELECT expressions into a single unifying SELECT. This is used to
+ * recognize composite atomic-array index applicability.
+ */
+public class MergedSelectRewrite implements IIntroduceAccessMethodRuleLocalRewrite<SelectOperator> {
+    private final Set<Mutable<ILogicalExpression>> selectConjuncts = new LinkedHashSet<>();
+    private final Deque<SelectOperator> selectRootStack = new ArrayDeque<>();
+    private Mutable<ILogicalOperator> originalDataSourceRef;
+
+    @Override
+    public SelectOperator createOperator(SelectOperator originalOperator, IOptimizationContext context)
+            throws AlgebricksException {
+        // Initialize the conjuncts for our SELECT.
+        selectConjuncts.clear();
+        List<Mutable<ILogicalExpression>> thisSelectConjuncts = new ArrayList<>();
+        if (!originalOperator.getCondition().getValue().splitIntoConjuncts(thisSelectConjuncts)) {
+            thisSelectConjuncts.add(originalOperator.getCondition());
+        }
+        selectConjuncts.addAll(thisSelectConjuncts);
+
+        // Explore all operators below this SELECT until the first data source operator.
+        selectRootStack.push(originalOperator);
+        collectSelectConjuncts(originalOperator.getInputs().get(0));
+
+        if (thisSelectConjuncts.size() == selectConjuncts.size()) {
+            // No other SELECTs were found. Return null to indicate there were no SELECTs to merge.
+            return null;
+
+        } else {
+            // We have found additional SELECTs. Form a conjunction.
+            AbstractFunctionCallExpression andCond = new ScalarFunctionCallExpression(
+                    context.getMetadataProvider().lookupFunction(AlgebricksBuiltinFunctions.AND));
+            andCond.setSourceLocation(originalOperator.getSourceLocation());
+            for (Mutable<ILogicalExpression> conjunct : selectConjuncts) {
+                andCond.getArguments().add(conjunct);
+            }
+
+            // Return a new plan that removes all SELECTs that were pushed up.
+            SelectOperator newSelectOperator = new SelectOperator(new MutableObject<>(andCond),
+                    originalOperator.getRetainMissing(), originalOperator.getMissingPlaceholderVariable());
+            newSelectOperator.setSourceLocation(originalOperator.getSourceLocation());
+            ILogicalPlan newSelectInputPlan = OperatorManipulationUtil
+                    .deepCopy(new ALogicalPlanImpl(originalOperator.getInputs().get(0)), context);
+            newSelectOperator.getInputs().add(newSelectInputPlan.getRoots().get(0));
+            removeSelectsFromPlan(newSelectOperator, newSelectInputPlan.getRoots().get(0));
+            OperatorManipulationUtil.computeTypeEnvironmentBottomUp(newSelectOperator, context);
+            return newSelectOperator;
+        }
+    }
+
+    @Override
+    public SelectOperator restoreBeforeRewrite(List<Mutable<ILogicalOperator>> afterOperatorRefs,
+            IOptimizationContext context) throws AlgebricksException {
+        return selectRootStack.pop();
+    }
+
+    private void collectSelectConjuncts(Mutable<ILogicalOperator> workingOp) {
+        switch (workingOp.getValue().getOperatorTag()) {
+            case DATASOURCESCAN:
+            case EMPTYTUPLESOURCE:
+            case UNNEST_MAP:
+                // If we have reached a datasource operator, stop our search.
+                originalDataSourceRef = workingOp;
+                break;
+
+            case INNERJOIN:
+            case LEFTOUTERJOIN:
+                // We are not interested in exploring joins in this class.
+                break;
+
+            case SELECT:
+                SelectOperator selectOperator = (SelectOperator) workingOp.getValue();
+                List<Mutable<ILogicalExpression>> thisSelectConjuncts = new ArrayList<>();
+                if (!selectOperator.getCondition().getValue().splitIntoConjuncts(thisSelectConjuncts)) {
+                    thisSelectConjuncts.add(selectOperator.getCondition());
+                }
+                selectConjuncts.addAll(thisSelectConjuncts);
+
+            default:
+                // Explore the rest of our plan in our DFS fashion.
+                for (Mutable<ILogicalOperator> input : workingOp.getValue().getInputs()) {
+                    collectSelectConjuncts(input);
+                }
+        }
+    }
+
+    private void removeSelectsFromPlan(ILogicalOperator parentOp, Mutable<ILogicalOperator> workingOp) {
+        Mutable<ILogicalOperator> workingOpInParent =
+                parentOp.getInputs().stream().filter(i -> i.equals(workingOp)).collect(Collectors.toList()).get(0);
+        int indexOfWorkingOpInParent = parentOp.getInputs().indexOf(workingOpInParent);
+
+        switch (workingOp.getValue().getOperatorTag()) {
+            case DATASOURCESCAN:
+            case EMPTYTUPLESOURCE:
+            case UNNEST_MAP:
+                // If we have reached a datasource operator, stop and replace this with our original datasource.
+                // (IntroduceSelectAccessMethodRule replaces this specific operator, so this must be the original.)
+                parentOp.getInputs().set(indexOfWorkingOpInParent, originalDataSourceRef);
+                break;
+
+            case SELECT:
+                parentOp.getInputs().set(indexOfWorkingOpInParent, workingOp.getValue().getInputs().get(0));
+
+            default:
+                // Explore the rest of our plan in our DFS fashion.
+                for (Mutable<ILogicalOperator> input : workingOp.getValue().getInputs()) {
+                    removeSelectsFromPlan(workingOp.getValue(), input);
+                }
+        }
+    }
+}
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/SelectFromSubplanCreator.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/SelectFromSubplanRewrite.java
similarity index 96%
rename from asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/SelectFromSubplanCreator.java
rename to asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/SelectFromSubplanRewrite.java
index 6211ea3..594ba41 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/subplan/SelectFromSubplanCreator.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/array/SelectFromSubplanRewrite.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.asterix.optimizer.rules.subplan;
+package org.apache.asterix.optimizer.rules.am.array;
 
 import java.util.ArrayDeque;
 import java.util.Deque;
@@ -78,7 +78,7 @@ import org.apache.hyracks.algebricks.core.algebra.operators.logical.UnnestOperat
  *
  * In the case of nested-subplans, we return a copy of the innermost SELECT followed by all relevant UNNEST/ASSIGNs.
  */
-public class SelectFromSubplanCreator extends AbstractOperatorFromSubplanCreator<SelectOperator> {
+public class SelectFromSubplanRewrite extends AbstractOperatorFromSubplanRewrite<SelectOperator> {
     private final static Set<FunctionIdentifier> optimizableFunctions = new HashSet<>();
     private final Deque<SelectOperator> selectRootStack = new ArrayDeque<>();
 
@@ -117,7 +117,7 @@ public class SelectFromSubplanCreator extends AbstractOperatorFromSubplanCreator
 
         // Traverse our subplan and generate a SELECT branch if applicable.
         SubplanOperator subplanOperator = (SubplanOperator) originalOperator.getInputs().get(0).getValue();
-        Pair<SelectOperator, UnnestOperator> traversalOutput = traverseSubplanBranch(subplanOperator, null);
+        Pair<SelectOperator, UnnestOperator> traversalOutput = traverseSubplanBranch(subplanOperator, null, true);
         return (traversalOutput == null) ? null : traversalOutput.first;
     }
 
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
index abef74e..cd58a69 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
@@ -1254,11 +1254,6 @@ public class QueryTranslator extends AbstractLangTranslator implements IStatemen
                     throw new CompilationException(ErrorCode.COMPILATION_INCOMPATIBLE_INDEX_TYPE, sourceLoc,
                             String.valueOf(indexType));
                 }
-                if (indexedElementsCount > 1) {
-                    // TODO (GLENN): Add in support for composite atomic / array indexes.
-                    throw new CompilationException(ErrorCode.COMPILATION_INCOMPATIBLE_INDEX_TYPE, sourceLoc,
-                            String.valueOf(indexType));
-                }
 
                 List<Index.ArrayIndexElement> indexElementList = new ArrayList<>(indexedElementsCount);
                 for (int i = 0; i < indexedElementsCount; i++) {
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query1.sqlpp
similarity index 57%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query1.sqlpp
index d2dc752..e12eeaf 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query1.sqlpp
@@ -18,21 +18,30 @@
  */
 SET `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
 
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
+CREATE TYPE       TestType AS {
+    _id: uuid
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Explicit UNNEST query with all fields of a composite index.
+FROM    Dataset1 D
+UNNEST  D.items DI
+WHERE   D.field1 = 1 AND
+        DI.field2 = 2 AND
+        DI.field3 = 3 AND
+        D.field4 = 4
+SELECT  *;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query2.sqlpp
similarity index 53%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query2.sqlpp
index d2dc752..f832a76 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query2.sqlpp
@@ -18,21 +18,32 @@
  */
 SET `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
 
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
+CREATE TYPE       TestType AS {
+    _id: uuid
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Explicit UNNEST query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM    Dataset1 D
+UNNEST  D.items DI
+WHERE   D.field1 = 1 AND
+        DI.field2 = 2 AND
+        DI.field3 = 3 AND
+        DI.field3_notindexed = 3 AND
+        D.field4 = 4 AND
+        D.field4_notindexed = 4
+SELECT  *;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query3.sqlpp
similarity index 51%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query3.sqlpp
index d2dc752..6c1f9b7 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query3.sqlpp
@@ -18,21 +18,34 @@
  */
 SET `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
 
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
+CREATE TYPE       TestType AS {
+    _id: uuid
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST outer_items
+      UNNEST inner_items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Explicit UNNEST query with all fields of a composite index, with extra clauses on non-indexed fields (2).
+FROM    Dataset1 D
+UNNEST  D.outer_items DOI
+UNNEST  DOI.inner_items DII
+WHERE   D.field1 = 1 AND
+        DOI.field2_notindexed = 2 AND
+        DII.field2 = 2 AND
+        DII.field3 = 3 AND
+        DII.field3_notindexed = 3 AND
+        D.field4 = 4
+SELECT  *;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query4.sqlpp
similarity index 56%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query4.sqlpp
index d2dc752..135a563 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query4.sqlpp
@@ -18,21 +18,30 @@
  */
 SET `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
 
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
+CREATE TYPE       TestType AS {
+    _id: uuid
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Existential quantification query with all fields of a composite index.
+FROM    Dataset1 D
+WHERE   D.field1 = 1 AND
+        ( SOME DI IN D.items
+          SATISFIES DI.field2 = 2 AND
+                    DI.field3 = 3 ) AND
+        D.field4 = 4
+SELECT  *;
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query5.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query5.sqlpp
new file mode 100644
index 0000000..8ce7532
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query5.sqlpp
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+SET `compiler.arrayindex` "true";
+
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
+
+CREATE TYPE       TestType AS {
+    _id: uuid
+};
+
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST outer_items
+      UNNEST inner_items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
+
+-- Existential quantification query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM    Dataset1 D
+WHERE   D.field1 = 1 AND
+        ( SOME DOI IN D.outer_items
+          SATISFIES DOI.field2_notindexed = 2 AND
+                    ( SOME DII IN DOI.inner_items
+                      SATISFIES DII.field2 = 2 AND
+                                DII.field3 = 3 AND
+                                DII.field3_notindexed = 3 ) ) AND
+        D.field4 = 4
+SELECT  *;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query6.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query6.sqlpp
new file mode 100644
index 0000000..39f2687
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query6.sqlpp
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+SET `compiler.arrayindex` "true";
+
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
+
+CREATE TYPE       TestType AS {
+    _id: uuid
+};
+
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE DATASET    Dataset2 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
+
+-- Explicit UNNEST join query with all fields of a composite index.
+FROM    Dataset2 D2
+INNER JOIN  (
+    FROM    Dataset1 D1
+    UNNEST  D1.items D1I
+    SELECT  D1.field1, D1I.field2, D1I.field3, D1.field4
+) AS    D1IV
+ON      D1IV.field1 /* +indexnl */ = TO_BIGINT(D2.field1) AND
+        D1IV.field2 /* +indexnl */ = TO_BIGINT(D2.field2) AND
+        D1IV.field3 /* +indexnl */ = TO_BIGINT(D2.field3) AND
+        D1IV.field4 /* +indexnl */ = TO_BIGINT(D2.field4)
+SELECT  *;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query7.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query7.sqlpp
new file mode 100644
index 0000000..5d0dece
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query7.sqlpp
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+SET `compiler.arrayindex` "true";
+
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
+
+CREATE TYPE       TestType AS {
+    _id: uuid
+};
+
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE DATASET    Dataset2 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
+
+-- Explicit UNNEST join query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM    Dataset2 D2
+INNER JOIN  (
+    FROM    Dataset1 D1
+    UNNEST  D1.items D1I
+    SELECT  D1.field1_notindexed, D1.field1, D1I.field2, D1I.field3, D1I.field3_notindexed, D1.field4
+) AS    D1IV
+ON      D1IV.field1 /* +indexnl */ = TO_BIGINT(D2.field1) AND
+        D1IV.field1_notindexed     = TO_BIGINT(D2.field1_notindexed) AND
+        D1IV.field2 /* +indexnl */ = TO_BIGINT(D2.field2) AND
+        D1IV.field3 /* +indexnl */ = TO_BIGINT(D2.field3) AND
+        D1IV.field3_notindexed     = TO_BIGINT(D2.field3_notindexed) AND
+        D1IV.field4 /* +indexnl */ = TO_BIGINT(D2.field4)
+SELECT  *;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query8.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query8.sqlpp
new file mode 100644
index 0000000..8801212
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query8.sqlpp
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+SET `compiler.arrayindex` "true";
+
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
+
+CREATE TYPE       TestType AS {
+    _id: uuid
+};
+
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE DATASET    Dataset2 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
+
+-- Existential quantification join query with all fields of a composite index.
+FROM    Dataset2 D2,
+        Dataset1 D1
+WHERE   D1.field1 /* +indexnl */ = TO_BIGINT(D2.field1) AND
+        ( SOME D1I IN D1.items
+          SATISFIES D1I.field2 /* +indexnl */ = TO_BIGINT(D2.field2) AND
+                    D1I.field3 /* +indexnl */ = TO_BIGINT(D2.field3) ) AND
+        D1.field4 /* +indexnl */ = TO_BIGINT(D2.field4)
+SELECT  *;
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query9.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query9.sqlpp
new file mode 100644
index 0000000..ac2c8c9
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/atomic-and-array-queries/query9.sqlpp
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+SET `compiler.arrayindex` "true";
+
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
+
+CREATE TYPE       TestType AS {
+    _id: uuid
+};
+
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE DATASET    Dataset2 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST outer_items
+      UNNEST inner_items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
+
+-- Existential quantification query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM    Dataset2 D2,
+        Dataset1 D1
+WHERE   D1.field1 /* +indexnl */ = TO_BIGINT(D2.field1) AND
+        ( SOME DOI IN D1.outer_items
+          SATISFIES DOI.field2_notindexed = TO_BIGINT(D2.field2_notindexed) AND
+                    ( SOME DII IN DOI.inner_items
+                      SATISFIES DII.field2 /* +indexnl */ = TO_BIGINT(D2.field2) AND
+                                DII.field3 /* +indexnl */ = TO_BIGINT(D2.field3) AND
+                                DII.field3_notindexed = TO_BIGINT(D2.field3_notindexed) ) ) AND
+        D1.field4 /* +indexnl */ = TO_BIGINT(D2.field4)
+SELECT  *;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/multiple-quantifiers/query9.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/multiple-quantifiers/query9.sqlpp
new file mode 100644
index 0000000..32ae924
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/multiple-quantifiers/query9.sqlpp
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+SET               `compiler.arrayindex` "true";
+
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
+
+CREATE TYPE       TestType AS {
+    _id: uuid,
+    outer_items: [{
+        inner_item: bigint,
+        inner_items: [{
+            item: bigint
+        }]
+    }]
+};
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY        _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 ( UNNEST outer_items
+                             UNNEST inner_items
+                             SELECT item );
+
+-- Double UNNEST w/ SELECT, open index and multiple quantification.
+FROM       Dataset1 D1
+WHERE      SOME AND EVERY I1 IN D1.outer_items,
+                           J IN D1.other_items
+           SATISFIES J.item = 2 AND
+                     I1.inner_item = 3 AND
+                     ( SOME AND EVERY I2 IN I1.inner_items,
+                                      J2 IN I1.other_inner_items
+                       SATISFIES I2.item = 1 AND
+                                 J2.item = 2 )
+SELECT     *;
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
index d2dc752..cc2346c 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
@@ -33,6 +33,6 @@ CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
 
 SELECT COUNT(*)
 FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
+WHERE 0 < LEN(C.dates) AND
       (EVERY D IN C.dates
        SATISFIES D > "2016" AND D < "2017");
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query5.sqlpp
similarity index 59%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query5.sqlpp
index d2dc752..b2407a8 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query5.sqlpp
@@ -16,23 +16,27 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET                `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestYelp IF EXISTS;
+CREATE DATAVERSE  TestYelp;
+USE               TestYelp;
 
-CREATE TYPE CheckinType AS {
+CREATE TYPE       CheckinType AS {
     checkin_id: uuid,
     business_id: string,
     dates: [string]
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    YelpCheckin(CheckinType)
+PRIMARY KEY       checkin_id AUTOGENERATED;
+CREATE INDEX      IdxYelpCheckinDates
+ON                YelpCheckin (UNNEST dates);
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- The following query should NOT be optimized.
+FROM     YelpCheckin C
+WHERE    0 < LEN(C.dates_notindexed) AND
+         0 < LEN(C.dates_notindexed_2) AND 
+         ( EVERY D IN C.dates
+           SATISFIES D > "2016" AND D < "2017" )
+SELECT   COUNT(*);
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-2/query4.sqlpp
similarity index 57%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-2/query4.sqlpp
index d2dc752..4a4a322 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-2/query4.sqlpp
@@ -16,23 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestYelp IF EXISTS;
+CREATE DATAVERSE  TestYelp;
+USE               TestYelp;
 
-CREATE TYPE CheckinType AS {
+CREATE TYPE       CheckinType AS {
     checkin_id: uuid,
     business_id: string,
-    dates: [string]
+    checkin_times: {
+        dates: [string],
+        times: [string]
+    }
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    YelpCheckin(CheckinType)
+PRIMARY KEY       checkin_id AUTOGENERATED;
+CREATE INDEX      IdxYelpCheckinDates
+ON                YelpCheckin ( UNNEST checkin_times.dates );
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- This query should NOT be optimized.
+FROM     YelpCheckin C
+WHERE    SOME AND EVERY D IN C.checkin_times.dates_notindexed
+         SATISFIES D > "2016" AND D < "2017"
+SELECT   COUNT(*);
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-2/query5.sqlpp
similarity index 58%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-2/query5.sqlpp
index d2dc752..7f67c73 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-2/query5.sqlpp
@@ -16,23 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestYelp IF EXISTS;
+CREATE DATAVERSE  TestYelp;
+USE               TestYelp;
 
-CREATE TYPE CheckinType AS {
+CREATE TYPE       CheckinType AS {
     checkin_id: uuid,
     business_id: string,
-    dates: [string]
+    checkin_times: {
+        dates: [string],
+        times: [string]
+    }
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    YelpCheckin(CheckinType)
+PRIMARY KEY       checkin_id AUTOGENERATED;
+CREATE INDEX      IdxYelpCheckinDates
+ON                YelpCheckin ( UNNEST checkin_times.dates );
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- This query should NOT be optimized.
+FROM     YelpCheckin C
+WHERE    SOME AND EVERY D IN C.checkin_times.dates
+         SATISFIES LOWER(D) = "2016"
+SELECT   COUNT(*);
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-3/query4.sqlpp
similarity index 57%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-3/query4.sqlpp
index d2dc752..129f151 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-3/query4.sqlpp
@@ -16,23 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestYelp IF EXISTS;
+CREATE DATAVERSE  TestYelp;
+USE               TestYelp;
 
-CREATE TYPE CheckinType AS {
+CREATE TYPE       CheckinType AS {
     checkin_id: uuid,
     business_id: string,
-    dates: [string]
+    checkin_times: [{
+        date: string,
+        time: string
+    }]
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    YelpCheckin(CheckinType)
+PRIMARY KEY       checkin_id AUTOGENERATED;
+CREATE INDEX      IdxYelpCheckinDates
+ON                YelpCheckin ( UNNEST checkin_times SELECT date );
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- The following query should NOT be optimized.
+FROM    YelpCheckin C
+WHERE   SOME AND EVERY D IN C.checkin_times
+        SATISFIES D.date_notindexed BETWEEN "2016" AND "2017"
+SELECT  C.business_id;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/error-handling/index-mixed-composite/index-mixed-composite.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-4/query7.sqlpp
similarity index 67%
rename from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/error-handling/index-mixed-composite/index-mixed-composite.1.ddl.sqlpp
rename to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-4/query7.sqlpp
index c26bf92..0d54915 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/error-handling/index-mixed-composite/index-mixed-composite.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-4/query7.sqlpp
@@ -16,24 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-/*
- * Description: Verify that an error is thrown when trying to create a mixed composite atomic and array index (not supported for now).
- */
+SET `compiler.arrayindex` "true";
 
 DROP DATAVERSE TestYelp IF EXISTS;
 CREATE DATAVERSE TestYelp;
 USE TestYelp;
 
-CREATE TYPE CheckinTimeType AS {
-    time: string
-};
 CREATE TYPE CheckinType AS {
     checkin_id: uuid,
     business_id: string,
-    checkin_times: [CheckinTimeType]
+    checkin_times: [{
+        dates: [string],
+        times: [string]
+    }]
 };
 
 CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinTimeBusiness ON YelpCheckin ((UNNEST checkin_times SELECT time), (business_id));
+CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST checkin_times UNNEST dates);
 
+FROM    YelpCheckin C
+WHERE   0 < LEN(C.checkin_times) AND 
+        ( EVERY CT IN C.checkin_times
+          SATISFIES ( ABS(CT.num) > 0 AND
+                      0 < LEN(CT.dates) AND
+                      ( EVERY D IN CT.dates
+                        SATISFIES D > "2019-06-07" ) ) )
+SELECT  C.business_id;
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-unnest-queries/closed/with-composite-sk/query3.sqlpp
similarity index 55%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-unnest-queries/closed/with-composite-sk/query3.sqlpp
index d2dc752..b37fe19 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-unnest-queries/closed/with-composite-sk/query3.sqlpp
@@ -16,23 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
+DROP DATAVERSE    TestYelp IF EXISTS;
+CREATE DATAVERSE  TestYelp;
+USE               TestYelp;
 
-CREATE TYPE CheckinType AS {
+CREATE TYPE       CheckinType AS {
     checkin_id: uuid,
     business_id: string,
-    dates: [string]
+    checkin_times: [{
+        date: string,
+        time: string
+    }]
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
+CREATE DATASET    YelpCheckin(CheckinType)
+PRIMARY KEY       checkin_id AUTOGENERATED;
+CREATE INDEX      IdxYelpCheckinDatesTimes
+ON                YelpCheckin ( UNNEST checkin_times SELECT date, time );
 
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- The following query should NOT be optimized.
+FROM     YelpCheckin C,
+         C.checkin_times D
+WHERE    D.date_notindexed BETWEEN "2016" AND "2017" AND
+         D.time BETWEEN "00:00:00" AND "12:00:00"
+SELECT   COUNT(*);
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query1.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query1.plan
new file mode 100644
index 0000000..d53724e
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query1.plan
@@ -0,0 +1,23 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_SELECT  |PARTITIONED|
+          -- STREAM_PROJECT  |PARTITIONED|
+            -- UNNEST  |PARTITIONED|
+              -- STREAM_SELECT  |PARTITIONED|
+                -- ASSIGN  |PARTITIONED|
+                  -- STREAM_PROJECT  |PARTITIONED|
+                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                      -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                          -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- STABLE_SORT [$$52(ASC)]  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- STREAM_PROJECT  |PARTITIONED|
+                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                      -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                          -- ASSIGN  |PARTITIONED|
+                                            -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query2.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query2.plan
new file mode 100644
index 0000000..ab9c768
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query2.plan
@@ -0,0 +1,23 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_SELECT  |PARTITIONED|
+          -- STREAM_PROJECT  |PARTITIONED|
+            -- UNNEST  |PARTITIONED|
+              -- STREAM_SELECT  |PARTITIONED|
+                -- ASSIGN  |PARTITIONED|
+                  -- STREAM_PROJECT  |PARTITIONED|
+                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                      -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                          -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- STABLE_SORT [$$58(ASC)]  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- STREAM_PROJECT  |PARTITIONED|
+                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                      -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                          -- ASSIGN  |PARTITIONED|
+                                            -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query3.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query3.plan
new file mode 100644
index 0000000..2c6241a
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query3.plan
@@ -0,0 +1,27 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_SELECT  |PARTITIONED|
+          -- STREAM_PROJECT  |PARTITIONED|
+            -- UNNEST  |PARTITIONED|
+              -- STREAM_SELECT  |PARTITIONED|
+                -- ASSIGN  |PARTITIONED|
+                  -- STREAM_PROJECT  |PARTITIONED|
+                    -- UNNEST  |PARTITIONED|
+                      -- STREAM_SELECT  |PARTITIONED|
+                        -- ASSIGN  |PARTITIONED|
+                          -- STREAM_PROJECT  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                      -- STABLE_SORT [$$70(ASC)]  |PARTITIONED|
+                                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                          -- STREAM_PROJECT  |PARTITIONED|
+                                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                              -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                  -- ASSIGN  |PARTITIONED|
+                                                    -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query4.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query4.plan
new file mode 100644
index 0000000..d25efc7
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query4.plan
@@ -0,0 +1,31 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- SUBPLAN  |PARTITIONED|
+                      {
+                        -- AGGREGATE  |LOCAL|
+                          -- STREAM_SELECT  |LOCAL|
+                            -- ASSIGN  |LOCAL|
+                              -- UNNEST  |LOCAL|
+                                -- NESTED_TUPLE_SOURCE  |LOCAL|
+                      }
+                -- STREAM_SELECT  |PARTITIONED|
+                  -- ASSIGN  |PARTITIONED|
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                        -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                            -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                -- STABLE_SORT [$$54(ASC)]  |PARTITIONED|
+                                  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                    -- STREAM_PROJECT  |PARTITIONED|
+                                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                        -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                            -- ASSIGN  |PARTITIONED|
+                                              -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query5.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query5.plan
new file mode 100644
index 0000000..df8e08b
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query5.plan
@@ -0,0 +1,40 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- SUBPLAN  |PARTITIONED|
+                      {
+                        -- AGGREGATE  |LOCAL|
+                          -- STREAM_SELECT  |LOCAL|
+                            -- SUBPLAN  |LOCAL|
+                                    {
+                                      -- AGGREGATE  |LOCAL|
+                                        -- STREAM_SELECT  |LOCAL|
+                                          -- ASSIGN  |LOCAL|
+                                            -- UNNEST  |LOCAL|
+                                              -- NESTED_TUPLE_SOURCE  |LOCAL|
+                                    }
+                              -- STREAM_SELECT  |LOCAL|
+                                -- ASSIGN  |LOCAL|
+                                  -- UNNEST  |LOCAL|
+                                    -- NESTED_TUPLE_SOURCE  |LOCAL|
+                      }
+                -- STREAM_SELECT  |PARTITIONED|
+                  -- ASSIGN  |PARTITIONED|
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                        -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                            -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                -- STABLE_SORT [$$76(ASC)]  |PARTITIONED|
+                                  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                    -- STREAM_PROJECT  |PARTITIONED|
+                                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                        -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                            -- ASSIGN  |PARTITIONED|
+                                              -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query6.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query6.plan
new file mode 100644
index 0000000..9c4ba10
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query6.plan
@@ -0,0 +1,31 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- ASSIGN  |PARTITIONED|
+                -- STREAM_PROJECT  |PARTITIONED|
+                  -- UNNEST  |PARTITIONED|
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- ASSIGN  |PARTITIONED|
+                        -- STREAM_PROJECT  |PARTITIONED|
+                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                            -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                -- STREAM_PROJECT  |PARTITIONED|
+                                  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                    -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                        -- STABLE_SORT [$$93(ASC), $$70(ASC)]  |PARTITIONED|
+                                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                            -- STREAM_PROJECT  |PARTITIONED|
+                                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                                  -- BROADCAST_EXCHANGE  |PARTITIONED|
+                                                    -- ASSIGN  |PARTITIONED|
+                                                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                        -- DATASOURCE_SCAN (TestDataverse.Dataset2)  |PARTITIONED|
+                                                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                            -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query7.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query7.plan
new file mode 100644
index 0000000..3dc4d4f
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query7.plan
@@ -0,0 +1,31 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- ASSIGN  |PARTITIONED|
+                -- STREAM_PROJECT  |PARTITIONED|
+                  -- UNNEST  |PARTITIONED|
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- ASSIGN  |PARTITIONED|
+                        -- STREAM_PROJECT  |PARTITIONED|
+                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                            -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                -- STREAM_PROJECT  |PARTITIONED|
+                                  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                    -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                        -- STABLE_SORT [$$111(ASC), $$80(ASC)]  |PARTITIONED|
+                                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                            -- STREAM_PROJECT  |PARTITIONED|
+                                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                                  -- BROADCAST_EXCHANGE  |PARTITIONED|
+                                                    -- ASSIGN  |PARTITIONED|
+                                                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                        -- DATASOURCE_SCAN (TestDataverse.Dataset2)  |PARTITIONED|
+                                                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                            -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query8.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query8.plan
new file mode 100644
index 0000000..9617727
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query8.plan
@@ -0,0 +1,35 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- SUBPLAN  |PARTITIONED|
+                      {
+                        -- AGGREGATE  |LOCAL|
+                          -- STREAM_SELECT  |LOCAL|
+                            -- ASSIGN  |LOCAL|
+                              -- UNNEST  |LOCAL|
+                                -- NESTED_TUPLE_SOURCE  |LOCAL|
+                      }
+                -- ASSIGN  |PARTITIONED|
+                  -- STREAM_PROJECT  |PARTITIONED|
+                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                      -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                          -- STREAM_PROJECT  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- STABLE_SORT [$$74(ASC), $$55(ASC)]  |PARTITIONED|
+                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                      -- STREAM_PROJECT  |PARTITIONED|
+                                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                          -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                            -- BROADCAST_EXCHANGE  |PARTITIONED|
+                                              -- ASSIGN  |PARTITIONED|
+                                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                  -- DATASOURCE_SCAN (TestDataverse.Dataset2)  |PARTITIONED|
+                                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                      -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query9.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query9.plan
new file mode 100644
index 0000000..0f26753
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/atomic-and-array-queries/query9.plan
@@ -0,0 +1,44 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- SUBPLAN  |PARTITIONED|
+                      {
+                        -- AGGREGATE  |LOCAL|
+                          -- STREAM_SELECT  |LOCAL|
+                            -- SUBPLAN  |LOCAL|
+                                    {
+                                      -- AGGREGATE  |LOCAL|
+                                        -- STREAM_SELECT  |LOCAL|
+                                          -- ASSIGN  |LOCAL|
+                                            -- UNNEST  |LOCAL|
+                                              -- NESTED_TUPLE_SOURCE  |LOCAL|
+                                    }
+                              -- STREAM_SELECT  |LOCAL|
+                                -- ASSIGN  |LOCAL|
+                                  -- UNNEST  |LOCAL|
+                                    -- NESTED_TUPLE_SOURCE  |LOCAL|
+                      }
+                -- ASSIGN  |PARTITIONED|
+                  -- STREAM_PROJECT  |PARTITIONED|
+                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                      -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                          -- STREAM_PROJECT  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- STABLE_SORT [$$104(ASC), $$76(ASC)]  |PARTITIONED|
+                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                      -- STREAM_PROJECT  |PARTITIONED|
+                                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                          -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                            -- BROADCAST_EXCHANGE  |PARTITIONED|
+                                              -- ASSIGN  |PARTITIONED|
+                                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                  -- DATASOURCE_SCAN (TestDataverse.Dataset2)  |PARTITIONED|
+                                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                                      -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/multiple-quantifiers/query9.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/multiple-quantifiers/query9.plan
new file mode 100644
index 0000000..c9574d6
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/multiple-quantifiers/query9.plan
@@ -0,0 +1,40 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- SUBPLAN  |PARTITIONED|
+                      {
+                        -- AGGREGATE  |LOCAL|
+                          -- SUBPLAN  |LOCAL|
+                                  {
+                                    -- AGGREGATE  |LOCAL|
+                                      -- ASSIGN  |LOCAL|
+                                        -- UNNEST  |LOCAL|
+                                          -- ASSIGN  |LOCAL|
+                                            -- UNNEST  |LOCAL|
+                                              -- NESTED_TUPLE_SOURCE  |LOCAL|
+                                  }
+                            -- ASSIGN  |LOCAL|
+                              -- UNNEST  |LOCAL|
+                                -- ASSIGN  |LOCAL|
+                                  -- UNNEST  |LOCAL|
+                                    -- NESTED_TUPLE_SOURCE  |LOCAL|
+                      }
+                -- ASSIGN  |PARTITIONED|
+                  -- STREAM_PROJECT  |PARTITIONED|
+                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                      -- BTREE_SEARCH (TestDataverse.Dataset1.Dataset1)  |PARTITIONED|
+                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                          -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- STABLE_SORT [$$88(ASC)]  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- STREAM_PROJECT  |PARTITIONED|
+                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                      -- BTREE_SEARCH (TestDataverse.Dataset1.d1Idx)  |PARTITIONED|
+                                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                          -- ASSIGN  |PARTITIONED|
+                                            -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-1/query5.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-1/query5.plan
new file mode 100644
index 0000000..9120584
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-1/query5.plan
@@ -0,0 +1,24 @@
+-- DISTRIBUTE_RESULT  |UNPARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |UNPARTITIONED|
+    -- STREAM_PROJECT  |UNPARTITIONED|
+      -- ASSIGN  |UNPARTITIONED|
+        -- AGGREGATE  |UNPARTITIONED|
+          -- RANDOM_MERGE_EXCHANGE  |PARTITIONED|
+            -- AGGREGATE  |PARTITIONED|
+              -- STREAM_SELECT  |PARTITIONED|
+                -- STREAM_PROJECT  |PARTITIONED|
+                  -- SUBPLAN  |PARTITIONED|
+                          {
+                            -- AGGREGATE  |LOCAL|
+                              -- STREAM_SELECT  |LOCAL|
+                                -- UNNEST  |LOCAL|
+                                  -- NESTED_TUPLE_SOURCE  |LOCAL|
+                          }
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- STREAM_SELECT  |PARTITIONED|
+                        -- ASSIGN  |PARTITIONED|
+                          -- STREAM_PROJECT  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- DATASOURCE_SCAN (TestYelp.YelpCheckin)  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-2/query4.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-2/query4.plan
new file mode 100644
index 0000000..2d561f4
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-2/query4.plan
@@ -0,0 +1,22 @@
+-- DISTRIBUTE_RESULT  |UNPARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |UNPARTITIONED|
+    -- STREAM_PROJECT  |UNPARTITIONED|
+      -- ASSIGN  |UNPARTITIONED|
+        -- AGGREGATE  |UNPARTITIONED|
+          -- RANDOM_MERGE_EXCHANGE  |PARTITIONED|
+            -- AGGREGATE  |PARTITIONED|
+              -- STREAM_SELECT  |PARTITIONED|
+                -- STREAM_PROJECT  |PARTITIONED|
+                  -- SUBPLAN  |PARTITIONED|
+                          {
+                            -- AGGREGATE  |LOCAL|
+                              -- UNNEST  |LOCAL|
+                                -- NESTED_TUPLE_SOURCE  |LOCAL|
+                          }
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- ASSIGN  |PARTITIONED|
+                        -- STREAM_PROJECT  |PARTITIONED|
+                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                            -- DATASOURCE_SCAN (TestYelp.YelpCheckin)  |PARTITIONED|
+                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-2/query5.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-2/query5.plan
new file mode 100644
index 0000000..2d561f4
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-2/query5.plan
@@ -0,0 +1,22 @@
+-- DISTRIBUTE_RESULT  |UNPARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |UNPARTITIONED|
+    -- STREAM_PROJECT  |UNPARTITIONED|
+      -- ASSIGN  |UNPARTITIONED|
+        -- AGGREGATE  |UNPARTITIONED|
+          -- RANDOM_MERGE_EXCHANGE  |PARTITIONED|
+            -- AGGREGATE  |PARTITIONED|
+              -- STREAM_SELECT  |PARTITIONED|
+                -- STREAM_PROJECT  |PARTITIONED|
+                  -- SUBPLAN  |PARTITIONED|
+                          {
+                            -- AGGREGATE  |LOCAL|
+                              -- UNNEST  |LOCAL|
+                                -- NESTED_TUPLE_SOURCE  |LOCAL|
+                          }
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- ASSIGN  |PARTITIONED|
+                        -- STREAM_PROJECT  |PARTITIONED|
+                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                            -- DATASOURCE_SCAN (TestYelp.YelpCheckin)  |PARTITIONED|
+                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-3/query4.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-3/query4.plan
new file mode 100644
index 0000000..7e503bb
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-3/query4.plan
@@ -0,0 +1,21 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- SUBPLAN  |PARTITIONED|
+                      {
+                        -- AGGREGATE  |LOCAL|
+                          -- ASSIGN  |LOCAL|
+                            -- UNNEST  |LOCAL|
+                              -- NESTED_TUPLE_SOURCE  |LOCAL|
+                      }
+                -- STREAM_PROJECT  |PARTITIONED|
+                  -- ASSIGN  |PARTITIONED|
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                        -- DATASOURCE_SCAN (TestYelp.YelpCheckin)  |PARTITIONED|
+                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                            -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-4/query7.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-4/query7.plan
new file mode 100644
index 0000000..0f3a84c
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-quantified-queries/use-case-4/query7.plan
@@ -0,0 +1,39 @@
+-- DISTRIBUTE_RESULT  |PARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+    -- STREAM_PROJECT  |PARTITIONED|
+      -- ASSIGN  |PARTITIONED|
+        -- STREAM_PROJECT  |PARTITIONED|
+          -- STREAM_SELECT  |PARTITIONED|
+            -- STREAM_PROJECT  |PARTITIONED|
+              -- SUBPLAN  |PARTITIONED|
+                      {
+                        -- AGGREGATE  |LOCAL|
+                          -- STREAM_SELECT  |LOCAL|
+                            -- SUBPLAN  |LOCAL|
+                                    {
+                                      -- AGGREGATE  |LOCAL|
+                                        -- STREAM_SELECT  |LOCAL|
+                                          -- UNNEST  |LOCAL|
+                                            -- NESTED_TUPLE_SOURCE  |LOCAL|
+                                    }
+                              -- ASSIGN  |LOCAL|
+                                -- UNNEST  |LOCAL|
+                                  -- NESTED_TUPLE_SOURCE  |LOCAL|
+                      }
+                -- STREAM_SELECT  |PARTITIONED|
+                  -- STREAM_PROJECT  |PARTITIONED|
+                    -- ASSIGN  |PARTITIONED|
+                      -- STREAM_PROJECT  |PARTITIONED|
+                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                          -- BTREE_SEARCH (TestYelp.YelpCheckin.YelpCheckin)  |PARTITIONED|
+                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                              -- PRE_SORTED_DISTINCT_BY  |PARTITIONED|
+                                -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                  -- STABLE_SORT [$$58(ASC)]  |PARTITIONED|
+                                    -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                      -- STREAM_PROJECT  |PARTITIONED|
+                                        -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                          -- BTREE_SEARCH (TestYelp.YelpCheckin.IdxYelpCheckinDates)  |PARTITIONED|
+                                            -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                              -- ASSIGN  |PARTITIONED|
+                                                -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-unnest-queries/closed/with-composite-sk/query3.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-unnest-queries/closed/with-composite-sk/query3.plan
new file mode 100644
index 0000000..4af61e1
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results/array-index/select-unnest-queries/closed/with-composite-sk/query3.plan
@@ -0,0 +1,19 @@
+-- DISTRIBUTE_RESULT  |UNPARTITIONED|
+  -- ONE_TO_ONE_EXCHANGE  |UNPARTITIONED|
+    -- STREAM_PROJECT  |UNPARTITIONED|
+      -- ASSIGN  |UNPARTITIONED|
+        -- AGGREGATE  |UNPARTITIONED|
+          -- RANDOM_MERGE_EXCHANGE  |PARTITIONED|
+            -- AGGREGATE  |PARTITIONED|
+              -- STREAM_SELECT  |PARTITIONED|
+                -- STREAM_PROJECT  |PARTITIONED|
+                  -- ASSIGN  |PARTITIONED|
+                    -- STREAM_PROJECT  |PARTITIONED|
+                      -- UNNEST  |PARTITIONED|
+                        -- STREAM_PROJECT  |PARTITIONED|
+                          -- ASSIGN  |PARTITIONED|
+                            -- STREAM_PROJECT  |PARTITIONED|
+                              -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                -- DATASOURCE_SCAN (TestYelp.YelpCheckin)  |PARTITIONED|
+                                  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                    -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.1.ddl.sqlpp
similarity index 54%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.1.ddl.sqlpp
index d2dc752..a650bd0 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.1.ddl.sqlpp
@@ -16,23 +16,32 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+DROP DATAVERSE    TestDataverse IF EXISTS;
+CREATE DATAVERSE  TestDataverse;
+USE               TestDataverse;
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
-
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
+CREATE TYPE       TestType AS {
+    _id: uuid
 };
 
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
-
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+CREATE DATASET    Dataset1 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE DATASET    Dataset2 (TestType)
+PRIMARY KEY       _id AUTOGENERATED;
+CREATE INDEX      d1Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
+CREATE INDEX      d2Idx
+ON                Dataset1 (
+    field1 : bigint,
+    ( UNNEST outer_items
+      UNNEST inner_items
+      SELECT field2 : bigint,
+             field3 : bigint ),
+    field4 : bigint
+);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.10.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.10.query.sqlpp
new file mode 100644
index 0000000..1f113ef
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.10.query.sqlpp
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
+
+-- Existential quantification query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM      Dataset2 D2,
+          Dataset1 D1
+WHERE     D1.field1 /* +indexnl */ = TO_BIGINT(D2.field1) AND
+          ( SOME DOI IN D1.outer_items
+            SATISFIES DOI.field2_notindexed = TO_BIGINT(D2.field2_notindexed) AND
+                      ( SOME DII IN DOI.inner_items
+                        SATISFIES DII.field2 /* +indexnl */ = TO_BIGINT(D2.field2) AND
+                                  DII.field3 /* +indexnl */ = TO_BIGINT(D2.field3) AND
+                                  DII.field3_notindexed = TO_BIGINT(D2.field3_notindexed) ) ) AND
+          D1.field4 /* +indexnl */ = TO_BIGINT(D2.field4)
+SELECT    D1.field1, D1.outer_items, D1.field4
+ORDER BY  D1.field1, D1.outer_items, D1.field4;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.2.update.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.2.update.sqlpp
new file mode 100644
index 0000000..f66b749
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.2.update.sqlpp
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+USE               TestDataverse;
+
+INSERT INTO       Dataset1 [
+    { "field1": 1, "items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ], "outer_items": [ { "field2_notindexed": 2, "inner_items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ] } ], "field4": 4, "field4_notindexed": 4 },
+    { "field1": 1, "items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ], "outer_items": [ { "field2_notindexed": 2, "inner_items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ] } ], "field4": 4, "field4_notindexed": 4 },
+    { "field1": 1, "items": [ { "field2": 2, "field3": 3, "field3_notindexed": 300 } ], "outer_items": [ { "field2_notindexed": 2, "inner_items": [ { "field2": 2, "field3": 3, "field3_notindexed": 300 } ] } ], "field4": 4, "field4_notindexed": 400 },
+    { "field1": 100, "items": [ { "field2": 200, "field3": 300, "field3_notindexed": 3 } ], "outer_items": [ { "field2_notindexed": 200, "inner_items": [ { "field2": 200, "field3": 300, "field3_notindexed": 3 } ] } ], "field4": 400, "field4_notindexed": 4 },
+    { "field1": -1, "items": [ { "field2": -2, "field3": -3, "field3_notindexed": -3 } ], "outer_items": [ { "field2_notindexed": -2, "inner_items": [ { "field2": -2, "field3": -3, "field3_notindexed": -3 } ] } ], "field4": -4, "field4_notindexed": -4 },
+    { "field1": -1, "items": [ { "field2": -2, "field3": -3, "field3_notindexed": -3 } ], "outer_items": [ { "field2_notindexed": -2, "inner_items": [ { "field2": -2, "field3": -3, "field3_notindexed": -3 } ] } ], "field4": -4, "field4_notindexed": -4 }
+];
+
+INSERT INTO       Dataset2 [
+    { "field1": 1, "field2_notindexed": 2, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 },
+    { "field1": 1, "field2_notindexed": 200, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 },
+    { "field1": 100, "field2_notindexed": 2, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 },
+    { "field1": 100, "field2_notindexed": 200, "field2": 200, "field3": 300, "field3_notindexed": 300, "field4": 400, "field4_notindexed": 400 },
+    { "field1": -100, "field2_notindexed": -200, "field2": -200, "field3": -300, "field3_notindexed": -300, "field4": -400, "field4_notindexed": -400 }
+];
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.3.query.sqlpp
similarity index 62%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.3.query.sqlpp
index 2a9497a..e860165 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.3.query.sqlpp
@@ -16,18 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
 
-/*
- * Description: Verify the index metadata for both a composite atomic, array->atomic and array->atomic, atomic index. 
- */
-
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
-
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid
-};
-
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpBusinessCheckinDates ON YelpCheckin (business_id : string ?, UNNEST dates : string ?) ;
+-- Explicit UNNEST query with all fields of a composite index.
+FROM      Dataset1 D
+UNNEST    D.items DI
+WHERE     D.field1 = 1 AND
+          DI.field2 = 2 AND
+          DI.field3 = 3 AND
+          D.field4 = 4
+SELECT    D.field1, DI.field2, DI.field3, DI.field3_notindexed, D.field4, D.field4_notindexed
+ORDER BY  D.field1, DI.field2, DI.field3, DI.field3_notindexed, D.field4, D.field4_notindexed;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.4.query.sqlpp
similarity index 57%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.4.query.sqlpp
index d2dc752..b19f711 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.4.query.sqlpp
@@ -16,23 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
-
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
-};
-
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
-
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Explicit UNNEST query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM      Dataset1 D
+UNNEST    D.items DI
+WHERE     D.field1 = 1 AND
+          DI.field2 = 2 AND
+          DI.field3 = 3 AND
+          DI.field3_notindexed = 3 AND
+          D.field4 = 4 AND
+          D.field4_notindexed = 4
+SELECT    D.field1, DI.field2, DI.field3, DI.field3_notindexed, D.field4, D.field4_notindexed
+ORDER BY  D.field1, DI.field2, DI.field3, DI.field3_notindexed, D.field4, D.field4_notindexed;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.5.query.sqlpp
similarity index 63%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.5.query.sqlpp
index 2a9497a..e6002c3 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.5.query.sqlpp
@@ -16,18 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
 
-/*
- * Description: Verify the index metadata for both a composite atomic, array->atomic and array->atomic, atomic index. 
- */
-
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
-
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid
-};
-
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpBusinessCheckinDates ON YelpCheckin (business_id : string ?, UNNEST dates : string ?) ;
+-- Existential quantification query with all fields of a composite index.
+FROM      Dataset1 D
+WHERE     D.field1 = 1 AND
+          ( SOME DI IN D.items
+            SATISFIES DI.field2 = 2 AND
+                      DI.field3 = 3 ) AND
+          D.field4 = 4
+SELECT    D.field1, D.items, D.field4, D.field4_notindexed
+ORDER BY  D.field1, D.items, D.field4, D.field4_notindexed;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.6.query.sqlpp
similarity index 54%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.6.query.sqlpp
index d2dc752..12b6f4f 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.6.query.sqlpp
@@ -16,23 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
-
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
-};
-
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
-
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Existential quantification query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM      Dataset1 D
+WHERE     D.field1 = 1 AND
+          ( SOME DOI IN D.outer_items
+            SATISFIES DOI.field2_notindexed = 2 AND
+                      ( SOME DII IN DOI.inner_items
+                        SATISFIES DII.field2 = 2 AND
+                                  DII.field3 = 3 AND
+                                  DII.field3_notindexed = 3 ) ) AND
+          D.field4 = 4
+SELECT    D.field1, D.outer_items, D.field4
+ORDER BY  D.field1, D.outer_items, D.field4;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.7.query.sqlpp
similarity index 56%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.7.query.sqlpp
index d2dc752..9ab9213 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.7.query.sqlpp
@@ -16,23 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
-
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
-};
-
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
-
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Explicit UNNEST join query with all fields of a composite index.
+FROM      Dataset2 D2
+INNER JOIN  (
+    FROM    Dataset1 D1
+    UNNEST  D1.items D1I
+    SELECT  D1.field1, D1I.field2, D1I.field3, D1.field4
+) AS      D1IV
+ON        D1IV.field1 /* +indexnl */ = D2.field1 AND
+          D1IV.field2 /* +indexnl */ = D2.field2 AND
+          D1IV.field3 /* +indexnl */ = D2.field3 AND
+          D1IV.field4 /* +indexnl */ = D2.field4
+SELECT    D2.field1, D2.field2, D2.field3, D2.field4
+ORDER BY  D2.field1, D2.field2, D2.field3, D2.field4;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.8.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.8.query.sqlpp
new file mode 100644
index 0000000..907817b
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.8.query.sqlpp
@@ -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.
+ */
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
+
+-- Explicit UNNEST join query with all fields of a composite index, with extra clauses on non-indexed fields.
+FROM      Dataset2 D2
+INNER JOIN  (
+    FROM    Dataset1 D1
+    UNNEST  D1.items D1I
+    SELECT  D1.field1, D1I.field2, D1I.field3, D1I.field3_notindexed, D1.field4
+) AS      D1IV
+ON        D1IV.field1 /* +indexnl */ = TO_BIGINT(D2.field1) AND
+          D1IV.field2 /* +indexnl */ = TO_BIGINT(D2.field2) AND
+          D1IV.field3 /* +indexnl */ = TO_BIGINT(D2.field3) AND
+          D1IV.field3_notindexed     = D2.field3_notindexed AND
+          D1IV.field4 /* +indexnl */ = TO_BIGINT(D2.field4)
+SELECT    D2.field1, D2.field2, D2.field3, D2.field3_notindexed, D2.field4, D2.field4_notindexed
+ORDER BY  D2.field1, D2.field2, D2.field3, D2.field3_notindexed, D2.field4, D2.field4_notindexed;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.9.query.sqlpp
similarity index 56%
copy from asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.9.query.sqlpp
index d2dc752..f84f82d 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/queries/array-index/select-quantified-queries/use-case-1/query3.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/composite-index-queries/composite-index-queries.9.query.sqlpp
@@ -16,23 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-SET `compiler.arrayindex` "true";
+SET               `compiler.arrayindex` "true";
+USE               TestDataverse;
 
-DROP DATAVERSE TestYelp IF EXISTS;
-CREATE DATAVERSE TestYelp;
-USE TestYelp;
-
-CREATE TYPE CheckinType AS {
-    checkin_id: uuid,
-    business_id: string,
-    dates: [string]
-};
-
-CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpCheckinDates ON YelpCheckin (UNNEST dates);
-
-SELECT COUNT(*)
-FROM YelpCheckin C
-WHERE LEN(C.dates) > 0 AND
-      (EVERY D IN C.dates
-       SATISFIES D > "2016" AND D < "2017");
+-- Existential quantification join query with all fields of a composite index.
+FROM      Dataset2 D2,
+          Dataset1 D1
+WHERE     D1.field1 /* +indexnl */ = TO_BIGINT(D2.field1) AND
+          ( SOME D1I IN D1.items
+            SATISFIES D1I.field2 /* +indexnl */ = TO_BIGINT(D2.field2) AND
+                      D1I.field3 /* +indexnl */ = TO_BIGINT(D2.field3) ) AND
+          D1.field4 /* +indexnl */ = TO_BIGINT(D2.field4)
+SELECT    D2.field1, D2.field2, D2.field3, D2.field4
+ORDER BY  D2.field1, D2.field2, D2.field3, D2.field4;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp
index 2a9497a..cea2674 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.1.ddl.sqlpp
@@ -30,4 +30,24 @@ CREATE TYPE CheckinType AS {
 };
 
 CREATE DATASET YelpCheckin(CheckinType) PRIMARY KEY checkin_id AUTOGENERATED;
-CREATE INDEX IdxYelpBusinessCheckinDates ON YelpCheckin (business_id : string ?, UNNEST dates : string ?) ;
+CREATE INDEX IdxYelpBusinessCheckinDates ON YelpCheckin (business_id : string, UNNEST dates : string);
+
+-- More complex indexes created below.
+CREATE INDEX IdxYelp1 ON YelpCheckin (
+    ( UNNEST outer_dates_1
+      SELECT date : string ),
+    backup_business_id : string
+);
+CREATE INDEX IdxYelp2 ON YelpCheckin (
+    business_id : string,
+    ( UNNEST outer_dates_2
+      SELECT time : string )
+);
+CREATE INDEX IdxYelp3 ON YelpCheckin (
+    business_id : string,
+    ( UNNEST outer_dates
+      UNNEST inner_dates
+      SELECT date : string,
+             time : string ),
+    backup_business_id : string
+);
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.2.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.2.query.sqlpp
index bee279e..738a69b 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.2.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/array-index/metadata/open/with-composite-sk/with-composite-sk.2.query.sqlpp
@@ -17,6 +17,7 @@
  * under the License.
  */
 
-SELECT D.SearchKey, D.SearchKeyElements, D.SearchKeyType
-FROM Metadata.`Index` D
-WHERE D.IndexName = "IdxYelpBusinessCheckinDates" AND D.DataverseName = "TestYelp";
+FROM       Metadata.`Index` D
+WHERE      D.IndexName LIKE "IdxYelp%" AND D.DataverseName = "TestYelp"
+SELECT     D.SearchKey, D.SearchKeyElements, D.SearchKeyType
+ORDER BY   D.IndexName;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.1.adm
new file mode 100644
index 0000000..b811602
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.1.adm
@@ -0,0 +1,3 @@
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 300, "field4": 4, "field4_notindexed": 400 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.2.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.2.adm
new file mode 100644
index 0000000..27ffff6
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.2.adm
@@ -0,0 +1,2 @@
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.3.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.3.adm
new file mode 100644
index 0000000..ee8a69b
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.3.adm
@@ -0,0 +1,3 @@
+{ "field1": 1, "items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ], "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ], "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "items": [ { "field2": 2, "field3": 3, "field3_notindexed": 300 } ], "field4": 4, "field4_notindexed": 400 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.4.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.4.adm
new file mode 100644
index 0000000..327735d
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.4.adm
@@ -0,0 +1,2 @@
+{ "field1": 1, "outer_items": [ { "field2_notindexed": 2, "inner_items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ] } ], "field4": 4 }
+{ "field1": 1, "outer_items": [ { "field2_notindexed": 2, "inner_items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ] } ], "field4": 4 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.5.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.5.adm
new file mode 100644
index 0000000..e6bba45
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.5.adm
@@ -0,0 +1,7 @@
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 100, "field2": 200, "field3": 300, "field4": 400 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.6.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.6.adm
new file mode 100644
index 0000000..34afde6
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.6.adm
@@ -0,0 +1,4 @@
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field3_notindexed": 3, "field4": 4, "field4_notindexed": 4 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.7.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.7.adm
new file mode 100644
index 0000000..e6bba45
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.7.adm
@@ -0,0 +1,7 @@
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 1, "field2": 2, "field3": 3, "field4": 4 }
+{ "field1": 100, "field2": 200, "field3": 300, "field4": 400 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.8.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.8.adm
new file mode 100644
index 0000000..327735d
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/composite-index-queries/composite-index-queries.8.adm
@@ -0,0 +1,2 @@
+{ "field1": 1, "outer_items": [ { "field2_notindexed": 2, "inner_items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ] } ], "field4": 4 }
+{ "field1": 1, "outer_items": [ { "field2_notindexed": 2, "inner_items": [ { "field2": 2, "field3": 3, "field3_notindexed": 3 } ] } ], "field4": 4 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/metadata/open/with-composite-sk/with-composite-sk.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/metadata/open/with-composite-sk/with-composite-sk.1.adm
index a2eea1e..9ba69cb 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/metadata/open/with-composite-sk/with-composite-sk.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/array-index/metadata/open/with-composite-sk/with-composite-sk.1.adm
@@ -1 +1,4 @@
-{ "SearchKey": [  ], "SearchKeyElements": [ [ [ "business_id" ] ], { "UnnestList": [ [ "dates" ] ] } ], "SearchKeyType": [ [ "string" ], [ "string" ] ] }
+{ "SearchKey": [  ], "SearchKeyElements": [ { "UnnestList": [ [ "outer_dates_1" ] ], "ProjectList": [ [ "date" ] ] }, [ [ "backup_business_id" ] ] ], "SearchKeyType": [ [ "string" ], [ "string" ] ] }
+{ "SearchKey": [  ], "SearchKeyElements": [ [ [ "business_id" ] ], { "UnnestList": [ [ "outer_dates_2" ] ], "ProjectList": [ [ "time" ] ] } ], "SearchKeyType": [ [ "string" ], [ "string" ] ] }
+{ "SearchKey": [  ], "SearchKeyElements": [ [ [ "business_id" ] ], { "UnnestList": [ [ "outer_dates" ], [ "inner_dates" ] ], "ProjectList": [ [ "date" ], [ "time" ] ] }, [ [ "backup_business_id" ] ] ], "SearchKeyType": [ [ "string" ], [ "string", "string" ], [ "string" ] ] }
+{ "SearchKey": [  ], "SearchKeyElements": [ [ [ "business_id" ] ], { "UnnestList": [ [ "dates" ] ] } ], "SearchKeyType": [ [ "string" ], [ "string" ] ] }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
index 29526f3..9211032 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
@@ -8071,13 +8071,6 @@
           <source-location>false</source-location>
         </compilation-unit>
       </test-case>
-      <test-case FilePath="array-index/error-handling">
-        <compilation-unit name="index-mixed-composite">
-          <output-dir compare="Text">index-mixed-composite</output-dir>
-          <expected-error>ASX1155: Incompatible index type ARRAY</expected-error>
-          <source-location>false</source-location>
-        </compilation-unit>
-      </test-case>
     </test-group>
     <test-group name="array-index/metadata">
       <test-case FilePath="array-index/metadata/closed">
@@ -8125,16 +8118,16 @@
           <output-dir compare="Text">complex-structures</output-dir>
         </compilation-unit>
       </test-case>
-      <!--      <test-case FilePath="array-index/metadata/closed">-->
-      <!--        <compilation-unit name="with-composite-sk">-->
-      <!--          <output-dir compare="Text">with-composite-sk</output-dir>-->
-      <!--        </compilation-unit>-->
-      <!--      </test-case>-->
-      <!--      <test-case FilePath="array-index/metadata/open">-->
-      <!--        <compilation-unit name="with-composite-sk">-->
-      <!--          <output-dir compare="Text">with-composite-sk</output-dir>-->
-      <!--        </compilation-unit>-->
-      <!--      </test-case>-->
+      <test-case FilePath="array-index/metadata/closed">
+        <compilation-unit name="with-composite-sk">
+          <output-dir compare="Text">with-composite-sk</output-dir>
+        </compilation-unit>
+      </test-case>
+      <test-case FilePath="array-index/metadata/open">
+        <compilation-unit name="with-composite-sk">
+          <output-dir compare="Text">with-composite-sk</output-dir>
+        </compilation-unit>
+      </test-case>
       <test-case FilePath="array-index/metadata/closed">
         <compilation-unit name="with-composite-array-different-indicators">
           <output-dir compare="Text">with-composite-array-different-indicators</output-dir>
@@ -8475,6 +8468,13 @@
         </compilation-unit>
       </test-case>
     </test-group>
+    <test-group name="array-index/composite-index-queries">
+      <test-case FilePath="array-index">
+        <compilation-unit name="composite-index-queries">
+          <output-dir compare="Text">composite-index-queries</output-dir>
+        </compilation-unit>
+      </test-case>
+    </test-group>
   </test-group>
   <test-group name="nestrecords">
     <test-case FilePath="nestrecords">