diff --git a/includes/database/sqlite/database.inc b/includes/database/sqlite/database.inc
index 0fc0b55..50064d8 100644
--- a/includes/database/sqlite/database.inc
+++ b/includes/database/sqlite/database.inc
@@ -103,6 +103,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
     $this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
     $this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
     $this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand'));
+    $this->sqliteCreateFunction('log', array($this, 'sqlFunctionLog'));
   }
 
   /**
@@ -204,6 +205,13 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
   }
 
   /**
+   * SQLite compatibility implementation for the LOG() SQL function.
+   */
+  public function sqlFunctionLog($base, $arg) {
+    return log($arg, $base);
+  }
+
+  /**
    * SQLite-specific implementation of DatabaseConnection::prepare().
    *
    * We don't use prepared statements at all at this stage. We just create
diff --git a/modules/search/search.module b/modules/search/search.module
index 518272a..8e96684 100644
--- a/modules/search/search.module
+++ b/modules/search/search.module
@@ -371,30 +371,37 @@ function search_cron() {
  * up to date (even if cron times out or otherwise fails).
  */
 function search_update_totals() {
-  // Update word IDF (Inverse Document Frequency) counts for new/changed words.
-  foreach (search_dirty() as $word => $dummy) {
-    // Get total count
-    $total = db_query("SELECT SUM(score) FROM {search_index} WHERE word = :word", array(':word' => $word), array('target' => 'slave'))->fetchField();
-    // Apply Zipf's law to equalize the probability distribution.
-    $total = log10(1 + 1/(max(1, $total)));
-    db_merge('search_total')
-      ->key(array('word' => $word))
-      ->fields(array('count' => $total))
+  // Update the search totals in chunks to avoid both many queries and queries
+  // that are too large. The chunk size defaults to 500 and can be configured
+  // through settings.php.
+  $dirty = array_keys(search_dirty());
+  while (count($dirty) > 0) {
+    $dirty_spliced = array_splice($dirty, 0, variable_get('search_update_totals_chunk_size', 500));
+    db_delete('search_total')
+      ->condition('word', $dirty_spliced)
+      ->execute();
+
+    // Update word IDF (Inverse Document Frequency) counts for new/changed
+    // words, Applying Zipf's law to equalize the probability distribution.
+    $select = db_select('search_index', 'si');
+    $select->addField('si', 'word');
+    $select->addExpression('LOG(10, 1+1/GREATEST(1, SUM(si.score)))', 'count');
+    $select
+      ->condition('si.word', $dirty_spliced)
+      ->groupBy('si.word');
+
+    db_insert('search_total')
+      ->from($select)
       ->execute();
   }
   // Find words that were deleted from search_index, but are still in
   // search_total. We use a LEFT JOIN between the two tables and keep only the
-  // rows which fail to join.
-  $result = db_query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL", array(), array('target' => 'slave'));
-  $or = db_or();
-  foreach ($result as $word) {
-    $or->condition('word', $word->realword);
-  }
-  if (count($or) > 0) {
-    db_delete('search_total')
-      ->condition($or)
-      ->execute();
-  }
+  // rows which fail to join. MySQL doesn’t allow referring to a table that is
+  // targeted for update/delete in a FROM clause so we use two nested subqueries
+  // to work around that limitation.
+  db_delete('search_total')
+    ->where('word IN (SELECT word FROM (SELECT t.word AS realword FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL) t1)')
+    ->execute();
 }
 
 /**
