diff --git a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
index 6dd6f7f..bb1dfd8 100644
--- a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -58,18 +58,20 @@ public function defaultConfiguration() {
    * {@inheritdoc}
    */
   protected function blockAccess(AccountInterface $account) {
+
+    $storage = \Drupal::service('statistics.statistics_storage');
     $access = AccessResult::allowedIfHasPermission($account, 'access content');
     if ($account->hasPermission('access content')) {
       $daytop = $this->configuration['top_day_num'];
-      if (!$daytop || !($result = statistics_title_list('daycount', $daytop)) || !($this->day_list = node_title_list($result, $this->t("Today's:")))) {
+      if (!$daytop || !($result = $storage->statisticsTitleList('daycount', $daytop)) || !($this->day_list = node_title_list($result, $this->t("Today's:")))) {
         return AccessResult::forbidden()->inheritCacheability($access);
       }
       $alltimetop = $this->configuration['top_all_num'];
-      if (!$alltimetop || !($result = statistics_title_list('totalcount', $alltimetop)) || !($this->all_time_list = node_title_list($result, $this->t('All time:')))) {
+      if (!$alltimetop || !($result = $storage->statisticsTitleList('totalcount', $alltimetop)) || !($this->all_time_list = node_title_list($result, $this->t('All time:')))) {
         return AccessResult::forbidden()->inheritCacheability($access);
       }
       $lasttop = $this->configuration['top_last_num'];
-      if (!$lasttop || !($result = statistics_title_list('timestamp', $lasttop)) || !($this->last_list = node_title_list($result, $this->t('Last viewed:')))) {
+      if (!$lasttop || !($result = $storage->statisticsTitleList('timestamp', $lasttop)) || !($this->last_list = node_title_list($result, $this->t('Last viewed:')))) {
         return AccessResult::forbidden()->inheritCacheability($access);
       }
       return $access;
diff --git a/core/modules/statistics/src/StatisticsStorage.php b/core/modules/statistics/src/StatisticsStorage.php
new file mode 100644
index 0000000..1fa9621
--- /dev/null
+++ b/core/modules/statistics/src/StatisticsStorage.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\statistics;
+
+use Drupal\Core\Database\Connection;
+
+class StatisticsStorage implements StatisticsStorageInterface {
+
+  /**
+   * The database connection used.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Construct the statistics storage.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection for the node view storage.
+   */
+  public function __construct(Connection $connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function recordHit($nid) {
+    return (bool) $this->connection->merge('node_counter')
+      ->key('nid', $nid)
+      ->fields(array(
+        'daycount' => 1,
+        'totalcount' => 1,
+        'timestamp' => REQUEST_TIME,
+      ))
+      ->expression('daycount', 'daycount + 1')
+      ->expression('totalcount', 'totalcount + 1')
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchViews($nid) {
+    if ($nid > 0) {
+      // Retrieve an array with both totalcount and daycount.
+      return $this->connection->select('node_counter', 'nc')
+        ->fields('nc', array('totalcount', 'daycount', 'timestamp'))
+        ->condition('nid', $nid, '=')->execute()->fetchAssoc();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clean($nid) {
+    return (bool) $this->connection->delete('node_counter')
+      ->condition('nid', $nid)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetDayCount() {
+    return (bool) $this->connection->update('node_counter')
+      ->fields(array('daycount' => 0))
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function maxTotalCount() {
+    $q = $this->connection->select('node_counter', 'nc');
+    $q->addExpression('MAX(totalcount)');
+    return (int) $q->execute()->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function statisticsTitleList($dbfield, $dbrows) {
+  if (in_array($dbfield, array('totalcount', 'daycount', 'timestamp'))) {
+    $query = $this->connection->select('node_field_data', 'n');
+    $query->addTag('node_access');
+    $query->join('node_counter', 's', 'n.nid = s.nid');
+    $query->join('users_field_data', 'u', 'n.uid = u.uid');
+
+    return $query
+      ->fields('n', array('nid', 'title'))
+      ->fields('u', array('uid', 'name'))
+      ->condition($dbfield, 0, '<>')
+      ->condition('n.status', 1)
+      // @todo This should be actually filtering on the desired node status
+      //   field language and just fall back to the default language.
+      ->condition('n.default_langcode', 1)
+      ->condition('u.default_langcode', 1)
+      ->orderBy($dbfield, 'DESC')
+      ->range(0, $dbrows)
+      ->execute();
+  }
+  return FALSE;
+  }
+}
diff --git a/core/modules/statistics/src/StatisticsStorageInterface.php b/core/modules/statistics/src/StatisticsStorageInterface.php
new file mode 100644
index 0000000..b54bbfc
--- /dev/null
+++ b/core/modules/statistics/src/StatisticsStorageInterface.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\statistics\StatisticsStorageInterface.
+ */
+
+namespace Drupal\statistics;
+
+/**
+ * Provides an interface defining Statistics Storage
+ */
+interface StatisticsStorageInterface {
+
+  /**
+   * Returns if node view is counted
+   *
+   * @param int $nid
+   *   The id of the node to count
+   *
+   * @return bool
+   *   TRUE if the node view has been counted
+   */
+  public function recordHit($nid);
+
+  /**
+   * Returns the number of times a node has been viewed
+   *
+   * @param int $nid
+   *   The id of the node to fetch the views for
+   *
+ * @return array
+ *   An associative array containing:
+ *   - totalcount: Integer for the total number of times the node has been
+ *     viewed.
+ *   - daycount: Integer for the total number of times the node has been viewed
+ *     "today". For the daycount to be reset, cron must be enabled.
+ *   - timestamp: Integer for the timestamp of when the node was last viewed.
+   */
+  public function fetchViews($nid);
+
+  /**
+   * Returns if node view counts are delete
+   *
+   * @param int $nid
+   *   The id of the node which views to delete
+   *
+   * @return bool
+   *   TRUE if the node views have been deleted
+   */
+  public function clean($nid);
+
+  /**
+   * Returns if day count is reset
+   *
+   * @return bool
+   *  TRUE if the day count is reset
+   */
+  public function resetDayCount();
+
+  /**
+   * Returns the highest 'totalcount' value
+   *
+   * @return int
+   *   The highest 'totalcount' value
+   */
+  public function maxTotalCount();
+
+  /**
+   * Returns the most viewed content of all time, today, or the last-viewed node.
+   *
+   * @param string $dbfield
+   *   The database field to use, one of:
+   *   - 'totalcount': Integer that shows the top viewed content of all time.
+   *   - 'daycount': Integer that shows the top viewed content for today.
+   *   - 'timestamp': Integer that shows only the last viewed node.
+   * @param int $dbrows
+   *   The number of rows to be returned.
+   *
+   * @return SelectQuery|FALSE
+   *   A query result containing the node ID, title, user ID that owns the node,
+   *   and the username for the selected node(s), or FALSE if the query could not
+   *   be executed correctly.
+   */
+  public function statisticsTitleList($dbfield, $dbrows);
+}
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index 9b3be36..675d6c8 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -68,61 +68,21 @@ function statistics_node_links_alter(array &$node_links, NodeInterface $entity,
  * Implements hook_cron().
  */
 function statistics_cron() {
+  $storage = \Drupal::service('statistics.statistics_storage');
   $statistics_timestamp = \Drupal::state()->get('statistics.day_timestamp') ?: 0;
 
   if ((REQUEST_TIME - $statistics_timestamp) >= 86400) {
-    // Reset day counts.
-    db_update('node_counter')
-      ->fields(array('daycount' => 0))
-      ->execute();
+    // Reset day counts
+    $storage->resetDayCount();
     \Drupal::state()->set('statistics.day_timestamp', REQUEST_TIME);
   }
 
   // Calculate the maximum of node views, for node search ranking.
-  \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, db_query('SELECT MAX(totalcount) FROM {node_counter}')->fetchField()));
+  $max_total_count = $storage->maxTotalCount();
+  \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, $max_total_count));
 }
 
 /**
- * Returns the most viewed content of all time, today, or the last-viewed node.
- *
- * @param string $dbfield
- *   The database field to use, one of:
- *   - 'totalcount': Integer that shows the top viewed content of all time.
- *   - 'daycount': Integer that shows the top viewed content for today.
- *   - 'timestamp': Integer that shows only the last viewed node.
- * @param int $dbrows
- *   The number of rows to be returned.
- *
- * @return SelectQuery|FALSE
- *   A query result containing the node ID, title, user ID that owns the node,
- *   and the username for the selected node(s), or FALSE if the query could not
- *   be executed correctly.
- */
-function statistics_title_list($dbfield, $dbrows) {
-  if (in_array($dbfield, array('totalcount', 'daycount', 'timestamp'))) {
-    $query = db_select('node_field_data', 'n');
-    $query->addTag('node_access');
-    $query->join('node_counter', 's', 'n.nid = s.nid');
-    $query->join('users_field_data', 'u', 'n.uid = u.uid');
-
-    return $query
-      ->fields('n', array('nid', 'title'))
-      ->fields('u', array('uid', 'name'))
-      ->condition($dbfield, 0, '<>')
-      ->condition('n.status', 1)
-      // @todo This should be actually filtering on the desired node status
-      //   field language and just fall back to the default language.
-      ->condition('n.default_langcode', 1)
-      ->condition('u.default_langcode', 1)
-      ->orderBy($dbfield, 'DESC')
-      ->range(0, $dbrows)
-      ->execute();
-  }
-  return FALSE;
-}
-
-
-/**
  * Retrieves a node's "view statistics".
  *
  * @param int $nid
@@ -137,10 +97,9 @@ function statistics_title_list($dbfield, $dbrows) {
  *   - timestamp: Integer for the timestamp of when the node was last viewed.
  */
 function statistics_get($nid) {
-
   if ($nid > 0) {
-    // Retrieve an array with both totalcount and daycount.
-    return db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = :nid', array(':nid' => $nid), array('target' => 'replica'))->fetchAssoc();
+    $storage = \Drupal::service('statistics.statistics_storage');
+    return $storage->fetchViews($nid);
   }
 }
 
@@ -149,9 +108,9 @@ function statistics_get($nid) {
  */
 function statistics_node_predelete(EntityInterface $node) {
   // Clean up statistics table when node is deleted.
-  db_delete('node_counter')
-    ->condition('nid', $node->id())
-    ->execute();
+  $nid = $node->id();
+  $storage = \Drupal::service('statistics.statistics_storage');
+  return $storage->clean($nid);
 }
 
 /**
diff --git a/core/modules/statistics/statistics.php b/core/modules/statistics/statistics.php
index fe1b9fd..b83a10f 100644
--- a/core/modules/statistics/statistics.php
+++ b/core/modules/statistics/statistics.php
@@ -23,16 +23,7 @@
 if ($views) {
   $nid = filter_input(INPUT_POST, 'nid', FILTER_VALIDATE_INT);
   if ($nid) {
-    \Drupal::database()->merge('node_counter')
-      ->key('nid', $nid)
-      ->fields(array(
-        'daycount' => 1,
-        'totalcount' => 1,
-        'timestamp' => REQUEST_TIME,
-      ))
-      ->expression('daycount', 'daycount + 1')
-      ->expression('totalcount', 'totalcount + 1')
-      ->execute();
+    $kernel->getContainer()->get('statistics.statistics_storage')->recordHit($nid);
   }
 }
 
diff --git a/core/modules/statistics/statistics.services.yml b/core/modules/statistics/statistics.services.yml
new file mode 100644
index 0000000..2e2fd1f
--- /dev/null
+++ b/core/modules/statistics/statistics.services.yml
@@ -0,0 +1,6 @@
+services:
+  statistics.statistics_storage:
+    class: Drupal\statistics\StatisticsStorage
+    arguments: ['@database']
+    tags:
+      - { name: backend_overridable }
