High performance in Drupal Part 2: Lightning fast code

Mark Pavlitski's picture
Mar 1st 2013Technical Director

Welcome to the second part of our series on High Performance Drupal. Here we will cover techniques for speeding up your custom modules.

Query builders

Drupal 7 adds an easy to use database abstraction layer letting you quickly produce complex, database independent queries.

Unfortunately there can be a noticeable performance penalty when using these query builders instead of straight SQL. Let's look at a few guidelines for deciding which option is best.

You should use a query builder if:

  • You are creating a dynamic query and want to avoid lots of messy string concatenation
  • You want to make use of query extenders (link is external) such as table column sorting or a pager
  • You want to allow other modules to alter your query, such as the node access system
  • You need to support multiple databases with the same codebase
  • Code readability is more important than performance of the query
  • The query is long-running or not used often so a small amount of overhead is negligible
<?php
// Create a select query for the `node` table.
$query = db_select('node', 'n')
         // Add a pager.
         ->extend('PagerDefault')
         // Add table click sorting.
         ->extend('TableSort');

// Join our example table.
$query->join('example_table', 'e', 'n.nid = e.nid');

$query
  // Specify the fields we require.
  ->fields('e', array('nid', 'example_data'))
  // We only want to load nodes which the user has access to.
  ->addTag('node_access')
  // We only want 10 results at a time. This works alongside the pager.
  ->limit(10);

if (!empty($nids)) {
  // Add an optional condition. $nids can be an int or an array.
  $query->condition('nid', $nids);
}

// Run our query.
$result = $query->execute();
?>

You should use db_query() if:

  • You have a simple or static query
  • Your query will be run multiple times per page
  • You will never need to switch databases or support more than one database engine
  • You don't need any of the extra functionality provided by query builders
<?php
// Run our simple query.
$result = db_query("SELECT nid, example_data FROM {example_table} WHERE nid IN :nids", array(':nids' => $nids));
?>

Scaling your data

One oft-overlooked area of Drupal's cache and entity systems are their ability to optimise loading data by doing so in bulk.

When loading entities (comments, nodes, terms users, etc.) you can call entity_load() (link is external) with an array of ID's to load several at a time. This will group together database queries wherever possible to reduce the number of queries per page and reduce page load time.

The corresponding comment_load_multiple(), node_load_multiple(), taxonomy_term_load_multiple() and user_load_multiple() provide entity specific wrappers around entity_load() for ease of use.

Don't do this:

<?php
foreach ($nids as $nid) {
  // Load each node one at a time.
  $node = node_load($nid);
  // Do something with the node.
  example_function($node);
}
?> 


This will be much faster:

<?php
$nodes = node_load_multiple($nids);
foreach ($nodes as $node) {
  // Do something with the node.
  example_function($node);
}
?> 

The Drupal cache system offers similar functionality using cache_get_multiple() for loading several cache entries at once.

Most but not all cache backends support this functionality, so it's best to double check first. The Drupal database cache, memcache and APC all support this optimisation.

Static caching

Often you need to make use of the same information more than once during a page load. Best practices denote that you should package this code up into a separate function for ease of maintenance.

To avoid expensive calculations or database calls you can use a static cache to keep hold of the data once it has been loaded once as the following example shows.

<?php
function example_function() {
  // Get our static data.
  $example_data = &drupal_static(__FUNCTION__);

  if (!isset($example_data)) {
    // We haven't calculated this yet so load it now.
    $example_data = example_data_load();
  }

  return $example_data;
}
?> 

Using drupal_static() allows you to access and clear the static data elsewhere in your page load, in case you need to load a fresh copy later on.

If your function is used heavily you should use the fast Drupal static method, as drupal_static() does have a small overhead associated with it:

<?php
function example_function() {
  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast = &drupal_static(__FUNCTION__);
  }

  // Get our static data.
  $example_data = &$drupal_static_fast;

  if (!isset($example_data)) {
    // We haven't calculated this yet so load it now.
    $example_data = example_data_load();
  }

  return $example_data;
}
?>

So you've got your data until the end of the page load, but why stop there? cache_get() and cache_set() let you keep hold of that data for as long as you need it:

<?php
function example_function() {
  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast = &drupal_static(__FUNCTION__);
  }

  // Get our static data.
  $example_data = &$drupal_static_fast;

  if (!isset($example_data)) {
    if ($cache = cache_get('example_data')) {
      // Load the data straight from the cache.
      $example_data = $cache->data;
    }
    else {
      // We haven't calculated this yet so load it now.
      $example_data = example_data_load();
      // Store our calculated data in the cache.
      cache_set('example_data', $example_data);
    }
  }

  return $example_data;
}
?>

Profiling your code

XHProf is a really helpful tool for finding bottlenecks in your code.

You can see the amount of time and memory used by each function on a page-by-page basis, so you can work out where there's room for improvement.

It's always worth looking through the most common and most time consuming functions to make sure they really should be running that frequently and for that long. Often seemingly simple, un-optimised functions end up being used much more often than you thought.

There are a couple of ways to use XHProf in your Drupal site:

The XHProf module is the quickest to set up as it all comes bundled together
If you're already using Devel on your site then you might find it easier to configure the built-in XHProf support

High speed hosting

Tuning your hosting can make or break your site when it comes to performance.

I will be writing a follow up post High speed Drupal hosting with some handy hints and tips.

In the mean time get in touch if you would like a hand tuning your Drupal hosting environment.

Update 1st June 2016

Things have come on a long way in the hosting world since I first wrote this article. With highly optimised Drupal-specific hosting now available from PAAS providers such as Pantheon, it's now easier, cheaper and more reliable to use an off-the-shelf offering instead of spending precious developer time performance tuning in-house hosting. Check out our guide to managed platform-as-a-service hosting.

Rick Donohoe's picture

You may also like...

Drupal Site building 101

Rick Donohoe, Feb 22nd 2013
Most developers who have had the pleasure to work with Drupal know that there is a steep learning curve to get over first before you can really begin...

Comments

2pha

Jul 3rd 2015 - 8:07amreply
2pha's picture

Very good post, it will be going in my bookmarks.

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Related Blogs

Sophie Shanahan-Kluth's picture
Rick Donohoe's picture
Mark Pavlitski's picture