PostgreSQL Database Instances
Complete guide to creating and managing PostgreSQL database instances on DanubeData.
Overview
PostgreSQL is a powerful, open-source object-relational database system. DanubeData provides fully managed PostgreSQL instances with:
- Automated backups - Daily snapshots with 3-day retention, plus continuous backups with 30-day retention
- High availability - Optional synchronized standby with automatic failover
- Performance monitoring - Real-time metrics and alerts
- Easy scaling - Vertical scaling, read replicas, and optional storage autoscaling
- In-place upgrades - Move to a newer PostgreSQL version without recreating
- Security - Encrypted connections and data
Every managed PostgreSQL instance also includes SQL Studio — an in-browser data studio for browsing, editing, and managing your schema, with no client to install.
Supported Versions
- PostgreSQL 18 - Latest, recommended
- PostgreSQL 17 - Current stable
- PostgreSQL 16 - Previous stable
- PostgreSQL 15 - Mature release
Version Selection
PostgreSQL 18 (Recommended):
- Latest features
- Asynchronous I/O (io_uring)
- Virtual generated columns
- Native OAuth support (requires manual configuration)
PostgreSQL 17:
- Stable and mature
- Improved partitioning
- Better JSON support
PostgreSQL 16:
- Wide adoption
- Logical replication improvements
PostgreSQL 15:
- Very stable
- Good for production
- Mature ecosystem
Upgrading Versions
You can move an existing PostgreSQL instance to a newer version in place — no need to recreate it or dump and reload:
- Open the Updates tab on your instance
- Choose an available target version
- Start the upgrade — a snapshot is taken automatically first, so you can roll back if needed
- Track progress live; the instance shows an Updating status while it runs
Take a manual snapshot first if you want an extra restore point.
Creating a PostgreSQL Instance
Step-by-Step
- Navigate to Databases
- Click Create Database Instance
- Select PostgreSQL as engine
- Choose version (18 recommended)
- Select resource profile
- Configure settings:
- Instance name
- Database name
- Master username
- Master password
- Region
- Optional: Parameter group, firewall
- Click Create Database
Creation Time: 2-3 minutes
Configuration Tips
Instance Name: prod-postgres-users
Database Name: production (initial database)
Master Username: postgres (default) or custom
Master Password: Auto-generated or custom (min 8 chars)
Connecting to PostgreSQL
Connection Information
After creation:
- Endpoint:
postgres-abc123.danubedata.ro - Port:
5432 - Username: Your master username
- Password: Master password
psql Command Line
psql -h postgres-abc123.danubedata.ro \
-p 5432 \
-U postgres \
-d production
# Or with full connection string
psql "postgresql://postgres:password@postgres-abc123.danubedata.ro:5432/production"
Connection String
postgresql://username:password@host:port/database
postgresql://postgres:mypassword@postgres-abc123.danubedata.ro:5432/production
From Applications
Python (psycopg2):
import psycopg2
conn = psycopg2.connect(
host="postgres-abc123.danubedata.ro",
port=5432,
database="production",
user="postgres",
password="your_password"
)
cur = conn.cursor()
cur.execute("SELECT version()")
version = cur.fetchone()
print(f"PostgreSQL version: {version[0]}")
cur.close()
conn.close()
Python (SQLAlchemy):
from sqlalchemy import create_engine
engine = create_engine(
'postgresql://postgres:password@postgres-abc123.danubedata.ro:5432/production'
)
with engine.connect() as conn:
result = conn.execute("SELECT 1")
print(result.fetchone())
Node.js (pg):
const { Client } = require('pg');
const client = new Client({
host: 'postgres-abc123.danubedata.ro',
port: 5432,
database: 'production',
user: 'postgres',
password: 'your_password',
});
client.connect();
client.query('SELECT NOW()', (err, res) => {
console.log(err ? err.stack : res.rows[0]);
client.end();
});
Java (JDBC):
import java.sql.*;
public class PostgreSQLExample {
public static void main(String[] args) {
String url = "jdbc:postgresql://postgres-abc123.danubedata.ro:5432/production";
String user = "postgres";
String password = "your_password";
try {
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println("Connected to PostgreSQL");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT version()");
while (rs.next()) {
System.out.println(rs.getString(1));
}
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
PHP (PDO):
<?php
$host = 'postgres-abc123.danubedata.ro';
$port = 5432;
$dbname = 'production';
$user = 'postgres';
$password = 'your_password';
try {
$dsn = "pgsql:host=$host;port=$port;dbname=$dbname";
$pdo = new PDO($dsn, $user, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->query('SELECT version()');
$version = $stmt->fetchColumn();
echo "PostgreSQL version: $version\n";
} catch (PDOException $e) {
echo "Error: " . $e->getMessage();
}
?>
Go:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func main() {
// A public *.danubedata.ro hostname is reached through the shared load
// balancer, where this instance has its own port — copy it from the
// dashboard (or the API's connection_port), it is not 5432. Use 5432 only
// with the private-network hostname.
connStr := "host=postgres-abc123.danubedata.ro port=YOUR_PUBLIC_PORT user=postgres password=your_password dbname=production sslmode=require"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
defer db.close()
var version string
err = db.QueryRow("SELECT version()").Scan(&version)
if err != nil {
panic(err)
}
fmt.Println("PostgreSQL version:", version)
}
Two sets of credentials (managed PostgreSQL)
For managed PostgreSQL clusters (currently PostgreSQL 18+), the dashboard exposes two distinct credentials:
| Role | Database | Use for | Privileges |
|---|---|---|---|
pguser | pgdb | Day-to-day application traffic | Standard owner of pgdb — full access inside it |
postgres | postgres | Admin tasks: CREATE DATABASE, installing extensions, role management | Full PostgreSQL superuser |
Most applications should use pguser / pgdb. Connect as postgres only when you need cluster-wide admin capabilities.
⚠️ Do not rotate the
postgrespassword via SQL. Statements likeALTER ROLE postgres PASSWORD '…'will break automated failover and backups managed by the DanubeData platform. Contact support if you need to rotate this credential.
Database Management
Create Databases
-- Create database
CREATE DATABASE myapp_production
WITH ENCODING 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8'
TEMPLATE template0;
-- Use database
\c myapp_production
-- List databases
\l
-- Drop database
DROP DATABASE old_database;
Create Users
-- Create user
CREATE USER appuser WITH PASSWORD 'strong_password';
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE myapp_production TO appuser;
-- Grant specific privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appuser;
-- Grant on future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO appuser;
-- List users
\du
-- Drop user
DROP USER olduser;
Create Tables
-- Create table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create index
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-- List tables
\dt
-- Describe table
\d users
-- Show create statement
\d+ users
PostgreSQL-Specific Features
PostGIS (One-Click Enable)
Turn a managed PostgreSQL database into a full geospatial database without any manual extension setup:
- Open the Connectivity tab on your PostgreSQL instance
- Use the Enable PostGIS card
One click sets up the PostGIS extension for geometry types, spatial indexes, and GIS functions. It's available on PostgreSQL 15 through 18. Your data and settings are preserved — single-node instances restart briefly, while high-availability clusters roll with near-zero downtime.
-- After enabling, PostGIS is ready to use
SELECT PostGIS_Version();
CREATE TABLE places (
id SERIAL PRIMARY KEY,
name TEXT,
geom GEOGRAPHY(Point, 4326)
);
CREATE INDEX idx_places_geom ON places USING GIST (geom);
JSONB Support
-- Create table with JSONB
CREATE TABLE events (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Insert JSON data
INSERT INTO events (data) VALUES
('{"user_id": 123, "action": "login", "ip": "192.168.1.1"}');
-- Query JSON
SELECT * FROM events WHERE data->>'action' = 'login';
-- JSON indexing
CREATE INDEX idx_events_action ON events ((data->>'action'));
-- GIN index for full JSON search
CREATE INDEX idx_events_data ON events USING GIN (data);
Full-Text Search
-- Add tsvector column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
-- Update search vector
UPDATE articles
SET search_vector = to_tsvector('english', title || ' ' || content);
-- Create GIN index
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- Search
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english', 'postgresql & database');
-- Rank results
SELECT *, ts_rank(search_vector, query) AS rank
FROM articles, to_tsquery('english', 'postgresql') query
WHERE search_vector @@ query
ORDER BY rank DESC;
Array Types
-- Create table with array
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
tags TEXT[]
);
-- Insert with array
INSERT INTO products (name, tags)
VALUES ('Laptop', ARRAY['electronics', 'computers', 'portable']);
-- Query array
SELECT * FROM products WHERE 'electronics' = ANY(tags);
-- Array contains
SELECT * FROM products WHERE tags @> ARRAY['computers'];
Window Functions
-- Rank employees by salary
SELECT
name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank
FROM employees;
-- Running total
SELECT
date,
amount,
SUM(amount) OVER (ORDER BY date) as running_total
FROM sales;
Common Table Expressions (CTEs)
-- Recursive CTE for hierarchy
WITH RECURSIVE org_chart AS (
-- Base case
SELECT id, name, manager_id, 1 as level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive case
SELECT e.id, e.name, e.manager_id, oc.level + 1
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY level, name;
Performance Optimization
Indexes
-- B-tree index (default)
CREATE INDEX idx_users_email ON users(email);
-- Partial index
CREATE INDEX idx_active_users ON users(email) WHERE active = true;
-- Composite index
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);
-- GIN index for full-text search
CREATE INDEX idx_articles_content ON articles USING GIN(to_tsvector('english', content));
-- List indexes
\di
-- Index usage stats
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
Query Optimization
-- Analyze query plan
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';
-- Analyze table
ANALYZE users;
-- Vacuum table
VACUUM ANALYZE users;
-- Full vacuum (requires maintenance window)
VACUUM FULL users;
Connection Pooling
Instances with the connection proxy enabled include built-in pgBouncer endpoints — you don't need to run your own pooler:
<instance>-proxy-rw— routes to the primary (writes)<instance>-proxy-ro— routes to read replicas only (HA instances)<instance>-proxy-r— routes to any instance (primary or replica)
The pools run in transaction mode with prepared-statement support enabled (max_prepared_statements = 200). Pooled server connections are recycled after at most one hour (server_lifetime = 3600, server_idle_timeout = 120).
Transaction pooling semantics: consecutive transactions from one client connection may execute on different server connections, so session state — LISTEN/NOTIFY, advisory locks held across statements, SET, temporary tables — does not carry over between transactions. Connect to the direct instance endpoint instead of the proxy if you need session-level features.
Prepared statements and DDL migrations: named prepared statements are cached on the pooler's server connections, which outlive your application's connections. DDL that changes a statement's result shape — an ALTER TABLE ... ADD COLUMN when a cached SELECT * targets that table, or an ALTER COLUMN ... TYPE for any cached statement returning that column — causes subsequent executions of the cached statement to fail with cached plan must not change result type (SQLSTATE 0A000). Restarting your application does not clear this: the affected connections belong to the pooler, not your app, and recycle naturally within at most one hour. To avoid it:
- Run result-shape-changing migrations using the simple query protocol, or with your driver's prepared-statement cache disabled.
- Avoid
SELECT *in hot query paths, so an added column cannot change a cached result shape. - If you hit
0A000errors after a migration, contact support — we can recycle the pooler connections immediately rather than waiting for the automatic turnover. Prepared-statement support can also be disabled per instance on request.
Application-level pooling (complements the proxy):
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
engine = create_engine(
'postgresql://postgres:password@postgres-abc123.danubedata.ro:5432/production',
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_pre_ping=True
)
Monitoring & Metrics
Key Metrics
Monitor in dashboard:
- CPU Usage
- Memory Usage
- Connections (active/max)
- Queries per second
- Cache hit ratio
- Replication lag
Database Statistics
-- Database size
SELECT pg_size_pretty(pg_database_size('production'));
-- Table sizes
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;
-- Connection stats
SELECT count(*), state
FROM pg_stat_activity
GROUP BY state;
-- Long-running queries
SELECT pid, now() - pg_stat_activity.query_start AS duration, query
FROM pg_stat_activity
WHERE state = 'active'
AND now() - pg_stat_activity.query_start > interval '5 minutes'
ORDER BY duration DESC;
-- Cache hit ratio
SELECT
sum(heap_blks_read) as heap_read,
sum(heap_blks_hit) as heap_hit,
sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as ratio
FROM pg_statio_user_tables;
Backup & Recovery
Automated Backups
- Daily at 2 AM UTC
- 3-day retention for automated snapshots
- Continuous backups with 30-day retention
- Point-in-time recovery
- No performance impact
Manual Snapshots
Create on-demand:
- Go to database page
- Snapshots tab
- Create Snapshot
- Name and create
Export Database
# Export entire database
pg_dump -h postgres-abc123.danubedata.ro \
-U postgres \
-d production \
-F custom \
-f backup.dump
# Export with compression
pg_dump -h postgres-abc123.danubedata.ro \
-U postgres \
-d production \
| gzip > backup.sql.gz
# Export schema only
pg_dump -h postgres-abc123.danubedata.ro \
-U postgres \
-d production \
--schema-only \
-f schema.sql
# Export specific tables
pg_dump -h postgres-abc123.danubedata.ro \
-U postgres \
-d production \
-t users -t orders \
-f tables.sql
Import Database
# Import from custom format
pg_restore -h postgres-abc123.danubedata.ro \
-U postgres \
-d production \
-F custom \
backup.dump
# Import from SQL
psql -h postgres-abc123.danubedata.ro \
-U postgres \
-d production \
< backup.sql
# Import compressed
gunzip < backup.sql.gz | psql -h postgres-abc123.danubedata.ro \
-U postgres \
-d production
Read Replicas
Create Replica
- Go to database page
- Click Add Replica
- Select node and profile
- Click Create
Benefits
- Scale read operations
- Reporting without impacting primary
- Load balancing via the reader endpoint
- High availability
Using Replicas
# Primary for writes
primary_engine = create_engine('postgresql://postgres:pass@primary-host:5432/db')
# Replica for reads
replica_engine = create_engine('postgresql://postgres:pass@replica-host:5432/db')
# Write to primary
with primary_engine.connect() as conn:
conn.execute("INSERT INTO users ...")
# Read from replica
with replica_engine.connect() as conn:
users = conn.execute("SELECT * FROM users").fetchall()
Security
SSL/TLS Connections
Always use SSL:
# psql with SSL
psql "postgresql://postgres:password@postgres-abc123.danubedata.ro:5432/production?sslmode=require"
Connection modes:
sslmode=require- Require SSLsslmode=verify-ca- Verify certificatesslmode=verify-full- Full verification
User Permissions
-- Application user
CREATE USER app_user WITH PASSWORD 'strong_pass';
GRANT CONNECT ON DATABASE production TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
-- Read-only user
CREATE USER readonly WITH PASSWORD 'strong_pass';
GRANT CONNECT ON DATABASE production TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
Troubleshooting
Connection Issues
-- Check max connections
SHOW max_connections;
-- Check current connections
SELECT count(*) FROM pg_stat_activity;
-- Kill idle connections
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND now() - state_change > interval '10 minutes';
Performance Issues
-- Find slow queries
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- Table bloat
SELECT schemaname, tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
-- Run VACUUM
VACUUM ANALYZE;
Best Practices
Schema Design
- Use appropriate data types
- Add foreign keys
- Create necessary indexes
- Normalize when appropriate
- Use constraints
Query Optimization
- Use EXPLAIN ANALYZE
- Add indexes for frequent queries
- Avoid SELECT *
- Use LIMIT for large results
- Optimize JOINs
Maintenance
- Regular VACUUM ANALYZE
- Monitor index usage
- Review slow queries
- Update statistics
- Monitor disk space
Next Steps
Need help? Contact support through the dashboard.