Buy our books! https://postgis.us/page_buy_book
SELECT p.name, p
FROM stlouis.points AS p
WHERE name = 'Smoking Barrels BBQ';
-[ RECORD 1 ]------------------------------------------------------------------------------- name | Smoking Barrels BBQ p | (2553435113,0101000020E6100000B53E9B0B129256C0EBF5381D124A4340,"Smoking Barrels BBQ",,,,,,,, "{""amenity"": ""restaurant"", ""cuisine"": ""barbecue"", ""website" ": ""https://www.smokingbarrels.net/"", ""addr:city"": ""Saint Louis"", ""addr:street"": ""South Kingshighway Boulevard"", ""addr:postcode"" ...
-- requires PostGIS 3.1+
SELECT p.name, ST_AsGeoJSON(p)::json AS p_geojson
FROM stlouis.points AS p
WHERE name = 'Smoking Barrels BBQ';
-[ RECORD 1 ]--------------------------------------------------------------------------------------------- name | Smoking Barrels BBQ p_geojson | {"type": "Feature", "geometry": {"type":"Point","coordinates":[-90.2823514,38.5786778]}, | "properties": {"osm_id": 2553435113, | "name": "Smoking Barrels BBQ", "barrier": null, "highway": null, "ref": null, | "address": null, "is_in": null, "place": null, | "man_made": null, | "other_tags": {"amenity": "restaurant", "cuisine": "barbecue", | "website": "https://www.smokingbarrels.net/", "addr:city": "Saint Louis", | "addr:street": "South Kingshighway Boulevard", "addr:postcode": "63109"...}}}
SELECT array_agg(p) AS p_array
FROM stlouis.points AS p
WHERE name ILIKE '%BBQ%';
SELECT json_agg(p) AS p_json
FROM st_louis.points AS p
WHERE name ILIKE '%BBQ%';
SELECT json_agg(p) AS p_json
FROM (SELECT osm_id, name FROM st_louis.points) AS p
WHERE name ILIKE '%BBQ%';
[{"osm_id":2553435113,"name":"Smoking Barrels BBQ"}, {"osm_id":4489679876,"name":"Sugarfire Smokehouse BBQ"}, {"osm_id":4974243121,"name":"Pappy's BBQ"}, {"osm_id":8993197283,"name":"The BBQ & Whiskey Saloon"}, {"osm_id":9244847732,"name":"Bootleggin' BBQ Tavern"}, {"osm_id":9710331857,"name":"Ravin’s BBQ"}]
This works too but probably doesn't give you what you want
SELECT json_agg( (p.osm_id, p.name )) AS p_json
FROM st_louis.points AS p
WHERE name ILIKE '%BBQ%';
[{"f1":2553435113,"f2":"Smoking Barrels BBQ"}, {"f1":4489679876,"f2":"Sugarfire Smokehouse BBQ"}, {"f1":4974243121,"f2":"Pappy's BBQ"}, {"f1":8993197283,"f2":"The BBQ & Whiskey Saloon"}, {"f1":9244847732,"f2":"Bootleggin' BBQ Tavern"}, {"f1":9710331857,"f2":"Ravin’s BBQ"}]
Just keep it in the FROM
SELECT json_agg( p_sub ) AS p_json
FROM st_louis.points AS p
CROSS JOIN LATERAL ( SELECT p.osm_id, p.name ) AS p_sub
WHERE p.name ILIKE '%BBQ%';
[{"osm_id":2553435113,"name":"Smoking Barrels BBQ"}, {"osm_id":4489679876,"name":"Sugarfire Smokehouse BBQ"}, {"osm_id":4974243121,"name":"Pappy's BBQ"}, {"osm_id":8993197283,"name":"The BBQ & Whiskey Saloon"}, {"osm_id":9244847732,"name":"Bootleggin' BBQ Tavern"}, {"osm_id":9710331857,"name":"Ravin’s BBQ"}]
Together they can create a geojson FeatureCollection.
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(ST_AsGeoJSON(p)::json)
)
FROM stlouis.points AS p
WHERE name ILIKE '%BBQ%';
View in pgAdminSeveral ST_AsGeoBuf, ST_FlatGeoBuf, ST_AsMVT pg_tileserv is essentially a wrapper around ST_AsMVT. Check out https://flatgeobuf.org/ to learn how to use FlatGeoBuf in your mapping apps.
SELECT ST_AsFlatGeoBuf(p) AS fgeobuf
FROM stlouis.points AS p
WHERE name ILIKE '%BBQ%';
When you care about order, you can apply an ORDER BY to most aggregates
SELECT array_agg(p ORDER BY name) AS points_by_name
FROM stlouis.points AS p
WHERE name ILIKE '%BBQ%';
You want your path ordered by time right?
SELECT name, ST_MakeLine(p.geom ORDER BY time) AS path
FROM gps_points AS p
GROUP BY name
ORDER BY name;
Or maybe you want to create 2-3 point linestrings consisting of visit point before and visit point after
SELECT name,
ST_MakeLine( p.geom )
OVER( PARTITION BY name ORDER BY time
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS path
FROM gps_points AS p
ORDER BY name, time;
SQL Standard casting syntax
SELECT name, CAST(geom As jsonb) AS geom
FROM st_louis.lines
WHERE name ILIKE 'Crescent%' LIMIT 1;
PostgreSQL casting is so common that there is short-hand for it. Also allows extensions to define casting behavior
SELECT name, geom::jsonb
FROM st_louis.lines
WHERE name ILIKE 'Crescent%' LIMIT 1;
If there is no direct cast from one dataype to another you can chain
SELECT name, rast::geometry::geography
FROM dems
WHERE LIMIT 1;
What does subtracting a text do for jsonb?
SELECT osm_id, osm_id - 1 AS id,
to_jsonb(p) - 'geom' - 'other_tags'
- 'ref' - 'osm_id' - 'man_made' - 'place'
- 'address' - 'barrier' - 'highway' AS p_json
FROM st_louis.points AS p
WHERE name ILIKE '%BBQ%';
osm_id | id | p_json ------------+------------+----------------------------------------------------- 2553435113 | 2553435112 | {"name": "Smoking Barrels BBQ", "is_in": null} 4489679876 | 4489679875 | {"name": "Sugarfire Smokehouse BBQ", "is_in": null} 4974243121 | 4974243120 | {"name": "Pappy's BBQ", "is_in": null} 8993197283 | 8993197282 | {"name": "The BBQ & Whiskey Saloon", "is_in": null} 9244847732 | 9244847731 | {"name": "Bootleggin' BBQ Tavern", "is_in": null} 9710331857 | 9710331856 | {"name": "RavinÆs BBQ", "is_in": null} (6 rows)
The ISO-SQL || operator concatenates text but it works with jsonb too in PostgreSQL to add or update.
SELECT name || ' Good' AS name2,
to_jsonb(p) || jsonb_build_object('name', p.name || ' Not Good') AS p_json
FROM (SELECt osm_id, name
FROM stlouis.points) AS p
WHERE name ILIKE '%BBQ%';
name2 | p_json -------------------------------+--------------------------------------------------------------------- Smoking Barrels BBQ Good | {"name": "Smoking Barrels BBQ Not Good", "osm_id": 2553435113} Sugarfire Smokehouse BBQ Good | {"name": "Sugarfire Smokehouse BBQ Not Good", "osm_id": 4489679876} Pappy's BBQ Good | {"name": "Pappy's BBQ Not Good", "osm_id": 4974243121} The BBQ & Whiskey Saloon Good | {"name": "The BBQ & Whiskey Saloon Not Good", "osm_id": 8993197283} Bootleggin' BBQ Tavern Good | {"name": "Bootleggin' BBQ Tavern Not Good", "osm_id": 9244847732} RavinÆs BBQ Good | {"name": "RavinÆs BBQ Not Good", "osm_id": 9710331857} (6 rows)
KNN distance operator has meaning both in PostGIS and pg_vector extension
Geometry
SELECT p.name, p.geom <-> ST_Point(-90.188, 38.62658, 4326) AS dist_deg
FROM st_louis.points AS p
WHERE name ILIKE '%BBQ%'
ORDER BY dist_deg LIMIT 6;
Geography
SELECT p.name, p.geog <-> ST_Point(-90.188, 38.62658, 4326)::geography AS dist_m
FROM st_louis.points AS p
WHERE name ILIKE '%BBQ%'
ORDER BY dist_m LIMIT 6;
name | dist_m --------------------------+-------------------- Sugarfire Smokehouse BBQ | 443.06224064314273 Pappy's BBQ | 3274.4233355063307 RavinÆs BBQ | 5384.221956799874 Bootleggin' BBQ Tavern | 6355.8718317511275 The BBQ & Whiskey Saloon | 6638.181704382918 Smoking Barrels BBQ | 9777.2332126643 (6 rows)
For each barbecue place, list the two closest streets to it.
SELECT p.name, s.name AS street, p.geog AS location, s.dist_m
FROM st_louis.points AS p
CROSS JOIN LATERAL
(SELECT l.name, l.geom, p.geog <-> l.geog AS dist_m
FROM st_louis.lines
WHERE highway > '' ORDER BY dist_m
LIMIT 2) AS s
WHERE p.name ILIKE '%BBQ%';
SELECT p.name, kv.*
FROM st_louis.points AS p
CROSS JOIN LATERAL jsonb_each_text(p.other_tags) AS kv;
Is equivalent to
SELECT p.name, kv.*
FROM st_louis.points AS p, jsonb_each_text(p.other_tags) AS kv;
SELECT p.name, kv.*
FROM st_louis.points AS p
LEFT JOIN LATERAL jsonb_each_text(p.other_tags) AS kv;
Is equivalent to
SELECT p.name, kv.*
FROM st_louis.points AS p LEFT JOIN jsonb_each_text(p.other_tags) AS kv ON true;
Chaining of functions in a lateral way
PostgreSQL / GDAL / PostGIS bump
SELECT d.district, gv.val AS elev_ft
/** PostGIS aggregate function,
will collect so one geometry per pixel value per district**/
, ST_Union(gv.geom) As geom
FROM dem
INNER JOIN districts AS d ON ST_Intersects(dem.rast, d.geom)
/** not a set returning function, returns
one row per dem.rast,geom intersection,
only consider elevation higher than 150 m and convert to ft**/
, ST_Reclass(ST_Clip(dem.rast,d.geom,1),
'150-400:328-1312', '16BUI' ) AS re(rast)
/** set returning function returns at
least one row per pixel value per district **/
, ST_DumpAsPolygons( re.rast ) AS gv
GROUP BY d.district, gv.val;
Use Ordinality to number results from set returning functions such as unnest, PostGIS ST_SubDivide that don't give you numbers.
SELECT town, f.ord, ST_NPoints(f.geom)
FROM shps.towns_polym, ST_SubDivide(geom, 100) WITH ordinality f(geom,ord)
WHERE town IN('BOSTON', 'CAMBRIDGE');
Before had 2 rowstown | st_npoints -----------+------------ BOSTON | 1893 CAMBRIDGE | 235 (2 rows) |
After have 68 rows, no geometry has more than 100 pointstown | ord | st_npoints -----------+-----+------------ BOSTON | 1 | 89 BOSTON | 2 | 62 : BOSTON | 22 | 97 : BOSTON | 64 | 6 CAMBRIDGE | 1 | 40 : CAMBRIDGE | 4 | 63 (68 rows) |
You'll often see PostGIS extensions paling around with these
If you have all sorts of data of both a spatial and non-spatial flavor to tame, make sure you have ogr_fdw foreign data wrapper in your tool belt.
You have the combined power of GDAL, PostgreSQL, and any PostgreSQL extension you want (including PostGIS) working seamlessly together. So many kinds of data you can query and take advantage of PostgreSQL functions and any extension functions and types such as PostGIS, hstore, built-in json and jsonb to tame your data.
Binaries available at least on OSGeoLive, Windows PostGIS Bundle, apt.postgresql.org, yum.postgresql.org
CREATE EXTENSION ogr_fdw;
Doesn't require PostGIS to use, but will expose spatial columns as PostGIS geometry if PostGIS is installed.
I call this the PostgreSQL/OGR/PostGIS bump.
SELECT name
FROM unnest(ogr_fdw_drivers()) AS f(name) ORDER BY f.name;
name ---------------- AmigoCloud : CAD : CSV : FlatGeobuf Geoconcept GeoJSON GeoJSONSeq GeoRSS GML GMLAS GPKG GPSBabel GPX GTFS HTTP : JML JP2OpenJPEG KML : MapInfo File MapML MBTiles Memory MSSQLSpatial MVT MySQL : ODBC ODS : WFS XLS XLSX (80 rows)
CREATE SERVER svr_shp FOREIGN DATA WRAPPER ogr_fdw
OPTIONS (datasource 'C:/fdw_data/massgis/shps',
format 'ESRI Shapefile'
);
CREATE SCHEMA shps;
IMPORT FOREIGN SCHEMA ogr_all
FROM SERVER svr_shp INTO shps;
\dE shps.*
List of relations Schema | Name | Type | Owner --------+---------------------+---------------+---------- shps | biketrails_arc | foreign table | postgres shps | towns_arc | foreign table | postgres shps | towns_poly | foreign table | postgres shps | towns_poly_areacode | foreign table | postgres shps | towns_polym | foreign table | postgres shps | towns_pop | foreign table | postgres shps | zipcodes_nt_poly | foreign table | postgres (7 rows)
SELECT f_table_name As tbl, f_geometry_column As geom, srid, type
FROM geometry_columns
WHERE f_table_schema = 'shps'
ORDER BY tbl;
tbl | geom | srid | type ------------------+------+--------+------------ biketrails_arc | geom | 26986 | LINESTRING towns_arc | geom | 26986 | LINESTRING towns_poly | geom | 26986 | POLYGON towns_polym | geom | 26896 | POLYGON zipcodes_nt_poly | geom | 26986 | POLYGON (5 rows)
Make sure the user that postgres runs under has edit/delete rights to the folder holding the shape files.
UPDATE shps.towns_polym
SET geom = ST_MakeValid(geom)
WHERE NOT ST_IsValid(geom)
RETURNING town;
NOTICE: Ring Self-intersection at or near point 241494.43330000341 890709.87110000104 NOTICE: Ring Self-intersection at or near point 306590.87370000035 822452.56080000103 NOTICE: Ring Self-intersection at or near point 273304.93349999934 802752.31069999933 Total query runtime: 320 msec town ----- QUINCY YARMOUTH TISBURY
If your GDAL is compiled with curl support (you see HTTP in list of driver options) , you can link files from the web using the /vsicurl switch. Use /vsizip//vsicurl if they are zipped as well.
CREATE SERVER svr_osm
FOREIGN DATA WRAPPER ogr_fdw
OPTIONS (datasource
'/vsizip//vsicurl/https://postgis.us/downloads/foss4gna2024/ST_Louis_MO.osm.zip',
format 'OSM',
updateable 'false');
CREATE SCHEMA IF NOT EXISTS osm;
IMPORT FOREIGN SCHEMA ogr_all
FROM SERVER svr_osm INTO osm;
SELECT f_table_name, f_geometry_column, srid, type
FROM geometry_columns WHERE f_table_schema = 'osm';
f_table_name | f_geometry_column | srid | type ------------------+-------------------+------+-------------------- points | geom | 4326 | POINT lines | geom | 4326 | LINESTRING multilinestrings | geom | 4326 | MULTILINESTRING multipolygons | geom | 4326 | MULTIPOLYGON other_relations | geom | 4326 | GEOMETRYCOLLECTION (5 rows)
-- requires CREATE EXTENSION hstore; to cast other_tags to hstore
-- and hstore extension has function hstore_to_jsonb that will cast hstore to jsonb
-- 56789 rows affected, 15.6 secs execution time.
-- 3602 is epsg stateplane meters missouri east
CREATE TABLE st_louis_polys AS
SELECT osm_id, name, geom::geography As geog,
ST_Transform(geom,3602)::geometry(MULTIPOLYGON,3602) AS geom,
other_tags::hstore::jsonb AS other_tags, building
FROM osm.multipolygons;
Each workbook is considered a server and each sheet a table
CREATE SERVER svr_fedex FOREIGN DATA WRAPPER ogr_fdw
OPTIONS (datasource 'C:/fdw_data/Fedex2016.xls',
format 'XLS'
);
-- link only 1 spreadsheet preserve headers
IMPORT FOREIGN SCHEMA ogr_all LIMIT TO (Fedex_Rates_IP)
FROM SERVER svr_fedex INTO public OPTIONS (launder_column_names 'false');
SELECT * FROM fedex_rates_ip;
Before
fid | Type | Weight | Zone A | Zone B | Zone C | Zone D | Zone E | Zone F | Zone G | Zone H | Zone I | Zone J | Zone K | Zone L | Zone M | Zone N | Zone O | Zone Puerto Rico -----+--------------+--------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+------------------ 2 | IntlPriority | 0 | 40.25 | 41.5 | 43 | 54.75 | 116.5 | 52 | 51.5 | 61.25 | 49.5 | 63.5 | 62.5 | 82 | 119.25 | 61 | 63.25 | 36.65 3 | IntlPriority | -1 | 66.25 | 67.75 | 62.25 | 74.25 | 132 | 68 | 68.25 | 85.75 | 66.25 | 84.5 | 82.25 | 99.5 | 136.5 | 79.75 | 85.5 | 4 | IntlPriority | -2 | 70.25 | 73.5 | 65.75 | 77.25 | 156.25 | 73 | 73.25 | 87.25 | 73.5 | 90.5 | 87.75 | 129.25 | 160.75 | 86 | 96 |
-- unpivot a subset of columns and keep others
WITH fkv AS (
SELECT f."Type" As type, f."Weight" As weight,
each(row_to_jsonb(f) - '{fid,Type,Weight}'::text[]) AS kv
from fedex_rates_ip AS f)
SELECT type, weight, (kv).key AS zone, (kv).value::numeric As price
FROM fkv;
After
type | weight | zone | price --------------+--------+------------------+--------- IntlPriority | 0 | Zone A | 40.25 IntlPriority | 0 | Zone B | 41.5 IntlPriority | 0 | Zone C | 43 IntlPriority | 0 | Zone D | 54.75 :
You can point at a single CSV file or a whole folder of CSV files. Each file is considered a table.
Folder of CSV files
CREATE SERVER svr_census FOREIGN DATA WRAPPER ogr_fdw
OPTIONS (datasource 'C:/fdw_data/census',
format 'CSV'
);
IMPORT FOREIGN SCHEMA ogr_all
FROM SERVER svr_census INTO public;
Single file
CREATE SERVER svr_census_income FOREIGN DATA WRAPPER ogr_fdw
OPTIONS (datasource '/fdw_data/census/income.csv',
format 'CSV'
);
IMPORT FOREIGN SCHEMA ogr_all
FROM SERVER svr_census_income INTO public;
Format for SQL Server 'ODBC:your_user/your_password@yourDSN,table1,table2'. ODBC can be slow with a lot of tables (more than 150) so filter list if you have over 200 tables
CREATE SERVER svr_sqlserver FOREIGN DATA WRAPPER ogr_fdw
OPTIONS (datasource 'ODBC:pguser/whatever@MSSQLTest,dbo.IssueLog,dbo.IssueNotes',
format 'ODBC'
);
CREATE SCHEMA IF NOT EXISTS ss;
IMPORT FOREIGN SCHEMA "dbo."
FROM SERVER svr_sqlserver INTO ss;
\dE ss.*
List of relations Schema | Name | Type | Owner --------+----------------+---------------+---------- ss | dbo_issuelog | foreign table | postgres ss | dbo_issuenotes | foreign table | postgres (2 rows)
pgsql-http now availabe on apt.postgresql.org
Debian / Ubuntu: Replace 17 with your PostgreSQL version
apt install postgresql-17-http
CREATE EXTENSION http;
Embrace the web without having to get out of your database.
Note the use of the PostGIS ST_GeomFromGeoJSON
working in conjunction with the http_get
function
packaged in http extension as well as built-in PostgreSQL json functions.
CREATE TABLE community_improvement_districts AS
SELECT (je->>'id')::integer AS id,
je->'properties'->>'Name' AS name,
ST_GeomFromGeoJSON(je->>'geometry') AS geom
FROM http_get(
'https://www.stlouis-mo.gov/data/upload/data-files/CID.geojson'
) AS h,
jsonb_array_elements(h.content::jsonb->'features') AS je;