October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk8 min

Geospatial Scale: Architecting PostGIS in Laravel 🗺️

A practical guide to PostGIS in Laravel on PostgreSQL: enabling the extension, choosing geometry or geography, creating GiST indexes, writing ST_DWithin queries and checking the plan before scaling.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To run location-aware queries in Laravel on PostgreSQL, enable PostGIS, store each location as a typed geography(POINT,4326) column, index it with GiST, and filter nearby-location searches with ST_DWithin instead of computing distance in the WHERE clause. Laravel can declare the spatial column, but PostgreSQL decides whether the spatial index is used, so most of the architecture choices sit at the database boundary. The official PostGIS and Laravel pages do not publish a row count, latency target, or partitioning trigger, so any scaling decision has to rest on measurements from your own workload.

Confirm PostGIS is installed before you write a migration

A PostgreSQL geography column cannot exist until the PostGIS extension is available in the database. The Laravel 11.x migrations documentation states that PostgreSQL users must install PostGIS before using the geography method, so set the extension up first. Check the version you run against the framework’s migrations page: https://laravel.com/framework/docs/11.x/migrations.

As an Amazon Associate I earn from qualifying purchases.

  1. Check that the server ships PostGIS. Connect with psql and run:
    SELECT name, default_version FROM pg_available_extensions WHERE name = 'postgis';

    A returned row means the extension files are installed. An empty result means the PostGIS package for your PostgreSQL major version must be installed on the server, or your hosting provider must enable it.

  2. Enable the extension in the application database with CREATE EXTENSION IF NOT EXISTS postgis;. Run it as a role that is allowed to create extensions in that database.
  3. Record the installed version with SELECT PostGIS_Version();. Function availability and units are determined by the PostGIS version you deploy, so check examples against that version rather than against whichever manual page you read first.
  4. Confirm that the connection in config/database.php uses the pgsql driver. Laravel’s database documentation covers the connection options: https://laravel.com/framework/docs/database.

Choose geometry or geography for each location column

PostGIS provides two spatial types, and the choice changes what coordinates mean and what distance functions return. A geography column measures on the earth’s surface, and the standard example uses WGS84 (SRID 4326) longitude and latitude. A geometry column treats coordinates as points on a plane, so its measurements are in whatever units the column’s SRID defines. The PostGIS management chapter documents both types: https://www.postgis.net/docs/using_postgis_dbmanagement.html.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Question geography geometry
Coordinate model Earth-surface (spheroid-aware) measurements Planar, in the units of the column’s SRID
Distance units from ST_Distance and ST_DWithin Meters, for the standard geography functions Units of the SRID; degrees when the SRID is 4326
Function coverage Narrower than geometry; confirm each function for your PostGIS version Broadest set of functions and operators
Typical fit Global point data, such as store or user locations, where distance in meters matters Data in a local projected coordinate system, or workloads that need geometry operations

For global point data, geography(POINT,4326) is the usual starting point. Avoid the common mistake of putting SRID 4326 coordinates in a geometry column and then passing a radius you think is in meters. The query runs, but the radius is interpreted in degrees.

Create the table, the column and the GiST index

Keep the schema in a migration and keep the spatial SQL explicit. Laravel has spatial column methods, but the generated SQL and the exact method signature depend on the framework version, so the raw statements below are the safe baseline. The index is created on an empty table here, so a plain CREATE INDEX is fine.

<?php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesDB;
use IlluminateSupportFacadesSchema;

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE EXTENSION IF NOT EXISTS postgis');

        Schema::create('places', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });

        DB::statement('ALTER TABLE places ADD COLUMN location geography(POINT,4326) NOT NULL');
        DB::statement('CREATE INDEX places_location_gist ON places USING GIST (location)');
    }

    public function down(): void
    {
        Schema::dropIfExists('places');
    }
};

The GiST index is required for spatial search. The PostGIS FAQ explains that spatial columns use USING GIST and that a conventional B-tree index will not help spatial queries: https://postgis.net/documentation/faq/spatial-indexes/.

Write locations with bound parameters

Build the point in the order PostGIS expects, longitude first and latitude second. Swapping them stores a valid-looking point in the wrong place, and no error is raised for many coordinate pairs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
DB::insert(
    'INSERT INTO places (name, location, created_at, updated_at)
     VALUES (?, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, now(), now())',
    [$name, $longitude, $latitude]
);

Write queries PostgreSQL can accelerate

Nearby places with ST_DWithin

ST_DWithin is the index-aware predicate for “within this distance”. PostGIS can use a bounding-box prefilter from the spatial index to narrow the candidate rows, then calculate the exact distance only for those candidates. With a geography column, the radius is in meters. The query below returns up to 20 places within the radius, closest first:

$nearby = DB::select(
    'SELECT id, name, ST_Distance(location, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography) AS meters
     FROM places
     WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ?)
     ORDER BY meters
     LIMIT 20',
    [$longitude, $latitude, $longitude, $latitude, $radiusMeters]
);

The bindings follow the placeholder order: the select-list point first, then the point and radius in the WHERE clause. Once ST_DWithin has filtered the rows, computing distance in the select list and sorting by it is fine.

The distance filter that scans every row

Filtering on ST_Distance looks equivalent but does not give the planner an index-aware predicate. The PostGIS query chapter describes this pattern as computing the distance for each row, and ST_DWithin as the index-aware alternative: https://postgis.net/docs/manual-dev/en/using_postgis_query.html (the development manual; check the page against your deployed release).

-- Avoid as the main filter on a large table
WHERE ST_Distance(location, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography) < ?

-- Prefer
WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ?)

Relationship predicates for zones and boundaries

For polygons such as delivery zones, ST_Intersects, ST_Contains and ST_Within are the relevant predicates, and they are index-aware. They are not interchangeable. A point lying exactly on a polygon’s boundary is matched by ST_Intersects but not by ST_Contains. Choose the predicate by that boundary rule, not by which name reads more naturally.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Ordering without a radius

Sorting by distance is safe after a radius filter. For nearest-neighbour searches without a radius, or for queries that combine distance with other filters, check the ordering operators the PostGIS query chapter documents for your column type, then read the query plan rather than assuming an index is used.

Choose the index type from data shape and update pattern

GiST is the starting point. Consider the alternatives only when your data layout or update pattern gives a reason, and compare them against your real queries.

GiST: the default choice

The PostGIS management chapter describes GiST as the most commonly used and most versatile spatial index. It handles mixed, unordered locations and ongoing inserts and updates without special maintenance. The cost is that it is generally larger and slower to build than BRIN for the same data, so measure index size and build time on a realistic copy of the table.

BRIN: only for spatially ordered, rarely changed data

BRIN can fit when rows are physically correlated with location, for example when data is loaded in spatial order, and when updates are infrequent. It stores a summary per range of table pages, so it is smaller and faster to build than GiST. The trade-offs are that it is lossy, so candidate rows are rechecked, and that new ranges are not summarized automatically. Run brin_summarize_new_values() or VACUUM after loads, and confirm which operator classes your PostGIS version offers for the column type before creating the index.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

SP-GiST: an alternative to test

SP-GiST is a partitioned search-tree index and is worth evaluating as an alternative. Official documentation does not rank it above GiST for general spatial workloads, so test it on your own data and query mix before adopting it, and confirm operator class support for your PostGIS version.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Build indexes on a table that is already taking writes

A plain CREATE INDEX on an existing large table blocks writes for the duration of the build. CREATE INDEX CONCURRENTLY avoids that blocking at the cost of a slower build, and the PostGIS management chapter documents this approach:

CREATE INDEX CONCURRENTLY places_location_gist ON places USING GIST (location);

PostgreSQL does not allow CREATE INDEX CONCURRENTLY inside a transaction block. A migration runner that wraps each migration in a transaction will reject it, so confirm how your Laravel version wraps migrations and run the statement in a separate step if needed.

  • If the build fails, PostgreSQL can leave an invalid index behind. Check with SELECT indexrelid::regclass, indisvalid FROM pg_index WHERE indrelid = 'places'::regclass;. Drop an invalid index with DROP INDEX CONCURRENTLY places_location_gist; and retry.
  • After the index exists and the table has loaded, run VACUUM ANALYZE places; so the planner has current statistics.

Check the query plan and troubleshoot

  1. Run the real query under EXPLAIN ANALYZE with representative coordinates and radius, for example:
    EXPLAIN ANALYZE
    SELECT id FROM places
    WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(-0.1276, 51.5072), 4326)::geography, 1000);
  2. Look for a scan that names places_location_gist, such as a Bitmap Index Scan followed by a Bitmap Heap Scan on places.
  3. Compare the timing lines with a run at production-like volume, not only with the small development table.

On a small table, PostgreSQL may choose a sequential scan even when the index is correct, because scanning a few pages is cheaper than using the index. That result alone does not indicate a faulty query.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Sequential scan on a large table: confirm the predicate is ST_DWithin, not ST_Distance, that the index exists in pg_indexes, and that VACUUM ANALYZE has run. Also check that the indexed column is not wrapped in a function in the WHERE clause, such as ST_Transform(location, ...), because that prevents the index from being used.
  • Points appear in the wrong place: longitude and latitude are swapped in ST_MakePoint.
  • Radius is off by orders of magnitude: the column is geometry with SRID 4326, so the radius is in degrees. Switch the column to geography or convert the radius to match the column’s units.
  • Type errors or unexpected matches: the point operand must match the column’s type. Cast it with ::geography or ::geometry to match.

Decide on scale from measurements

The PostGIS and Laravel documentation does not give a row count, latency target, or partitioning trigger, so this article does not provide one. Collect these measurements before you change the architecture:

  • The current row count and monthly growth rate of the location table.
  • The write rate for inserts and updates, and how often existing coordinates change.
  • The distribution of query centres and radii, and the typical number of rows each query returns.
  • The latency objective each query must meet, measured at the percentile your application uses.
  • The index size, from SELECT pg_size_pretty(pg_relation_size('places_location_gist'));, and the EXPLAIN ANALYZE timings at production-like volume.

Partitioning or a separate spatial service becomes a question only after the schema, index type and query shape are correct and the plan still misses the latency objective. At that point, test each option with the same measurements rather than adopting it by default.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

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

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.