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

To use PostGIS with Laravel, enable the extension in PostgreSQL, declare the location column with an explicit spatial type and SRID in a migration, index it with GiST, and write nearby-location filters with ST_DWithin so PostgreSQL can use that index. Laravel’s schema builder handles the column and index. The spatial query itself is normally raw SQL inside the query builder.

This guide covers the database boundary: the extension, the choice between geometry and geography, index selection, query shapes that the planner can accelerate, and how to check the plan. It does not give a row count at which you must partition, shard, or add a dedicated spatial service, because the official PostGIS and Laravel documentation does not define one. Instead, it shows what to measure before you decide.

Prerequisites: enable PostGIS in PostgreSQL

Laravel does not provide spatial types on its own. The PostgreSQL server must have the PostGIS extension available before a migration creates a spatial column. Laravel’s Database: Migrations guide for 11.x states that PostgreSQL users must install PostGIS before using the geography method, and every spatial column depends on the extension because the types come from it.

  1. Install the PostGIS package that matches your PostgreSQL major version. Package names differ between operating systems and hosting providers, so follow your platform’s PostGIS instructions.
  2. In each database that stores spatial data, enable the extension with CREATE EXTENSION IF NOT EXISTS postgis;. The role that runs this statement needs permission to create extensions, which on many managed hosts means the provider’s administrative role.
  3. Confirm the install with SELECT PostGIS_Version();. It returns a version string. If the function is not found, the extension is not enabled in the database you are connected to.

Match function availability to the release you actually run. The PostGIS Spatial Queries chapter is on the development manual, which can describe functions that a stable installation does not yet include.

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.

Choose geometry or geography before you write the migration

PostGIS offers two spatial types. The choice changes what a coordinate means, what unit a distance is returned in, and which functions you can call. The PostGIS Data Management chapter uses WGS84 with SRID 4326 for its geography examples, and describes geography spatial indexing as spheroid-aware.

Aspect geometry geography
Coordinate model Planar. Coordinates are treated as points on a flat plane defined by the column’s SRID. Geodetic. Coordinates are longitude and latitude on a spheroid.
Typical SRID A projected coordinate system suited to your service area, such as a local metric grid. 4326 (WGS84) for global point data.
Distance unit The unit of the column’s coordinate system. With SRID 4326, geometry distances come back in degrees, not meters. Meters for distance functions such as ST_DWithin.
Function coverage The broadest set of PostGIS functions, including containment predicates such as ST_Contains and ST_Within. A narrower set. Check each function you need against the manual before relying on it.
Index method GiST GiST, with spheroid-aware distance calculations

Use geography(POINT,4326) for global point data when distances need to be real meters. Use geometry when the application already works in a projected coordinate system for its region, or when it needs geometry operations that geography does not provide. Do not make every location column geography by default. Check the function you need against the type you chose.

Create the column and index in a migration

The following migration enables the extension, then creates a point column with an explicit subtype and SRID, and a spatial index. Confirm the method signatures against the migrations documentation for your Laravel version.

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->geography('location', subtype: 'point', srid: 4326)->spatialIndex();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('places');
    }
};
  • The subtype and SRID arguments make the column accept only point values in SRID 4326, so bad data fails at insert time.
  • The spatialIndex() modifier adds a spatial index to the column. On PostgreSQL this is a GiST index, as described below.
  • Run php artisan migrate --pretend before the real migration to review the SQL Laravel generates.

Laravel’s documented spatial support stops at the schema. Distance and containment logic is written as SQL, passed to the query builder with bindings. The insert below shows the pattern in both directions. Note that ST_MakePoint takes longitude first and latitude second.

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())',
    ['British Museum', -0.1270, 51.5194]
);

Choose the index type

A spatial column needs a spatial index. The PostGIS FAQ on spatial indexes shows USING GIST for spatial columns and warns that a conventional B-tree index will not help spatial queries. The three index methods below are the ones the PostGIS Data Management chapter discusses.

GiST: the starting point

  • The PostGIS manual describes GiST as the most commonly used and most versatile spatial index.
  • Create it explicitly with CREATE INDEX places_location_gist ON places USING GIST (location); if you are not using the migration modifier.
  • Start here for general spatial workloads, and change only when your measurements point elsewhere.

BRIN: spatially ordered, rarely updated data

  • BRIN fits when physical row order follows spatial order closely, for example a bulk import sorted by region, and when updates are infrequent.
  • It is smaller and faster to build than GiST, but it is lossy. It narrows the search to block ranges that PostgreSQL then rechecks.
  • It does not summarise later changes automatically. Plan maintenance, for example by running SELECT brin_summarize_new_values('index_name'); after loads, or by running VACUUM.

SP-GiST: a partitioned search-tree alternative

  • SP-GiST supports partitioned search trees. The PostGIS Data Management chapter presents it as an alternative to evaluate, not a default.
  • Compare it with GiST on your own data distribution and your own queries, using the plans described below.

When you compare these methods, check the same five things for each: how closely the table’s physical order follows spatial order, how often rows change, index size and build time, which operators the index supports for your query, and read timings measured on your real queries. The docs do not establish a winner without that data.

Write queries the planner can accelerate

Query shape decides whether PostgreSQL can use the spatial index at all. The rules below come from the PostGIS Spatial Queries chapter, which describes index-aware functions and the alternatives to avoid.

Nearby places with ST_DWithin

ST_DWithin is index-aware. PostgreSQL can use the spatial index for a bounding-box prefilter, then computes the exact distance only for candidate rows. On a geography column the radius is in meters.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
SELECT id, name
FROM places
WHERE ST_DWithin(
    location,
    ST_SetSRID(ST_MakePoint(-0.1276, 51.5072), 4326)::geography,
    1000
);

The Laravel equivalent uses the query builder with bindings:

$places = DB::table('places')
    ->select('id', 'name')
    ->whereRaw(
        'ST_DWithin(location, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ?)',
        [-0.1276, 51.5072, 1000]
    )
    ->get();
  • Keep both operands the same type. Comparing a geography column with a geometry argument needs an explicit cast, and the cast can stop the planner from using an index built on the original column.
  • Pass the coordinates in longitude, latitude order. Swapped values still produce a valid point, so the query runs and returns the wrong places.

Relationship predicates

ST_Intersects, ST_Contains, and ST_Within express spatial relationships. Choose the one whose meaning matches the question. ST_Contains(A, B) is true when ST_Within(B, A) is true, so the argument order matters. Both ST_Contains and ST_Within are defined for geometry, so check the manual before using them with geography. A point-in-polygon lookup on a geometry column looks like this:

SELECT id, name
FROM zones
WHERE ST_Intersects(
    boundary,
    ST_SetSRID(ST_MakePoint(-0.1276, 51.5072), 4326)
);

The filter that bypasses the index

A common first draft filters on distance directly:

-- Computes the distance for every row; avoid on large tables
SELECT id FROM places
WHERE ST_Distance(location, ST_SetSRID(ST_MakePoint(-0.1276, 51.5072), 4326)::geography) < 1000;

PostGIS documents this pattern as computing distance for each row, and points to ST_DWithin as the index-aware alternative. Replace the comparison with the function call shown earlier.

Ordering results by distance

Sorting nearest-first is a separate question from filtering. Check the current manual for the distance operator and index support for your type and PostGIS version, then confirm the choice with the plan. Do not assume that an ORDER BY on a distance expression uses the index.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Confirm that PostgreSQL uses the index

  1. Run EXPLAIN (ANALYZE, BUFFERS) on the real query with realistic coordinates and radius. The statement executes the query, so use a read-only query or a staging copy.
  2. Look for a Bitmap Index Scan or Index Scan on the spatial index. A Seq Scan on a large table with a small radius means the index is not being used.
  3. Run ANALYZE places; after a bulk load or index creation so the planner has current statistics. The PostGIS Data Management chapter recommends VACUUM ANALYZE after index creation where appropriate.
  4. Run EXPLAIN (ANALYZE, BUFFERS) again and compare the plan and the timings.

A sequential scan is not always a fault. On a small table, or with a radius that matches most rows, scanning the table is the cheaper plan. The usual causes of an unused index are these:

  • The query uses ST_Distance in a filter instead of ST_DWithin.
  • The query casts the indexed column, for example location::geometry, so the index on the original type no longer matches.
  • The index does not exist on the table you are querying. Check with d places in psql.
  • Statistics are stale after a large load.

Build indexes on a live table

A normal CREATE INDEX blocks writes to the table while it runs. The PostGIS Data Management chapter describes CREATE INDEX CONCURRENTLY as a way to avoid blocking writes during construction, at the cost of a slower build:

CREATE INDEX CONCURRENTLY places_location_gist ON places USING GIST (location);
  • PostgreSQL does not allow CREATE INDEX CONCURRENTLY inside a transaction block. Check whether your migration runs inside a transaction before you use it, because the statement fails in that case.
  • If the build fails or is cancelled, PostgreSQL can leave an invalid index behind. Find it with SELECT indexrelid::regclass AS index_name FROM pg_index WHERE NOT indisvalid;, drop it with DROP INDEX CONCURRENTLY index_name;, and build it again.

Decide when to scale further

The official PostGIS and Laravel pages do not give a row count, a latency target, or a partitioning trigger. A table that is fine at one size can need a different design at another, depending on how its rows are distributed and how it is queried. Decide from measurements, not from a number borrowed from another system. Measure these before you choose a scaling step:

  • Row count and spatial distribution. Are rows clustered in a few areas or spread across the whole coverage region?
  • Write rate, and whether writes concentrate in the same regions that reads hit.
  • Plan and timing for your slowest real queries, using the steps above, with production-like radii and parameters.
  • The latency objective the application must meet, and whether the current plans meet it.
  • Operational limits such as backup duration, restore time, and maintenance windows.

If the index-backed queries still miss the objective after the index, query, and statistics steps, then evaluate partitioning, read replicas, or a dedicated spatial service. Choose the option that addresses the measured bottleneck, and re-measure afterwards.

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

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.