Empty geometry semantics
Empty geometries are valid geometry values with no coordinates. They are not
SQL NULL: NULL means an unknown or absent SQL value, while EMPTY is a
known geometry value that can be produced by a successful spatial operation.
The old DevWikiEmptyGeometry page treated this as an open design question.
Most of those questions are now resolved in code, regression tests, and user
manual behavior. The useful rule of thumb is:
- Constructors, serializers, and accessors preserve typed empties where the type is meaningful.
- Measurements of size usually collapse empties to zero.
- Distance and nearest-point style functions usually return
NULLwhen no point pair exists. - Topological predicates involving empties usually return false; relation matrix behavior can still report exterior/exterior overlap for two empties.
- Empty collections and collections containing empty members need explicit handling because serialized emptiness and collection membership are not the same question.
ST_IsEmpty(geom) tests the geometry value itself. It returns true for
values such as POINT EMPTY, POLYGON EMPTY, and GEOMETRYCOLLECTION EMPTY.
PostGIS keeps normal SQL NULL semantics: strict SQL functions return NULL
before reaching C code, so ST_IsEmpty(NULL::geometry) is NULL, not false.
Typed empty values are preserved by WKT/WKB output. Regression tests in
regress/core/regress.sql, regress/core/wkb.sql, and
regress/core/tickets.sql cover empty points, lines, polygons, curves,
triangles, TINs, polyhedral surfaces, and collections.
Serialized geometry can be empty even when it has a concrete geometry type.
In liblwgeom/gserialized1.c, empty points and zero-length coordinate arrays
are represented without a coordinate payload and without a calculable bounding
box. gserialized_is_empty() is the cheap test used throughout the PostgreSQL
extension layer.
Indexes have to represent “no box” separately from a normal bounding box:
- GiST and SP-GiST code use empty box sentinels when a geometry has no bounding box.
- Many bounding-box operators short-circuit when either side is empty because there is no spatial extent to compare.
- Some box predicates still model set-theoretic intuition for the box layer: non-empty boxes can contain empty boxes, and empty boxes can be within non-empty boxes. That does not automatically define the SQL predicate behavior for full geometries.
This distinction matters when changing filters or planner support. Do not infer
the behavior of ST_Contains, ST_Within, or ST_Intersects only from
bounding-box helper behavior.
Current behavior generally treats the size of an empty geometry as zero:
| Operation family | Empty behavior |
|---|---|
ST_Area |
Returns 0 for empty geometry/geography inputs. |
ST_Length and geography perimeter/length helpers |
Return 0 for empty inputs where the measurement applies. |
ST_NumGeometries |
Returns 0 for empty geometries. |
ST_NumInteriorRings / ST_NRings |
Return 0 for empty polygonal inputs. |
ST_NumPoints |
Returns 0 for empty linear inputs and NULL for non-linear inputs where the accessor does not apply. |
ST_GeometryN |
Returns NULL for empty inputs. |
ST_InteriorRingN |
Returns NULL for empty polygonal inputs. |
ST_ExteriorRing |
Returns an empty line for empty polygonal inputs. |
The implementation anchors are postgis/lwgeom_ogc.c and
postgis/lwgeom_functions_basic.c. The accessor behavior is also reflected in
doc/reference_accessor.xml and regression coverage such as
regress/core/polyhedralsurface.sql and regress/core/tickets.sql.
Many geometry operations use GEOS, but PostGIS still handles common empty cases before calling GEOS. That keeps output types stable and avoids passing work to GEOS when no coordinate operation is possible.
Examples already fixed by regression tests:
ST_Buffer(POLYGON EMPTY, radius)returns an empty polygon.ST_BuildArea(POINT EMPTY)returns an empty polygon.ST_ConvexHull(POLYGON EMPTY)returns an empty polygon.ST_Centroid(POLYGON EMPTY)returnsPOINT EMPTY.ST_IsValid(POLYGON EMPTY)returns true.ST_DFullyWithin(non_empty, empty, distance)returns false.ST_Relate(POINT EMPTY, POINT EMPTY)returnsFFFFFFFF2.
The relevant tests are in regress/core/tickets.sql, with expected values in
regress/core/tickets_expected for tickets such as #682, #683, #684,
#685, #687, #712, and #1060.
The old Trac page debated whether empty geometries should follow pure set-theoretic laws or existing spatial-database convention. Current PostGIS behavior is closer to the latter for SQL predicates:
| Predicate | Empty behavior |
|---|---|
ST_Intersects(empty, anything) |
false |
ST_DWithin(empty, anything, distance) |
false, even for infinite distance |
ST_DFullyWithin(empty, anything, distance) |
false |
ST_Contains(geometry, empty) |
false |
ST_Within(empty, geometry) |
false |
ST_CoveredBy(empty, geometry) |
false in ticket #689 for an empty polygon covered by a line |
ST_Disjoint(empty, geometry) |
true for the empty cases recorded on the old wiki and consistent with the tested FFFFFFFF2 relate matrix |
ST_Equals(POINT EMPTY, POINT EMPTY) |
true for matching empty point values |
The infinite-distance ST_DWithin case is intentionally tested by ticket
#5008: both ST_DWithin('POINT EMPTY', 'POINT(0 0)', 'Inf') and
ST_DWithin('POINT(1 1)', 'POLYGON EMPTY', 'Inf') return false.
When changing a predicate, check both the C short-circuit and the indexed SQL
wrapper. Public predicates such as ST_Intersects, ST_Contains,
ST_Within, and ST_DWithin have planner support functions and index-aware
operator paths in postgis/postgis.sql.in; their uncached _ST_* variants
can share C functions but do not prove the indexed wrapper behavior by
themselves.
Collections require a separate mental model:
GEOMETRYCOLLECTION EMPTYis empty.GEOMETRYCOLLECTION(POINT EMPTY)has a member, but the member is empty.GEOMETRYCOLLECTION(POINT EMPTY, LINESTRING(0 0, 1 1))is not empty because at least one member has coordinates.
This distinction shows up in serialization and collection handling. The
gserialized1_is_empty() comment explicitly notes that a shallow zero-count
test is not enough to catch collections such as
GEOMETRYCOLLECTION(POINT EMPTY). Code that recursively deserializes or
normalizes collections needs to decide whether it is asking “does the container
have members?” or “does any member contribute coordinates?”
The old Trac guidance still gives the right design instinct: operations over mixed collections should generally ignore empty members when non-empty members determine the result, but all-empty collections should still follow the empty rules above.
When adding or changing empty-geometry behavior:
- Preserve SQL
NULLsemantics separately from empty geometry semantics. - Add regression coverage for typed empties, not only
GEOMETRYCOLLECTION EMPTY. - Test collection cases containing empty members and non-empty members.
- Check geometry and geography behavior separately; geography measurement code has its own empty short-circuits.
- Check both exact operators and indexed wrappers when predicates are involved.
- Prefer returning typed empty results for constructive operations where a stable output type is already established by existing behavior.