diff --git a/core/modules/user/src/Exception/ExistingConditionException.php b/core/modules/user/src/Exception/ExistingConditionException.php
new file mode 100644
index 0000000..f89b4fa
--- /dev/null
+++ b/core/modules/user/src/Exception/ExistingConditionException.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\user\Exception;
+
+/**
+ * Thrown by UserDataQuery when a condition has already been set.
+ *
+ * @see \Drupal\user\UserDataQuery
+ */
+class ExistingConditionException extends \Exception {
+}
diff --git a/core/modules/user/src/UserData.php b/core/modules/user/src/UserData.php
index 3418c83..c22b589 100644
--- a/core/modules/user/src/UserData.php
+++ b/core/modules/user/src/UserData.php
@@ -77,6 +77,13 @@ public function get($module, $uid = NULL, $name = NULL) {
   /**
    * {@inheritdoc}
    */
+  public function getUserDataQuery() {
+    return new UserDataQuery($this->connection->select('users_data', 'ud'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function set($module, $uid, $name, $value) {
     $serialized = 0;
     if (!is_scalar($value)) {
diff --git a/core/modules/user/src/UserDataInterface.php b/core/modules/user/src/UserDataInterface.php
index 24cd45d..453ac8f 100644
--- a/core/modules/user/src/UserDataInterface.php
+++ b/core/modules/user/src/UserDataInterface.php
@@ -32,6 +32,15 @@
   public function get($module, $uid = NULL, $name = NULL);
 
   /**
+   * Gets an object that allows the user data to be queried.
+   *
+   * @return \Drupal\user\UserDataQueryInterface
+   *   An object implementing UserDataQueryInterface that allows the user data
+   *   to be queried.
+   */
+  public function getUserDataQuery();
+
+  /**
    * Stores data for a user account.
    *
    * @param string $module
diff --git a/core/modules/user/src/UserDataQuery.php b/core/modules/user/src/UserDataQuery.php
new file mode 100644
index 0000000..276a6b0
--- /dev/null
+++ b/core/modules/user/src/UserDataQuery.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\user;
+
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\user\Exception\ExistingConditionException;
+
+/**
+ * Returns data stored for a user account.
+ */
+class UserDataQuery implements UserDataQueryInterface {
+
+  /**
+   * Stores which conditions have been set.
+   *
+   * @var array
+   */
+  protected $existingConditions;
+
+  /**
+   * The select query on the users_data table.
+   *
+   * @var \Drupal\Core\Database\Query\SelectInterface
+   */
+  protected $select;
+
+  /**
+   * Constructs a new user data service.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $select
+   *   A select query on the users_data table.
+   */
+  public function __construct(SelectInterface $select) {
+    $this->select = $select->fields('ud');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setModule($module) {
+    $this->createCondition('module', $module);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUid($uid) {
+    $this->createCondition('uid', $uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setName($name) {
+    $this->createCondition('name', $name);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($value) {
+    $is_scalar_value = is_scalar($value);
+    if (!$is_scalar_value) {
+      $value = serialize($value);
+    }
+    $this->createCondition('value', $value);
+    return $this;
+  }
+
+  /**
+   * Creates a condition on the query for the provided name using the value.
+   *
+   * @param string $name
+   *   The name of the field to check.
+   * @param string|int $value
+   *   The value to test the field against.
+   *
+   * @throws \Drupal\user\Exception\ExistingConditionException
+   *   Exception thrown when the select object already has a condition on the
+   *   field.
+   */
+  protected function createCondition($name, $value) {
+    if (isset($this->existingConditions[$name]) && $this->existingConditions[$name]) {
+      throw new ExistingConditionException("Can not add another condition on '$name'");
+    }
+    $this->select->condition($name, $value);
+    $this->existingConditions[$name] = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    foreach ($this->select->execute() as $row) {
+      if ($row->serialized) {
+        $row->value = unserialize($row->value);
+      }
+      unset($row->serialized);
+      yield $row;
+    }
+  }
+
+}
diff --git a/core/modules/user/src/UserDataQueryInterface.php b/core/modules/user/src/UserDataQueryInterface.php
new file mode 100644
index 0000000..9fa8daa
--- /dev/null
+++ b/core/modules/user/src/UserDataQueryInterface.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\user;
+
+/**
+ * Objects that implement this interface can query user data.
+ *
+ * @see \Drupal\user\UserDataInterface
+ */
+interface UserDataQueryInterface {
+
+  /**
+   * Sets the module value.
+   *
+   * @param string $module
+   *   The name of the module the data is associated with.
+   *
+   * @return $this
+   *
+   * @throws \Drupal\user\Exception\ExistingConditionException
+   *   Exception thrown when the select object already has a module condition.
+   */
+  public function setModule($module);
+
+  /**
+   * Sets the user account ID the data is associated with.
+   *
+   * @param int $uid
+   *   The user account ID the data is associated with.
+   *
+   * @return $this
+   *
+   * @throws \Drupal\user\Exception\ExistingConditionException
+   *   Exception thrown when the select object already has a user account ID
+   *   condition.
+   */
+  public function setUid($uid);
+
+  /**
+   * Sets the name of the data key.
+   *
+   * @param string $name
+   *   The name of the data key.
+   *
+   * @return $this
+   *
+   * @throws \Drupal\user\Exception\ExistingConditionException
+   *   Exception thrown when the select object already has a name condition.
+   */
+  public function setName($name);
+
+  /**
+   * Sets the value stored.
+   *
+   * @param mixed $value
+   *   The value stored.
+   *
+   * @return $this
+   *
+   * @throws \Drupal\user\Exception\ExistingConditionException
+   *   Exception thrown when the select object already has a value condition.
+   */
+  public function setValue($value);
+
+  /**
+   * Runs the query.
+   *
+   * @return \Traversable
+   *   A list of result objects. Each object has module, uid, name and value
+   *   properties.
+   */
+  public function execute();
+
+}
diff --git a/core/modules/user/tests/src/Kernel/UserDataQueryTest.php b/core/modules/user/tests/src/Kernel/UserDataQueryTest.php
new file mode 100644
index 0000000..810d9a4
--- /dev/null
+++ b/core/modules/user/tests/src/Kernel/UserDataQueryTest.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace Drupal\Tests\user\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\user\Exception\ExistingConditionException;
+
+/**
+ * @coversDefaultClass \Drupal\user\UserDataQuery
+ *
+ * @group user
+ */
+class UserDataQueryTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installSchema('user', ['users_data']);
+  }
+
+  /**
+   * @covers ::setModule
+   * @covers ::setUid
+   * @covers ::setName
+   * @covers ::setValue
+   * @covers ::execute
+   *
+   * @dataProvider userDataQueryProvider
+   */
+  public function testUserDataQuery($contents, $module, $uid, $name, $value, $expected) {
+    /** @var \Drupal\user\UserData $user_data */
+    $user_data = \Drupal::service('user.data');
+    foreach ($contents as $row) {
+      $user_data->set($row['module'], $row['uid'], $row['name'], $row['value']);
+    }
+    $user_data_query = $user_data->getUserDataQuery();
+    if (isset($module)) {
+      $user_data_query->setModule($module);
+    }
+    if (isset($uid)) {
+      $user_data_query->setUid($uid);
+    }
+    if (isset($name)) {
+      $user_data_query->setName($name);
+    }
+    if (isset($value)) {
+      $user_data_query->setValue($value);
+    }
+    $this->assertEquals([(object) $expected], iterator_to_array($user_data_query->execute()));
+  }
+
+  /**
+   * Data provider for testUserDataQuery.
+   *
+   * @return array
+   */
+  public function userDataQueryProvider() {
+    $uid = mt_rand(1, 10);
+    $name = $this->randomMachineName();
+    $module = $this->randomMachineName();
+    $value = mt_rand(20, 30);
+    $scalar_result = [
+      'module' => $module,
+      'uid' => $uid,
+      'name' => $name,
+      'value' => $value,
+    ];
+    $array_result = [
+      'module' => $module . '_',
+      'uid' => $uid + 1,
+      'name' => $name . '_',
+      'value' => [$value],
+    ];
+    $contents = [
+      $scalar_result,
+      $array_result,
+    ];
+    return [
+      [$contents, $module, $uid, $name, NULL, $scalar_result],
+      [$contents, $module, $uid, NULL, $value, $scalar_result],
+      [$contents, $module, NULL, $name, $value, $scalar_result],
+      [$contents, $module, $uid, NULL, NULL, $scalar_result],
+      [$contents, $module, NULL, $name, NULL, $scalar_result],
+      [$contents, $module, NULL, NULL, $value, $scalar_result],
+      [$contents, $module, NULL, NULL, NULL, $scalar_result],
+      [$contents, $module . '_', $uid + 1, $name . '_', NULL, $array_result],
+      [$contents, $module . '_', $uid + 1, NULL, [$value], $array_result],
+      [$contents, $module . '_', NULL, $name . '_', [$value], $array_result],
+      [$contents, $module . '_', $uid + 1, NULL, NULL, $array_result],
+      [$contents, $module . '_', NULL, $name . '_', NULL, $array_result],
+      [$contents, $module . '_', NULL, NULL, [$value], $array_result],
+      [$contents, $module . '_', NULL, NULL, NULL, $array_result],
+    ];
+  }
+
+  /**
+   * @covers ::setModule
+   * @covers ::setUid
+   * @covers ::setName
+   * @covers ::setValue
+   *
+   * @dataProvider userDataQueryMultipleMethodCallsProvider
+   */
+  public function testUserDataQueryMultipleMethodCalls($method, $name, $value) {
+    $this->setExpectedException(ExistingConditionException::class, "Can not add another condition on '$name'");
+    /** @var \Drupal\user\UserData $user_data */
+    $user_data = \Drupal::service('user.data');
+    $user_data_query = $user_data->getUserDataQuery();
+    $user_data_query
+      ->$method($value)
+      ->$method($value);
+  }
+
+  /**
+   * Data provider for testUserDataQueryMultipleMethodCalls.
+   *
+   * @return array
+   */
+  public function userDataQueryMultipleMethodCallsProvider() {
+    return [
+      ['setUid', 'uid', 1],
+      ['setModule', 'module', 'test'],
+      ['setName', 'name', 'test'],
+      ['setValue', 'value', 'test'],
+    ];
+  }
+
+  /**
+   * @covers ::execute
+   */
+  public function testEmpty() {
+    /** @var \Drupal\user\UserData $user_data */
+    $user_data = \Drupal::service('user.data');
+    $user_data_query = $user_data->getUserDataQuery();
+    $user_data_query->setUid(101);
+    $this->assertSame([], iterator_to_array($user_data_query->execute()));
+  }
+
+  /**
+   * @covers ::setValue
+   * @covers ::execute
+   */
+  public function testSetValue() {
+    $contents = [
+      // Create a NULL value.
+      ['module' => 'test', 'uid' => '100', 'name' => 'nullable', 'value' => NULL],
+      // Create a non NULL value.
+      ['module' => 'test', 'uid' => '100', 'name' => 'foo', 'value' => 'bar'],
+      // A non-scalar value.
+      ['module' => 'test', 'uid' => '100', 'name' => 'foo_array', 'value' => ['bar']],
+    ];
+    /** @var \Drupal\user\UserData $user_data */
+    $user_data = \Drupal::service('user.data');
+    foreach ($contents as $row) {
+      $user_data->set($row['module'], $row['uid'], $row['name'], $row['value']);
+    }
+    $user_data_query = $user_data->getUserDataQuery();
+
+    // Test getting NULL values.
+    $user_data_query->setValue(NULL);
+    $this->assertEquals([(object) $contents[0]], iterator_to_array($user_data_query->execute()));
+
+    $user_data_query = $user_data->getUserDataQuery();
+    $user_data_query->setValue('bar');
+    $this->assertEquals([(object) $contents[1]], iterator_to_array($user_data_query->execute()));
+
+    // Test that catching \Drupal\user\Exception\ExistingConditionException
+    // and re-running the query does not break is the value is not scalar.
+    try {
+      $user_data_query->setValue(['value']);
+    }
+    catch (ExistingConditionException $e) {
+    }
+    $this->assertEquals([(object) $contents[1]], iterator_to_array($user_data_query->execute()));
+
+    // Test getting a pre-serialized value.
+    $user_data_query = $user_data->getUserDataQuery();
+    $user_data_query->setValue(serialize(['bar']));
+    $this->assertEquals([(object) $contents[2]], iterator_to_array($user_data_query->execute()));
+  }
+
+}
