Faster Database search

Recommended Posts

Hi Everyone,

Since a couple of months, we have our new website live with processwire as CMS system.

In this website there is a huge database (1900+ trees), wich can be found with different filters. Take a look at
The website does not to be seen very fast when you select a couple of filters. Every time there is a delay between 3-5 seconds. 

Now we are migrate the website to another sever, with more preformance. So we hope this give us some timeprofits.

Does anyone have any kind of suggest, to make this filter faster. Now the website is on PHP5.6, is php7 better? Can this make a different? We used ProCache already.

Thanks in advance


  • Like 1

Share this post

Link to post
Share on other sites

Cool thanks @abdus,

Is processwire and the modules like procache and formbuilder etc ready for PHP 7. Or is this a heel of a job to make it work on php7?

We have processwire 3.0.

Are there any other sollution to make this huge database faster?

Share this post

Link to post
Share on other sites

It's usually plug and play, I dont remeber having any problems with the upgrade.

Unless you're using deprecated features, you should be ok

Here are some guides on Tuts+ and DigitalOcean

  • Like 1

Share this post

Link to post
Share on other sites

Definitely with @abdus on this one. PHP7 is noticeably faster than 5.6, and PW has no problems (that I'm aware of?) running under 7. If it is available, do it (taking the usual precautions, obvs).

Also, there might be optimisations you could make to your selectors and there are probably mysterious optimisations you may be able to make to your mySQL setup, depending on how much control you have within your hosting. First of those you will probably get help with here, if you are able to share some code, second is down to black magic, voodoo and selling the soul of your firstborn. Or hiring an expert.

All of that having been said, it's a very good looking website, and it works well - initial loading doesn't feel slow, just the filtering isn't instant.

<edit>Man, 3 more replies while I write this one.</edit>

  • Like 3
  • Haha 1

Share this post

Link to post
Share on other sites

Hi @sreeb

Do you cache results of searches? 

I have a similar project with a lot of filtering options. I use WireCache for caching search results ( PageArrays) and MarkupCache for result cards. It works quite fast on shared hosting. 

  • Like 1

Share this post

Link to post
Share on other sites

Superb website. It should be included in the featured sites if it isn't already.

1 hour ago, sreeb said:

The website does not to be seen very fast when you select a couple of filters.

Perhaps your filter queries are not optimal. Feel free to post the filter code if you would like any feedback on this.

  • Like 1

Share this post

Link to post
Share on other sites

A generous use of WireCache can help with generating overviews for a large number trees.

<?php namespace ProcessWire;
/** @var $cache WireCache */

foreach ($trees as $tree) {
    $overview = $cache->getFor($tree, "tree-overview");
    if (!$overview) {
        $overview = wireRenderFile('parts/tree-overview');
        $cache->saveFor($tree, 'tree-overview', WireCache::expireWeekly);
    echo $overview;


Share this post

Link to post
Share on other sites

Wow, thanks a lot for al the suggets.

I will send this to our developers, so they can expertmate with it. 

@Zeka, do you have an example website so i can see the search?

Share this post

Link to post
Share on other sites

I am actually wondering if you might be better off not relying on ajax calls for each filter step. Have you considered storing all the details of all trees in a wireCache'd JSON object that you load up when the page originally loads and then filter through that in memory? I have done this before - I have 3MB of data in JSON which is cached, I make sure that data is transferred compressed (so it's only a couple of hundred KBs). The initially page load takes a couple of seconds, but after that, the filtering is instant. I think so long as you know that the data will not expand to an outrageous size, this might be a good approach.

PS - Great looking site!

  • Like 7

Share this post

Link to post
Share on other sites

Unfortunately, php7 does not give the desired speed gain. Do you have experience with Varnish Cache and PW?
Also extra server capacity did not do the job. It looks like its a code technical problem.

As i can see, all search query's are set as an URL, like:


The page will not be change very often. they are quite static. It looks like below. But i'am not sure.
Unfortainly our developers cant find a sollution, so this is my last hope;)


 public function getTreesSelector($filterParams){
        $selector = array('template=tree-detail');
        foreach ($filterParams as $key => $value) {
            if(trim($value) != ""){
                $key = $this->getFilterKey($key);
                switch ($key) {
                    case "key":
                        $selector[] = "title|tree_specific_name|tree_family|tree_subfamily%=".urldecode($value);



Share this post

Link to post
Share on other sites

i think you need to do some more debugging! i just played around with a local test-setup and got this results with 10.000 pages:

creation of pages:

//foreach($pages->find('parent=8181') as $p) $pages->delete($p);

$tmp = range('A', 'Z');
while($i<10000) {
    $p = new Page();
    $p->template = 'filtertest';
    $p->parent = 8181;
    $p->title = "test$i";
    $p->a = $tmp[array_rand($tmp)];
    $p->b = $tmp[array_rand($tmp)];
    $p->c = $tmp[array_rand($tmp)];

dump (results):

d($pages->find('template=filtertest, a=a, b=b')->each('title'));
array (13)
14.48ms, 0.09MB

d($pages->find('template=filtertest, a|b|c=a|b|c')->each('title'));
array (3084)
1110.54ms, 6.54MB

d($pages->find('template=filtertest, a|b|c%=a|b|c')->each('title'));
array (3084)
1339.68ms, 6.54MB

your filter function looks totally weird to me. you have ID values as filter but then you use the slow %= selector. why? see my first example using " = " as selector should give you an instant result! also i don't understand why you are using the OR operator ( | ) for searching different fields. is the information spread over multiple fields?? shouldn't every filter-value be stored in a separate field?


PS: my template "filtertest" has fields A, B and C holding letters from A-Z

PPS: are you sure the selector is slowing the site down? maybe it is the way you count your number of results in the filter sidebar?

  • Like 3

Share this post

Link to post
Share on other sites

you have 175 labels showing the tree count:


i tried a loop with 200 iterations on my test-install with the SLOW selector from my above example and got all the counts within 500ms:


so i think it should be no problem to get the site to returning results plus all the filter count-labels under 500ms.

but as i said in my pm: that can only be a wild guess as we don't know any details about your setup.

Share this post

Link to post
Share on other sites
11 minutes ago, bernhard said:

i tried a loop with 200 iterations on my test-install with the SLOW selector from my above example and got all the counts within 500ms:

Once PW fetches pages from DB, it saves them to memory, meaning subsequent find() operations with the same selector don't really have an effect, so it's essentially one DB operation.

// PagesLoaderCache.php

 * Cache the given selector string and options with the given PageArray
 * @param string $selector
 * @param array $options
 * @param PageArray $pages
 * @return bool True if pages were cached, false if not
public function selectorCache($selector, array $options, PageArray $pages) {

    // get the string that will be used for caching
    $selector = $this->getSelectorCache($selector, $options, true);

    // optimization: don't cache single pages that have an unpublished status or higher
    if(count($pages) && !empty($options['findOne']) && $pages->first()->status >= Page::statusUnpublished) return false;

    $this->pageSelectorCache[$selector] = clone $pages;

    return true;


  • Like 1
  • Thanks 1

Share this post

Link to post
Share on other sites

ah, thanks, you are right :)

ok, so now i get 15seconds. if you really need the count-labels it seems you would need to implement some caching...


  • Like 2

Share this post

Link to post
Share on other sites

I haven't been following this closely for a while, but perhaps an SQL query would be helpful?

FROM `pages` 
WHERE (pages.templates_id=29) 
AND (pages.title='My Page') 
AND (pages.status<1024) 

You might find this snippet helpful. It will generate an SQL query from a find selector. I just modified the returned result to be a COUNT instead.


Share this post

Link to post
Share on other sites

No if it makes the speed  much faster, the countlabels can go away.

I will ask to our developers to look at this. Maby this give them some information.

Is Varnish caching a good sollution?

Share this post

Link to post
Share on other sites
23 minutes ago, adrian said:

It will generate an SQL query from a find selector. I just modified the returned result to be a COUNT instead.

A simpler way could be passing a second parameter to $pages->count() method like this

$count = $pages->count('id>0', ['getTotalType' => 'calc']); // default, slower, fetches everything then counts
$count = $pages->count('id>0', ['getTotalType' => 'count']); // uses SQL query, faster

From the core:


 * Return all pages matching the given selector.
 * @param Selectors|string|array $selectors Selectors object or selector string
 * @param array $options
 * ....
 *  - `getTotalType` (string): Method to use to get total, specify 'count' or 'calc' (default='calc').
 * ....
public function ___find($selectors, array $options = [])
    // ...
    if ($this->getTotal) {
        if ($this->getTotalType === 'count') {
            $query->set('select', ['COUNT(*)']);
            $query->set('orderby', []);
            $query->set('groupby', []);
            $query->set('limit', []);
            $stmt = $query->execute();
            $errorInfo = $stmt->errorInfo();
            if ($stmt->errorCode() > 0) throw new PageFinderException($errorInfo[2]);
            list($this->total) = $stmt->fetch(\PDO::FETCH_NUM);
        } else {
            $this->total = (int)$database->query("SELECT FOUND_ROWS()")->fetchColumn();

    } else {
        $this->total = count($matches);

    // ...




  • Like 2

Share this post

Link to post
Share on other sites
2 minutes ago, sreeb said:

hi @adrian, how can i run that snippet?

Which one - the one to generate an SQL query from a PW selector, or the actual SQL query itself?

Share this post

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By flashmaster
      Hi, i need some help with a calendar script (Full Calendar)
      Issue 1
      Its not saving to the database. This function dont work either when the script is running alone outside Processwire.
      Issue 2 (Fixed)
      When i click to ad an event and click save it wont turn up in blue, it just disapear. This function works when the script is running alone outside Processwire.
      I have attached the script running alone so you can se how it works. I have also pasted the code below that im trying to work under the template folder inside Processwire. If you can look at the code and se whats wrong with it. Im not that good at PHP or Javascript so i need you help.

      <?php include( "database.php" ); if ( isset( $_POST[ 'action' ] )or isset( $_GET[ 'view' ] ) ) //show all events { if ( isset( $_GET[ 'view' ] ) ) { header( 'Content-Type: application/json' ); $start = mysqli_real_escape_string( $connection, $_GET[ "start" ] ); $end = mysqli_real_escape_string( $connection, $_GET[ "end" ] ); $result = mysqli_query( $connection, "SELECT id, start ,end ,title FROM events where (date(start) >= ‘$start’ AND date(start) <= ‘$end’)" ); while ( $row = mysqli_fetch_assoc( $result ) ) { $events[] = $row; } echo json_encode( $events ); exit; } elseif ( $_POST[ 'action' ] == "add" ) // add new event section { mysqli_query( $connection, "INSERT INTO events ( title , start , end ) VALUES ( '" . mysqli_real_escape_string( $connection, $_POST[ "title" ] ) . "', '" . mysqli_real_escape_string( $connection, date( 'Y-m-d H:i:s', strtotime( $_POST[ "start" ] ) ) ) . "‘, '" . mysqli_real_escape_string( $connection, date( 'Y-m-d H:i:s', strtotime( $_POST[ "end" ] ) ) ) . "‘ )" ); header( 'Content-Type: application/json' ); echo '{"id":"' . mysqli_insert_id( $connection ) . '"}'; exit; } elseif ( $_POST[ 'action' ] == "update" ) // update event { mysqli_query( $connection, "UPDATE events set start = '" . mysqli_real_escape_string( $connection, date( 'Y-m-d H:i:s', strtotime( $_POST[ "start" ] ) ) ) . "', end = '" . mysqli_real_escape_string( $connection, date( 'Y-m-d H:i:s', strtotime( $_POST[ "end" ] ) ) ) . "' where id = '" . mysqli_real_escape_string( $connection, $_POST[ "id" ] ) . "'" ); exit; } elseif ( $_POST[ 'action' ] == "delete" ) // remove event { mysqli_query( $connection, "DELETE from events where id = '" . mysqli_real_escape_string( $connection, $_POST[ "id" ] ) . "'" ); if ( mysqli_affected_rows( $connection ) > 0 ) { echo "1"; } exit; } } ?> <!doctype html> <html lang="sv-se"> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <style type="text/css"> img { border-width: 0 } * { font-family: 'Lucida Grande', sans-serif; } </style> <style type="text/css"> .block a:hover { color: silver; } .block a { color: #fff; } .block { position: fixed; background: #2184cd; padding: 20px; z-index: 1; top: 240px; } </style> <script src=""></script> <script src="<?=$config->urls->templates;?>js/script.js" type="text/javascript"></script> <script src="" crossorigin="anonymous"></script> <link href="" rel="stylesheet"> <link href="<?=$config->urls->templates;?>css/fullcalendar.css" rel="stylesheet"/> <link href="<?=$config->urls->templates;?>css/fullcalendar.print.css" rel="stylesheet" media="print"/> <script src="<?=$config->urls->templates;?>js/moment.min.js"></script> <script src="<?=$config->urls->templates;?>js/fullcalendar.js"></script> </head> <body> <div class="container">fsefsefes <div class="row"> <div id="calendar"></div> </div> </div> <!-- Modal --> <div id="createEventModal" class="modal fade" role="dialog"> <div class="modal-dialog"> <!-- Modal content--> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">&times;</button> <h4 class="modal-title">Add Event</h4> </div> <div class="modal-body"> <div class="control-group"> <label class="control-label" for="inputPatient">Event:</label> <div class="field desc"> <input class="form-control" id="title" name="title" placeholder="Event" type="text" value=""> </div> </div> <input type="hidden" id="startTime"/> <input type="hidden" id="endTime"/> <div class="control-group"> <label class="control-label" for="when">When:</label> <div class="controls controls-row" id="when" style="margin-top:5px;"> </div> </div> </div> <div class="modal-footer"> <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button> <button type="submit" class="btn btn-primary" id="submitButton">Save</button> </div> </div> </div> </div> <div id="calendarModal" class="modal fade"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">&times;</button> <h4 class="modal-title">Event Details</h4> </div> <div id="modalBody" class="modal-body"> <h4 id="modalTitle" class="modal-title"></h4> <div id="modalWhen" style="margin-top:5px;"></div> </div> <input type="hidden" id="eventID"/> <div class="modal-footer"> <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button> <button type="submit" class="btn btn-danger" id="deleteButton">Delete</button> </div> </div> </div> </div> <!--Modal--> <div style='margin-left: auto;margin-right: auto;text-align: center;'> </div> </body> </html>  
      database.php (i have configure this in my file at localhost) this is just an example.
      <?php $connection = mysqli_connect('host','username','password','database') or die(mysqli_error($connection)); ?>  
      $(document).ready(function(){ var calendar = $('#calendar').fullCalendar({ header:{ left: 'prev,next today', center: 'title', right: 'agendaWeek,agendaDay' }, defaultView: 'agendaWeek', editable: true, selectable: true, allDaySlot: false, events: "home.php?view=1", eventClick: function(event, jsEvent, view) { endtime = $.fullCalendar.moment(event.end).format('h:mm'); starttime = $.fullCalendar.moment(event.start).format('dddd, MMMM Do YYYY, h:mm'); var mywhen = starttime + ' - ' + endtime; $('#modalTitle').html(event.title); $('#modalWhen').text(mywhen); $('#eventID').val(; $('#calendarModal').modal(); }, //header and other values select: function(start, end, jsEvent) { endtime = $.fullCalendar.moment(end).format('h:mm'); starttime = $.fullCalendar.moment(start).format('dddd, MMMM Do YYYY, h:mm'); var mywhen = starttime + ' - ' + endtime; start = moment(start).format(); end = moment(end).format(); $('#createEventModal #startTime').val(start); $('#createEventModal #endTime').val(end); $('#createEventModal #when').text(mywhen); $('#createEventModal').modal('toggle'); }, eventDrop: function(event, delta){ $.ajax({ url: 'home.php', data: 'action=update&title='+event.title+'&start='+moment(event.start).format()+'&end='+moment(event.end).format()+'&id=' , type: "POST", success: function(json) { //alert(json); } }); }, eventResize: function(event) { $.ajax({ url: 'home.php', data: 'action=update&title='+event.title+'&start='+moment(event.start).format()+'&end='+moment(event.end).format()+'&id=', type: "POST", success: function(json) { //alert(json); } }); } }); $('#submitButton').on('click', function(e){ // We don't want this to act as a link so cancel the link action e.preventDefault(); doSubmit(); }); $('#deleteButton').on('click', function(e){ // We don't want this to act as a link so cancel the link action e.preventDefault(); doDelete(); }); function doDelete(){ $("#calendarModal").modal('hide'); var eventID = $('#eventID').val(); $.ajax({ url: 'home.php', data: 'action=delete&id='+eventID, type: "POST", success: function(json) { if(json == 1) $("#calendar").fullCalendar('removeEvents',eventID); else return false; } }); } function doSubmit(){ $("#createEventModal").modal('hide'); var title = $('#title').val(); var startTime = $('#startTime').val(); var endTime = $('#endTime').val(); $.ajax({ url: 'home.php', data: 'action=add&title='+title+'&start='+startTime+'&end='+endTime, type: "POST", success: function(json) { $("#calendar").fullCalendar('renderEvent', { id:, title: title, start: startTime, end: endTime, }, true); } }); } });  
      database table
      CREATE TABLE events ( id int(11) NOT NULL AUTO_INCREMENT, start datetime DEFAULT NULL, end datetime DEFAULT NULL, title text, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;  

    • By artaylor
      I have a project that, based on initial specs, I decided to build in another framework due to the client wanting the ability to regularly import data from an existing manufacturing system. I decided a platform that would allow me to match the imported data to the system would make sense. Since then, the project has morphed (I know, it never happens) and the regular import is unnecessary and so a comprehensive admin is needed. I want to switch to PW but don't want to have to manually link thousands of tables.
      I have the import routine working for the main table but I am not sure how to import the support tables and automatically create the page fields for them.
      Oh, in case it matter, the links are not autoincrement id fields, but character codes. Eg. Category table - code: TRN, description: Transportation
      If someone can point me in the right direction it would be much appreciated.
    • By alxndre
      This is a simple loader for LessQL, an ORM alternative for PHP. It is based on NotORM, and provides a quick way to access and find things in a database, including traversals and back-traversals.
      As discussed in some earlier topics, there are times when you'd like to store some data away from ProcessWire's pages/fields/templates structure for whatever reasons. However ORMs are sometimes cumbersome and requires a lot more effort to deploy. LessQL offers a quick way to just up and go like you're using an ORM but without the added complexity and configuration files.
      This modules simply loads the LessQL library into ProcessWire and exposes a $lessQL variable (configurable in settings) that gives access to your database. It uses the same database specified in $config by default, but can be set to use a separate database, along with its credentials.
      Usage given a table person :
      $people = $lessQL->person()->select("id, firstname, lastname")->where("firstname LIKE ?", "%alex%")->orderBy("firstname")->limit(10); It uses lazy loading and doesn't execute the query until it needs to. Checkout for more info on LessQL.
      Module wrapper is pretty much lifted from @teppo's RedBeanPHP module, but with a few modifications.

    • By louisstephens
      I was going to start working on a new site for myself and wife (a new hobby we have taken up), and had decided to try out Runcloud and Digital Ocean. I got my drop set up on digital ocean, as well as setting up various hooks/databases etc between github and run cloud. However, now I have hit a wall. 
      I cloned my "blank" repository into my local host (managed through MAMP) and dropped in a fresh install of PW, but now I have no idea of how to move forward. Is it best to just work locally, and then push this into a branch, and when ready, change branches and commit all to run cloud? Run cloud gives me an IP address to use for the database, but I can't get my localhost setup to recognize (I just get "Connection refused"). I am also unsure how to actually get my commits to push to run cloud, and handling the new set up.
      I am probably in over my head, but I thought I would try something new as a good learning experience. However, now I am just drowning  . Hopefully someone has some ideas on how to approach this, as I am very eager to get under way.
    • By Macrura
      I encountered a situation over the past few months where tables have been crashing when a user saves a page in PW.
      I'm assuming it is something related to the server/hosting provider (Site5), because it only happens on this host, but across completely different unrelated accounts.
      When it happens the table in question gets "marked as crashed", and then shows "in use" when you see the table in PHPMyAdmin.  No data is retrievable by PW from whichever table/field is crashed, so if the body table crashes, then the front end doesn't show any body text anymore until someone goes into PHPMyAdmin and repairs the table. I'm trying to make a module or at least some button the client can click from their admin that will run a repair on the tables so i don't have to help them and go to their cPanel etc..
      I added a button on the dashboard of the sites in question (for sites that i use a dashboard on), or i told them to bookmark the link to the repair process, something like; so far it seems to work but wanted to check to see if anyone sees any problems or improvements to this, it was done in only a few minutes, so may have left out something...
      I'm not sure if this could/should be made into a module, since it is conceivable that a table could crash that would render the modules system non functional, so thought maybe better to be a bootstrapped script(?)
      <?php // in root of pw installation - this is the 3.0+ version; repair_database.php /* Bootstrap PW ----------------------------------------- */ include("/home/path/to/index.php"); $config = \ProcessWire\wire('config'); $user = \ProcessWire\wire('user'); if(!$user->isLoggedin()) die("access denied"); function optimizeTables() { $tables = array(); $db = \ProcessWire\wire('db'); $result = $db->query("SHOW TABLES"); while ($row = $result->fetch_assoc()) { $tables[] = array_shift($row); } foreach ($tables as $table) { $result = $db->query("OPTIMIZE TABLE `$table`"); while ($row = $result->fetch_assoc()) { echo $row['Table'] . ': ' . $row['Msg_text'] . "<br /> \n"; } } } function repairTables() { $tables = array(); $db = \ProcessWire\wire('db'); $result = $db->query("SHOW TABLES"); while ($row = $result->fetch_assoc()) { $tables[] = array_shift($row); } foreach ($tables as $table) { $result = $db->query("REPAIR TABLE `$table`"); while ($row = $result->fetch_assoc()) { echo $row['Table'] . ': ' . $row['Msg_text'] . "<br /> \n"; } } } ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Database Repair &amp; Optimize Tool</title> </head> <body> <pre> ____ _ ____ __ __ / __ \___ ____ ____ _(_)____ / __ \____ _/ /_____ _/ /_ ____ _________ / /_/ / _ \/ __ \/ __ `/ / ___/ / / / / __ `/ __/ __ `/ __ \/ __ `/ ___/ _ \ / _, _/ __/ /_/ / /_/ / / / / /_/ / /_/ / /_/ /_/ / /_/ / /_/ (__ ) __/ /_/ |_|\___/ .___/\__,_/_/_/ /_____/\__,_/\__/\__,_/_.___/\__,_/____/\___/ /_/ </pre> <?php if($input->action == 'repair') { repairTables(); } if($input->action == 'optimize') { optimizeTables(); } ?> </body> </html> example button: