Explain Plans PostgreSQL 9.0 - Part 2: JSON and JQuery Plan Viewer

In part 1 of this series on PostgreSQL 9.0 planner outputs, we demonstrated how to render explain plans in YAML, JSON, and XML using the new explain features in PostgreSQL 9.0. In this second part, we'll demonstrate how to build a user interface that allows you input a JSON formatted explain plan and have it render into a printable, navigateable display using JQuery, javascript and a little bit of HTML coding. In part 3 we'll do something similar using XML and XSLT programming.

For those who aren't familiar with JQuery, it is an MIT licensed javascript library that is fairly light weight and allows you to inspect and change html elements with fairly intuitive syntax, has some nice ajax methods and tools for converting xml/json to native objects that can be manipulated. You can check it out at JQUERY.

We are not experts in JQuery, but from what we have used of it, we really like it and the fact the base package is MIT licensed, fairly light weight and lots of plugins available for it are real pluses.

The most difficult thing I think most people find about reading explain plans is that they are upside down; it starts with a conclusion and backtracks how to arrive at it. Humans by nature think about planning steps from start to finish. In order to make an explain plan understandable to mere mortals, we generally display them upside down or having the child-nodes shown left most. We shall follow that approach.

Our Interface for inputing plans

For this exercise and subsequent exercise, we are going to create a simple web form interface that has a few parts.

For those that are impatient, our little interface can be tried here PostgreSQL JSON Explain Plan Viewer. We left a snippet of JSON in the text area for easier testing. That JSON can be replaced with any PostgreSQL Explain Plan JSON output.

You can also download the source code from here

The basic form looks like

The html for our simple form is shown below

<form id="frmplanner">
    <b>Paste JSON Planner Explain </b><br />
    <b>Cost Flag Limit</b> <input type="text" id="costflag" name="costflag" value="10" /> 
    <br />
    <textarea id="planneroutput" name="planneroutput" cols="80" rows="10"></textarea><br />
    <input type="button" id="cmdrender" name="cmdrender" value="Render" onclick="drawPlan()"/> 
    <input type="button" value="Toggle Show/hide Detail" onclick="toggleDetail()" /> 
</form>

The breathtaking visual plan

Now we come to the harder part of rendering our mouth watering explain plan view. For this piece we have chosen to use JQuery and a JQuery plugin called corner plugin, to get some stylistic rouned corners.

Since we are lazy programmers who lack any artistic ability, we borrowed some of the images used for explain plans from PgAdmin III project. The problem with borrowing those icons, is that we needed them in PNG or JPG format, but PgAdmin source code has them in xpm format in the \include\images folder of the project.

ImageMagick, another great Apache style licensed open source toolkit, came to our rescue to convert XPM -> PNG. The trick we used was to use ImageMagick mogrify commandline tool.

mogrify -format png *.xpm

When we are done with all our programming, our rendered plan will look like this (when detail is hidden):

Nodes will look like below when expanded. We only show 2 in this picture since it takes up a lot of space.

The JQuery secret sauce

The key elements for unraveling the JSON plan are:

full javascript is shown below

var detailState = 'show'
    
    function drawPlan(){
        var obj = jQuery.parseJSON($('#planneroutput').val());
        $('#planholder').html("<table id='tblplan'></table>");
        drawSubPlan(obj[0].Plan, '#tblplan', 'plan');
        detailState = 'show'
    }

    function getPlanImg(pgplan) {
        var imgname = "ex_" +  pgplan['Node Type'].replace(' ', '_').toLowerCase()
        if ("Join Type" in pgplan) {
            if ((pgplan['Join Type'] == 'Left' || pgplan['Join Type'] == 'Left')) {
                imgname += "_semi_join"
            }
            else
            if (pgplan['Join Type'] != 'Inner') {
                imgname += pgplan['Join Type'].replace(' ', '_').toLowerCase()
            }
        }
        imgname += ".png"
        return imgname
    }
    function drawSubPlan(pgplan, elid, pname) {
        var curplan;
        var str;
        var dive = $(elid);
        var childpanel;
        /** We insert the cell before so our display goes in opposite direction of the JSON structure **/

        curplan = pname;
        childpanel = pname + 'childs';

        dive.append("<tr><td><table id='"
            + pname + "childs'></table></td><td><div id='"
            + curplan
            + "' class='pgplan' style='background-repeat:no-repeat;background-image:url(images/" + getPlanImg(pgplan) +")'>"
            + pgplan['Node Type']
            + " <br /><div class='pgplandetail'>" 
            + getPlanDetail(pgplan)
            + "</div></div></td></tr>")
        dive = $('#' + pname + 'childs');
        $('#' + curplan).corner();
        /** highlingt the plan red if the cost is above our cut off **/
        if (parseFloat(pgplan['Total Cost']) > parseFloat($('#costflag').val())) {
            $('#' + curplan).css('background-color', 'red')
        }
        /** If this plan has sub plans, unravel them **/
        if ("Plans" in pgplan) {
            for (i = 0; i < pgplan.Plans.length; i++) {
                curplan = pname + '_' + i;
                drawSubPlan(pgplan.Plans[i], '#' + childpanel, curplan);
            }
        }
    }
    
    function getPlanDetail(obj){
        var temp = "";
        if ("Plans" in obj) {
            temp += '<b># sub plans</b>: ' + obj.Plans.length + "<br />";
        }
        for (x in obj){
            if (x != 'Plans'){
                temp += "<b>" + x + "</b>: " + obj[x] + "<br />";  
            }
        }

        return temp; 
        
    }
    
    function toggleDetail(){
        if (detailState == 'show'){
            $('.pgplandetail').hide('slow');  
            detailState = 'hide';
        }
        else {
            $('.pgplandetail').show('slow');  
            detailState = 'show';
        }
    }