A self-paced journey through spatial databases, SQL, geoprocessing, web mapping, and cartographic visualization — built from training materials at Politeknik Negeri Batam.
Start Learning ↓Geographic Information Systems turn raw location data into decisions that shape cities, protect forests, and save lives.
Imagine you're the mayor of a growing city. A new hospital needs to be built, but where? It must be close to the most people, reachable by road, and far from flood zones. A GIS can answer that.
Collect data from satellites, GPS, surveys, and open data portals like OpenStreetMap.
Organize it in spatial databases where every row knows its location on Earth.
Run operations like buffering, overlay, and network analysis to extract insights.
Present findings as beautiful, readable maps that drive real-world decisions.
This course is built around an open-source stack — free, professional-grade tools used worldwide.
QGIS is your main workspace — load layers, run analyses, design maps.
PostGIS stores geometry alongside regular data, so you can ask “which villages touch this district?”
GeoServer publishes your data to the web; MapStore2 gives users a browser-based map viewer.
Every GIS dataset is either vector or raster.
Points ● (hospitals, villages)
Lines ─ (roads, rivers)
Polygons ■ (districts, forests)
Each feature has a shape
+ an attribute table (name,
population, type, etc.)A grid of cells (pixels), like a photo. Each cell holds one numeric value.
Elevation models: each cell = height in meters.
Population density (WorldPop): each cell = people per km².
Satellite imagery: each cell = reflectance values.
Vector = drawing with a pen (precise boundaries). Raster = painting with a grid (continuous surfaces). Most GIS projects use both.
Bad database design leads to data chaos. Good design makes every query fast and every answer trustworthy.
A relational database is like a library with a perfect catalog system.
1 subjectEach table represents one thing (customers, roads, hospitals)1 PKEvery table has a Primary KeyNo multi-partSplit “302 Walker Bldg, University Park, PA” into street, city, stateNo duplicatesNo Author1, Author2, Author3 — use a separate tableNo derivedDon’t store Wage if you have PayRate × HoursNormalization is tidying a messy room in three passes — each catches a different kind of mess.
Unique entities, no duplicate columns (no Flavor1, Flavor2, Flavor3), and a primary key.
In 1NF + no column depends on only part of a composite key.
In 2NF + no column depends on another non-key column. Remove derived values.
Higher normalization = more tables = more joins. Database design balances data integrity against query complexity.
Jen and Barry sell ice cream and need to track orders. Watch how a messy table gets normalized:
Attempt 1
Flavors crammed in one column
Attempt 2
Flavor1, Flavor2, Flavor3 columns
Final
4 tables: Customers, Flavors, Orders, OrderItems
Before building tables, you draw an ER diagram.
Real-world objects: Users, Cars, Ads. Each becomes a table. Strong (exists alone) or weak (depends on another).
Simple (color), composite (address), derived (age from birthdate), multi-valued (phone numbers).
How entities connect. Cardinality defines how many (1:1, 1:N, M:N).
Each health facility can have multiple phone numbers. A colleague suggests Phone1, Phone2, Phone3 columns.
SQL is the language you use to talk to databases. Follow along step by step — every command uses real examples from this training.
SQL is how you talk to a database. Let’s start with the most fundamental operation — creating a new table from existing data.
CREATE TABLE kelurahan AS
SELECT geom, giskemen_2, giskemen_4,
giskemen18, giskemen19, giskemen20,
giskemen21, giskemen27, giskemen28
FROM public.desa_batamCreate a brand new table called “kelurahan” (sub-district)…
Fill it by copying specific columns (geometry + 7 data fields)…
…from the existing “desa_batam” villages table.
Like photocopying certain pages from a binder into a new folder.
This pattern is incredibly powerful: create a new table that’s a filtered/transformed copy of another. You control exactly which columns and rows end up in the new table.
Real-world data arrives with cryptic column names. SQL lets you clean house with ALTER TABLE.
ALTER TABLE public.kelurahan
RENAME COLUMN giskemen_2
TO nama_desa;Go to the “kelurahan” table…
Find the cryptic column “giskemen_2”…
Rename it to “nama_desa” (village name) — so humans can read it.
Sometimes you need to restructure the identity of a table — change which column serves as the unique identifier.
-- Step A: Remove old primary key
ALTER TABLE desa_batam
DROP CONSTRAINT id;
-- Step B: Rename column to something clear
ALTER TABLE desa_batam
RENAME COLUMN giskemen_4 TO id;
-- Step C: Set new primary key
ALTER TABLE desa_batam
ADD PRIMARY KEY (id);A: Remove the old primary key constraint…
B: Rename the cryptic “giskemen_4” column to “id”…
C: Declare this renamed column as the new primary key.
Like relabeling the filing system and assigning a new master index.
SELECT is the most-used SQL command. It retrieves data that matches your criteria.
-- Find all hospitals in the data
SELECT nama, jenis, alamat
FROM fasilitas_kesehatan
WHERE jenis = 'Rumah Sakit';
-- Count facilities per type
SELECT jenis, COUNT(*) AS jumlah
FROM fasilitas_kesehatan
GROUP BY jenis
ORDER BY jumlah DESC;Query 1: Show me the name, type, and address of every facility that is a hospital (“Rumah Sakit”).
Query 2: Count how many facilities exist for each type…
…group them by type…
…and sort from most to least.
JOINs are where relational databases shine — combining data from multiple tables.
SELECT
k.nama_desa,
k.jumlah_penduduk,
f.nama AS fasilitas_terdekat
FROM kelurahan k
JOIN fasilitas_kesehatan f
ON ST_DWithin(
k.geom, f.geom, 5000
);Show me village name, population, and nearest health facility…
Combine the kelurahan table with the health facilities table…
…where the village geometry is within 5,000 meters of a facility.
This is a spatial join — connecting tables by location instead of a shared ID.
Regular JOINs match rows by shared column values (like matching IDs). Spatial JOINs match rows by geographic proximity — “find all facilities within 5km of each village.” This is what makes PostGIS extraordinary.
A View is a query saved as a virtual table. Every time you open it, the database re-runs the query with latest data.
CREATE VIEW TestView AS
SELECT * FROM kelurahan
WHERE ST_Touches(
kelurahan.geom,
(SELECT geom FROM kelurahan
WHERE nama_desa = 'SADAI')
);Save this query as a reusable “view” called TestView…
Whenever someone opens it, automatically show all sub-districts adjacent to SADAI…
If boundaries change, the view updates automatically.
You imported government data with a column called “kol_23”. You want to rename it to “population”.
You need to know how many villages exist in each district. The table has columns: village_name, district_name.
PostGIS adds geographic superpowers to SQL. Now your queries can answer: “what’s nearby?”, “what borders what?”, and “how far apart?”
The training’s first spatial query finds all sub-districts adjacent to a specific area — like asking “who lives next door?”
SELECT * INTO qlayer
FROM kelurahan
WHERE ST_Touches(
kelurahan.geometry,
(SELECT geometry
FROM kelurahan
WHERE nama_desa = 'SADAI')
)Create a new layer called “qlayer”…
…with all sub-districts whose boundary physically touches…
…the boundary of the sub-district named “SADAI”.
The inner SELECT fetches SADAI’s geometry; the outer query finds everything touching it.
Two essential spatial functions for proximity analysis:
-- How far is each village from the
-- nearest hospital? (in meters)
SELECT
v.nama_desa,
MIN(ST_Distance(
v.geom, h.geom
)) AS jarak_m
FROM kelurahan v, rumah_sakit h
GROUP BY v.nama_desa
ORDER BY jarak_m DESC;For each village, calculate the distance to every hospital…
…then keep only the minimum (nearest) distance.
Sort results so the most remote villages appear first.
This reveals underserved areas — villages farthest from healthcare.
-- Find all schools within 2km of a road
SELECT s.nama, s.jenis
FROM sekolah s, jalan j
WHERE ST_DWithin(
s.geom, j.geom, 2000
);Find every school whose geometry is within 2,000 meters of any road…
ST_DWithin is faster than calculating ST_Distance and filtering — it uses a spatial index.
Think of it as: “draw a 2km buffer around the road and check what’s inside.”
-- Count hospitals per district
SELECT
d.nama_kecamatan,
COUNT(h.id) AS jumlah_rs
FROM kecamatan d
LEFT JOIN rumah_sakit h
ON ST_Contains(d.geom, h.geom)
GROUP BY d.nama_kecamatan
ORDER BY jumlah_rs DESC;For each district polygon, count how many hospital points fall inside it.
LEFT JOIN ensures districts with zero hospitals still appear (with count 0).
ST_Contains checks: “does this polygon completely contain that point?”
Sort from most hospitals to fewest — revealing healthcare distribution.
-- Create 500m buffer around rivers
SELECT
nama_sungai,
ST_Buffer(geom, 500) AS geom
INTO sempadan_sungai
FROM sungai;
-- Count houses in the flood plain
SELECT COUNT(*) AS rumah_di_sempadan
FROM rumah r, sempadan_sungai s
WHERE ST_Within(r.geom, s.geom);Query 1: Draw a 500-meter zone around every river. Save these zones as a new table called “sempadan_sungai” (river buffer/floodplain).
Query 2: Count how many houses fall inside those flood zones.
This is from the training’s case study: “counting houses in the river floodplain.”
Real-world use: disaster risk assessment and land-use regulation.
-- Calculate area of each district in km²
SELECT
nama_kecamatan,
ST_Area(geom) / 1000000 AS area_km2
FROM kecamatan
ORDER BY area_km2 DESC;
-- Calculate road length per type
SELECT
road_type,
SUM(ST_Length(geom)) / 1000
AS total_km
FROM jalan
GROUP BY road_type;Query 1: Calculate each district’s area in square meters, divide by 1,000,000 to get km².
Sort largest first — useful for density calculations.
Query 2: Sum up all road lengths per type (highway, secondary, etc.), convert to km.
Important: these work correctly only in a projected CRS (like UTM) that uses meters!
Here’s your quick-lookup card for the most important PostGIS spatial functions:
| Function | What It Does | Returns |
|---|---|---|
ST_Touches(A, B) | Do A and B share a boundary? | true / false |
ST_Intersects(A, B) | Do A and B overlap at all? | true / false |
ST_Contains(A, B) | Is B completely inside A? | true / false |
ST_Within(A, B) | Is A completely inside B? (reverse of Contains) | true / false |
ST_DWithin(A, B, d) | Are A and B within distance d? (uses spatial index — fast!) | true / false |
ST_Distance(A, B) | Shortest distance between A and B | number (meters in UTM) |
ST_Buffer(A, d) | Create a zone of distance d around A | polygon geometry |
ST_Area(A) | Calculate area of polygon A | number (m² in UTM) |
ST_Length(A) | Calculate length of line A | number (meters in UTM) |
ST_Centroid(A) | Center point of A | point geometry |
ST_Union(A, B) | Merge geometries into one | geometry |
ST_Intersection(A, B) | Area shared by both A and B | geometry |
ST_Difference(A, B) | Part of A not overlapping B | geometry |
ST_Transform(A, srid) | Reproject geometry to a different CRS | geometry |
A flood hit district SADAI. You need to warn all adjacent districts.
A city planner needs all roads within 1km of a school for a “slow zone” regulation.
How many houses are within 200m of a river? You have tables: “rumah” (houses) and “sungai” (rivers).
Follow this recipe to design, build, and populate a real spatial database for service area analysis — from ER diagram to working PostGIS tables.
A health department needs to know: How many people live within reach of each clinic? Which areas are underserved?
HospitalsRumah Sakit, Puskesmas, Posyandu, Rumah BersalinAdmin boundsDistrict and sub-district polygonsRoadsRoad network for service area calculationPopulationCensus counts + WorldPop density rasterBefore writing any SQL, sketch the relationships using pgModeler:
Facilities, Districts, Roads, Population
Facility: name, type, beds, phone, geom. District: name, code, geom, population.
Facility → District (many-to-one: many facilities per district). Road → District (many-to-many).
Check each table against 1NF/2NF/3NF rules. Split multi-valued attributes.
-- Connect to PostgreSQL via pgAdmin4
-- Create database with PostGIS extension
CREATE DATABASE health_service_db;
-- Enable spatial support
\c health_service_db
CREATE EXTENSION postgis;
-- Create facilities table
CREATE TABLE fasilitas (
id SERIAL PRIMARY KEY,
nama VARCHAR(255),
jenis VARCHAR(50),
tempat_tidur INTEGER,
kecamatan_id INTEGER,
geom GEOMETRY(Point, 32648)
);Create a new database and enable PostGIS…
Build a “fasilitas” table with auto-incrementing ID…
Columns for name, type, bed count, and district link…
The geom column stores points in UTM Zone 48N (EPSG:32648).
Data sources from the training:
Download Indonesia extract from download.geofabrik.de. Contains roads, buildings, POIs as shapefiles.
Indonesian Ministry of Interior publishes admin boundaries via REST service.
High-resolution population density raster — download for your study area.
OSM data arrives in WGS 84 (EPSG:4326 — degrees). For accurate distance and area calculations, reproject to UTM Zone 48N (EPSG:32648) when loading into your database.
Load
Roads + facilities in UTM
Network Analysis
Service Area via QGIS
Buffer
Zone around service lines
Zonal Stats
Sum population in zones
-- After service areas are computed,
-- count population per service zone
SELECT
sa.facility_name,
SUM(
ST_SummaryStats(
ST_Clip(pop.rast, sa.geom)
)
).sum AS pop_served
FROM service_areas sa,
worldpop pop
WHERE ST_Intersects(
pop.rast, sa.geom)
GROUP BY sa.facility_name;For each facility’s service area polygon…
Clip the WorldPop raster to just that polygon…
Sum all the pixel values (each pixel = people/km²)…
Result: estimated population served by each facility.
Determine which villages are within 15 minutes of a fire station.
Models chain multiple operations to answer complex questions — like predicting delivery times or identifying underserved populations.
The training includes a delivery-time prediction model showing how GIS data feeds statistics:
LTIME =
+0.2663 * CARVOLUME
+0.6984 * LLENGTH
+0.0203 * LPOP
+0.0605 * TWOWAY
+0.1681 * SCHOOL
+0.0317 * RESIDENTIAL
-0.5497Delivery time depends on: traffic volume (0.27), route length (0.70 — strongest), population (0.02), two-way roads (0.06), schools (0.17 — slow zones), residential areas (0.03).
Each variable comes from a GIS layer. GIS turns geographic features into numbers that feed regression models.
The core toolkit of GIS — filter, dissolve, reclassify, analyze terrain, and create density maps.
The training extracts village locations from a massive OpenStreetMap dataset for protected area monitoring.
Thousands of points — restaurants, ATMs, villages, everything.
Right-click → Open Attribute Table. Find the “place” column.
Type: "place" = 'village'
Right-click → Save As. Check “Save only selected features”. Change CRS to UTM Zone 48 (EPSG:32648).
Dissolve merges polygons by attribute. The training uses Laos boundaries (gadm36_LAO_2.shp):
Load gadm36_LAO_2.shp
(district boundaries)
Vector → Geoprocessing → Dissolve
Field: NAME_1
Province-level polygons
Shared boundaries removed
The training covers DEM-derived analyses using the Processing Toolbox:
First derivative of the DEM — rate of elevation change. Shows how steep the terrain is at each pixel. Essential for landslide risk and road planning.
Simulates sunlight and shadow on terrain. Overlaid semi-transparently on a DEM, it creates a 3D-like effect. Use Multiply blending + 50% transparency.
Group continuous values into classes. E.g., elevation: <1000m (low), 1000-2000m (mid), >2000m (high). Uses r.reclass in the Processing Toolbox.
Terrain algorithms assume square pixels. If your raster has non-square pixels, re-export it with equal X/Y resolution (e.g., 30m × 30m) before running slope or hillshade. Right-click → Save As → set pixel size.
When you have thousands of points (noise complaints, crime locations, facility positions), heatmaps reveal patterns invisible in the raw data.
For density per km²: r = √(1,000,000 / π) ≈ 564m. Larger radius = smoother; smaller = more detail.
Quartic (default), triangular, Epanechnikov. Quartic is standard for crime/health analysis.
Balance quality vs. speed. Aim for 10-100 cells per unit area (search radius area).
Weight schools by student count, or facilities by capacity. Changes from “density of locations” to “density of service.”
| Operation | Input | Result | Use Case |
|---|---|---|---|
| Select by expression | Layer + expression | Subset of features | Extract villages from OSM |
| Dissolve | Polygons + attribute | Merged polygons | Districts → provinces |
| Buffer | Features + distance | Zone polygons | 500m zone around rivers |
| Clip | Layer + clip layer | Cropped to boundary | Cut raster to study area |
| Extract extent | Layer | Bounding box polygon | Define study area limits |
| Slope | DEM raster | Slope raster (degrees) | Landslide risk assessment |
| Hillshade | DEM raster | Shadow relief raster | 3D visualization effect |
| Reclassify | Raster + rules | Classified raster | Elevation into 3 classes |
| Heatmap | Point layer + radius | Density raster | Crime or noise hotspots |
| Zonal statistics | Raster + polygons | Stats per polygon | Population per district |
500 sub-districts, boss wants 34 provinces. Data has a “province_name” column.
Desktop GIS is powerful, but the web makes your maps accessible to everyone with a browser.
Web Map Service — returns images. Fast but no attribute queries.
Web Feature Service — returns actual data. Click-to-query works.
Web Map Tile Service — pre-cached tiles. Fastest option.
Web Coverage Service — raw raster data for analysis.
<multipart-config>
<!-- Increase upload limit -->
<max-file-size>
183886080
</max-file-size>
<max-request-size>
183886080
</max-request-size>
</multipart-config>In Tomcat’s web.xml, increase the upload limit to ~175MB…
…because the GeoServer WAR file exceeds the default ~80MB limit.
Without this, deployment fails with “file too large” error.
Users click hospitals on a web map and see name, bed count, phone.
Data collection is half the work. The other half is presenting it so clearly that the map speaks for itself.
Think of layers like transparencies stacked on a projector — order matters.
Hospitals, schools, villages — small features visible above everything.
Roads, rivers — the map’s skeleton.
Land use, elevation — the background.
One uniform style for all features. Simplest renderer.
Different colors per category. Land use: forest=green, water=blue.
Colors scale with a numeric value. Creates choropleth maps.
Maximum flexibility — custom expressions. “If highway AND lanes>2, thick red.”
Spreads overlapping points so all are visible.
Styles the outside of polygons — darken everything except your study area.
Point density as continuous surface. Dense = hot (red), sparse = cool.
Create a population map where bigger dots = more people:
Double-click the villages layer.
Select the “pop” column. Method: Size.
Natural Breaks (Jenks) for most maps. Equal Interval if data is evenly distributed. 5-7 classes recommended.
QGIS creates size classes automatically. Adjust manually if needed.
Equal Interval: equal-width ranges (0-20, 20-40). Quantile: equal number of features per class. Natural Breaks: clusters similar values. Std Deviation: shows variation from the mean. Choose based on what story you want to tell.
The training overlays a heatmap of villages on Hin Nam No National Park:
Select the villages_utm layer.
Orange-to-Red preset. Edit or create custom ramps if needed.
Controls search area. Bigger radius = smoother. Units: pixels, mm, or map units.
Weight by population to show “density of people” instead of “density of villages.”
Labels turn data into information. Without them, polygons are just colored shapes.
TextFont, size, color, bold/italic. Use readable fonts at appropriate sizes.BufferHalo around text for readability. White buffer, 1mm, round joins.PlacementPoint labels: offset from marker. Line labels: follow the road curve.Scale rulesShow labels only at certain zoom levels. Max scale 1:10000 = visible only when zoomed in.ExpressionsCombine fields: “name” || ’\n’ || “Line” creates multi-line labels.The training creates a stunning terrain visualization by combining two techniques:
Renderer: Singleband pseudocolor. Color ramp: “elevation” from cpt-city topography group. Mode: Continuous.
Drag hillshade layer above the DEM in the layer panel.
In hillshade properties: Layer transparency = 50%, Blending mode = Multiply. This creates a semi-3D effect without washing out the DEM colors.
Darker colors = higher population density across districts.
Highways = thick red, secondary = thin gray, unpaved = dashed.
Your cheat sheet — keep this open while working in pgAdmin or QGIS DB Manager.
| Command | Syntax | What It Does |
|---|---|---|
| Create table | CREATE TABLE t AS SELECT ... FROM ... | New table from query results |
| Rename column | ALTER TABLE t RENAME COLUMN a TO b | Change column name |
| Add column | ALTER TABLE t ADD COLUMN c TYPE | Add new column |
| Set primary key | ALTER TABLE t ADD PRIMARY KEY (id) | Declare unique identifier |
| Drop constraint | ALTER TABLE t DROP CONSTRAINT name | Remove a constraint |
| Select + filter | SELECT cols FROM t WHERE condition | Query rows matching criteria |
| Aggregate | SELECT col, COUNT(*) FROM t GROUP BY col | Summarize per group |
| Sort | ORDER BY col ASC|DESC | Sort results |
| Join tables | SELECT ... FROM a JOIN b ON a.id = b.id | Combine related tables |
| Create view | CREATE VIEW v AS SELECT ... | Saved query (auto-updates) |
| Function | Returns | Use Case |
|---|---|---|
ST_Touches(A, B) | bool | Adjacent districts |
ST_Intersects(A, B) | bool | Any overlap |
ST_Contains(A, B) | bool | Points inside polygons |
ST_Within(A, B) | bool | Reverse of Contains |
ST_DWithin(A, B, d) | bool | Fast proximity check |
ST_Distance(A, B) | meters | Measure distance |
ST_Buffer(A, d) | geom | Create zone around feature |
ST_Area(A) | m² | Polygon area |
ST_Length(A) | meters | Line length |
ST_Centroid(A) | point | Center of geometry |
ST_Union(A, B) | geom | Merge geometries |
ST_Intersection(A, B) | geom | Shared area |
ST_Difference(A, B) | geom | A minus B overlap |
ST_Transform(A, srid) | geom | Reproject to different CRS |
| EPSG Code | Name | Units | When to Use |
|---|---|---|---|
4326 | WGS 84 | Degrees | GPS coordinates, OpenStreetMap data, web maps |
3857 | Web Mercator | Meters | Google Maps, Bing, web tile services |
32648 | UTM Zone 48N | Meters | Batam, Indonesia — accurate distances/areas |
You’ve completed the GIS Analyst Training course — from database design through spatial SQL, geoprocessing, web mapping, and cartographic visualization.
✓ Practice with real OSM datasets
✓ Build a PostGIS database for your study area
✓ Publish a web map with GeoServer
✓ Prepare for the GIS Analyst certification exam
Built from training materials by Firman Hadi
Geomatics Engineering, Politeknik Negeri Batam