Google Charts for DBA: Tablespaces Allocation

Feb 15, 2009 / By Alex Gorbachev

Tags:

Pythian DBA’s have daily reports for each monitored database and some of the components are using charts to visualize the data. I’m a big fan of charts myself (when applied appropriately) and want to show how you can generate simple charts directly from the database. You’d be very surprised how easy it can be done from *any* database without installing any additional software or configuring something special.

This method is not limited to Oracle by any means — use it with MySQL, SQL Server or any other database as well as without a database — yes, visualize your sar data now!

In this example, we will plot a pie diagram with Oracle tablespaces. This would be very handy when you are starting to analyze the space allocation for a database. Here is the end result of the report for my Grid Control repository test database:



The secret weapon is Google Chart API — it produces nice charts by sending specially crafted URL’s.

Here is the PL/SQL block that generates required URL:
Updated 16-FEB-09 AEDT: fixed the query to group by tablespace_name. Thanks to Neil Kodner.
Updated 21-FEB-09 AEDT: fixed serverout. Thanks Mike.

SET define OFF
SET serverout ON
DECLARE
  t    VARCHAR2(30);
  u    NUMBER;
  chd  VARCHAR2(4000);
  chdl VARCHAR2(4000);
  chl  VARCHAR2(4000);
  CURSOR c
  IS
     SELECT tablespace_name tsname,
      ROUND(bytes / SUM(bytes) over () * 100, 2) pct,
      ROUND(bytes / 1024 / 1024) mb
       FROM
      (SELECT tablespace_name,
        SUM(bytes) bytes
         FROM dba_data_files
     GROUP BY tablespace_name
      )
 ORDER BY 3 DESC;
BEGIN
  FOR usage IN c
  LOOP
    IF chd IS NULL THEN
      chd  := usage.pct;
    ELSE
      chd := chd || ',' || usage.pct;
    END IF;
    IF chdl IS NULL THEN
      chdl  := usage.tsname;
    ELSE
      chdl := chdl || '|' || usage.tsname;
    END IF;
    IF chl IS NULL THEN
      chl  := usage.mb;
    ELSE
      chl := chl || '|' || usage.mb;
    END IF;
  END LOOP;
  dbms_output.put_line('http://chart.apis.google.com/chart?cht=p&chs=400x200'
    || '&chtt=Database Tablespaces (MB)'
    || '&chl=' || chl || '&chd=t:' || chd || '&chdl=' || chdl);
END;
/

In this example, we prepare 3 sets in the cursor for loop — chart data (percentage), chart labels (tablespace sizes in MB) and chart legend labels. At the end, the code prints out the URL including pre-generated sets as well as some other attributes like chart size and title. Google Chart API has all the glory details.

Here is the result:

SQL> @usage_chart
http://chart.apis.google.com/chart?cht=p&chs=400×200&chtt=Database Tablespaces (MB)&chl=1960|400|250|160|100|5&chd=t:68,14,9,6,3,0&chdl=MGMT_TABLESPACE|SYSTEM|UNDOTBS1|SYSAUX|MGMT_ECM_DEPOT_TS|USERS

PL/SQL procedure successfully completed.

Now just copy this URL into your browser and you are done! Make sure you copy it as one line without breaks. If you want to take it further, you can produce the whole HTML pages as reports with <img> tags.

OK. I see you are rushing to try it out and generate your own charts… Good for you but don’t be selfish — share your helpful code fragments with the community! ;-)

36 comments on “Google Charts for DBA: Tablespaces Allocation

  1. Jornica on said:

    By using Oracle Application Express (APEX), you can skip the “copy the URL step” by integrating the PL/SQL block in an dynamic generated HTML page. For instructions see http://jornica.blogspot.com/2007/12/integrating-google-chart-api-in-apex.html.

    With kind regards,

    Jornica

  2. Very clever trick.

    I would add that this can be used directly in a CGI script, in particular you could issue a Redirect header to the generated URL.

  3. That’s seriously a great idea. I am pondering how to add this to some of the Maatkit tools right now.

  4. Asif Momen on said:

    Alex,

    Thanks for sharing this info. Its working like a charm.

    But a small correction is required, the pl/sql block is anonymous and you need to convert it into stored procedure (usage_chart).

  5. thierry on said:

    If you use Apex to show the chart, why not using the build in Chart functionalities of Apex: Flash (anychart), svg, …?

  6. Alex Gorbachev on said:

    Thanks to all leaving the feedback. Keep ‘em coming…

    @Jornica: Being a production DBA, it’s practically impossible to add APEX to each *production* instance. I had few other ideas how to try to make it real-time and generate pages from the DB directly:
    * I thought to try configuring Oracle Protocol Server (used for XDB) but I couldn’t figure out quickly how to call specific procedure. Perhaps, it can be returned as XML and XSLT-processed to get the right format but that’s above that simple example I needed.
    * I thought to write a simple HTTP server in Perl using HTTP::Daemon and forward requests to the DB.
    Any other ideas?

    @Toby: integrating into CGI would be cool. Any idea how to make it *without* installing anything on the server? I keep thinking about light-weight Perl HTTP sever.

    @Barob: Woohoo! Great idea.

    @Asif: I didn’t want to create a PL/SQL stored object deliberately — some production environments prefer to avoid it. In my example, I created file usage_chart.sql and called it from SQL*Plus using ‘@’ directive. For procedure, I would use EXEC, so no corrections on this part.

  7. Alex Gorbachev on said:

    @thierry: Again, APEX requires rolling out the whole environment. We can argue as much as we want that it’s dead easy. The reality is — APEX doesn’t get deployed to production environments to give DBA’s a tool to plot performance stats and etc.

  8. neil kodner on said:

    Nice article! I’m a huge fan of the Google Charts API. This query works better if you have more than one datafile in any of your tablespaces. I think you’ve inspired me to create a stacked bar graph which will show allocated vs free space in each tablespace.

    SELECT tablespace_name tsname
    , SUM(bytes)/1024/1024 mb
    , ROUND(SUM(bytes)/1024/1024 / SUM(SUM(bytes)/1024/1024) OVER () *100,2) pct
    FROM dba_data_files
    GROUP BY tablespace_name
    ORDER BY 3;

  9. Alex Gorbachev on said:

    @neil: Oh no. Shame on me! :) I forgot it while playing with analytical functions. Thanks — fixed now.

    However, it has to be a bit more complex as I couldn’t combine analytical functions with standard GROUP BY. I’m sure it’s jut my “deep” knowledge of analytical functions. :)

  10. neil kodner on said:

    did you run the query I posted? :D

  11. William Robertson on said:

    You could also just call the browser from the command line, e.g. in Windows with Firefox:

    set define ~
    set autoprint off

    var chart_url varchar2(1000)

    DECLARE
    chd VARCHAR2(4000);
    chdl VARCHAR2(4000);
    chl VARCHAR2(4000);

    CURSOR c IS
    SELECT tablespace_name tsname
    , ROUND(bytes / SUM(bytes) OVER() * 100, 2) pct
    , ROUND(bytes / 1024 / 1024) mb
    FROM ( SELECT tablespace_name
    , SUM(bytes) bytes
    FROM dba_data_files
    GROUP BY tablespace_name )
    ORDER BY 3 DESC;
    BEGIN
    FOR usage IN c LOOP
    IF chd IS NULL THEN
    chd := usage.pct;
    ELSE
    chd := chd || ‘,’ || usage.pct;
    END IF;

    IF chdl IS NULL THEN
    chdl := usage.tsname;
    ELSE
    chdl := chdl || ‘|’ || usage.tsname;
    END IF;

    IF chl IS NULL THEN
    chl := usage.mb;
    ELSE
    chl := chl || ‘|’ || usage.mb;
    END IF;
    END LOOP;

    :chart_url := ‘http://chart.apis.google.com/chart?cht=p&chs=400×200′ ||
    ‘&chtt=Database Tablespaces (MB)’ || ‘&chl=’ || chl || ‘&chd=t:’ || chd || ‘&chdl=’ || chdl;

    dbms_output.put_line(:chart_url);

    :chart_url := replace(:chart_url,’ ‘,’%20′);
    END;
    /

    col chart_url new_value chart_url
    select :chart_url from dual;

    ho “C:Program FilesFirefoxfirefox.exe” “~chart_url”

  12. William Robertson on said:

    …only without the smart quotes…

  13. Tanel Poder on said:

    Hi Alex, nice trick.

    In sqlplus you can do something like this to automate the trick further:

    set define ^

    host start ^url

    set define &

    (or on linux with firefox you could run host firefox ^url) instead

  14. Kim Njeru on said:

    Hi Alex.
    I like it a lot! This opens up a whole new other world.

  15. Alex Gorbachev on said:

    @neil: Ah, I see the trick now. I even understand why. ;-)

    @William & @Tanel: Good idea. Cheers.

    @Kim: Very welcome. Share your new world with us. ;-)

  16. William Robertson on said:

    @Tanel: I tried “start” first (rather than hardcoding an application path) but XP couldn’t seem to detect the type and just opened a blank command window.

    btw small bug in my version – the “new_value” expression at the bottom needs to take the leading colon of “:chart_url” into account (or the column needs an alias).

    @neil: The “Percent” expression could also be “ROUND(100 * RATIO_TO_REPORT(SUM(bytes/1048573)) OVER (),2)”.

  17. Mike O'Regan on said:

    -set server_out on

    +set serverout on

  18. Andy Lester on said:

    I wouldn’t write your daemon with HTTP::Daemon directly. Use something like HTTP::Server::Simple which takes care of a lot of the drudgery.

  19. Pingback: Log Buffer #136: A Carnival of the Vanities for DBAs

  20. Alex Gorbachev on said:

    @Mike: Hehe… how did I mange to screw this one up? :) thanks. fixed.

    @Andy: Yep. You are right — I had a look at HTTP::Server::Simple and its CGI stuff — looks dead simple.

  21. Re: CGI
    One of my favourite hacks is calling the mysql client from a bash CGI. Doesn’t even need Perl.

    e.g.

    #!/bin/bash
    echo Content-type: text/plain
    echo
    mysql -u foo -pbar db -B -e ‘SELECT name FROM emp’

    A similar technique could emit the Redirect from your query.

  22. @toby

    ouch, yes its nice it produces html output. but static passwords readable my a webserver, that is likely readable by anyone else that has a cgi on that system isn’t something that should be a favoured practice.

  23. John Dixon on said:

    Updated by Alex Gorbachev (formatting)

    Hi Alex,
    Great idea…this got me thinking about a stacked bar chart for tablespace usage, so I’ve modified your script a little.
    An example of the result is:

    Modified script is as follows. Thanks again.

    SET define OFF
    SET serverout ON
    DECLARE
    t VARCHAR2(30);
    u NUMBER;
    chd VARCHAR2(4000);
    chd2 VARCHAR2(4000);
    chxl1 VARCHAR2(4000);
    chxl2 VARCHAR2(4000);
    chxl VARCHAR2(4000);
    chl VARCHAR2(4000);
    CURSOR c
    IS
    select ts.tablespace_name tsname,round((1-(free/total))*100) as used_pct, round((free/total*100)) as free_pct from
    (select tablespace_name,sum(bytes) as total from dba_data_files group by tablespace_name) ts,
    (select tablespace_name,sum(bytes) as free from dba_free_space group by tablespace_name) fs
    where ts.tablespace_name=fs.tablespace_name order by ts.tablespace_name;

    BEGIN
    FOR usage IN c
    LOOP
    IF chd IS NULL THEN
    chd := usage.used_pct;
    ELSE
    chd := chd || \',\' || usage.used_pct;
    END IF;
    IF chd2 IS NULL THEN
    chd2 := usage.free_pct;
    ELSE
    chd2 := chd2 || \',\' || usage.free_pct;
    END IF;

    IF chxl1 IS NULL THEN
    chxl1 := usage.tsname;
    ELSE
    chxl1 := chxl1 || \'|\' || usage.tsname;
    END IF;
    IF chxl2 IS NULL THEN
    chxl2 := usage.used_pct;
    ELSE
    chxl2 := usage.used_pct ||\'|\'||chxl2;
    END IF;
    END LOOP;
    chd := chd ||\'|\'|| chd2;
    chxl:=\'1:|\'||chxl1||\'|2:|\'||chxl2;
    dbms_output.put_line(\'http://chart.apis.google.com/chart?cht=bhs&chs=400x200&chco=4D89F9,C6D9FD\'
    || \'&chtt=Tablespace Usage (%)\'
    || \'&chxt=x,y,r\'
    || \'&chl=\' || chl || \'&chd=t:\' || chd || \'&chxl=\' || chxl);
    END;
    /

  24. Alex Gorbachev on said:

    Thanks John. I modified your comment for formatting if you don’t mind.

  25. Pingback: Realizar gráficas con MySQL y Google Graph | dominios, diseño web, ecommerce - Mantis Technology Solutions Blog

  26. Andy Rivenes on said:

    Hi Alex, cool thread. There is a problem with display for lots of tablespaces. In the code the chart is set to 400×200 which works great for 6 tablespaces. It looks like the max Google supports is 300000 pixels, so the only thing to add is some restriction on the number of tablespaces charted per URL based on some useful criteria. Perhaps sorted by free space left for the top n tablespaces that will fit in the allocated chart size.

  27. Pingback: Gildus» Blog Archive » Realizar gráficas con MySQL y Google Graph

  28. Alex Gorbachev on said:

    Hi Andy,

    Thanks for the feedback. Glad you liked it.

    It’s indeed a rather trivial example and I didn’t want to complicate the query. The way to do it is to display top 10 or top 20 tablespaces by size or so. The rest would be aggregated as “Others” but it would complicate the query and make it more difficult to understand.

    Cheers,
    Alex

  29. Google Chart API Team on said:

    This is great! We’ve added it to the list of related links in the documentation:

    http://groups.google.com/group/google-chart-api/web/useful-links-to-api-libraries?hl=en

    Thanks and keep up the cool work!

    The Google Chart API Team

  30. Alex Gorbachev on said:

    Thanks Google Chart Team! Keep up good work!

  31. Made a minor change to John Dixon’s bar chart script to properly line up tbspace name and % used:

    SET define OFF
    SET serverout ON
    set lines 500

    DECLARE
    t VARCHAR2(30);
    u NUMBER;
    chd VARCHAR2(4000);
    chd2 VARCHAR2(4000);
    chxl1 VARCHAR2(4000);
    chxl2 VARCHAR2(4000);
    chxl VARCHAR2(4000);
    chl VARCHAR2(4000);
    dbname varchar(10) := \'\';
    CURSOR c
    IS
    select ts.tablespace_name tsname,round((1-(free/total))*100) as used_pct, round((free/total*100)) as free_pct from
    (select tablespace_name,sum(bytes) as total from dba_data_files group by tablespace_name) ts,
    (select tablespace_name,sum(bytes) as free from dba_free_space group by tablespace_name) fs
    where ts.tablespace_name=fs.tablespace_name order by ts.tablespace_name;
    BEGIN
    select database_name into dbname from v$database;
    FOR usage IN c
    LOOP

    IF chd IS NULL THEN
    chd := usage.used_pct;
    ELSE
    chd := chd || \',\' || usage.used_pct;
    END IF;

    IF chd2 IS NULL THEN
    chd2 := usage.free_pct;
    ELSE
    chd2 := chd2 || \',\' || usage.free_pct;
    END IF;

    IF chxl1 IS NULL THEN
    chxl1 := usage.tsname;
    ELSE
    chxl1 := usage.tsname|| \'|\' ||chxl1;-- || \'|\' || usage.tsname;
    END IF;

    IF chxl2 IS NULL THEN
    chxl2 := usage.used_pct;
    ELSE
    chxl2 := usage.used_pct ||\'|\'||chxl2;
    -- chxl2 := chxl2 ||\'|\'||usage.used_pct;
    END IF;
    END LOOP;
    chd := chd ||\'|\'|| chd2;
    chxl:=\'1:|\'||chxl1||\'|2:|\'||chxl2;
    dbms_output.put_line(\'DATABASE: \'||dbname);
    dbms_output.put_line(\'http://chart.apis.google.com/chart?cht=bhs&chs=400x600&chco=4D89F9,C6D9FD\'
    || \'&chtt=Tablespace_Usage_(%)_for_SID:_\'||dbname
    || \'&chxt=x,y,r\'
    || \'&chl=\' || chl || \'&chd=t:\' || chd || \'&chxl=\' || chxl);

    END;
    /
    quit

  32. Alex Gorbachev on said:

    Thanks Hoang. I edited your post — added the code formatting.

  33. poratips on said:

    Hi Alex,
    Its a wonderful articles and ver very helpful for people woh works with DB.
    I tried to copy your code but i am getting follwoing error even though i have SET serverout ON size 1000000.
    Could you please email me the solution to how to correct this error?
    I tried to google it but couldn’t fine nay useful information which i can use it.
    I am running Oracle 9i R2.

    My email is: [redacted]

    Thanks in advance!

  34. Alex Gorbachev on said:

    @poratips: Thanks for your feedback. did you forget to paste your error?

  35. kartik on said:

    Hi Alex,

    I tried this scirpt with one of our test isntance.. the proc gor excuted and URL also was displayed but whenI copied in the ie.. it says Bad Request .. i have cleared cahce and cookies as well.. any thoughts as to why this is happening??

  36. Pingback: A Faster MySQL Database Size Google Chart « JZ Talk Blogger

Leave a Reply

Your email address will not be published. Required fields are marked *

*

HTML tags are not allowed.