PostgreSQL Tutorial
PostgreSQL, usually called Postgres, is a free and open-source object-relational database. It is known for strict data correctness, full ACID transactions, standards-compliant SQL and an unusual degree of extensibility: you can add your own data types, functions, index methods and even whole features through extensions.
This tutorial explains how PostgreSQL works and how to use it well. It covers the server architecture, how MVCC lets readers and writers work without blocking each other, data types (including JSONB), constraints, indexes, transactions, query tuning with EXPLAIN, replication, and connecting from Java. Every section includes small, runnable examples.
1. What Is PostgreSQL?
PostgreSQL is a relational database management system (RDBMS). It stores data in tables of rows and columns, links tables with keys and lets you query them with SQL. It is called object-relational because it also supports ideas from object-oriented programming, such as custom data types, table inheritance and functions that can be written in several languages.
What makes PostgreSQL stand out is its combination of reliability and flexibility:
- Correctness first: every change happens inside a transaction, constraints are always enforced, and committed data survives crashes.
- Rich SQL: window functions, common table expressions,
MERGE, lateral joins and JSON querying are all built in. - Extensible: extensions such as PostGIS (maps and geography), pgvector (AI embeddings) and pg_stat_statements (query statistics) add major capabilities without changing the core server.
- Truly open: it is developed by an independent global community and released under the permissive PostgreSQL License, so no single company controls it.
Because of this, PostgreSQL is used for everything from small web applications to banking systems, analytics platforms and geographic information systems.
2. A Short History of PostgreSQL
PostgreSQL grew out of the POSTGRES research project, started in 1986 at the University of California, Berkeley, by Professor Michael Stonebraker. The name meant "post-Ingres", because it followed his earlier Ingres database.
| Year | Milestone |
|---|---|
| 1986 | The POSTGRES project began at UC Berkeley. |
| 1995 | Postgres95 replaced the original query language with SQL. |
| 1996 | The project was renamed PostgreSQL and became a community-run open-source project. |
| 2010 | Version 9.0 added built-in streaming replication and hot standby servers. |
| 2014 | Version 9.4 introduced the binary JSONB type. |
| 2017 | Version 10 added declarative table partitioning and logical replication. |
| 2022 | Version 15 added the SQL standard MERGE command. |
| 2024 | Version 17 added incremental backups and JSON_TABLE. |
| 2025 | Version 18 introduced an asynchronous I/O subsystem, the uuidv7() function and virtual generated columns. |
A new major version is released every year, usually in September or October, and each major version receives bug and security fixes for five years.
3. Key Features of PostgreSQL
| Feature | What it means for you |
|---|---|
| ACID transactions | Groups of changes either all succeed or all fail, and committed data is never lost, even after a crash. |
| MVCC | Readers never block writers and writers never block readers, so the database stays fast under concurrent load. |
| Advanced data types | JSONB, arrays, ranges, UUIDs, network addresses, geometric types and your own custom types. |
| Many index types | B-tree, Hash, GIN, GiST, SP-GiST and BRIN, plus partial, expression and covering indexes. |
| Full-text search | Built-in search with stemming, ranking and language support, with no separate search engine needed for many use cases. |
| Procedural languages | Write functions and procedures in PL/pgSQL, SQL, and through extensions in Python, Perl and other languages. |
| Replication | Physical streaming replication for standby servers and logical replication for copying selected tables. |
| Security | Roles, fine-grained privileges, row-level security, SSL/TLS and SCRAM password authentication. |
| Extensions | Add features with a single CREATE EXTENSION command. |
4. PostgreSQL Architecture
PostgreSQL uses a process-per-connection model. Understanding its main parts helps you configure and troubleshoot it.
4.1 Processes
- Postmaster: the main server process. It listens on port 5432 and starts a new backend process for each client connection.
- Backend processes: each one parses, plans and executes the SQL of a single connection.
- Background processes: the checkpointer and background writer flush changed pages to disk, the WAL writer writes the transaction log, autovacuum cleans up old row versions, and WAL senders stream changes to replicas.
Because every connection is a separate operating-system process, thousands of idle connections waste memory. Applications therefore use a connection pool, such as HikariCP inside the application or PgBouncer in front of the database.
4.2 Memory
- shared_buffers: the shared cache of table and index pages (8 KB each). A common starting value is about 25% of the server's RAM.
- WAL buffers: hold transaction log records until they are written to disk.
- work_mem: memory per sort or hash operation in a query. Each query may use several multiples of it, so raise it carefully.
- maintenance_work_mem: memory for maintenance tasks such as
VACUUMandCREATE INDEX.
4.3 The write-ahead log (WAL)
Before a change to a data page is written to disk, a description of the change is first written to the write-ahead log. When a transaction commits, PostgreSQL only has to make sure its WAL records are safely on disk, which is a fast sequential write. The changed data pages can be written later. If the server crashes, it replays the WAL from the last checkpoint to restore every committed change. The same WAL is used for replication and point-in-time recovery.
5. MVCC and VACUUM
PostgreSQL handles concurrent users with Multi-Version Concurrency Control (MVCC). Instead of changing a row in place, an UPDATE writes a new version of the row and marks the old one as expired. Each row version records which transaction created it (xmin) and which transaction deleted or replaced it (xmax).
Every transaction works with a snapshot: a record of which transactions were committed when it started. It then sees only the row versions that were valid for that snapshot. As a result:
- A long report can read consistent data while other users keep updating the same rows.
- Readers do not need to lock rows, so they never wait for writers.
- Writers only wait for other writers changing the same row.
5.1 Why VACUUM matters
Expired row versions, called dead tuples, stay in the table until VACUUM removes them and makes the space reusable. VACUUM also updates planner statistics (with ANALYZE) and protects against transaction ID wraparound, a limit that would otherwise stop the database from accepting writes.
The autovacuum daemon does this automatically, and it should almost never be turned off. Tables with very heavy updates may need more aggressive autovacuum settings so that they do not grow "bloated" with dead tuples.
-- Tables with the most dead row versions and when they were last vacuumed
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
-- Clean up one table manually and refresh its statistics
VACUUM (ANALYZE, VERBOSE) orders;
6. Running PostgreSQL Locally with Docker
The fastest way to get a working server is the official Docker image:
# Start PostgreSQL 18 on port 5432 with a password for the 'postgres' superuser
docker run --name pg -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:18
# Open the interactive psql shell inside the container
docker exec -it pg psql -U postgres
# Or connect from your own machine if psql is installed
psql -h localhost -U postgres
For real projects, mount a volume so your data survives when the container is removed, and check the image documentation for the correct volume path for your PostgreSQL version.
7. Databases, Schemas, Roles and psql
A PostgreSQL server (often called a cluster) is organised in layers:
| Object | Description |
|---|---|
| Cluster | One running server instance with one data directory. It can hold many databases. |
| Database | An isolated collection of schemas. A connection is always to one database, and a query cannot join tables in different databases directly. |
| Schema | A namespace inside a database that groups tables, views and functions. Every database starts with a public schema. |
| Role | A user or group. Roles are shared by all databases in the cluster and are granted privileges on objects. |
-- Run as the postgres superuser
CREATE ROLE shop_app WITH LOGIN PASSWORD 'change-me';
CREATE DATABASE shop OWNER shop_app;
\c shop shop_app -- psql: connect to the new database as the new role
CREATE SCHEMA sales;
SET search_path TO sales, public; -- look in 'sales' first for unqualified names
Useful psql meta-commands, which start with a backslash:
\llists databases and\c dbnameconnects to one.\dnlists schemas,\dtlists tables and\d tablenamedescribes a table.\dulists roles.\xtoggles expanded output, which is handy for wide rows.\timingshows how long each query takes.\i file.sqlruns a script and\qquits.
8. Data Types Worth Knowing
| Type | Use it for |
|---|---|
bigint GENERATED ALWAYS AS IDENTITY | Auto-incrementing primary keys. This is the modern, standard replacement for serial. |
uuid | Globally unique IDs. PostgreSQL 18 adds uuidv7(), which creates time-ordered UUIDs that index well. |
text, varchar(n) | Strings. text has no length limit and performs the same as varchar. |
numeric(p, s) | Exact decimal numbers such as money amounts. Avoid float for money. |
timestamptz | Points in time. It stores the moment in UTC and displays it in the session's time zone. |
jsonb | Semi-structured JSON stored in a binary format that can be indexed and queried efficiently. |
Arrays, such as text[] | Small lists of values, such as tags. |
Ranges, such as tstzrange | Periods of time or numeric ranges, for example room bookings. |
| Enums and domains | A fixed set of values, or a base type with built-in rules. |
9. SQL Basics with Examples
The examples below build a small online shop. Run them in psql connected to the shop database.
9.1 Create tables with constraints
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
full_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
price numeric(10, 2) NOT NULL CHECK (price >= 0),
attributes jsonb NOT NULL DEFAULT '{}'
);
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers (id),
status text NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'PAID', 'SHIPPED', 'CANCELLED')),
placed_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
order_id bigint NOT NULL REFERENCES orders (id) ON DELETE CASCADE,
product_id bigint NOT NULL REFERENCES products (id),
quantity int NOT NULL CHECK (quantity > 0),
unit_price numeric(10, 2) NOT NULL,
PRIMARY KEY (order_id, product_id)
);
Constraints are your safety net: NOT NULL, UNIQUE, CHECK and REFERENCES (foreign keys) are enforced by the database on every change, no matter which application or script makes it.
9.2 Insert, query, update and delete
-- RETURNING gives back generated values without a second query
INSERT INTO customers (email, full_name)
VALUES ('meera@example.com', 'Meera Iyer')
RETURNING id, created_at;
INSERT INTO products (sku, name, price, attributes) VALUES
('KET-200', 'Steel Kettle', 1499.00, '{"colour": "silver", "litres": 1.7}'),
('MUG-010', 'Ceramic Mug', 299.00, '{"colour": "blue", "dishwasher_safe": true}');
-- Join tables and aggregate
SELECT c.full_name, o.id AS order_id, SUM(i.quantity * i.unit_price) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items i ON i.order_id = o.id
GROUP BY c.full_name, o.id
ORDER BY total DESC;
UPDATE products SET price = price * 0.90 WHERE sku = 'MUG-010' RETURNING name, price;
DELETE FROM orders WHERE status = 'CANCELLED' AND placed_at < now() - interval '1 year';
9.3 Upserts with ON CONFLICT
An upsert inserts a row, or updates it if it already exists, in one atomic statement:
INSERT INTO products (sku, name, price)
VALUES ('KET-200', 'Steel Kettle (2026 model)', 1599.00)
ON CONFLICT (sku)
DO UPDATE SET name = EXCLUDED.name, price = EXCLUDED.price;
EXCLUDED refers to the row that you tried to insert. For more complex synchronisation, such as applying a whole batch of changes from a staging table, use the SQL standard MERGE command (PostgreSQL 15 and later).
10. Powerful SQL Features
10.1 Querying JSONB
-- ->> returns a field as text; @> tests whether the document contains a JSON fragment
SELECT name, attributes ->> 'colour' AS colour
FROM products
WHERE attributes @> '{"dishwasher_safe": true}';
-- A GIN index makes containment queries fast on large tables
CREATE INDEX products_attributes_gin ON products USING gin (attributes);
-- Change one key without rewriting the whole document in the application
UPDATE products
SET attributes = jsonb_set(attributes, '{colour}', '"black"')
WHERE sku = 'KET-200';
JSONB is ideal for attributes that vary from row to row. Keep core fields that you filter, join or constrain on as normal columns, and put the flexible extras in JSONB.
10.2 Common table expressions and window functions
-- Each customer's orders with a running total and a rank by order value
WITH order_totals AS (
SELECT o.customer_id, o.id AS order_id, o.placed_at,
SUM(i.quantity * i.unit_price) AS total
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.customer_id, o.id, o.placed_at
)
SELECT customer_id, order_id, placed_at, total,
SUM(total) OVER (PARTITION BY customer_id ORDER BY placed_at) AS running_total,
RANK() OVER (PARTITION BY customer_id ORDER BY total DESC) AS value_rank
FROM order_totals
ORDER BY customer_id, placed_at;
A common table expression (the WITH part) names an intermediate result so a long query reads top to bottom. Window functions calculate values across related rows, such as running totals, rankings and moving averages, without collapsing them as GROUP BY would.
10.3 Full-text search
ALTER TABLE products
ADD COLUMN search tsvector
GENERATED ALWAYS AS (to_tsvector('english', name)) STORED;
CREATE INDEX products_search_idx ON products USING gin (search);
SELECT name, ts_rank(search, query) AS rank
FROM products, websearch_to_tsquery('english', 'steel kettles') AS query
WHERE search @@ query
ORDER BY rank DESC;
The English configuration reduces words to their stems, so a search for "kettles" also finds "Kettle". websearch_to_tsquery accepts the kind of input users type into a search box, including quotes and a minus sign to exclude words.
10.4 Functions and triggers
ALTER TABLE orders ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$;
CREATE TRIGGER orders_touch
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
Triggers are useful for small, universal rules such as timestamps and audit records. Keep business logic that changes often in your application, where it is easier to test and deploy.
11. Indexes and Query Tuning
An index lets PostgreSQL find rows without scanning the whole table. Choosing the right type matters:
| Index type | Best for |
|---|---|
| B-tree (default) | Equality and range comparisons (=, <, BETWEEN), sorting and LIKE 'abc%'. |
| Hash | Equality-only lookups. |
| GIN | Values that contain many items: JSONB, arrays and full-text search. |
| GiST / SP-GiST | Geometric data, ranges, nearest-neighbour searches and PostGIS. |
| BRIN | Very large tables where values follow the physical order, such as append-only logs by time. Tiny and cheap to maintain. |
-- Foreign keys are NOT indexed automatically; index them for joins and deletes
CREATE INDEX orders_customer_idx ON orders (customer_id);
-- Multicolumn index: filter by customer, then sort by date
CREATE INDEX orders_customer_placed_idx ON orders (customer_id, placed_at DESC);
-- Partial index: only the rows a hot query actually needs
CREATE INDEX orders_pending_idx ON orders (placed_at) WHERE status = 'PENDING';
-- Expression index for case-insensitive lookups
CREATE INDEX customers_email_lower_idx ON customers (lower(email));
-- Build an index on a busy production table without blocking writes
CREATE INDEX CONCURRENTLY order_items_product_idx ON order_items (product_id);
11.1 Reading EXPLAIN ANALYZE
EXPLAIN shows the plan PostgreSQL chooses for a query, and EXPLAIN ANALYZE actually runs it and reports real timings and row counts:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, placed_at
FROM orders
WHERE customer_id = 42
ORDER BY placed_at DESC
LIMIT 10;
Things to look for in the output:
- Seq Scan on a large table where you expected an Index Scan: a missing or unusable index.
- A big difference between estimated rows and actual rows: outdated statistics, so run
ANALYZE. - Sort nodes that spill to disk (external merge):
work_memmay be too small, or an index could provide the order. - Many shared read buffers: data is coming from disk rather than the cache.
pg_stat_statements extension. It records how often each query runs and how much total time it takes, so you can tune the queries that matter most instead of guessing.12. Transactions and Isolation Levels
A transaction groups statements so they succeed or fail together. Here, money moves between two wallets, and nobody can ever see the money missing from both:
CREATE TABLE wallets (
id bigint PRIMARY KEY,
balance numeric(12, 2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO wallets VALUES (1, 1000.00), (2, 250.00);
BEGIN;
UPDATE wallets SET balance = balance - 300 WHERE id = 1;
UPDATE wallets SET balance = balance + 300 WHERE id = 2;
COMMIT; -- or ROLLBACK; to undo both changes
-- Lock a row you are about to change, so concurrent transactions wait their turn
BEGIN;
SELECT balance FROM wallets WHERE id = 1 FOR UPDATE;
-- ... check the balance in the application, then update ...
COMMIT;
If either update breaks the CHECK constraint, the whole transaction fails and neither balance changes. PostgreSQL also supports SAVEPOINT to roll back part of a transaction.
| Isolation level | What a transaction sees | When to use it |
|---|---|---|
| Read Committed (default) | Each statement sees data committed before that statement began. | Most OLTP work, combined with row locks or atomic updates where needed. |
| Repeatable Read | The whole transaction sees one snapshot taken at its first statement. | Reports and multi-step reads that must be consistent with each other. |
| Serializable | Results are as if transactions ran one at a time; conflicting transactions fail with a serialization error. | Complex rules that are hard to protect with locks. The application must retry failed transactions. |
PostgreSQL accepts READ UNCOMMITTED but treats it as Read Committed. Thanks to MVCC, it never shows uncommitted data from other transactions.
13. Backup, Replication and Scaling
13.1 Backups
- Logical backups with
pg_dump(one database) orpg_dumpall(the whole cluster) export SQL or archive files. Restore them withpsqlorpg_restore. They are simple and portable between versions. - Physical backups with
pg_basebackupcopy the data files. Combined with archived WAL, they allow point-in-time recovery: restoring the database to any moment, such as one second before an accidentalDELETE. PostgreSQL 17 added incremental physical backups.
# Compressed custom-format dump of one database
pg_dump -h localhost -U postgres -Fc -f shop.dump shop
# Restore it into a new, empty database using 4 parallel jobs
createdb -h localhost -U postgres shop_restored
pg_restore -h localhost -U postgres -d shop_restored -j 4 shop.dump
13.2 Replication
- Streaming (physical) replication sends WAL from a primary to one or more standby servers, which can serve read-only queries. Replication can be asynchronous (faster) or synchronous (no data loss on failover).
- Logical replication publishes changes to selected tables, which other PostgreSQL servers subscribe to. It is used for upgrades with minimal downtime, data distribution and feeding other systems.
PostgreSQL does not include automatic failover itself. Tools such as Patroni, or a managed cloud service, monitor the primary and promote a standby when it fails.
13.3 Scaling
A single PostgreSQL server goes a long way with good indexes, connection pooling and enough memory. Beyond that, common steps are read replicas for read-heavy traffic, declarative partitioning to split huge tables by range, list or hash, and, for very large write workloads, sharding with extensions such as Citus.
14. Connecting to PostgreSQL from Java
Java applications use the PostgreSQL JDBC driver. Add it with Maven (check Maven Central for the latest 42.x version):
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
</dependency>
The program below inserts a customer inside a transaction and reads it back. It uses PreparedStatement parameters, which prevent SQL injection and let the server reuse query plans.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CustomerDemo {
private static final String URL = "jdbc:postgresql://localhost:5432/shop";
public static void main(String[] args) throws SQLException {
try (Connection conn = DriverManager.getConnection(URL, "shop_app", "change-me")) {
conn.setAutoCommit(false); // start an explicit transaction
long id;
try (PreparedStatement insert = conn.prepareStatement(
"INSERT INTO customers (email, full_name) VALUES (?, ?) RETURNING id")) {
insert.setString(1, "ravi@example.com");
insert.setString(2, "Ravi Menon");
try (ResultSet rs = insert.executeQuery()) {
rs.next();
id = rs.getLong("id");
}
conn.commit();
} catch (SQLException e) {
conn.rollback(); // undo everything on failure
throw e;
}
try (PreparedStatement select = conn.prepareStatement(
"SELECT full_name, created_at FROM customers WHERE id = ?")) {
select.setLong(1, id);
try (ResultSet rs = select.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("full_name") + " joined at "
+ rs.getObject("created_at", java.time.OffsetDateTime.class));
}
}
}
}
}
}
In a Spring Boot application, add spring-boot-starter-data-jpa (or spring-boot-starter-jdbc) and the driver, then configure the connection. Spring Boot uses the HikariCP connection pool by default:
spring.datasource.url=jdbc:postgresql://localhost:5432/shop
spring.datasource.username=shop_app
spring.datasource.password=change-me
spring.datasource.hikari.maximum-pool-size=10
# Let a migration tool such as Flyway or Liquibase manage the schema in production
spring.jpa.hibernate.ddl-auto=validate
15. PostgreSQL vs. MySQL
Both are excellent open-source relational databases. The differences are mostly about emphasis:
| Aspect | PostgreSQL | MySQL (InnoDB) |
|---|---|---|
| Licence and governance | PostgreSQL License; community-run. | GPL community edition plus commercial editions; owned by Oracle. |
| SQL features | Very broad: advanced window functions, MERGE, RETURNING, rich CTEs, transactional DDL. | Solid core SQL; fewer advanced features. |
| Data types | JSONB with GIN indexes, arrays, ranges, custom types. | JSON type with generated-column indexes; fewer specialised types. |
| Extensibility | Extensions such as PostGIS and pgvector. | Mainly through storage engines and plugins. |
| Connections | Process per connection; pooling recommended. | Thread per connection; handles many connections more cheaply. |
| Typical strengths | Complex queries, data integrity, analytics, geospatial and mixed workloads. | Simple, read-heavy web workloads with wide hosting support. |
If you are learning both, the MySQL tutorial covers MySQL in depth, and the MySQL vs. PostgreSQL article compares them further.
16. Best Practices and Common Mistakes
Best practices
- Use the right types:
timestamptzfor time,numericfor money and identity columns or UUIDs for keys. - Let the database protect your data with
NOT NULL, foreign keys,UNIQUEandCHECKconstraints. - Index foreign keys and the columns used in frequent filters, joins and sorts, then check plans with
EXPLAIN ANALYZE. - Use a connection pool and keep transactions short.
- Manage schema changes with migrations (Flyway or Liquibase), and use
CREATE INDEX CONCURRENTLYon live tables. - Give each application its own role with only the privileges it needs; never connect as the superuser.
- Test your backups by restoring them regularly.
- Stay on a supported major version and apply minor releases promptly.
Common mistakes to avoid
| Mistake | Why it hurts | Better approach |
|---|---|---|
| Leaving transactions open (state "idle in transaction") | Holds locks and stops VACUUM from cleaning dead rows, causing bloat. | Commit promptly and set idle_in_transaction_session_timeout. |
| Turning off autovacuum | Tables bloat, statistics go stale and wraparound becomes a risk. | Tune autovacuum per table instead of disabling it. |
| Opening hundreds of direct connections | Each one is a process that uses memory and CPU. | Use HikariCP or PgBouncer. |
Using timestamp without time zone for events | Ambiguous times when servers or users are in different zones. | Use timestamptz. |
| Building SQL by joining strings | Opens the door to SQL injection. | Always use bound parameters. |
| Putting everything in one JSONB column | Loses constraints, type checking and efficient joins. | Use columns for core fields and JSONB for optional extras. |
17. Summary
- PostgreSQL is a free, community-developed, object-relational database known for correctness, rich SQL and extensibility.
- It uses one process per connection, a shared buffer cache, and a write-ahead log that guarantees durability and powers replication.
- MVCC keeps multiple row versions so readers and writers do not block each other; autovacuum cleans up old versions.
- Constraints, transactions and three isolation levels keep data consistent.
- Features such as JSONB, CTEs, window functions, full-text search and many index types let one database serve many needs.
EXPLAIN ANALYZEandpg_stat_statementsare the main tools for performance tuning.- Java applications connect through the JDBC driver, usually via HikariCP and Spring Boot.
A good next step is to create the shop schema above, load a few thousand rows with generate_series, and compare query plans before and after adding indexes. Then open the FAQ tab for answers to the questions developers most often ask about PostgreSQL.
Frequently Asked Questions (FAQ)
Q1. Is PostgreSQL really free for commercial use?
Yes. PostgreSQL is released under the PostgreSQL License, a short, permissive licence similar to the BSD and MIT licences. You can use, modify and distribute it in commercial products without paying fees or publishing your own source code.
Paid offerings exist, such as managed cloud databases and support contracts, but the database itself is free.
Q2. Should I say "Postgres" or "PostgreSQL"?
Both are correct. PostgreSQL is the official name and is pronounced "post-gress-Q-L". Postgres, the original project name, is an accepted nickname used widely in the community and in the official documentation.
Q3. Which PostgreSQL version should I use?
For new projects, use the latest major version that your hosting provider and tools support, and always apply the newest minor release (for example 18.x) because minor releases contain only bug and security fixes.
Each major version is supported for five years. Plan an upgrade before your version reaches end of life; you can check the dates on the official versioning policy page.
Q4. What is the difference between a database and a schema in PostgreSQL?
A database is a fully separate container: a connection belongs to one database, and you cannot directly join tables from two databases. A schema is a namespace inside a database, and queries can freely combine tables from different schemas.
Use separate databases to isolate unrelated applications, and schemas to organise one application's objects, for example sales and inventory.
Q5. Should I use SERIAL or IDENTITY columns?
Use GENERATED ALWAYS AS IDENTITY (or GENERATED BY DEFAULT AS IDENTITY) for new tables. Identity columns follow the SQL standard, keep the sequence tightly attached to the column, and with ALWAYS stop accidental manual inserts of ID values.
serial still works, but it is an older PostgreSQL-specific shortcut that creates a separate sequence with looser permissions and ownership handling.
Q6. Should I use UUIDs or integers as primary keys?
bigint identity keys are small, fast and easy to read, which makes them a great default. UUIDs are useful when IDs must be generated outside the database, merged from several systems, or hidden from users so they cannot guess other IDs.
Random UUIDs (version 4) spread inserts across the whole index. Time-ordered UUIDs (version 7), available through uuidv7() in PostgreSQL 18, keep new values close together and index much better.
Q7. What is the difference between JSON and JSONB?
json stores the exact text you supplied, including spaces, key order and duplicate keys, and parses it every time you query it. jsonb stores a parsed binary form: it is slightly slower to write but much faster to query, and it supports GIN indexes and operators such as @>.
Use jsonb in almost all cases. Choose json only when you must keep the original text exactly as received.
Q8. Can PostgreSQL replace a NoSQL document database?
For many applications, yes. With jsonb, GIN indexes and JSON path queries, PostgreSQL can store and search flexible documents while keeping transactions, joins and constraints for the rest of your data.
A dedicated document database can still be a better fit for very large datasets that need built-in automatic sharding, or for teams that want document-first tooling throughout.
Q9. Why is my table getting bigger even though I delete rows?
Because of MVCC, deleted and updated rows leave behind dead tuples. VACUUM marks their space as reusable for new rows in the same table, but it normally does not return space to the operating system, so the file size stays the same.
If a table grows far larger than its live data (called bloat), check that autovacuum is keeping up and that no long-running transactions are blocking cleanup. VACUUM FULL rewrites the table compactly but locks it completely while it runs; the pg_repack extension can do a similar job with minimal locking.
Q10. What is the difference between VACUUM, VACUUM FULL and ANALYZE?
- VACUUM removes dead row versions so their space can be reused. It runs alongside normal reads and writes.
- VACUUM FULL rewrites the whole table into a new, compact file and returns space to the operating system, but it takes an exclusive lock that blocks all access.
- ANALYZE collects statistics about the data distribution so the planner can choose good query plans.
Autovacuum runs VACUUM and ANALYZE automatically. Use VACUUM FULL only for occasional, planned maintenance.
Q11. Why is PostgreSQL not using my index?
Common reasons include:
- The query returns a large share of the table, so a sequential scan really is cheaper.
- Statistics are out of date; run
ANALYZE. - The condition does not match the index, for example
WHERE lower(email) = ?against an index on plainemail, or a type mismatch between the column and the parameter. - A multicolumn index is used without its leading column.
LIKE '%text'searches cannot use an ordinary B-tree index; a trigram (pg_trgm) GIN index can help.
Use EXPLAIN ANALYZE to see the plan and the estimated versus actual row counts.
Q12. How many connections can PostgreSQL handle?
The default max_connections is 100. You can raise it, but because each connection is a separate process, hundreds of active connections compete for CPU and memory and can make the server slower overall.
The usual approach is a small application-side pool (for example 10 to 20 connections per instance with HikariCP) and, when many application instances connect, PgBouncer in front of the database to share a limited number of server connections.
Q13. What is the default transaction isolation level?
The default is Read Committed: each statement sees the data committed before that statement started. It is a good default for most applications.
Use Repeatable Read when several queries in one transaction must see the same snapshot, and Serializable when you need full protection against concurrency anomalies and can retry transactions that fail with a serialization error.
Q14. How do I find and stop a slow or stuck query?
Look at the pg_stat_activity view, which shows every session, its state, its current query and when it started:
SELECT pid, state, now() - query_start AS runtime, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY runtime DESC;
Then cancel the query with SELECT pg_cancel_backend(pid);, or, if that does not work, end the whole session with pg_terminate_backend(pid). Setting statement_timeout prevents runaway queries in the first place.
Q15. How do I upgrade to a new major version?
There are three common methods:
- pg_upgrade: converts the data directory in place, usually in minutes, with downtime during the switch.
- pg_dump and restore: simple and reliable, but slow for large databases.
- Logical replication: replicate into a new server running the new version, then switch applications over with very little downtime.
Always test the upgrade on a copy first and run ANALYZE afterwards so the planner has fresh statistics.
Q16. Does PostgreSQL support stored procedures?
Yes. Functions, created with CREATE FUNCTION, return a value and can be used inside queries. Procedures, created with CREATE PROCEDURE and run with CALL, do not return a value in the same way but can COMMIT and ROLLBACK inside their body, which suits long batch jobs.
Both are usually written in PL/pgSQL or plain SQL, and extensions add languages such as PL/Python.
Q17. What is row-level security?
Row-level security (RLS) lets the database decide which rows each user may see or change. After ALTER TABLE ... ENABLE ROW LEVEL SECURITY, you create policies such as "users can only read orders where tenant_id matches their own".
It is especially useful for multi-tenant applications, because the rule is enforced by the database even if application code forgets a WHERE condition.
Q18. Can PostgreSQL be used for AI and vector search?
Yes, through the popular open-source pgvector extension. It adds a vector type for embeddings, distance operators for similarity search, and approximate-nearest-neighbour indexes (HNSW and IVFFlat).
Keeping embeddings next to your normal data means you can combine similarity search with ordinary filters, joins and transactions in one query.
Q19. How do I store files or images in PostgreSQL?
You can store binary data in a bytea column, which works well for small files such as thumbnails or documents up to a few megabytes. For large or numerous files, it is usually better to keep them in object storage (such as an S3-compatible service) or the file system, and store only the path, size and checksum in PostgreSQL.
This keeps the database and its backups small and fast.
Q20. Which programming languages and frameworks work with PostgreSQL?
Practically all of them. There are mature drivers for Java (JDBC and R2DBC), Python (psycopg), Node.js (node-postgres), Go (pgx), .NET (Npgsql), Ruby, PHP and Rust.
Popular frameworks and ORMs, including Spring Data JPA and Hibernate, Django, Rails, Entity Framework and Prisma, support PostgreSQL out of the box.