Table of Contents
pcpoint and pcpatchpcpointset and pcpatchsettpcboxtpcpointtpcpatchreaders.tpcpatch and writers.tpcpatchThe pgPointCloud extension stores LIDAR point cloud data in PostgreSQL using two static types: pcpoint, a single multi-dimensional point whose dimensions are described by a schema stored in the pointcloud_formats catalog table; and pcpatch, a compressed batch of pcpoint values that share the same pcid (schema id). Each schema fixes the dimensions present (X, Y, Z, Intensity, …), their numeric type, scale, and offset.
MobilityDB lifts these types into the temporal world via tpcpoint (a moving LIDAR/GPS sensor) and tpcpatch (a time series of compressed point clusters). It also adds set types pcpointset / pcpatchset and the spatiotemporal bounding-box type tpcbox. A complete reference for the static pcpoint and pcpatch types is given in the pgPointCloud documentation; this chapter covers MobilityDB's additions on top of pgPointCloud.
To use the types described here, MobilityDB must be built with the POINTCLOUD=ON CMake option. On such a build the generated mobilitydb.control declares requires = 'postgis, pointcloud', so a single CASCADE creates the full stack:
CREATE EXTENSION mobilitydb CASCADE; -- NOTICE: installing required extension "postgis" -- NOTICE: installing required extension "pointcloud"
This section walks through a minimal end-to-end example: registering a schema, building values, lifting to a temporal type, querying with the bbox surface, and indexing. The goal is to make every later section in this chapter pin to a concrete usage. Follow the steps in order against a fresh database.
Every pcpoint and pcpatch value carries a pcid that resolves through pointcloud_formats to an XML schema. The schema declares the dimensions (X, Y, Z, …), their on-disk numeric type, and per-dimension scale. A minimal 3D schema with all dimensions stored as int32 at unit scale:
INSERT INTO pointcloud_formats (pcid, srid, schema) VALUES (1, 4326, '
<?xml version="1.0" encoding="UTF-8"?>
<pc:PointCloudSchema xmlns:pc="http://pointcloud.org/schemas/PC/1.1">
<pc:dimension><pc:position>1</pc:position><pc:size>4</pc:size>
<pc:name>X</pc:name><pc:interpretation>int32_t</pc:interpretation>
<pc:scale>1</pc:scale></pc:dimension>
<pc:dimension><pc:position>2</pc:position><pc:size>4</pc:size>
<pc:name>Y</pc:name><pc:interpretation>int32_t</pc:interpretation>
<pc:scale>1</pc:scale></pc:dimension>
<pc:dimension><pc:position>3</pc:position><pc:size>4</pc:size>
<pc:name>Z</pc:name><pc:interpretation>int32_t</pc:interpretation>
<pc:scale>1</pc:scale></pc:dimension>
</pc:PointCloudSchema>');
Schemas are global to the database; you usually register them once at fixture-load time. SRID is per-schema and inherited by every value with that pcid.
MobilityDB ships ergonomic constructors over pgPointCloud's PC_MakePoint / PC_Patch:
-- a single point at (1, 2, 3) under pcid=1
SELECT pcpoint(1, 1.0, 2.0, 3.0);
-- a patch holding two points
SELECT pcpatch(1, pcpoint(1, 1.0, 2.0, 3.0),
pcpoint(1, 4.0, 5.0, 6.0));
-- a single-instant tpcpoint at (10, 20, 30) at 2024-01-01
SELECT tpcpoint(pcpoint(1, 10, 20, 30), '2024-01-01'::timestamptz);
-- a 3-instant moving sensor track (step interpolation, the default for
-- pointcloud temporals because dimensions like Intensity don't interpolate
-- linearly the way coordinates do)
SELECT tpcpointSeq(ARRAY[
tpcpoint(pcpoint(1, 0, 0, 0), '2024-01-01'::timestamptz),
tpcpoint(pcpoint(1, 1, 1, 1), '2024-01-02'::timestamptz),
tpcpoint(pcpoint(1, 2, 2, 2), '2024-01-03'::timestamptz)]);
For long sessions, declare the column with a typmod so every row's pcid is enforced at INSERT time:
CREATE TABLE scans (id int, traj tpcpoint(1), full_scan tpcpatch(1));
INSERT INTO scans VALUES (1,
tpcpointSeq(ARRAY[
tpcpoint(pcpoint(1, 0, 0, 0), '2024-01-01'::timestamptz),
tpcpoint(pcpoint(1, 1, 1, 1), '2024-01-02'::timestamptz)]),
tpcpatch(pcpatch(1, pcpoint(1,1,1,1), pcpoint(1,2,2,2)),
'2024-01-01'::timestamptz));
Inserting a value whose pcid disagrees with the column's typmod raises ERROR: Pcid of tpcpoint value (N) does not match column typmod pcid (M); mixing pcids in an unconstrained column is also rejected at extent aggregation time.
Every tpcpoint and tpcpatch carries a tpcbox (mirror of stbox + pcid) computed at construction time. The bbox-level operator surface — topological (&&, @>, <@, ~=, -|-), directional on X/Y/Z/time, and KNN distance |=| — works between any pair of (tpcbox, tpcpoint, tpcpatch):
-- rows whose track ever entered a 3D-temporal box SELECT id FROM scans WHERE traj && tpcbox_zt(0, 0, 0, 1, 1, 1, tstzspan '[2024-01-01, 2024-01-02]', 1, 4326); -- 5 nearest tracks to a query box (KNN, GiST-orderable) SELECT id, traj |=| my_box AS dist FROM scans ORDER BY traj |=| my_box LIMIT 5;
Patch-level filtering (drop entire instants whose patch does not overlap a box) is provided by atTpcbox / minusTpcbox. See Per-point operations for the granularity scope.
R-tree GiST and quadtree / kd-tree SP-GiST opclasses accelerate every bbox predicate. Pick GiST for general-purpose workloads (and for KNN ordering); SP-GiST kd-tree is competitive on read-heavy point-only data:
CREATE INDEX scans_traj_gist ON scans USING gist(traj); CREATE INDEX scans_full_spgist ON scans USING spgist(full_scan);
The SP-GiST opclasses store an stbox internally and recover the pcid filter at recheck time, so a column with mixed pcids returns a slightly larger candidate set for recheck — a non-issue when each table holds values of a single schema, which the typmod recipe in Build base and temporal values enforces.
-- spatiotemporal extent of every track (one tpcbox) SELECT extent(traj) FROM scans; -- count of active tracks at each timestamp (tint) SELECT tcount(traj) FROM scans; -- merge per-source instants into a single tpcpoint SELECT id, merge(traj) FROM scans GROUP BY id;
extent rejects mixed pcids (the bbox dimensions would be uninterpretable across schemas); merge and tcount follow the same rules as their non-pointcloud counterparts.
The points set-returning function explodes each instant's patch into individual pcpoints, useful for joins against per-point predicates that the bbox layer cannot express:
-- explode every patch in the column into one row per (instant timestamp, point) SELECT id, t, point FROM scans, points(full_scan); -- total points across every instant of every track SELECT id, numPoints(full_scan) FROM scans;
Per-point filtering — building a new patch from a subset of the original's points — is provided by atTpcboxFine / minusTpcboxFine (against a tpcbox) and atGeometry / minusGeometry (against a 2D geometry); see Per-point operations for the granularity matrix.
The rest of the chapter expands on each of the surfaces touched here: pcpoint / pcpatch accessors, pcpointset / pcpatchset, the tpcbox bounding box, tpcpoint, tpcpatch, indexes, and aggregations.