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:

  1. Open the Updates tab on your instance
  2. Choose an available target version
  3. Start the upgrade — a snapshot is taken automatically first, so you can roll back if needed
  4. 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

  1. Navigate to Databases
  2. Click Create Database Instance
  3. Select PostgreSQL as engine
  4. Choose version (18 recommended)
  5. Select resource profile
  6. Configure settings:
    • Instance name
    • Database name
    • Master username
    • Master password
    • Region
  7. Optional: Parameter group, firewall
  8. 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

Bash
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

Text
postgresql://username:password@host:port/database
postgresql://postgres:mypassword@postgres-abc123.danubedata.ro:5432/production

From Applications

Python (psycopg2):

Python
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):

Python
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):

JavaScript
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):

Java
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
<?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:

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:

RoleDatabaseUse forPrivileges
pguserpgdbDay-to-day application trafficStandard owner of pgdb — full access inside it
postgrespostgresAdmin tasks: CREATE DATABASE, installing extensions, role managementFull PostgreSQL superuser

Most applications should use pguser / pgdb. Connect as postgres only when you need cluster-wide admin capabilities.

⚠️ Do not rotate the postgres password via SQL. Statements like ALTER 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

SQL
-- 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

SQL
-- 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

SQL
-- 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:

  1. Open the Connectivity tab on your PostgreSQL instance
  2. 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.

SQL
-- 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

SQL
-- 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);
SQL
-- 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

SQL
-- 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

SQL
-- 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)

SQL
-- 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

SQL
-- 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

SQL
-- 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 0A000 errors 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):

Python
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

SQL
-- 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:

  1. Go to database page
  2. Snapshots tab
  3. Create Snapshot
  4. Name and create

Export Database

Bash
# 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

Bash
# 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

  1. Go to database page
  2. Click Add Replica
  3. Select node and profile
  4. Click Create

Benefits

  • Scale read operations
  • Reporting without impacting primary
  • Load balancing via the reader endpoint
  • High availability

Using Replicas

Python
# 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:

Bash
# psql with SSL
psql "postgresql://postgres:password@postgres-abc123.danubedata.ro:5432/production?sslmode=require"

Connection modes:

  • sslmode=require - Require SSL
  • sslmode=verify-ca - Verify certificate
  • sslmode=verify-full - Full verification

User Permissions

SQL
-- 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

SQL
-- 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

SQL
-- 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

  1. Use appropriate data types
  2. Add foreign keys
  3. Create necessary indexes
  4. Normalize when appropriate
  5. Use constraints

Query Optimization

  1. Use EXPLAIN ANALYZE
  2. Add indexes for frequent queries
  3. Avoid SELECT *
  4. Use LIMIT for large results
  5. Optimize JOINs

Maintenance

  1. Regular VACUUM ANALYZE
  2. Monitor index usage
  3. Review slow queries
  4. Update statistics
  5. Monitor disk space

Next Steps

Need help? Contact support through the dashboard.