Skip to content

Commit b18febe

Browse files
committed
blog: postgres tips for application developer
1 parent f216e33 commit b18febe

File tree

2 files changed

+361
-0
lines changed

2 files changed

+361
-0
lines changed
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
---
2+
title: 5 Postgres Production Tips for Application Developers
3+
author: Tianzhou
4+
updated_at: 2025/08/28 18:00
5+
feature_image: /content/blog/postgres-tips-for-application-developers/banner.webp
6+
tags: Explanation
7+
description: 5 quick wins for application developers to improve Postgres reliability and make both their own and DBA/infrastructure teams' lives easier.
8+
---
9+
10+
As an application developer, you can make your PostgreSQL applications more reliable and maintainable with just a few key practices. Here are 5 essential tips that focus on what's within your control as a developer—not database administration tasks—plus a bonus technique for managing index. Implementing these will make your DBA and infrastructure teams much happier.
11+
12+
## 1. Set application_name for Better Debugging
13+
14+
Setting `application_name` in your database connections helps with debugging. Without it, all connections look identical in monitoring tools, making troubleshooting difficult.
15+
16+
### How to Implement
17+
18+
**PostgreSQL connection string:**
19+
20+
```plain
21+
postgresql://user:password@localhost:5432/mydb?application_name=user-service
22+
```
23+
24+
**Node.js with pg:**
25+
26+
```javascript
27+
const { Pool } = require('pg');
28+
29+
const pool = new Pool({
30+
host: 'localhost',
31+
database: 'mydb',
32+
user: 'myuser',
33+
password: 'mypassword',
34+
application_name: 'user-service',
35+
});
36+
```
37+
38+
**Python with psycopg2:**
39+
40+
```python
41+
import psycopg2
42+
43+
conn = psycopg2.connect(
44+
host="localhost",
45+
database="mydb",
46+
user="myuser",
47+
password="mypassword",
48+
application_name="user-service"
49+
)
50+
```
51+
52+
**Java with JDBC:**
53+
54+
```java
55+
String url = "jdbc:postgresql://localhost:5432/mydb?ApplicationName=user-service";
56+
Connection conn = DriverManager.getConnection(url, "myuser", "mypassword");
57+
```
58+
59+
**Go with lib/pq:**
60+
61+
```go
62+
import (
63+
"database/sql"
64+
_ "github.com/lib/pq"
65+
)
66+
67+
connStr := "host=localhost dbname=mydb user=myuser password=mypassword application_name=user-service"
68+
db, err := sql.Open("postgres", connStr)
69+
```
70+
71+
### What You Get
72+
73+
Once configured, you can easily identify your application's database activity:
74+
75+
```sql
76+
-- See all active connections from your service
77+
SELECT pid, usename, application_name, state, query
78+
FROM pg_stat_activity
79+
WHERE application_name = 'user-service';
80+
81+
-- Monitor long-running queries from specific services
82+
SELECT application_name, query, now() - query_start as duration
83+
FROM pg_stat_activity
84+
WHERE state = 'active'
85+
AND now() - query_start > interval '30 seconds';
86+
```
87+
88+
## 2. Configure Sane Timeouts
89+
90+
Database timeouts prevent applications from hanging indefinitely. Without proper timeout configuration, your application can consume all connection pool slots and impact service availability.
91+
92+
### The Four Critical Timeouts
93+
94+
1. **`statement_timeout`** - Maximum time for any single SQL statement. Prevents runaway queries from consuming resources indefinitely.
95+
96+
1. **`lock_timeout`** - Maximum time to wait for a lock. Prevents deadlock scenarios from blocking your application forever.
97+
98+
1. **`idle_in_transaction_timeout`** - Maximum time a transaction can remain idle. Prevents abandoned transactions from holding locks and blocking other operations.
99+
100+
1. **`transaction_timeout`** - Maximum time for an entire transaction (PostgreSQL 17+). Prevents long-running transactions from holding locks too long.
101+
102+
### How to Configure
103+
104+
**In connection string:**
105+
106+
```plain
107+
postgresql://user:pass@localhost/db?options=-c%20statement_timeout=30s%20-c%20lock_timeout=10s%20-c%20transaction_timeout=60s
108+
```
109+
110+
**At transaction level:**
111+
112+
```sql
113+
BEGIN;
114+
SET LOCAL statement_timeout = '30s';
115+
SET LOCAL lock_timeout = '10s';
116+
SET LOCAL transaction_timeout = '60s'; -- PostgreSQL 17+
117+
-- Your transaction operations here
118+
COMMIT;
119+
```
120+
121+
### Recommended Values by Use Case
122+
123+
| Use Case | statement_timeout | lock_timeout | idle_in_transaction_timeout | transaction_timeout |
124+
| ----------------------- | ----------------- | ------------ | --------------------------- | ------------------- |
125+
| **Web Applications** | 30s | 10s | 60s | 60s |
126+
| **Background Jobs** | 300s+ | 30s | 300s | 600s |
127+
| **Reporting/Analytics** | 0 (disabled) | 60s | 600s | 0 (disabled) |
128+
129+
**Reasoning:**
130+
131+
- **Web Applications**: User-facing requests should fail fast; clean up abandoned web requests
132+
- **Background Jobs**: Data processing can take longer; jobs can wait a bit more for locks
133+
- **Reporting/Analytics**: Reports can take hours; shouldn't block operations; longer analysis sessions
134+
135+
<HintBlock type="info">
136+
137+
To learn more, please refer [Postgres Timeout Explained](/blog/postgres-timeout).
138+
139+
</HintBlock>
140+
141+
## 3. Use Online Schema Migration Options
142+
143+
Schema migrations in production require careful planning. Standard migrations can lock your tables for extended periods, impacting application availability. Here's how to perform schema changes safely without downtime.
144+
145+
### The Problem with Standard Migrations
146+
147+
Traditional migrations acquire exclusive locks that block all reads and writes to your tables:
148+
149+
```sql
150+
-- DON'T: This blocks ALL SELECT, INSERT, UPDATE, DELETE on users table
151+
ALTER TABLE users ADD COLUMN email_verified boolean DEFAULT false;
152+
153+
-- DON'T: This blocks ALL writes (INSERT, UPDATE, DELETE) on orders table for hours
154+
CREATE INDEX ON orders (user_id, created_at);
155+
```
156+
157+
When these operations run on large tables, they can:
158+
159+
- **Block all application queries** for minutes or hours
160+
- **Cause connection pool exhaustion** as queries queue up waiting for locks
161+
- **Trigger cascading failures** across your entire application stack
162+
163+
### Online Migration Strategies
164+
165+
1. Use `CREATE INDEX CONCURRENTLY`
166+
167+
```sql
168+
-- Safe: Builds index without blocking reads/writes
169+
-- Note: Cannot be run inside a transaction
170+
CREATE INDEX CONCURRENTLY idx_orders_user_created
171+
ON orders (user_id, created_at);
172+
```
173+
174+
<HintBlock type="info">
175+
176+
To learn more production tips, please refer [Postgres CREATE INDEX CONCURRENTLY](/blog/postgres-create-index-concurrently).
177+
178+
</HintBlock>
179+
180+
1. Add columns and constraints safely
181+
182+
```sql
183+
-- Step 1: Add constraint without validation (fast)
184+
ALTER TABLE users ADD CONSTRAINT email_verified_not_null
185+
CHECK (email_verified IS NOT NULL) NOT VALID;
186+
187+
-- Step 2: Validate constraint (can be done later, non-blocking)
188+
ALTER TABLE users VALIDATE CONSTRAINT email_verified_not_null;
189+
190+
-- Step 3: Convert to NOT NULL constraint
191+
ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;
192+
```
193+
194+
## 4. Split PostgreSQL Roles for Different Operations
195+
196+
Implementing separate database roles for different operations improves security. This approach prevents accidental damage and makes operations more manageable.
197+
198+
### The Problem with Single "Super User" Approach
199+
200+
Many applications use a single database user with broad permissions:
201+
202+
```sql
203+
-- DON'T: Single user with excessive privileges
204+
GRANT ALL PRIVILEGES ON DATABASE myapp TO app_user;
205+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user;
206+
```
207+
208+
This creates several problems:
209+
210+
- **Security risk**: Application has unnecessary DDL permissions
211+
- **Operational risk**: Code bugs can drop tables or modify schema
212+
- **Audit trail**: Can't distinguish between different operation types
213+
- **Performance**: Can't optimize connections for specific use cases
214+
215+
### The Three-Role Strategy
216+
217+
1. Read-Only Role - For analytics, reporting, read replicas
218+
1. Application Role - For normal CRUD operations
219+
1. Migration Role - For schema changes and DDL operations
220+
221+
### Implementation Example
222+
223+
**Create the roles:**
224+
225+
```sql
226+
-- 1. Read-only role for analytics/reporting
227+
CREATE ROLE app_reader;
228+
GRANT CONNECT ON DATABASE myapp TO app_reader;
229+
GRANT USAGE ON SCHEMA public TO app_reader;
230+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_reader;
231+
-- Automatically grant SELECT on future tables
232+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
233+
GRANT SELECT ON TABLES TO app_reader;
234+
235+
-- 2. Application role for normal operations
236+
CREATE ROLE app_writer;
237+
GRANT CONNECT ON DATABASE myapp TO app_writer;
238+
GRANT USAGE ON SCHEMA public TO app_writer;
239+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_writer;
240+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_writer;
241+
-- Auto-grant on future objects
242+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
243+
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_writer;
244+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
245+
GRANT USAGE, SELECT ON SEQUENCES TO app_writer;
246+
247+
-- 3. Migration role for schema changes
248+
CREATE ROLE app_migrator;
249+
GRANT CONNECT ON DATABASE myapp TO app_migrator;
250+
GRANT ALL PRIVILEGES ON SCHEMA public TO app_migrator;
251+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_migrator;
252+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app_migrator;
253+
```
254+
255+
**Create actual users:**
256+
257+
```sql
258+
-- Create users and assign roles
259+
CREATE USER analytics_user PASSWORD 'secure_password';
260+
GRANT app_reader TO analytics_user;
261+
262+
CREATE USER application_user PASSWORD 'secure_password';
263+
GRANT app_writer TO application_user;
264+
265+
CREATE USER migration_user PASSWORD 'secure_password';
266+
GRANT app_migrator TO migration_user;
267+
```
268+
269+
## 5. Use UPSERT for Robust Operations
270+
271+
In production environments, operations fail, networks are unreliable, and retries are inevitable. UPSERT (`INSERT ... ON CONFLICT`) helps you handle these scenarios:
272+
273+
- Avoiding duplicate records when retrying failed requests
274+
- Handling race conditions between concurrent operations
275+
- Building idempotent operations that can be safely repeated
276+
- Simplifying application logic for "create or update" patterns
277+
278+
### Ignore Duplicates
279+
280+
```sql
281+
-- Pattern 1: Ignore duplicates
282+
INSERT INTO users (email, name, created_at)
283+
VALUES ('[email protected]', 'John Doe', NOW())
284+
ON CONFLICT (email) DO NOTHING;
285+
```
286+
287+
### Update on Conflict
288+
289+
```sql
290+
-- Pattern 2: Update on conflict
291+
INSERT INTO users (email, name, updated_at)
292+
VALUES ('[email protected]', 'John Smith', NOW())
293+
ON CONFLICT (email)
294+
DO UPDATE SET
295+
name = EXCLUDED.name,
296+
updated_at = EXCLUDED.updated_at;
297+
```
298+
299+
### Conditional Update
300+
301+
```sql
302+
-- Pattern 3: Conditional update
303+
INSERT INTO user_stats (user_id, login_count, last_login)
304+
VALUES ($1, 1, NOW())
305+
ON CONFLICT (user_id)
306+
DO UPDATE SET
307+
login_count = user_stats.login_count + 1,
308+
last_login = EXCLUDED.last_login
309+
WHERE user_stats.last_login < EXCLUDED.last_login;
310+
```
311+
312+
## Bonus: Implement "Invisible" Indexes
313+
314+
PostgreSQL doesn't have native invisible indexes like some other databases, but you can simulate this behavior by manipulating the `indisvalid` flag in the `pg_index` system catalog. This technique allows you to temporarily disable an index without dropping it, which is useful for A/B testing query performance.
315+
316+
### Making an Index "Invisible"
317+
318+
```sql
319+
-- Disable an existing index (make it invisible to the query planner)
320+
UPDATE pg_index
321+
SET indisvalid = false
322+
WHERE indexrelid = 'idx_orders_status'::regclass;
323+
```
324+
325+
### Making the Index "Visible" Again
326+
327+
```sql
328+
-- Re-enable the index (make it visible to the query planner)
329+
UPDATE pg_index
330+
SET indisvalid = true
331+
WHERE indexrelid = 'idx_orders_status'::regclass;
332+
```
333+
334+
### Checking Index Status
335+
336+
```sql
337+
-- See which indexes are currently disabled
338+
SELECT
339+
schemaname,
340+
tablename,
341+
indexname,
342+
indisvalid AS is_valid
343+
FROM pg_indexes
344+
JOIN pg_index ON pg_indexes.indexname = pg_class.relname
345+
JOIN pg_class ON pg_index.indexrelid = pg_class.oid
346+
WHERE NOT indisvalid;
347+
```
348+
349+
**Note**: PostgreSQL itself uses the `indisvalid` flag internally. When you run `CREATE INDEX CONCURRENTLY`, PostgreSQL automatically sets `indisvalid = false` until the index is fully built.
350+
351+
## Conclusion
352+
353+
These 5 tips are quick wins that focus on what's within your control as a developer. Adopting them will make both your life and your DBA/infrastructure teams' lives much easier:
354+
355+
1. **Set `application_name`** - Get instant visibility into which service is doing what
356+
1. **Configure timeouts** - Prevent your application from hanging indefinitely
357+
1. **Use online migrations** - Deploy schema changes without downtime
358+
1. **Split database roles** - Follow the principle of least privilege
359+
1. **Use UPSERT** - Handle failures and retries effectively
360+
361+
Start with `application_name` and timeouts—they take minutes to implement but provide immediate benefits. These practices make your applications more reliable while making everyone's job easier.
30.9 KB
Loading

0 commit comments

Comments
 (0)