<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" /><style type="text/css"><!--
#msg dl { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fc0 solid; padding: 6px; }
#msg ul, pre { overflow: auto; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<title>[13576] trunk: Improve user listing performance.</title>
</head>
<body>

<div id="msg">
<dl>
<dt>Revision</dt> <dd><a href="http://trac.wordpress.org/changeset/13576">13576</a></dd>
<dt>Author</dt> <dd>ryan</dd>
<dt>Date</dt> <dd>2010-03-03 19:08:30 +0000 (Wed, 03 Mar 2010)</dd>
</dl>

<h3>Log Message</h3>
<pre>Improve user listing performance. Props miqrogroove. see <a href="http://trac.wordpress.org/ticket/11914">#11914</a></pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkwpadminincludesschemaphp">trunk/wp-admin/includes/schema.php</a></li>
<li><a href="#trunkwpadminincludestemplatephp">trunk/wp-admin/includes/template.php</a></li>
<li><a href="#trunkwpadminusersphp">trunk/wp-admin/users.php</a></li>
<li><a href="#trunkwpincludesauthortemplatephp">trunk/wp-includes/author-template.php</a></li>
<li><a href="#trunkwpincludespluggablephp">trunk/wp-includes/pluggable.php</a></li>
<li><a href="#trunkwpincludespostphp">trunk/wp-includes/post.php</a></li>
<li><a href="#trunkwpincludesuserphp">trunk/wp-includes/user.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkwpadminincludesschemaphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/includes/schema.php (13575 => 13576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/includes/schema.php        2010-03-03 17:13:38 UTC (rev 13575)
+++ trunk/wp-admin/includes/schema.php        2010-03-03 19:08:30 UTC (rev 13576)
</span><span class="lines">@@ -146,7 +146,8 @@
</span><span class="cx">   PRIMARY KEY  (ID),
</span><span class="cx">   KEY post_name (post_name),
</span><span class="cx">   KEY type_status_date (post_type,post_status,post_date,ID),
</span><del>-  KEY post_parent (post_parent)
</del><ins>+  KEY post_parent (post_parent),
+  KEY post_author (post_author)
</ins><span class="cx"> ) $charset_collate;
</span><span class="cx"> CREATE TABLE $wpdb-&gt;users (
</span><span class="cx">   ID bigint(20) unsigned NOT NULL auto_increment,
</span></span></pre></div>
<a id="trunkwpadminincludestemplatephp"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/includes/template.php (13575 => 13576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/includes/template.php        2010-03-03 17:13:38 UTC (rev 13575)
+++ trunk/wp-admin/includes/template.php        2010-03-03 19:08:30 UTC (rev 13576)
</span><span class="lines">@@ -1824,16 +1824,17 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><del>- * {@internal Missing Short Description}}
</del><ins>+ * Generate HTML for a single row on the users.php admin panel.
</ins><span class="cx">  *
</span><del>- * @since unknown
</del><ins>+ * @since 2.1.0
</ins><span class="cx">  *
</span><del>- * @param unknown_type $user_object
- * @param unknown_type $style
- * @param unknown_type $role
- * @return unknown
</del><ins>+ * @param object $user_object
+ * @param string $style Optional. Attributes added to the TR element.  Must be sanitized.
+ * @param string $role Key for the $wp_roles array.
+ * @param int $numposts Optional. Post count to display for this user.  Defaults to zero, as in, a new user has made zero posts.
+ * @return string
</ins><span class="cx">  */
</span><del>-function user_row( $user_object, $style = '', $role = '' ) {
</del><ins>+function user_row( $user_object, $style = '', $role = '', $numposts = 0 ) {
</ins><span class="cx">         global $wp_roles;
</span><span class="cx"> 
</span><span class="cx">         $current_user = wp_get_current_user();
</span><span class="lines">@@ -1849,7 +1850,6 @@
</span><span class="cx">                 $short_url = substr( $short_url, 0, -1 );
</span><span class="cx">         if ( strlen( $short_url ) &gt; 35 )
</span><span class="cx">                 $short_url = substr( $short_url, 0, 32 ).'...';
</span><del>-        $numposts = get_usernumposts( $user_object-&gt;ID );
</del><span class="cx">         $checkbox = '';
</span><span class="cx">         // Check if the user for this row is editable
</span><span class="cx">         if ( current_user_can( 'edit_user', $user_object-&gt;ID ) ) {
</span></span></pre></div>
<a id="trunkwpadminusersphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/users.php (13575 => 13576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/users.php        2010-03-03 17:13:38 UTC (rev 13575)
+++ trunk/wp-admin/users.php        2010-03-03 19:08:30 UTC (rev 13576)
</span><span class="lines">@@ -208,9 +208,15 @@
</span><span class="cx">         $userspage = isset($_GET['userspage']) ? $_GET['userspage'] : null;
</span><span class="cx">         $role = isset($_GET['role']) ? $_GET['role'] : null;
</span><span class="cx"> 
</span><del>-        // Query the users
</del><ins>+        // Query the user IDs for this page
</ins><span class="cx">         $wp_user_search = new WP_User_Search($usersearch, $userspage, $role);
</span><span class="cx"> 
</span><ins>+        // Query the post counts for this page
+        $post_counts = count_many_users_posts($wp_user_search-&gt;get_results());
+
+        // Query the users for this page
+        cache_users($wp_user_search-&gt;get_results());
+
</ins><span class="cx">         $messages = array();
</span><span class="cx">         if ( isset($_GET['update']) ) :
</span><span class="cx">                 switch($_GET['update']) {
</span><span class="lines">@@ -263,22 +269,14 @@
</span><span class="cx"> &lt;form id=&quot;list-filter&quot; action=&quot;&quot; method=&quot;get&quot;&gt;
</span><span class="cx"> &lt;ul class=&quot;subsubsub&quot;&gt;
</span><span class="cx"> &lt;?php
</span><del>-$role_links = array();
-$avail_roles = array();
-$users_of_blog = get_users_of_blog();
-$total_users = count( $users_of_blog );
-foreach ( (array) $users_of_blog as $b_user ) {
-        $b_roles = unserialize($b_user-&gt;meta_value);
-        foreach ( (array) $b_roles as $b_role =&gt; $val ) {
-                if ( !isset($avail_roles[$b_role]) )
-                        $avail_roles[$b_role] = 0;
-                $avail_roles[$b_role]++;
-        }
-}
</del><ins>+$users_of_blog = count_users();
+$total_users = $users_of_blog['total_users'];
+$avail_roles =&amp; $users_of_blog['avail_roles'];
</ins><span class="cx"> unset($users_of_blog);
</span><span class="cx"> 
</span><span class="cx"> $current_role = false;
</span><span class="cx"> $class = empty($role) ? ' class=&quot;current&quot;' : '';
</span><ins>+$role_links = array();
</ins><span class="cx"> $role_links[] = &quot;&lt;li&gt;&lt;a href='users.php'$class&gt;&quot; . sprintf( _nx( 'All &lt;span class=&quot;count&quot;&gt;(%s)&lt;/span&gt;', 'All &lt;span class=&quot;count&quot;&gt;(%s)&lt;/span&gt;', $total_users, 'users' ), number_format_i18n( $total_users ) ) . '&lt;/a&gt;';
</span><span class="cx"> foreach ( $wp_roles-&gt;get_names() as $this_role =&gt; $name ) {
</span><span class="cx">         if ( !isset($avail_roles[$this_role]) )
</span><span class="lines">@@ -372,7 +370,7 @@
</span><span class="cx">         $role = array_shift($roles);
</span><span class="cx"> 
</span><span class="cx">         $style = ( ' class=&quot;alternate&quot;' == $style ) ? '' : ' class=&quot;alternate&quot;';
</span><del>-        echo &quot;\n\t&quot; . user_row($user_object, $style, $role);
</del><ins>+        echo &quot;\n\t&quot;, user_row($user_object, $style, $role, $post_counts[(string)$userid]);
</ins><span class="cx"> }
</span><span class="cx"> ?&gt;
</span><span class="cx"> &lt;/tbody&gt;
</span></span></pre></div>
<a id="trunkwpincludesauthortemplatephp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/author-template.php (13575 => 13576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/author-template.php        2010-03-03 17:13:38 UTC (rev 13575)
+++ trunk/wp-includes/author-template.php        2010-03-03 19:08:30 UTC (rev 13576)
</span><span class="lines">@@ -151,12 +151,12 @@
</span><span class="cx">  *
</span><span class="cx">  * @since 1.5
</span><span class="cx">  * @uses $post The current post in the Loop's DB object.
</span><del>- * @uses get_usernumposts()
</del><ins>+ * @uses count_user_posts()
</ins><span class="cx">  * @return int The number of posts by the author.
</span><span class="cx">  */
</span><span class="cx"> function get_the_author_posts() {
</span><span class="cx">         global $post;
</span><del>-        return get_usernumposts($post-&gt;post_author);
</del><ins>+        return count_user_posts($post-&gt;post_author);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span></span></pre></div>
<a id="trunkwpincludespluggablephp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/pluggable.php (13575 => 13576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/pluggable.php        2010-03-03 17:13:38 UTC (rev 13575)
+++ trunk/wp-includes/pluggable.php        2010-03-03 19:08:30 UTC (rev 13576)
</span><span class="lines">@@ -139,6 +139,38 @@
</span><span class="cx"> }
</span><span class="cx"> endif;
</span><span class="cx"> 
</span><ins>+if ( !function_exists('cache_users') ) :
+/**
+ * Retrieve info for user lists to prevent multiple queries by get_userdata()
+ *
+ * @since 3.0.0
+ *
+ * @param array $users User ID numbers list
+ */
+function cache_users( $users ) {
+        global $wpdb;
+
+        $clean = array();
+        foreach($users as $id) {
+                $id = (int) $id;
+                if (wp_cache_get($id, 'users')) {
+                        // seems to be cached already
+                } else {
+                        $clean[] = $id;
+                }
+        }
+
+        if ( 0 == count($clean) )
+                return;
+
+        $list = implode(',', $clean);
+
+        $results = $wpdb-&gt;get_results(&quot;SELECT * FROM $wpdb-&gt;users WHERE ID IN ($list)&quot;);
+    
+        _fill_many_users($results);
+}
+endif;
+
</ins><span class="cx"> if ( !function_exists('get_user_by') ) :
</span><span class="cx"> /**
</span><span class="cx">  * Retrieve user info by a given field
</span></span></pre></div>
<a id="trunkwpincludespostphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/post.php (13575 => 13576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/post.php        2010-03-03 17:13:38 UTC (rev 13575)
+++ trunk/wp-includes/post.php        2010-03-03 19:08:30 UTC (rev 13576)
</span><span class="lines">@@ -3652,9 +3652,23 @@
</span><span class="cx">  * @return string SQL code that can be added to a where clause.
</span><span class="cx">  */
</span><span class="cx"> function get_private_posts_cap_sql($post_type) {
</span><del>-        global $user_ID;
-        $cap = '';
</del><ins>+        return get_posts_by_author_sql($post_type, FALSE);
+}
</ins><span class="cx"> 
</span><ins>+/**
+ * Retrieve the post SQL based on capability, author, and type.
+ *
+ * See above for full description.
+ *
+ * @since 3.0.0
+ * @param string $post_type currently only supports 'post' or 'page'.
+ * @param bool $full Optional.  Returns a full WHERE statement instead of just an 'andalso' term.
+ * @param int $post_author Optional.  Query posts having a single author ID.
+ * @return string SQL WHERE code that can be added to a query.
+ */
+function get_posts_by_author_sql($post_type, $full = TRUE, $post_author = NULL) {
+        global $user_ID, $wpdb;
+
</ins><span class="cx">         // Private posts
</span><span class="cx">         if ($post_type == 'post') {
</span><span class="cx">                 $cap = 'read_private_posts';
</span><span class="lines">@@ -3663,24 +3677,40 @@
</span><span class="cx">                 $cap = 'read_private_pages';
</span><span class="cx">         // Dunno what it is, maybe plugins have their own post type?
</span><span class="cx">         } else {
</span><ins>+                $cap = '';
</ins><span class="cx">                 $cap = apply_filters('pub_priv_sql_capability', $cap);
</span><span class="cx"> 
</span><span class="cx">                 if (empty($cap)) {
</span><span class="cx">                         // We don't know what it is, filters don't change anything,
</span><span class="cx">                         // so set the SQL up to return nothing.
</span><del>-                        return '1 = 0';
</del><ins>+                        return ' 1 = 0 ';
</ins><span class="cx">                 }
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        $sql = '(post_status = \'publish\'';
</del><ins>+        if ($full) {
+                if (is_null($post_author)) {
+                        $sql = $wpdb-&gt;prepare('WHERE post_type = %s AND ', $post_type);
+                } else {
+                        $sql = $wpdb-&gt;prepare('WHERE post_author = %d AND post_type = %s AND ', $post_author, $post_type);
+                }
+        } else {
+                $sql = '';
+        }
</ins><span class="cx"> 
</span><ins>+        $sql .= &quot;(post_status = 'publish'&quot;;
+
</ins><span class="cx">         if (current_user_can($cap)) {
</span><span class="cx">                 // Does the user have the capability to view private posts? Guess so.
</span><del>-                $sql .= ' OR post_status = \'private\'';
</del><ins>+                $sql .= &quot; OR post_status = 'private'&quot;;
</ins><span class="cx">         } elseif (is_user_logged_in()) {
</span><span class="cx">                 // Users can view their own private posts.
</span><del>-                $sql .= ' OR post_status = \'private\' AND post_author = \'' . $user_ID . '\'';
-        }
</del><ins>+                $id = (int) $user_ID;
+                if (is_null($post_author) || !$full) {
+                        $sql .= &quot; OR post_status = 'private' AND post_author = $id&quot;;
+                } elseif ($id == (int)$post_author) {
+                        $sql .= &quot; OR post_status = 'private'&quot;;
+                } // else none
+        } // else none
</ins><span class="cx"> 
</span><span class="cx">         $sql .= ')';
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkwpincludesuserphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/user.php (13575 => 13576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/user.php        2010-03-03 17:13:38 UTC (rev 13575)
+++ trunk/wp-includes/user.php        2010-03-03 19:08:30 UTC (rev 13576)
</span><span class="lines">@@ -148,14 +148,49 @@
</span><span class="cx">  * @param int $userid User ID.
</span><span class="cx">  * @return int Amount of posts user has written.
</span><span class="cx">  */
</span><del>-function get_usernumposts($userid) {
</del><ins>+function count_user_posts($userid) {
</ins><span class="cx">         global $wpdb;
</span><del>-        $userid = (int) $userid;
-        $count = $wpdb-&gt;get_var( $wpdb-&gt;prepare(&quot;SELECT COUNT(*) FROM $wpdb-&gt;posts WHERE post_author = %d AND post_type = 'post' AND &quot;, $userid) . get_private_posts_cap_sql('post'));
</del><ins>+
+        $where = get_posts_by_author_sql('post', TRUE, $userid);
+
+        $count = $wpdb-&gt;get_var( &quot;SELECT COUNT(*) FROM $wpdb-&gt;posts $where&quot; );
+
</ins><span class="cx">         return apply_filters('get_usernumposts', $count, $userid);
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><ins>+ * Number of posts written by a list of users.
+ *
+ * @since 3.0.0
+ * @param array $userid User ID number list.
+ * @return array Amount of posts each user has written.
+ */
+function count_many_users_posts($users) {
+        global $wpdb;
+        
+        if (0 == count($users))
+                return array();
+            
+        $userlist = implode(',', $users);
+        $where = get_posts_by_author_sql('post');
+
+        $result = $wpdb-&gt;get_results( &quot;SELECT post_author, COUNT(*) FROM $wpdb-&gt;posts $where AND post_author IN ($userlist) GROUP BY post_author&quot;, ARRAY_N );
+
+        $count = array();
+        foreach($result as $row) {
+                $count[$row[0]] = $row[1];
+        }
+
+        foreach($users as $id) {
+                $id = (string) $id;
+                if (!isset($count[$id]))
+                        $count[$id] = 0;
+        }
+
+        return $count;
+}
+
+/**
</ins><span class="cx">  * Check that the user login name and password is correct.
</span><span class="cx">  *
</span><span class="cx">  * @since 0.71
</span><span class="lines">@@ -342,6 +377,79 @@
</span><span class="cx">         return update_metadata('user', $user_id, $meta_key, $meta_value, $prev_value);
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+/**
+ * Count number of users who have each of the user roles.
+ *
+ * Assumes there are neither duplicated nor orphaned capabilities meta_values.
+ * Assumes role names are unique phrases.  Same assumption made by WP_User_Search::prepare_query()
+ * Using $strategy = 'time' this is CPU-intensive and should handle around 10^7 users.
+ * Using $strategy = 'memory' this is memory-intensive and should handle around 10^5 users, but see WP Bug #12257.
+ *
+ * @since 3.0.0
+ * @param string $strategy 'time' or 'memory'
+ * @return array Includes a grand total and an array of counts indexed by role strings.
+ */
+function count_users($strategy = 'time') {
+        global $wpdb, $blog_id, $wp_roles;
+
+        // Initialize
+        $id = (int) $blog_id;
+        $blog_prefix = $wpdb-&gt;get_blog_prefix($id);
+        $result = array();
+
+        if ('time' == $strategy) {
+                $avail_roles = $wp_roles-&gt;get_names();
+
+                // Build a CPU-intensive query that will return concise information.
+                $select_count = array();
+                foreach ( $avail_roles as $this_role =&gt; $name ) {
+                        $select_count[] = &quot;COUNT(NULLIF(`meta_value` LIKE '%&quot; . like_escape($this_role) . &quot;%', FALSE))&quot;;
+                }
+                $select_count = implode(', ', $select_count);
+
+                // Add the meta_value index to the selection list, then run the query.
+                $row = $wpdb-&gt;get_row( &quot;SELECT $select_count, COUNT(*) FROM $wpdb-&gt;usermeta WHERE meta_key = '{$blog_prefix}capabilities'&quot;, ARRAY_N );
+
+                // Run the previous loop again to associate results with role names.
+                $col = 0;
+                $role_counts = array();
+                foreach ( $avail_roles as $this_role =&gt; $name ) {
+                        $count = (int) $row[$col++];
+                        if ($count &gt; 0) {
+                                $role_counts[$this_role] = $count;
+                        }
+                }
+
+                // Get the meta_value index from the end of the result set.
+                $total_users = (int) $row[$col];
+
+                $result['total_users'] = $total_users;
+                $result['avail_roles'] =&amp; $role_counts;
+        } else {
+                $avail_roles = array();
+
+                $users_of_blog = $wpdb-&gt;get_col( &quot;SELECT meta_value FROM $wpdb-&gt;usermeta WHERE meta_key = '{$blog_prefix}capabilities'&quot; );
+
+                foreach ( $users_of_blog as $caps_meta ) {
+                        $b_roles = unserialize($caps_meta);
+                        if ( is_array($b_roles) ) {
+                                foreach ( $b_roles as $b_role =&gt; $val ) {
+                                        if ( isset($avail_roles[$b_role]) ) {
+                                                $avail_roles[$b_role]++;
+                                        } else {
+                                                $avail_roles[$b_role] = 1;
+                                        }
+                                }
+                        }
+                }
+
+                $result['total_users'] = count( $users_of_blog );
+                $result['avail_roles'] =&amp; $avail_roles;
+        }
+
+        return $result;
+}
+
</ins><span class="cx"> //
</span><span class="cx"> // Private helper functions
</span><span class="cx"> //
</span><span class="lines">@@ -498,8 +606,8 @@
</span><span class="cx">  *
</span><span class="cx">  * The finished user data is cached, but the cache is not used to fill in the
</span><span class="cx">  * user data for the given object. Once the function has been used, the cache
</span><del>- * should be used to retrieve user data. The purpose seems then to be to ensure
- * that the data in the object is always fresh.
</del><ins>+ * should be used to retrieve user data. The intention is if the current data
+ * had been cached already, there would be no need to call this function.
</ins><span class="cx">  *
</span><span class="cx">  * @access private
</span><span class="cx">  * @since 2.5.0
</span><span class="lines">@@ -508,19 +616,56 @@
</span><span class="cx">  * @param object $user The user data object.
</span><span class="cx">  */
</span><span class="cx"> function _fill_user( &amp;$user ) {
</span><ins>+        $metavalues = get_user_metavalues(array($user-&gt;ID));
+        _fill_single_user($user, $metavalues[$user-&gt;ID]);
+}
+
+/**
+ * Perform the query to get the $metavalues array(s) needed by _fill_user and _fill_many_users
+ *
+ * @since 3.0.0
+ * @param array $ids User ID numbers list.
+ * @return array of arrays. The array is indexed by user_id, containing $metavalues object arrays.
+ */
+function get_user_metavalues($ids) {
</ins><span class="cx">         global $wpdb;
</span><span class="cx"> 
</span><ins>+        $clean = array_map('intval', $ids);
+        if ( 0 == count($clean) )
+                return $objects;
+
+        $list = implode(',', $clean);
+
</ins><span class="cx">         $show = $wpdb-&gt;hide_errors();
</span><del>-        $metavalues = $wpdb-&gt;get_results($wpdb-&gt;prepare(&quot;SELECT meta_key, meta_value FROM $wpdb-&gt;usermeta WHERE user_id = %d&quot;, $user-&gt;ID));
</del><ins>+        $metavalues = $wpdb-&gt;get_results(&quot;SELECT user_id, meta_key, meta_value FROM $wpdb-&gt;usermeta WHERE user_id IN ($list)&quot;);
</ins><span class="cx">         $wpdb-&gt;show_errors($show);
</span><span class="cx"> 
</span><del>-        if ( $metavalues ) {
-                foreach ( (array) $metavalues as $meta ) {
-                        $value = maybe_unserialize($meta-&gt;meta_value);
-                        $user-&gt;{$meta-&gt;meta_key} = $value;
-                }
</del><ins>+        $objects = array();
+        foreach($clean as $id) {
+                $objects[$id] = array();
</ins><span class="cx">         }
</span><ins>+        foreach($metavalues as $meta_object) {
+                $objects[$meta_object-&gt;user_id][] = $meta_object;
+        }
</ins><span class="cx"> 
</span><ins>+        return $objects;
+}
+
+/**
+ * Unserialize user metadata, fill $user object, then cache everything.
+ *
+ * @since 3.0.0
+ * @param object $user The User object.
+ * @param array $metavalues An array of objects provided by get_user_metavalues()
+ */
+function _fill_single_user( &amp;$user, &amp;$metavalues ) {
+        global $wpdb;
+
+        foreach ( $metavalues as $meta ) {
+                $value = maybe_unserialize($meta-&gt;meta_value);
+                $user-&gt;{$meta-&gt;meta_key} = $value;
+        }
+
</ins><span class="cx">         $level = $wpdb-&gt;prefix . 'user_level';
</span><span class="cx">         if ( isset( $user-&gt;{$level} ) )
</span><span class="cx">                 $user-&gt;user_level = $user-&gt;{$level};
</span><span class="lines">@@ -533,13 +678,32 @@
</span><span class="cx">         if ( isset($user-&gt;description) )
</span><span class="cx">                 $user-&gt;user_description = $user-&gt;description;
</span><span class="cx"> 
</span><del>-        wp_cache_add($user-&gt;ID, $user, 'users');
-        wp_cache_add($user-&gt;user_login, $user-&gt;ID, 'userlogins');
-        wp_cache_add($user-&gt;user_email, $user-&gt;ID, 'useremail');
-        wp_cache_add($user-&gt;user_nicename, $user-&gt;ID, 'userslugs');
</del><ins>+        update_user_caches($user);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><ins>+ * Take an array of user objects, fill them with metas, and cache them.
+ *
+ * @since 3.0.0
+ * @param array $users User objects
+ * @param array $metas User metavalues objects
+ */
+function _fill_many_users( &amp;$users ) {
+        $ids = array();
+        foreach($users as $user_object) {
+                $ids[] = $user_object-&gt;ID;
+        }
+
+    $metas = get_user_metavalues($ids);
+
+        foreach($users as $user_object) {
+                if (isset($metas[$user_object-&gt;ID])) {
+                _fill_single_user($user_object, $metas[$user_object-&gt;ID]);
+                }
+        }
+}
+
+/**
</ins><span class="cx">  * Sanitize every user field.
</span><span class="cx">  *
</span><span class="cx">  * If the context is 'raw', then the user object or array will get minimal santization of the int fields.
</span><span class="lines">@@ -656,12 +820,25 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><ins>+ * Update all user caches
+ *
+ * @since 3.0.0
+ *
+ * @param object $user User object to be cached
+ */
+function update_user_caches(&amp;$user) {
+        wp_cache_add($user-&gt;ID, $user, 'users');
+        wp_cache_add($user-&gt;user_login, $user-&gt;ID, 'userlogins');
+        wp_cache_add($user-&gt;user_email, $user-&gt;ID, 'useremail');
+        wp_cache_add($user-&gt;user_nicename, $user-&gt;ID, 'userslugs');
+}
+
+/**
</ins><span class="cx">  * Clean all user caches
</span><span class="cx">  *
</span><del>- * @since 3.0
</del><ins>+ * @since 3.0.0
</ins><span class="cx">  *
</span><span class="cx">  * @param int $id User ID
</span><del>- * @return void
</del><span class="cx">  */
</span><span class="cx"> function clean_user_cache($id) {
</span><span class="cx">         $user = new WP_User($id);
</span></span></pre>
</div>
</div>

</body>
</html>