[postgis-users] Improving Performance

David Blasby dblasby at refractions.net
Wed Feb 18 15:33:29 PST 2004


I was recently doing some spatial joins between a table with 10,000 
points to a table with 17,000,000 lines.

Here's some of my thoughts on the matter.  In the middle of this 
message, I give 4 ideas for improving speed.  The last part of the 
message is a more in-depth analysis of one of these techniques.


The query I was running looked something like:

  EXPLAIN ANALYSE
  SELECT  ...
  FROM all_lake_points, csn_edges
  WHERE
    csn_edges.the_Geom && expand(all_lake_points.the_Geom,0.5)
    ;

I repeatedly ran the process on one of our machines, and got
the following times (it returns about 40,000 rows):

Total runtime: 113813.259 ms  (very low CPU utilization - <5%)
Total runtime:  51843.412 ms
Total runtime:  37013.140 ms
Total runtime:  27280.564 ms
Total runtime:  17084.181 ms
Total runtime:  12869.891 ms
Total runtime:  10867.587 ms
Total runtime:  10803.591 ms (very high CPU utilization - >70%)

On another machine:

  Total runtime: 163636.58 msec
  Total runtime:  65834.27 msec
  Total runtime:   9506.53 msec
  Total runtime:   7880.42 msec


Starting and stopping the postmaster between queries doesnt affect
these times, so the diminishing time is a result of the OS caching
the Spatial Index and actual csn_edges tuples.

The size of the spatial index is approximately O(n log n),
where n= number of geometries being index.

Since the index is composed of a BOX (xmin,ymin, xmax, ymax -- all 
doubles) of size 32, the actual index size is:

17,000,000 rows * 32bytes/row  + 17,000,000 rows * GiST overhead +
(index hiearchy overhead)

= 520 mb + 340mb + hierarchy overhead
= 860 mb + hierarchy overhead

The GiST overhead is quite large - see ggeometry_compress() - its about 
21 bytes.  The index hierarchy account for the "internal" nodes in the 
GiST tree.

I looked in the postgresql data directory, and it appears that the index 
is actually 1.2 Gb.
The table csn_edges is about 14 Gb.

The query actually works like this:

get a row from all_lake_points
make a bounding box
Search csn_edges GiST index using the bounding box:
    GiST init
    Traverse GiST index
    For each matching GiST entry
        get tuple from csn_edges



Experimentation (with explain analyse) leads me to believe that an 
uncached index search takes about 100 to 400 ms.  A search with the 
index mostly cached is about 100ms, and with the index and actual tuples 
cached, about 1.5ms.
This is also verified with the timings given above.

The very low CPU utilization numbers indicate that the uncached version 
is IO bound, while the cached version is CPU bound.  Running the same 
query serveral times hints to the OS to cache those disk pages.

If a GiST entry is 32 (box) + 21 (GISTENTRY) =53 bytes, there are about 
154 on an 8k disk page.


Immediately, I see four ways to improve performance:

1. Have the OS allocate more space to the disk cache
2. Have the index be smaller
3. Have the actual tuples be smaller on disk
4. Order the table so that nearby geometries are very close together on 
the disk.

#1 is up to the system administrator
#3 we can change the size of a geometry by a small amount or go to a
    WKB-type datatype like I've talked about in a few discussions.  This
    will not be a major difference unless your individual geometries only
    have a few points in them.
#4 There's no easy way to do this.  One way would be to make a Quad-tree
    type index for your geometries and insert them into the database on a
    bucket-by-bucket basis.
    (The PostGIS stats package does something like this in the
    histogram2d functions)

    This makes queries faster because it should minimize the number of
    *different* disk pages a query will access.
    It still reads the same number of tuples off the disk, but these
    tuples are more likely to be on the same page.  For example, if your
    tuples are 1kb long and the disk page size is 8kb and you're
    selecting 1000 tuples.  With random location,
    you'll likely read 1000 disk pages, but with a high degree of
    auto-correlation you could read as few as 1000/8 = 125 pages.

    This is definately worth persuing - but its a fair bit of work and
    would probably be an external program instead of an SQL command. 
Its
    hard to say how much this would improve performance.



I'd like to talk more about #2 (smaller index).

I'm proposing we change the index type (currently BOX - 32 bytes) to a:

typedef struct BOX2DFLOAT_
{
     xmin float;
     xmax float;
     ymin float;
     ymax float;
} BOX2DFLOAT;


This saves 16 bytes, meaning we go from 154 GiST entries/page to 221 
(43%).  My original thoughts one this was it would be a 50% saving (16 
vs 32), but the 21 byte GiST overhead reduces this. In the end my GiST 
index goes from about 1.2 Gb to 840 Mbs...

We'll need to write a function that takes a BOX3D and converts it to a 
BOX2DFLOAT.  The BOX2DFLOAT's xmin/ymin will have to be strictly less 
than the BOX3D's xmin/ymin to ensure that all the queries work properly.

This will make all the search boxes "bigger" by about 1m if the 
coordinates are in the 10,000,000 ranges (a float4 cannot represent 
small differences in numbers as well as a float8). For boxes near the 
origin, we'll only see a few micrometers difference.
This means the bounding box queries will always work correctly, but they 
might grab a few more nearby "canidate" geometries.

Next, we'll have to write a set of BOX2DFLOAT high-level GiST support 
functions, and a set of low-level GiST support functions.  These are all 
quite easy.


At the end of this, the index query should be a bit faster, require 
about 40% less disk space, and require about 40% less cache space.

In the end, the biggest wait will be pulling individual csn_edges tuples 
from the disk (see comments on #4, above).


dave

For DAVE's reference only:
-------------------------

  explain analyse  SELECT  all_lake_points.gid, all_lake_points.the_Geom 
,csn_edges.code, csn_edges.edge_id,csn_edges.group_code 
,startpoint(csn_edges.the_geom), endpoint(csn_edges.the_geom)
   WHERE
    all_lake_points.gid >= 650001 and
    all_lake_points.gid <= 660000 and
    all_lake_points.code != 1400 and
    csn_edges.the_Geom && expand(all_lake_points.the_Geom,0.5) and
    csn_edges.code in (1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 
1400, 1410, 1425, 1450, 1475, 2000, 2300, 6010)
    ;




More information about the postgis-users mailing list