CQL often queries static columns unnecessarily

patch by Sylvain Lebresne; reviewed by Tyler Hobbs for CASSANDRA-12768


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/95d0b671
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/95d0b671
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/95d0b671

Branch: refs/heads/cassandra-3.X
Commit: 95d0b671d1af154eaf1c1e81992c7f3f51469eee
Parents: dd8244c
Author: Sylvain Lebresne <sylv...@datastax.com>
Authored: Mon Oct 10 13:36:15 2016 +0200
Committer: Sylvain Lebresne <sylv...@datastax.com>
Committed: Wed Dec 7 11:16:47 2016 +0100

----------------------------------------------------------------------
 CHANGES.txt                                     |  1 +
 .../cql3/statements/SelectStatement.java        | 26 ++++++--
 .../db/filter/ClusteringIndexNamesFilter.java   | 12 ++--
 .../cassandra/db/filter/ColumnFilter.java       | 67 +++++++++++++++-----
 .../org/apache/cassandra/db/rows/BTreeRow.java  |  2 +-
 5 files changed, 81 insertions(+), 27 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/95d0b671/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index 5242adf..21cf5be 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 3.0.11
+ * CQL often queries static columns unnecessarily (CASSANDRA-12768)
  * Make sure sstables only get committed when it's safe to discard commit log 
records (CASSANDRA-12956)
  * Reject default_time_to_live option when creating or altering MVs 
(CASSANDRA-12868)
  * Nodetool should use a more sane max heap size (CASSANDRA-12739)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/95d0b671/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
index aca6146..f2aa030 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
@@ -141,13 +141,19 @@ public class SelectStatement implements CQLStatement
         if (selection.isWildcard())
             return ColumnFilter.all(cfm);
 
-        ColumnFilter.Builder builder = ColumnFilter.allColumnsBuilder(cfm);
+        ColumnFilter.Builder builder = 
ColumnFilter.allRegularColumnsBuilder(cfm);
         // Adds all selected columns
         for (ColumnDefinition def : selection.getColumns())
             if (!def.isPrimaryKeyColumn())
                 builder.add(def);
         // as well as any restricted column (so we can actually apply the 
restriction)
         builder.addAll(restrictions.nonPKRestrictedColumns(true));
+
+        // In a number of cases, we want to distinguish between a partition 
truly empty and one with only static content
+        // (but no rows). In those cases, we should force querying all static 
columns (to make the distinction).
+        if (cfm.hasStaticColumns() && 
returnStaticContentOnPartitionWithNoRows())
+            builder.addAll(cfm.partitionColumns().statics);
+
         return builder.build();
     }
 
@@ -734,6 +740,20 @@ public class SelectStatement implements CQLStatement
         }
     }
 
+    // Determines whether, when we have a partition result with not rows, we 
still return the static content (as a
+    // result set row with null for all other regular columns.)
+    private boolean returnStaticContentOnPartitionWithNoRows()
+    {
+        // The general rational is that if some rows are specifically selected 
by the query, we ignore partitions that
+        // are empty outside of static content, but if it's a full partition 
query, then we include that content.
+        // In practice, we consider rows are specifically selected if either 
there is some restrictions on the
+        // clustering columns or it's a 2ndary index query (the later is 
debatable but historical). An exception however
+        // is 'static compact' table, for which 2ndary index indexes full 
partition (and so for which we consider 2ndary
+        // indexquery to be full partition query).
+        return !restrictions.hasClusteringColumnsRestriction()
+            && (!restrictions.usesSecondaryIndexing() || 
cfm.isStaticCompactTable());
+    }
+
     // Used by ModificationStatement for CAS operations
     void processPartition(RowIterator partition, QueryOptions options, 
Selection.ResultSetBuilder result, int nowInSec)
     throws InvalidRequestException
@@ -744,12 +764,10 @@ public class SelectStatement implements CQLStatement
 
         Row staticRow = partition.staticRow();
         // If there is no rows, then provided the select was a full partition 
selection
-        // (i.e. not a 2ndary index search and there was no condition on 
clustering columns),
         // we want to include static columns and we're done.
         if (!partition.hasNext())
         {
-            if (!staticRow.isEmpty() && (!restrictions.usesSecondaryIndexing() 
|| cfm.isStaticCompactTable())
-                    && !restrictions.hasClusteringColumnsRestriction())
+            if (!staticRow.isEmpty() && 
returnStaticContentOnPartitionWithNoRows())
             {
                 result.newRow(protocolVersion);
                 for (ColumnDefinition def : selection.getColumns())

http://git-wip-us.apache.org/repos/asf/cassandra/blob/95d0b671/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
----------------------------------------------------------------------
diff --git 
a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java 
b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
index a81a7a6..ea5cf55 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
@@ -178,12 +178,12 @@ public class ClusteringIndexNamesFilter extends 
AbstractClusteringIndexFilter
     {
         final SearchIterator<Clustering, Row> searcher = 
partition.searchIterator(columnFilter, reversed);
         return new AbstractUnfilteredRowIterator(partition.metadata(),
-                                        partition.partitionKey(),
-                                        partition.partitionLevelDeletion(),
-                                        columnFilter.fetchedColumns(),
-                                        
searcher.next(Clustering.STATIC_CLUSTERING),
-                                        reversed,
-                                        partition.stats())
+                                                 partition.partitionKey(),
+                                                 
partition.partitionLevelDeletion(),
+                                                 columnFilter.fetchedColumns(),
+                                                 
searcher.next(Clustering.STATIC_CLUSTERING),
+                                                 reversed,
+                                                 partition.stats())
         {
             private final Iterator<Clustering> clusteringIter = 
clusteringsInQueryOrder.iterator();
 

http://git-wip-us.apache.org/repos/asf/cassandra/blob/95d0b671/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java 
b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
index 05eade5..8d4f8b8 100644
--- a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
@@ -37,8 +37,8 @@ import org.apache.cassandra.io.util.DataOutputPlus;
  * by a query.
  *
  * In practice, this class cover 2 main cases:
- *   1) most user queries have to internally query all columns, because the 
CQL semantic requires us to know if
- *      a row is live or not even if it has no values for the columns 
requested by the user (see #6588for more
+ *   1) most user queries have to internally query all (regular) columns, 
because the CQL semantic requires us to know
+ *      if a row is live or not even if it has no values for the columns 
requested by the user (see #6588 for more
  *      details). However, while we need to know for columns if it has live 
values, we can actually save from
  *      sending the values for those columns that will not be returned to the 
user.
  *   2) for some internal queries (and for queries using #6588 if we introduce 
it), we're actually fine only
@@ -51,8 +51,11 @@ public class ColumnFilter
 {
     public static final Serializer serializer = new Serializer();
 
-    // Distinguish between the 2 cases described above: if 'isFetchAll' is 
true, then all columns will be retrieved
-    // by the query, but the values for column/cells not selected by 
'selection' and 'subSelections' will be skipped.
+    // Distinguish between the 2 cases described above: if 'isFetchAll' is 
true, then all regular columns will be
+    // retrieved by the query. If selection is also null, then all static 
columns will be fetched too. If 'isFetchAll'
+    // is true and selection is not null, then 1) for static columns, only the 
ones in selection are read and 2) for
+    // regular columns, while all are fetches, the values for column/cells not 
selected by 'selection' and
+    // 'subSelections' will be skipped.
     // Otherwise, only the column/cells returned by 'selection' and 
'subSelections' will be returned at all.
     private final boolean isFetchAll;
 
@@ -102,12 +105,21 @@ public class ColumnFilter
      */
     public PartitionColumns fetchedColumns()
     {
-        return isFetchAll ? metadata.partitionColumns() : selection;
+        if (!isFetchAll)
+            return selection;
+
+        // We always fetch all regulars, but only fetch the statics in 
selection. Unless selection is null, in which
+        // case it's a wildcard and we fetch everything.
+        PartitionColumns all = metadata.partitionColumns();
+        return selection == null || all.statics.isEmpty()
+             ? all
+             : new PartitionColumns(selection.statics, all.regulars);
     }
 
-    public boolean includesAllColumns()
+    public boolean includesAllColumns(boolean isStatic)
     {
-        return isFetchAll;
+        // Static columns are never all included, unless selection == null
+        return isStatic ? selection == null : isFetchAll;
     }
 
     /**
@@ -115,6 +127,11 @@ public class ColumnFilter
      */
     public boolean includes(ColumnDefinition column)
     {
+        // For statics, it is included only if it's part of selection, or if 
selection is null (wildcard query).
+        if (column.isStatic())
+            return selection == null || selection.contains(column);
+
+        // For regulars, if 'isFetchAll', then it's included automatically. 
Otherwise, it depends on 'selection'.
         return isFetchAll || selection.contains(column);
     }
 
@@ -166,8 +183,13 @@ public class ColumnFilter
     }
 
     /**
-     * Creates a new {@code Tester} to efficiently test the inclusion of cells 
of complex column
-     * {@code column}.
+     * Creates a new {@code Tester} to efficiently test the inclusion of cells
+     * of an included complex column.
+     *
+     * @param column the complex column, which *must* be included by this
+     * filter (that is, we must have {@code this.includes(column)}).
+     * @retun the created tester or {@code null} if all the cells from {@code
+     * column} are included.
      */
     public Tester newTester(ColumnDefinition column)
     {
@@ -178,14 +200,15 @@ public class ColumnFilter
         if (s.isEmpty())
             return null;
 
-        return new Tester(isFetchAll, s.iterator());
+        // isFetchAll only imply everything if fetches for regular
+        return new Tester(isFetchAll && !column.isStatic(), s.iterator());
     }
 
     /**
      * Returns a {@code ColumnFilter}} builder that includes all columns (so 
the selections
      * added to the builder are the columns/cells for which we shouldn't skip 
the values).
      */
-    public static Builder allColumnsBuilder(CFMetaData metadata)
+    public static Builder allRegularColumnsBuilder(CFMetaData metadata)
     {
         return new Builder(metadata);
     }
@@ -201,24 +224,36 @@ public class ColumnFilter
 
     public static class Tester
     {
-        private final boolean isFetchAll;
+        private final boolean isFetched; // if true, all cells are included
         private ColumnSubselection current;
         private final Iterator<ColumnSubselection> iterator;
 
-        private Tester(boolean isFetchAll, Iterator<ColumnSubselection> 
iterator)
+        private Tester(boolean isFetched, Iterator<ColumnSubselection> 
iterator)
         {
-            this.isFetchAll = isFetchAll;
+            this.isFetched = isFetched;
             this.iterator = iterator;
         }
 
         public boolean includes(CellPath path)
         {
-            return isFetchAll || includedBySubselection(path);
+            // It's included if either all cells are fetched (because it's a
+            // regular column and the filter has 'isFetchAll == true'), or if
+            // it's explicitely selected.
+            return isFetched || includedBySubselection(path);
         }
 
+        /**
+         * Must only be called if {@code includes(path) == true}.
+         */
         public boolean canSkipValue(CellPath path)
         {
-            return isFetchAll && !includedBySubselection(path);
+            // We can skip the value of an included column only if it's a
+            // regular column included due to the 'isFetchAll' flag, but which
+            // isn't explicitely selected. In practice, it's enough to not have
+            // the path explicitly selected as it implies the column was
+            // included due to 'isFetchAll' (since we require includes(path) to
+            // be called first).
+            return !includedBySubselection(path);
         }
 
         private boolean includedBySubselection(CellPath path)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/95d0b671/src/java/org/apache/cassandra/db/rows/BTreeRow.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java 
b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
index ea1d9e0..18f3dec 100644
--- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java
+++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
@@ -237,7 +237,7 @@ public class BTreeRow extends AbstractRow
     {
         Map<ByteBuffer, CFMetaData.DroppedColumn> droppedColumns = 
metadata.getDroppedColumns();
 
-        if (filter.includesAllColumns() && (activeDeletion.isLive() || 
deletion.supersedes(activeDeletion)) && droppedColumns.isEmpty())
+        if (filter.includesAllColumns(isStatic()) && (activeDeletion.isLive() 
|| deletion.supersedes(activeDeletion)) && droppedColumns.isEmpty())
             return this;
 
         boolean mayHaveShadowed = activeDeletion.supersedes(deletion.time());

Reply via email to