← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 9341: Query planner, impl support for multiple period types and org unit levels in same query

 

------------------------------------------------------------
revno: 9341
committer: Lars Helge Øverland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Mon 2012-12-17 17:58:29 +0100
message:
  Query planner, impl support for multiple period types and org unit levels in same query
added:
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.java
renamed:
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/QueryPlanner.java => dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java
modified:
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml
  dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java


--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk

Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java	2012-12-14 15:04:48 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java	2012-12-17 16:58:29 +0000
@@ -47,6 +47,7 @@
     public static final String CATEGORYOPTIONCOMBO_DIM_ID = "coc";
     public static final String PERIOD_DIM_ID = "pe";
     public static final String ORGUNIT_DIM_ID = "ou";
+    public static final String VALUE_ID = "value";
         
     private List<String> indicators = new ArrayList<String>();
     
@@ -59,9 +60,17 @@
     private Map<String, List<String>> dimensions = new HashMap<String, List<String>>();
     
     private boolean categories = false;
+
+    // -------------------------------------------------------------------------
+    // Transient properties
+    // -------------------------------------------------------------------------
     
     private transient String tableName;
 
+    private transient String periodType;
+    
+    private transient int organisationUnitLevel;
+    
     // -------------------------------------------------------------------------
     // Constructors
     // -------------------------------------------------------------------------
@@ -89,18 +98,26 @@
         this.organisationUnits = params.getOrganisationUnits();
         this.dimensions = params.getDimensions();
         this.categories = params.isCategories();
+        
+        this.tableName = params.getTableName();
+        this.periodType = params.getPeriodType();
+        this.organisationUnitLevel = params.getOrganisationUnitLevel();
     }
 
     // -------------------------------------------------------------------------
     // Logic
     // -------------------------------------------------------------------------
 
+    /**
+     * Prioritizing to split on dimensions with high cardinality, which is typically 
+     * organisation unit and data element in that order.
+     */
     public SortedMap<String, List<String>> getDimensionValuesMap()
     {
         SortedMap<String, List<String>> map = new TreeMap<String, List<String>>();
-        
+
+        map.put( ORGUNIT_DIM_ID, organisationUnits );
         map.put( DATAELEMENT_DIM_ID, dataElements );
-        map.put( ORGUNIT_DIM_ID, organisationUnits );
         map.put( PERIOD_DIM_ID, periods );
         
         if ( dimensions != null )
@@ -192,12 +209,114 @@
         
         return dimension;
     }
-    
+
+    @Override
+    public int hashCode()
+    {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ( categories ? 1231 : 1237);
+        result = prime * result + ( ( indicators == null ) ? 0 : indicators.hashCode() );
+        result = prime * result + ( ( dataElements == null ) ? 0 : dataElements.hashCode() );
+        result = prime * result + ( ( periods == null ) ? 0 : periods.hashCode() );
+        result = prime * result + ( ( organisationUnits == null ) ? 0 : organisationUnits.hashCode() );
+        result = prime * result + ( ( dimensions == null ) ? 0 : dimensions.hashCode() );
+        return result;
+    }
+
+    @Override
+    public boolean equals( Object object )
+    {
+        if ( this == object )
+        {
+            return true;
+        }
+        
+        if ( object == null )
+        {
+            return false;
+        }
+        
+        if ( getClass() != object.getClass() )
+        {
+            return false;
+        }
+        
+        DataQueryParams other = (DataQueryParams) object;
+
+        if ( indicators == null )
+        {
+            if ( other.indicators != null )
+            {
+                return false;
+            }
+        }
+        else if ( !indicators.equals( other.indicators ) )
+        {
+            return false;
+        }
+        
+        if ( dataElements == null )
+        {
+            if ( other.dataElements != null )
+            {
+                return false;
+            }
+        }
+        else if ( !dataElements.equals( other.dataElements ) )
+        {
+            return false;
+        }
+
+        if ( periods == null )
+        {
+            if ( other.periods != null )
+            {
+                return false;
+            }
+        }
+        else if ( !periods.equals( other.periods ) )
+        {
+            return false;
+        }
+
+        if ( organisationUnits == null )
+        {
+            if ( other.organisationUnits != null )
+            {
+                return false;
+            }
+        }
+        else if ( !organisationUnits.equals( other.organisationUnits ) )
+        {
+            return false;
+        }
+        
+        if ( dimensions == null )
+        {
+            if ( other.dimensions != null )
+            {
+                return false;
+            }
+        }
+        else if ( !dimensions.equals( other.dimensions ) )
+        {
+            return false;
+        }
+        
+        if ( categories != other.categories )
+        {
+            return false;
+        }
+        
+        return true;
+    }
+
     @Override
     public String toString()
     {
         return "[in: " + indicators + ", de: " + dataElements + ", pe: " + periods
-            + ", ou: " + organisationUnits + "]";
+            + ", ou: " + organisationUnits + ", categories: " + categories + "]";
     }
         
     // -------------------------------------------------------------------------
@@ -279,4 +398,24 @@
     {
         this.tableName = tableName;
     }
+
+    public String getPeriodType()
+    {
+        return periodType;
+    }
+
+    public void setPeriodType( String periodType )
+    {
+        this.periodType = periodType;
+    }
+
+    public int getOrganisationUnitLevel()
+    {
+        return organisationUnitLevel;
+    }
+
+    public void setOrganisationUnitLevel( int organisationUnitLevel )
+    {
+        this.organisationUnitLevel = organisationUnitLevel;
+    }
 }

=== added file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.java	2012-12-17 16:58:29 +0000
@@ -0,0 +1,58 @@
+package org.hisp.dhis.analytics;
+
+/*
+ * Copyright (c) 2004-2012, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ * * Neither the name of the HISP project nor the names of its contributors may
+ *   be used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import java.util.List;
+
+public interface QueryPlanner
+{
+    /**
+     * Creates a list of DataQueryParams. It is mandatory to group the queries by
+     * the following criteria: 1) partition / year 2) period type 3) organisation 
+     * unit level. If the number of queries produced by this grouping is equal or
+     * larger than the number of optimal queries, those queries are returned.
+     * 
+     * @param params the data query params.
+     * @param optimalQueries the number of optimal queries for the planner to return.
+     * @return
+     */
+    List<DataQueryParams> planQuery( DataQueryParams params, int optimalQueries );
+
+    /**
+     * Gets the data dimension must suitable as partition key. Will first check
+     * if any of the dimensions have enough values to satisfy a optimal number of
+     * queries, and return that dimension if so. If not returns the dimension
+     * with the highest number of values. The order of the fixed dimensions are
+     * data element, organisation unit, period.
+     * 
+     * @param params the data query parameters.
+     * @param optimalQueries the optimal number of queries to create.
+     */
+    String getPartitionDimension( DataQueryParams params, int optimalQueries );
+
+}

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java	2012-12-14 17:23:37 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java	2012-12-17 16:58:29 +0000
@@ -36,6 +36,7 @@
 import org.hisp.dhis.analytics.AnalyticsManager;
 import org.hisp.dhis.analytics.AnalyticsService;
 import org.hisp.dhis.analytics.DataQueryParams;
+import org.hisp.dhis.analytics.QueryPlanner;
 import org.hisp.dhis.common.Grid;
 import org.hisp.dhis.common.GridHeader;
 import org.hisp.dhis.system.grid.ListGrid;
@@ -45,17 +46,23 @@
 public class DefaultAnalyticsService
     implements AnalyticsService
 {
-    private static final String VALUE_NAME = "value";
     private static final String VALUE_HEADER_NAME = "Value";
     
     //TODO period aggregation for multiple period types
     //TODO hierarchy aggregation for org units at multiple levels
     //TODO indicator aggregation
     //TODO category sub-totals and totals
+    //TODO use data mart when query can be satisfied
+    //TODO create data mart for average, less-than yearly data elements
+    
+    //NOTE split on high-cardinality columns like data element and org unit, not period, improves performance
     
     @Autowired
     private AnalyticsManager analyticsManager;
     
+    @Autowired
+    private QueryPlanner queryPlanner;
+    
     public Grid getAggregatedDataValueTotals( DataQueryParams params ) throws Exception
     {
         Map<String, Double> map = getAggregatedDataValueMap( params );
@@ -67,7 +74,7 @@
             grid.addHeader( new GridHeader( col, col, String.class.getName(), false, true ) );
         }
         
-        grid.addHeader( new GridHeader( VALUE_NAME, VALUE_HEADER_NAME, Double.class.getName(), false, false ) );
+        grid.addHeader( new GridHeader( DataQueryParams.VALUE_ID, VALUE_HEADER_NAME, Double.class.getName(), false, false ) );
         
         for ( Map.Entry<String, Double> entry : map.entrySet() )
         {
@@ -83,7 +90,7 @@
     {
         Timer t = new Timer().start();
 
-        List<DataQueryParams> queries = QueryPlanner.planQuery( params, 6 );
+        List<DataQueryParams> queries = queryPlanner.planQuery( params, 6 );
         
         t.getTime( "Planned query" );
         

=== renamed file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/QueryPlanner.java' => 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/QueryPlanner.java	2012-12-12 15:29:04 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java	2012-12-17 16:58:29 +0000
@@ -28,82 +28,100 @@
  */
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.SortedMap;
 
 import org.hisp.dhis.analytics.DataQueryParams;
+import org.hisp.dhis.analytics.QueryPlanner;
 import org.hisp.dhis.analytics.table.PartitionUtils;
+import org.hisp.dhis.organisationunit.OrganisationUnitService;
+import org.hisp.dhis.period.PeriodType;
 import org.hisp.dhis.system.util.ListMap;
 import org.hisp.dhis.system.util.PaginatedList;
+import org.springframework.beans.factory.annotation.Autowired;
 
-public class QueryPlanner
+public class DefaultQueryPlanner
+    implements QueryPlanner
 {
-    /**
-     * Creates a list of DataQueryParams.
-     * 
-     * @param params the data query params.
-     * @param optimalQueries the number of optimal queries for the planner to return.
-     * @return
-     */
-    public static List<DataQueryParams> planQuery( DataQueryParams params, int optimalQueries )
+    @Autowired
+    private OrganisationUnitService organisationUnitService;
+    
+    // -------------------------------------------------------------------------
+    // DefaultQueryPlanner implementation
+    // -------------------------------------------------------------------------
+    
+    public List<DataQueryParams> planQuery( DataQueryParams params, int optimalQueries )
     {
+        // ---------------------------------------------------------------------
+        // Group queries by partition, period type and organisation unit level
+        // ---------------------------------------------------------------------
+        
         params = new DataQueryParams( params );
         
         List<DataQueryParams> queries = new ArrayList<DataQueryParams>();
-
-        ListMap<String, String> tablePeriodMap = PartitionUtils.getTablePeriodMap( params.getPeriods() );
-        
-        boolean periodSatisfies = tablePeriodMap.size() >= optimalQueries;
-        
-        if ( periodSatisfies )
-        {
-            for ( String tableName : tablePeriodMap.keySet() )
-            {
-                DataQueryParams query = new DataQueryParams( params );
-                query.setPeriods( tablePeriodMap.get( tableName ) );
-                query.setTableName( tableName );
-                queries.add( query );
-            }
-        }
-        else
-        {
-            int pages = optimalQueries / tablePeriodMap.size(); // TODO individual no per table
-
-            for ( String tableName : tablePeriodMap.keySet() )
-            {
-                params.setPeriods( tablePeriodMap.get( tableName ) );
-                
-                String dimension = getPartitionDimension( params, pages );
-                
-                List<String> partitionValues = params.getDimension( dimension );
-                
-                List<List<String>> partitionValuePages = new PaginatedList<String>( partitionValues ).setNumberOfPages( pages ).getPages();
+        
+        List<DataQueryParams> groupedByPartition = groupByPartition( params );
+        
+        for ( DataQueryParams byPartition : groupedByPartition )
+        {
+            List<DataQueryParams> groupedByPeriodType = groupByPeriodType( byPartition );
             
-                for ( List<String> valuePage : partitionValuePages )
+            for ( DataQueryParams byPeriodType : groupedByPeriodType )
+            {
+                List<DataQueryParams> groupedByOrgUnitLevel = groupByOrgUnitLevel( byPeriodType );
+                
+                for ( DataQueryParams byOrgUnitLevel : groupedByOrgUnitLevel )
                 {
-                    DataQueryParams query = new DataQueryParams( params );
-                    query.setPeriods( tablePeriodMap.get( tableName ) );
-                    query.setDimension( dimension, valuePage );
-                    query.setTableName( tableName );
-                    queries.add( query );
+                    byOrgUnitLevel.setTableName( byPartition.getTableName() );
+                    byOrgUnitLevel.setPeriodType( byPeriodType.getPeriodType() );
+                    
+                    queries.add( byOrgUnitLevel );
                 }
             }
         }
-        
-        return queries;
+
+        // ---------------------------------------------------------------------
+        // Return if number of queries are equal or larger than optimal
+        // ---------------------------------------------------------------------
+        
+        boolean satisfied = queries.size() >= optimalQueries;
+        
+        if ( satisfied )
+        {
+            return queries;
+        }
+
+        // ---------------------------------------------------------------------
+        // Group by dimensions
+        // ---------------------------------------------------------------------
+        
+        List<DataQueryParams> subQueries = new ArrayList<DataQueryParams>();
+        
+        Double pages = Math.ceil( (double) optimalQueries / queries.size() );
+        
+        int optimal = pages.intValue();
+        
+        for ( DataQueryParams query : queries )
+        {
+            String dimension = getPartitionDimension( query, optimal );
+            
+            List<String> partitionValues = params.getDimension( dimension );
+            
+            List<List<String>> partitionValuePages = new PaginatedList<String>( partitionValues ).setNumberOfPages( optimal ).getPages();
+        
+            for ( List<String> valuePage : partitionValuePages )
+            {
+                DataQueryParams subQuery = new DataQueryParams( query );
+                subQuery.setDimension( dimension, valuePage );
+                subQueries.add( subQuery );
+            }
+        }
+        
+        return subQueries;
     }
     
-    /**
-     * Gets the data dimension must suitable as partition key. Will first check
-     * if any of the dimensions have enough values to satisfy a optimal number of
-     * queries, and return that dimension if so. If not returns the dimension
-     * with the highest number of values. The order of the fixed dimensions are
-     * data element, organisation unit, period.
-     * 
-     * @param params the data query parameters.
-     * @param optimalQueries the optimal number of queries to create.
-     */
-    public static String getPartitionDimension( DataQueryParams params, int optimalQueries )
+    public String getPartitionDimension( DataQueryParams params, int optimalQueries )
     {
         SortedMap<String, List<String>> map = params.getDimensionValuesMap();
         
@@ -117,4 +135,112 @@
         
         return params.getLargestDimension();
     }
+    
+    public boolean canQueryFromDataMart( DataQueryParams params )
+    {
+        return true;
+    }
+
+    // -------------------------------------------------------------------------
+    // Supportive methods
+    // -------------------------------------------------------------------------
+    
+    /**
+     * Groups the given query into sub queries based on its periods and which 
+     * partition it should be executed against. Sets the partition table name on
+     * each query.
+     */
+    private List<DataQueryParams> groupByPartition( DataQueryParams params )
+    {
+        List<DataQueryParams> queries = new ArrayList<DataQueryParams>();
+
+        ListMap<String, String> tablePeriodMap = PartitionUtils.getTablePeriodMap( params.getPeriods() );
+        
+        for ( String tableName : tablePeriodMap.keySet() )
+        {
+            DataQueryParams query = new DataQueryParams( params );
+            query.setPeriods( tablePeriodMap.get( tableName ) );
+            query.setTableName( tableName );
+            queries.add( query );            
+        }
+        
+        return queries;
+    }
+    
+    /**
+     * Groups the given query into sub queries based on the period type of its
+     * periods. Sets the period type name on each query.
+     */
+    private List<DataQueryParams> groupByPeriodType( DataQueryParams params )
+    {
+        List<DataQueryParams> queries = new ArrayList<DataQueryParams>();
+        
+        ListMap<String, String> periodTypePeriodMap = getPeriodTypePeriodMap( params.getPeriods() );
+
+        for ( String periodType : periodTypePeriodMap.keySet() )
+        {
+            DataQueryParams query = new DataQueryParams( params );
+            query.setPeriods( periodTypePeriodMap.get( periodType ) );
+            query.setPeriodType( periodType );
+            queries.add( query );            
+        }
+        
+        return queries;        
+    }
+    
+    /**
+     * Groups the given query into sub queries based on the level of its organisation 
+     * units. Sets the organisation unit level on each query.
+     */
+    private List<DataQueryParams> groupByOrgUnitLevel( DataQueryParams params )
+    {
+        List<DataQueryParams> queries = new ArrayList<DataQueryParams>();
+        
+        ListMap<Integer, String> levelOrgUnitMap = getLevelOrgUnitMap( params.getOrganisationUnits() );
+        
+        for ( Integer level : levelOrgUnitMap.keySet() )
+        {
+            DataQueryParams query = new DataQueryParams( params );
+            query.setOrganisationUnits( levelOrgUnitMap.get( level ) );
+            query.setOrganisationUnitLevel( level );
+            queries.add( query );
+        }
+        
+        return queries;    
+    }
+    
+    /**
+     * Creates a mapping between period type name and period for the given periods.
+     */
+    private ListMap<String, String> getPeriodTypePeriodMap( Collection<String> isoPeriods )
+    {
+        ListMap<String, String> map = new ListMap<String, String>();
+        
+        for ( String period : isoPeriods )
+        {
+            String periodTypeName = PeriodType.getPeriodTypeFromIsoString( period ).getName();
+            
+            map.putValue( periodTypeName, period );
+        }
+        
+        return map;
+    }
+    
+    /**
+     * Creates a mapping between level and organisation unit for the given organisation
+     * units.
+     */
+    private ListMap<Integer, String> getLevelOrgUnitMap( Collection<String> orgUnits )
+    {
+        ListMap<Integer, String> map = new ListMap<Integer, String>();
+        
+        for ( String orgUnit : orgUnits )
+        {
+            int level = organisationUnitService.getLevelOfOrganisationUnit( orgUnit );
+            
+            map.putValue( level, orgUnit );
+        }
+        
+        return map;
+    }
 }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java	2012-12-14 15:04:48 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java	2012-12-17 16:58:29 +0000
@@ -39,10 +39,6 @@
 import org.apache.commons.logging.LogFactory;
 import org.hisp.dhis.analytics.AnalyticsManager;
 import org.hisp.dhis.analytics.DataQueryParams;
-import org.hisp.dhis.expression.ExpressionService;
-import org.hisp.dhis.organisationunit.OrganisationUnitService;
-import org.hisp.dhis.period.PeriodService;
-import org.hisp.dhis.period.PeriodType;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.support.rowset.SqlRowSet;
@@ -64,15 +60,6 @@
     @Autowired
     private JdbcTemplate jdbcTemplate;
     
-    @Autowired
-    private OrganisationUnitService organisationUnitService;
-    
-    @Autowired
-    private PeriodService periodService;
-    
-    @Autowired
-    private ExpressionService expressionService;
-    
     // -------------------------------------------------------------------------
     // Implementation
     // -------------------------------------------------------------------------
@@ -82,28 +69,26 @@
     @Async
     public Future<Map<String, Double>> getAggregatedDataValueTotals( DataQueryParams params )
     {
-        int level = organisationUnitService.getLevelOfOrganisationUnit( params.getOrganisationUnits().iterator().next() );
-        
-        String periodType = PeriodType.getPeriodTypeFromIsoString( params.getPeriods().iterator().next() ).getName().toLowerCase();
-        
-        List<String> dimensions = params.getDimensionNames();        
+        int level = params.getOrganisationUnitLevel();
+        String periodType = params.getPeriodType();
+        List<String> dimensions = params.getDimensionNames();
         List<String> extraDimensions = params.getDynamicDimensionNames();
         
         String sql = 
-            "SELECT " + dimensions.get( 0 ) + ", " + 
+            "select " + dimensions.get( 0 ) + ", " + 
             dimensions.get( 1 ) + ", " +
             periodType + " as " + dimensions.get( 2 ) + ", " + 
             "uidlevel" + level + " as " + dimensions.get( 3 ) + ", " +
             getCommaDelimitedString( extraDimensions, false, true ) +
-            "SUM(value) as value " +
+            "sum(value) as value " +
             
-            "FROM " + params.getTableName() + " " +
-            "WHERE " + dimensions.get( 0 ) + " IN ( " + getQuotedCommaDelimitedString( params.getDataElements() ) + " ) " +
-            "AND " + periodType + " IN ( " + getQuotedCommaDelimitedString( params.getPeriods() ) + " ) " +
-            "AND uidlevel" + level + " IN ( " + getQuotedCommaDelimitedString( params.getOrganisationUnits() ) + " ) " +
+            "from " + params.getTableName() + " " +
+            "where " + dimensions.get( 0 ) + " in ( " + getQuotedCommaDelimitedString( params.getDataElements() ) + " ) " +
+            "and " + periodType + " in ( " + getQuotedCommaDelimitedString( params.getPeriods() ) + " ) " +
+            "and uidlevel" + level + " in ( " + getQuotedCommaDelimitedString( params.getOrganisationUnits() ) + " ) " +
             getExtraDimensionQuery( params ) +
         
-            "GROUP BY " + dimensions.get( 0 ) + ", " + 
+            "group by " + dimensions.get( 0 ) + ", " + 
             dimensions.get( 1 ) + ", " +
             periodType + ", " + 
             "uidlevel" + level +
@@ -111,7 +96,7 @@
 
         log.info( sql );
         
-        SqlRowSet rowSet = jdbcTemplate.queryForRowSet( sql );
+        SqlRowSet rowSet = jdbcTemplate.queryForRowSet( sql.toLowerCase() );
         
         Map<String, Double> map = new HashMap<String, Double>();
         
@@ -126,7 +111,7 @@
             
             key.deleteCharAt( key.length() - SEP.length() );
             
-            Double value = rowSet.getDouble( "value" );
+            Double value = rowSet.getDouble( DataQueryParams.VALUE_ID );
             
             map.put( key.toString(), value );
         }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java	2012-12-13 20:45:26 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java	2012-12-17 16:58:29 +0000
@@ -68,7 +68,7 @@
         final Date earliest = tableManager.getEarliestData();
         final Date latest = tableManager.getLatestData();
         final List<String> tables = PartitionUtils.getTempTableNames( earliest, latest );        
-        clock.logTime( "Checked data timespan" );
+        clock.logTime( "Checked data timespan and got tables: " + tables );
         
         //dropTables( tables );
         
@@ -107,11 +107,18 @@
     private void populateTables( List<String> tables )
     {
         int pageSize = Math.max( ( SystemUtils.getCpuCores() - 1 ), 1 );
+
+        log.info( "Page size: " + pageSize );
         
         List<List<String>> tablePages = new PaginatedList<String>( tables ).setPageSize( pageSize ).getPages();
         
+        log.info( "No of pages: " + tablePages.size() );
+        
         for ( List<String> tablePage : tablePages )
         {
+            System.out.println();
+            System.out.println(tablePage);
+            
             List<Future<?>> futures = new ArrayList<Future<?>>();
             
             for ( String table : tablePage )

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java	2012-12-14 17:27:05 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java	2012-12-17 16:58:29 +0000
@@ -190,7 +190,9 @@
             "left join period pe on dv.periodid=pe.periodid " +
             "where de.valuetype='" + valueType + "' " +
             "and pe.startdate >= '" + start + "' " +
-            "and pe.startdate <= '" + end + "'";
+            "and pe.startdate <= '" + end + "'" +
+            "and dv.value != ''" +
+            "and dv.value is not null";
 
         final String sql = insert + select;
         

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml	2012-12-14 13:46:47 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml	2012-12-17 16:58:29 +0000
@@ -6,5 +6,6 @@
   <bean id="org.hisp.dhis.analytics.AnalyticsTableService" class="org.hisp.dhis.analytics.table.DefaultAnalyticsTableService" />
   <bean id="org.hisp.dhis.analytics.AnalyticsManager" class="org.hisp.dhis.analytics.data.JdbcAnalyticsManager" />
   <bean id="org.hisp.dhis.analytics.AnalyticsService" class="org.hisp.dhis.analytics.data.DefaultAnalyticsService" />
+  <bean id="org.hisp.dhis.analytics.QueryPlanner" class="org.hisp.dhis.analytics.data.DefaultQueryPlanner" />
 
 </beans>

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java	2012-12-12 15:29:04 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java	2012-12-17 16:58:29 +0000
@@ -27,17 +27,66 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import static org.hisp.dhis.analytics.DataQueryParams.DATAELEMENT_DIM_ID;
+import static org.hisp.dhis.analytics.DataQueryParams.ORGUNIT_DIM_ID;
+import static org.hisp.dhis.analytics.DataQueryParams.PERIOD_DIM_ID;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 
+import org.hisp.dhis.DhisSpringTest;
 import org.hisp.dhis.analytics.DataQueryParams;
+import org.hisp.dhis.analytics.QueryPlanner;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.organisationunit.OrganisationUnitService;
+import org.hisp.dhis.period.Cal;
+import org.hisp.dhis.period.PeriodType;
 import org.junit.Test;
-
-import static org.hisp.dhis.analytics.DataQueryParams.*;
-import static org.junit.Assert.*;
+import org.springframework.beans.factory.annotation.Autowired;
 
 public class QueryPlannerTest
+    extends DhisSpringTest
 {
+    @Autowired
+    private QueryPlanner queryPlanner;
+    
+    @Autowired
+    private OrganisationUnitService organisationUnitService;
+
+    // -------------------------------------------------------------------------
+    // Fixture
+    // -------------------------------------------------------------------------
+
+    private OrganisationUnit ouA;
+    private OrganisationUnit ouB;
+    private OrganisationUnit ouC;
+    private OrganisationUnit ouD;
+    private OrganisationUnit ouE;
+
+    @Override
+    public void setUpTest()
+    {
+        ouA = createOrganisationUnit( 'A' );
+        ouB = createOrganisationUnit( 'B' );
+        ouC = createOrganisationUnit( 'C' );
+        ouD = createOrganisationUnit( 'D' );
+        ouE = createOrganisationUnit( 'E' );
+        
+        organisationUnitService.addOrganisationUnit( ouA );
+        organisationUnitService.addOrganisationUnit( ouB );
+        organisationUnitService.addOrganisationUnit( ouC );
+        organisationUnitService.addOrganisationUnit( ouD );
+        organisationUnitService.addOrganisationUnit( ouE );
+    }
+
+    // -------------------------------------------------------------------------
+    // Tests
+    // -------------------------------------------------------------------------
+
     @Test
     public void getPartitionDimension()
     {
@@ -46,23 +95,160 @@
         params.setOrganisationUnits( Arrays.asList( "a", "b", "c", "d", "e" ) );
         params.setPeriods( Arrays.asList( "2000Q1", "2000Q2", "2000Q3", "2000Q4", "2001Q1", "2001Q2" ) );
         
-        assertEquals( DATAELEMENT_DIM_ID, QueryPlanner.getPartitionDimension( params, 3 ) );
-        assertEquals( DATAELEMENT_DIM_ID, QueryPlanner.getPartitionDimension( params, 4 ) );
-        assertEquals( ORGUNIT_DIM_ID, QueryPlanner.getPartitionDimension( params, 5 ) );
-        assertEquals( PERIOD_DIM_ID, QueryPlanner.getPartitionDimension( params, 6 ) );
-        assertEquals( PERIOD_DIM_ID, QueryPlanner.getPartitionDimension( params, 7 ) );
+        assertEquals( DATAELEMENT_DIM_ID, queryPlanner.getPartitionDimension( params, 3 ) );
+        assertEquals( DATAELEMENT_DIM_ID, queryPlanner.getPartitionDimension( params, 4 ) );
+        assertEquals( ORGUNIT_DIM_ID, queryPlanner.getPartitionDimension( params, 5 ) );
+        assertEquals( PERIOD_DIM_ID, queryPlanner.getPartitionDimension( params, 6 ) );
+        assertEquals( PERIOD_DIM_ID, queryPlanner.getPartitionDimension( params, 7 ) );
     }
     
+    /**
+     * Query spans 2 partitions. Splits in 2 queries for each partition, then
+     * splits in 2 queries on data elements to satisfy optimal for a total  of 4 
+     * queries.
+     */
     @Test
-    public void planQuery()
+    public void planQueryA()
     {
         DataQueryParams params = new DataQueryParams();
         params.setDataElements( Arrays.asList( "a", "b", "c", "d" ) );
-        params.setOrganisationUnits( Arrays.asList( "a", "b", "c", "d", "e" ) );
+        params.setOrganisationUnits( Arrays.asList( ouA.getUid(), ouB.getUid(), ouC.getUid(), ouD.getUid(), ouE.getUid() ) );
         params.setPeriods( Arrays.asList( "2000Q1", "2000Q2", "2000Q3", "2000Q4", "2001Q1", "2001Q2" ) );
         
-        List<DataQueryParams> queries = QueryPlanner.planQuery( params, 4 );
+        List<DataQueryParams> queries = queryPlanner.planQuery( params, 4 );
         
         assertEquals( 4, queries.size() );
+        
+        for ( DataQueryParams query : queries )
+        {
+            assertTrue( samePeriodType( query.getPeriods() ) );
+            assertTrue( samePartition( query.getPeriods() ) );
+        }
+    }
+    
+    /**
+     * Query spans 3 period types. Splits in 3 queries for each period type, then
+     * splits in 2 queries on data elements to satisfy optimal for a total of 6
+     * queries.
+     */
+    @Test
+    public void planQueryB()
+    {
+        DataQueryParams params = new DataQueryParams();
+        params.setDataElements( Arrays.asList( "a", "b", "c", "d" ) );
+        params.setOrganisationUnits( Arrays.asList( ouA.getUid(), ouB.getUid(), ouC.getUid(), ouD.getUid(), ouE.getUid() ) );
+        params.setPeriods( Arrays.asList( "2000Q1", "2000Q2", "2000", "200002", "200003", "200004" ) );
+        
+        List<DataQueryParams> queries = queryPlanner.planQuery( params, 4 );
+        
+        assertEquals( 6, queries.size() );
+        
+        for ( DataQueryParams query : queries )
+        {
+            assertTrue( samePeriodType( query.getPeriods() ) );
+            assertTrue( samePartition( query.getPeriods() ) );
+        }
+    }
+    
+    /**
+     * Query spans 3 organisation unit levels. Splits in 3 queries for each level,
+     * then splits in 2 queries on data elements to satisfy optimal for a total 
+     * of 6 queries.
+     */
+    @Test
+    public void planQueryC()
+    {
+        ouB.setParent( ouA );
+        ouC.setParent( ouA );
+        ouD.setParent( ouB );
+        ouE.setParent( ouC );
+        ouA.getChildren().add( ouB );
+        ouA.getChildren().add( ouC );
+        ouD.getChildren().add( ouB );
+        ouC.getChildren().add( ouE );
+        organisationUnitService.updateOrganisationUnit( ouA );
+        organisationUnitService.updateOrganisationUnit( ouB );
+        organisationUnitService.updateOrganisationUnit( ouC );
+        organisationUnitService.updateOrganisationUnit( ouD );
+        organisationUnitService.updateOrganisationUnit( ouE );
+        
+        DataQueryParams params = new DataQueryParams();
+        params.setDataElements( Arrays.asList( "a", "b", "c", "d" ) );
+        params.setOrganisationUnits( Arrays.asList( ouA.getUid(), ouB.getUid(), ouC.getUid(), ouD.getUid(), ouE.getUid() ) );
+        params.setPeriods( Arrays.asList( "2000Q1", "2000Q2", "2000Q3" ) );
+        
+        List<DataQueryParams> queries = queryPlanner.planQuery( params, 4 );
+        
+        assertEquals( 6, queries.size() );
+        
+        for ( DataQueryParams query : queries )
+        {
+            assertTrue( samePeriodType( query.getPeriods() ) );
+            assertTrue( samePartition( query.getPeriods() ) );
+        }
+    }
+    
+    /**
+     * Splits on largest dimension which is period.
+     */
+    @Test
+    public void planQueryD()
+    {
+        DataQueryParams params = new DataQueryParams();
+        params.setDataElements( Arrays.asList( "a", "b", "c" ) );
+        params.setOrganisationUnits( Arrays.asList( ouA.getUid() ) );
+        params.setPeriods( Arrays.asList( "200001", "200002", "200003", "200004", "200005", "200006", "200007", "200008", "200009" ) );
+        
+        List<DataQueryParams> queries = queryPlanner.planQuery( params, 6 );
+        
+        assertEquals( 5, queries.size() );
+        
+        for ( DataQueryParams query : queries )
+        {
+            assertTrue( samePeriodType( query.getPeriods() ) );
+            assertTrue( samePartition( query.getPeriods() ) );
+        }
+    }
+    
+    // -------------------------------------------------------------------------
+    // Supportive methods
+    // -------------------------------------------------------------------------
+
+    private static boolean samePeriodType( List<String> isoPeriods )
+    {
+        Iterator<String> periods = new ArrayList<String>( isoPeriods ).iterator();
+        
+        PeriodType first = PeriodType.getPeriodTypeFromIsoString( periods.next() );
+        
+        while ( periods.hasNext() )
+        {
+            PeriodType next = PeriodType.getPeriodTypeFromIsoString( periods.next() );
+            
+            if ( !first.equals( next ) )
+            {   
+                return false;
+            }
+        }
+        
+        return true;
+    }
+    
+    private static boolean samePartition( List<String> isoPeriods )
+    {
+        Iterator<String> periods = new ArrayList<String>( isoPeriods ).iterator();
+        
+        int year = new Cal().set( PeriodType.getPeriodFromIsoString( periods.next() ).getStartDate() ).getYear();
+        
+        while ( periods.hasNext() )
+        {
+            int next = new Cal().set( PeriodType.getPeriodFromIsoString( periods.next() ).getStartDate() ).getYear();
+            
+            if ( year != next )
+            {   
+                return false;
+            }
+        }
+        
+        return true;
     }
 }