diff --git a/core/lib/Drupal/Core/Entity/Query/QueryBase.php b/core/lib/Drupal/Core/Entity/Query/QueryBase.php
index d22dbac..1e7cbc3 100644
--- a/core/lib/Drupal/Core/Entity/Query/QueryBase.php
+++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.php
@@ -75,6 +75,23 @@
   protected $pager = array();
 
   /**
+   * The aggregation data.
+   *
+   * @var array
+   */
+  protected $aggregation = array();
+
+  /**
+   * @var array
+   */
+  protected $alterTags;
+
+  /**
+   * @var array
+   */
+  protected $alterMetaData;
+
+  /**
    * Constructs this object.
    */
   public function __construct($entity_type, $conjunction) {
@@ -195,6 +212,18 @@ public function pager($limit = 10, $element = NULL) {
   }
 
   /**
+   * Implements Drupal\Core\Entity\Query\QueryInterface::aggregation().
+   */
+  function aggregation($aggregation_function, $aggregation_property, $group_by_property, $langcode = NULL) {
+    $this->aggregation[] = array(
+      'function' => $aggregation_function,
+      'aggregation_property' => $aggregation_property,
+      'group_by_property' => $group_by_property,
+      'langcode' => NULL,
+    );
+  }
+
+  /**
    * Gets the total number of results and initialize a pager for the query.
    *
    * The pager can be disabled by either setting the pager limit to 0, or by
diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
index ec9627e..0d6c117 100644
--- a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
+++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
@@ -242,4 +242,10 @@ public function andConditionGroup();
    */
   public function orConditionGroup();
 
+  /**
+   * @param $aggregation_function
+   * @param $aggregation_property
+   * @param $group_by_property
+   */
+  public function aggregation($aggregation_function, $aggregation_property, $group_by_property, $langcode = NULL);
 }
diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php
index 4a0b440..821815d 100644
--- a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php
+++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php
@@ -48,53 +48,13 @@ public function execute() {
     $entity_type = $this->entityType;
     $entity_info = entity_get_info($entity_type);
     if (!isset($entity_info['base_table'])) {
-      throw new QueryException("No base table, nothing to query.");
+      throw new QueryException("No base table, invalid query.");
     }
-    $configurable_fields = array_map(function ($data) use ($entity_type) {
-      return isset($data['bundles'][$entity_type]);
-    }, field_info_field_map());
+    $simple_query = !isset($entity_info['data_table']);
     $base_table = $entity_info['base_table'];
-    // Assemble a list of entity tables, primarily for use in
-    // \Drupal\field_sql_storage\Entity\Tables::ensureEntityTable().
-    $entity_tables = array();
-    $simple_query = TRUE;
-    // ensureEntityTable() decides whether an entity property will be queried
-    // from the data table or the base table based on where it finds the
-    // property first. The data table is prefered, which is why it gets added
-    // before the base table.
-    if (isset($entity_info['data_table'])) {
-      $entity_tables[$entity_info['data_table']] = drupal_get_schema($entity_info['data_table']);
-      $simple_query = FALSE;
-    }
-    $entity_tables[$base_table] = drupal_get_schema($base_table);
     $sqlQuery = $this->connection->select($base_table, 'base_table', array('conjunction' => $this->conjunction));
-    $sqlQuery->addMetaData('configurable_fields', $configurable_fields);
     $sqlQuery->addMetaData('entity_type', $entity_type);
-    // Determines the key of the column to join on. This is either the entity
-    // id key or the revision id key, depending on whether the entity type
-    // supports revisions.
-    $id_key = 'id';
     $id_field = $entity_info['entity_keys']['id'];
-    $fields[$id_field] = TRUE;
-    if (empty($entity_info['entity_keys']['revision'])) {
-      // Add the key field for fetchAllKeyed(). When there is no revision
-      // support, this is the entity key.
-      $sqlQuery->addField('base_table', $entity_info['entity_keys']['id']);
-    }
-    else {
-      // Add the key field for fetchAllKeyed(). When there is revision
-      // support, this is the revision key.
-      $revision_field = $entity_info['entity_keys']['revision'];
-      $fields[$revision_field] = TRUE;
-      $sqlQuery->addField('base_table', $revision_field);
-      // Now revision id is column 0 and the value column is 1.
-      if ($this->age == FIELD_LOAD_CURRENT) {
-        $id_key = 'revision';
-      }
-    }
-    // Now add the value column for fetchAllKeyed(). This is always the
-    // entity id.
-    $sqlQuery->addField('base_table', $id_field);
     if ($this->accessCheck) {
       $sqlQuery->addTag($entity_type . '_access');
     }
@@ -116,15 +76,35 @@ public function execute() {
     }
     // This now contains first the table containing entity properties and
     // last the entity base table. They might be the same.
-    $sqlQuery->addMetaData('entity_tables', $entity_tables);
     $sqlQuery->addMetaData('age', $this->age);
-    // This contains the relevant SQL field to be used when joining entity
-    // tables.
-    $sqlQuery->addMetaData('entity_id_field', $entity_info['entity_keys'][$id_key]);
-    // This contains the relevant SQL field to be used when joining field
-    // tables.
-    $sqlQuery->addMetaData('field_id_field', $id_key == 'id' ? 'entity_id' : 'revision_id');
     $sqlQuery->addMetaData('simple_query', $simple_query);
+    if (empty($this->aggregation)) {
+      $fields[$id_field] = TRUE;
+      if (empty($entity_info['entity_keys']['revision'])) {
+        // Add the key field for fetchAllKeyed(). When there is no revision
+        // support, this is the entity key.
+        $sqlQuery->addField('base_table', $entity_info['entity_keys']['id']);
+      }
+      else {
+        // Add the key field for fetchAllKeyed(). When there is revision
+        // support, this is the revision key.
+        $revision_field = $entity_info['entity_keys']['revision'];
+        $fields[$revision_field] = TRUE;
+        $sqlQuery->addField('base_table', $revision_field);
+      }
+      // Now add the value column for fetchAllKeyed(). This is always the
+      // entity id.
+      $sqlQuery->addField('base_table', $id_field);
+    }
+    else {
+      $tables = new Tables($sqlQuery);
+      foreach ($this->aggregation as $data) {
+        $alias = $tables->addField($data['group_by_property'], 'LEFT', $data['langcode']);
+        $sqlQuery->groupBy($alias);
+        $alias = $tables->addField($data['aggregation_property'], 'LEFT', $data['langcode']);
+        $sqlQuery->addExpression($data['aggregation_function'] . "($alias)");
+      }
+    }
     $this->condition->compile($sqlQuery);
     if ($this->count) {
       $this->sort = FALSE;
@@ -145,7 +125,7 @@ public function execute() {
     // involved then each revision might appear in several rows and this needs
     // a significantly more complex query.
     $simple_query = (!$this->pager && !$this->range && !$this->count) || $sqlQuery->getMetaData('simple_query');
-    if (!$simple_query) {
+    if (!$simple_query && !$this->aggregation) {
       // First, GROUP BY revision id (if it has been added) and entity id.
       // Now each group contains a single revision of an entity.
       foreach (array_keys($fields) as $field) {
diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php
index 22739df..d8b36ba 100644
--- a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php
+++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php
@@ -18,7 +18,7 @@ function __construct(Connection $connection) {
     $this->connection = $connection;
   }
 
-  function get($entity_type, $conjunction) {
+  function get($entity_type, $conjunction = 'AND') {
     return new Query($entity_type, $conjunction, $this->connection);
   }
 }
diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php
index 7ba89b3..8a90a8f 100644
--- a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php
+++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php
@@ -59,19 +59,140 @@ function __construct(SelectInterface $sql_query) {
    *   of this in a query for a condition or sort.
    */
   function addField($field, $type, $langcode) {
-    $parts = explode('.', $field);
-    $property = $parts[0];
-    $configurable_fields = $this->sqlQuery->getMetaData('configurable_fields');
-    if (!empty($configurable_fields[$property]) || substr($property, 0, 3) == 'id:') {
-      $field_name = $property;
-      $table = $this->ensureFieldTable($field_name, $type, $langcode);
-      // Default to .value.
-      $column = isset($parts[1]) ? $parts[1] : 'value';
-      $sql_column = _field_sql_storage_columnname($field_name, $column);
-    }
-    else {
-      $sql_column = $property;
-      $table = $this->ensureEntityTable($property, $type, $langcode);
+    $entity_type = $this->sqlQuery->getMetaData('entity_type');
+    $age = $this->sqlQuery->getMetaData('age');
+    // This variables ensures grouping works correctly. For example:
+    // ->condition('tags', 2, '>')
+    // ->condition('tags', 20, '<')
+    // ->condition('node_reference.nid.entity.tags', 2)
+    // The first two should use the same table but the last one needs to be a
+    // new table. So for the first two, the table array index will be '0.tags'
+    // while the third will be '1.tags'.
+    $index_prefix = 0;
+    $specifiers = explode('.', $field);
+    $base_table = 'base_table';
+    $count = count($specifiers) - 1;
+    // This will contain the definitions of the last specifier seen by the
+    // system.
+    $propertyDefinitions = array();
+    $entity_info = entity_get_info($entity_type);
+    for ($key = 0; $key <= $count; $key ++) {
+      // This can either be the name of an entity property (non-configurable
+      // field), a field API field (a configurable field) or an indication
+      // that the next specifier will be on another entity (here it'll be
+      // called relationship field).
+      $specifier = $specifiers[$key];
+      // If there is revision support and only the current revision is being
+      // queried then use the revision id. Otherwise, the entity id will do.
+      if (!empty($entity_info['entity_keys']['revision']) && $age == FIELD_LOAD_CURRENT) {
+        // This contains the relevant SQL field to be used when joining entity
+        // tables.
+        $entity_id_field = $entity_info['entity_keys']['revision'];
+        // This contains the relevant SQL field to be used when joining field
+        // tables.
+        $field_id_field = 'revision_id';
+      }
+      else {
+        $entity_id_field = $entity_info['entity_keys']['id'];
+        $field_id_field = 'entity_id';
+      }
+      // field_purge_batch() is passing in id:$field_id.
+      if (substr($specifier, 0, 3) == 'id:') {
+        $field = field_info_field_by_id(substr($specifier, 3));
+      }
+      else {
+        $field = field_info_field($specifier);
+      }
+      if ($field) {
+        // Find the field column.
+        $column = FALSE;
+        if ($key < $count) {
+          $next = $specifiers[$key + 1];
+          // Is this a field column?
+          if (isset($field['columns'][$next]) || in_array($next, field_reserved_columns())) {
+            // Use it.
+            $column = $next;
+            // Do not process it again.
+            $key++;
+          }
+          // If there are more specifiers, the next one must be a
+          // relationship. Either the field name followed by a relationship
+          // specifier, for example $node->field_image->entity. Or a field
+          // column followed by a relationship specifier, for example
+          // $node->field_image->fid->entity. In both cases, prepare the
+          // property definitions for the relationship. In the first case,
+          // also use the property definitions for column.
+          if ($key < $count) {
+            $relationship_specifier = $specifiers[$key + 1];
+            $propertyDefinitions = typed_data()
+              ->create(array('type' => $field['type'] . '_field'))
+              ->getPropertyDefinitions();
+            // If the column is not yet known, ie. the
+            // $node->field_image->entity case then use the id source as the
+            // column.
+            if (!$column && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) {
+              // If this is a valid relationship, use the id source.
+              // Otherwise, the code executing the relationship will throw an
+              // exception anyways so no need to do it here.
+              $column = $propertyDefinitions[$relationship_specifier]['settings']['id source'];
+            }
+          }
+        }
+        else {
+          // If this is the last specifier, default to value.
+          $column = 'value';
+        }
+        $table = $this->ensureFieldTable("$index_prefix.", $field, $type, $langcode, $base_table, $entity_id_field, $field_id_field);
+        $sql_column = _field_sql_storage_columnname($field['field_name'], $column);
+      }
+      else {
+        // ensureEntityTable() decides whether an entity property will be
+        // queried from the data table or the base table based on where it
+        // finds the property first. The data table is prefered, which is why
+        // it gets added before the base table.
+        $entity_tables = array();
+        if (isset($entity_info['data_table'])) {
+          $this->sqlQuery->addMetaData('simple_query', FALSE);
+          $entity_tables[$entity_info['data_table']] = drupal_get_schema($entity_info['data_table']);
+        }
+        $entity_tables[$entity_info['base_table']] = drupal_get_schema($entity_info['base_table']);
+        $sql_column = $specifier;
+        $table = $this->ensureEntityTable("$index_prefix.", $specifier, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
+      }
+      // If there are more specifiers to come, it's a relationship.
+      if ($key < $count) {
+        // Computed fields have prepared their property definition already, do
+        // it for properties as well.
+        if (!$propertyDefinitions) {
+          // Create a relevant entity to find the definition for this
+          // property.
+          $values = array();
+          // If there are bundles, pick one. It does not matter which,
+          // properties exist on all bundles.
+          if (!empty($entity_info['entity keys']['bundle'])) {
+            $bundles = array_keys($entity_info['bundles']);
+            $values[$entity_info['entity keys']['bundle']] = reset($bundles);
+          }
+          $entity = entity_create($entity_type, $values);
+          $propertyDefinitions = $entity->$specifier->getPropertyDefinitions();
+          $relationship_specifier = $specifiers[$key + 1];
+        }
+        // Check for a valid relationship.
+        if (isset($propertyDefinitions[$relationship_specifier]['constraints']['entity type']) && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) {
+          // If it is, use the entity type.
+          $entity_type = $propertyDefinitions[$relationship_specifier]['constraints']['entity type'];
+          $entity_info = entity_get_info($entity_type);
+          // Add the new entity base table using the table and sql column.
+          $join_condition= '%alias.' . $entity_info['entity_keys']['id'] . " = $table.$sql_column";
+          $base_table = $this->sqlQuery->leftJoin($entity_info['base_table'], NULL, $join_condition);
+          $index_prefix++;
+          $propertyDefinitions = array();
+          $key++;
+        }
+        else {
+          throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $next)));
+        }
+      }
     }
     return "$table.$sql_column";
   }
@@ -83,18 +204,13 @@ function addField($field, $type, $langcode) {
    * @return string
    * @throws \Drupal\Core\Entity\Query\QueryException
    */
-  protected function ensureEntityTable($property, $type, $langcode) {
-    $entity_tables = $this->sqlQuery->getMetaData('entity_tables');
-    if (!$entity_tables) {
-      throw new QueryException('Can not query entity properties without entity tables.');
-    }
+  protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
     foreach ($entity_tables as $table => $schema) {
       if (isset($schema['fields'][$property])) {
-        if (!isset($this->entityTables[$table])) {
-          $id_field = $this->sqlQuery->getMetaData('entity_id_field');
-          $this->entityTables[$table] = $this->addJoin($type, $table, "%alias.$id_field = base_table.$id_field", $langcode);
+        if (!isset($this->entityTables[$index_prefix . $table])) {
+          $this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
         }
-        return $this->entityTables[$table];
+        return $this->entityTables[$index_prefix . $table];
       }
     }
     throw new QueryException(format_string('@property not found', array('@property' => $property)));
@@ -108,31 +224,17 @@ protected function ensureEntityTable($property, $type, $langcode) {
    * @return string
    * @throws \Drupal\Core\Entity\Query\QueryException
    */
-  protected function ensureFieldTable(&$field_name, $type, $langcode) {
-    if (!isset($this->fieldTables[$field_name])) {
-      // This is field_purge_batch() passing in a field id.
-      if (substr($field_name, 0, 3) == 'id:') {
-        $field = field_info_field_by_id(substr($field_name, 3));
-      }
-      else {
-        $field = field_info_field($field_name);
-      }
-      if (!$field) {
-        throw new QueryException(format_string('field @field_name not found', array('@field_name' => $field_name)));
-      }
-      // This is really necessary only for the id: case but it can't be run
-      // before throwing the exception.
-      $field_name = $field['field_name'];
+  protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) {
+    $field_name = $field['field_name'];
+    if (!isset($this->fieldTables[$index_prefix . $field_name])) {
       $table = $this->sqlQuery->getMetaData('age') == FIELD_LOAD_CURRENT ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field);
-      $field_id_field = $this->sqlQuery->getMetaData('field_id_field');
-      $entity_id_field = $this->sqlQuery->getMetaData('entity_id_field');
       if ($field['cardinality'] != 1) {
         $this->sqlQuery->addMetaData('simple_query', FALSE);
       }
       $entity_type = $this->sqlQuery->getMetaData('entity_type');
-      $this->fieldTables[$field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = base_table.$entity_id_field AND %alias.entity_type = '$entity_type'", $langcode);
+      $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field AND %alias.entity_type = '$entity_type'", $langcode);
     }
-    return $this->fieldTables[$field_name];
+    return $this->fieldTables[$index_prefix . $field_name];
   }
 
   protected function addJoin($type, $table, $join_condition, $langcode) {
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php
new file mode 100644
index 0000000..231692f
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Entity\Tests\EntityQueryRelationshipTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests Entity Query API relationship functionality.
+ */
+class EntityQueryRelationshipTest extends WebTestBase  {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('entity_test', 'taxonomy');
+
+  /**
+   * @var \Drupal\field_sql_storage\Entity\QueryFactory
+   */
+  protected $factory;
+
+  /**
+   * Term entities.
+   *
+   * @var array
+   */
+  protected $terms;
+
+  /**
+   * User entities.
+   *
+   * @var array
+   */
+  public $accounts;
+
+  /**
+   * entity_test entities.
+   *
+   * @var array
+   */
+  protected $entities;
+
+  /**
+   * The name of the taxonomy field used for test.
+   *
+   * @var string
+   */
+  protected $fieldName;
+
+  /**
+   * The results returned by EntityQuery.
+   *
+   * @var array
+   */
+  protected $queryResults;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity Query relationship',
+      'description' => 'Tests the Entity Query relationship API',
+      'group' => 'Entity API',
+    );
+  }
+
+  /**
+   * Overrides \Drupal\simpletest\WebTestBase::setUp().
+   */
+  protected function setUp() {
+    parent::setUp();
+    $vocabulary = entity_create('taxonomy_vocabulary', array(
+      'machine_name' => strtolower($this->randomName()),
+    ));
+    $vocabulary->save();
+    $this->fieldName = strtolower($this->randomName());
+    $field = array(
+      'field_name' => $this->fieldName,
+      'type' => 'taxonomy_term_reference',
+    );
+    $field['settings']['allowed_values']['vocabulary'] = $vocabulary->machine_name;
+    field_create_field($field);
+    $instance = array(
+      'entity_type' => 'entity_test',
+      'field_name' => $this->fieldName,
+      'bundle' => 'entity_test',
+    );
+    field_create_instance($instance);
+    for ($i = 0; $i <= 1; $i++) {
+      $term = entity_create('taxonomy_term', array(
+        'name' => $this->randomName(),
+        'vid' => $vocabulary->vid,
+      ));
+      $term->save();
+      $this->terms[] = $term;
+      $this->accounts[] = $this->drupalCreateUser();
+    }
+    for ($i = 0; $i <= 2; $i++) {
+      $entity = entity_create('entity_test', array());
+      $entity->name->value = $this->randomName();
+      $index = $i ? 1 : 0;
+      $entity->user_id->value = $this->accounts[$index]->uid;
+      $entity->{$this->fieldName}->tid = $this->terms[$index]->tid;
+      $entity->save();
+      $this->entities[] = $entity;
+    }
+    $this->factory = drupal_container()->get('entity.query');
+  }
+
+  /**
+   * Tests querying.
+   */
+  public function testQuery() {
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("user_id.entity.name", $this->accounts[0]->name)
+      ->execute();
+    $this->assertResults(array(0));
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("user_id.entity.name", $this->accounts[0]->name, '<>')
+      ->execute();
+    $this->assertResults(array(1, 2));
+    $this->queryResults = $this->factory->get('entity_test')
+      ->exists("user_id.entity.name")
+      ->execute();
+    $this->assertResults(array(0, 1, 2));
+    $this->queryResults = $this->factory->get('entity_test')
+      ->notExists("user_id.entity.name")
+      ->execute();
+    $this->assertEqual(count($this->queryResults), 0);
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("$this->fieldName.entity.name", $this->terms[0]->name)
+      ->execute();
+    $this->assertResults(array(0));
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("$this->fieldName.tid.entity.name", $this->terms[0]->name)
+      ->execute();
+    $this->assertResults(array(0));
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("$this->fieldName.entity.name", $this->terms[0]->name, '<>')
+      ->execute();
+    $this->assertResults(array(1, 2));
+  }
+
+  protected function assertResults($expected) {
+    $this->assertEqual(count($this->queryResults), count($expected));
+    foreach ($expected as $key) {
+      $id = $this->entities[$key]->id();
+      $this->assertEqual($this->queryResults[$id], $id);
+    }
+  }
+}
