PostgreSQL UPSERT Operations: Comprehensive Guide to ON CONFLICT DO UPDATE

Nov 21, 2025 · Programming · 10 views · 7.8

Keywords: PostgreSQL | UPSERT | ON CONFLICT | EXCLUDED | Data Conflict Handling

Abstract: This technical article provides an in-depth exploration of PostgreSQL's UPSERT functionality, focusing on the ON CONFLICT DO UPDATE clause implementation in versions 9.5 and above. Through detailed code examples and performance analysis, we examine how PostgreSQL handles data insertion conflicts, compares with SQLite's INSERT OR REPLACE approach, and demonstrates best practices for using the EXCLUDED pseudo-table to access original insertion values during conflict resolution.

Overview of PostgreSQL UPSERT Functionality

In database operations, UPSERT (UPDATE or INSERT) represents a common requirement where records are updated if they exist or inserted if they don't. PostgreSQL introduced the ON CONFLICT clause starting from version 9.5, marking a significant advancement in data manipulation flexibility within the PostgreSQL ecosystem.

Basic Syntax of ON CONFLICT DO UPDATE

PostgreSQL implements UPSERT functionality primarily through the ON CONFLICT DO UPDATE clause. The fundamental syntax structure is as follows:

INSERT INTO table_name (column1, column2, ...) 
VALUES (value1, value2, ...)
ON CONFLICT (conflict_target) DO UPDATE 
SET column1 = EXCLUDED.column1, column2 = EXCLUDED.column2, ...

Here, conflict_target specifies the constraint or index used for conflict detection, while EXCLUDED is a special pseudo-table containing the row data that was originally intended for insertion but was excluded due to conflicts.

Comparison with SQLite INSERT OR REPLACE

Many developers are familiar with SQLite's INSERT OR REPLACE syntax, which provides a concise UPSERT implementation:

INSERT OR REPLACE INTO tablename (id, user, password, level, email) 
VALUES (1, 'John', 'qwerty', 5, 'john@mail.com')

However, PostgreSQL currently does not implement a similar shorthand syntax. According to official documentation, the ON CONFLICT DO UPDATE clause requires developers to explicitly specify the details of the update operation. While this design increases code verbosity, it offers greater flexibility and control.

Deep Dive into the EXCLUDED Pseudo-Table

The EXCLUDED pseudo-table is a core component of PostgreSQL's UPSERT functionality. It represents the row that was originally intended for insertion but was excluded due to unique constraint violations. Within the DO UPDATE clause, we can access these excluded values using EXCLUDED.column_name.

Consider this comprehensive example:

INSERT INTO users (id, username, password, level, email) 
VALUES (1, 'John', 'qwerty', 5, 'john@mail.com') 
ON CONFLICT (id) DO UPDATE SET 
  username = EXCLUDED.username,
  password = EXCLUDED.password, 
  level = EXCLUDED.level,
  email = EXCLUDED.email

In this example, when a record with id=1 already exists, PostgreSQL uses values from the EXCLUDED table to update all fields of the existing record.

Advantages of PostgreSQL's Design Philosophy

Although PostgreSQL doesn't provide shorthand syntax like SQLite, this explicit specification approach offers significant advantages:

Flexibility: Developers can compute new values based on existing data rather than simply replacing values. For example:

INSERT INTO users (id, level)
VALUES (1, 0)
ON CONFLICT (id) DO UPDATE
SET level = users.level + 1

This example demonstrates how to increment an existing value during conflict resolution instead of simply replacing it.

Selective Updates: You can update specific columns while leaving others unchanged:

INSERT INTO users (id, username, email)
VALUES (1, 'John', 'john@newmail.com')
ON CONFLICT (id) DO UPDATE
SET email = EXCLUDED.email

Advanced Usage and Best Practices

Conditional Updates: The WHERE clause can restrict which conflicting rows should be updated:

INSERT INTO users (id, status, last_login)
VALUES (1, 'active', NOW())
ON CONFLICT (id) DO UPDATE
SET last_login = EXCLUDED.last_login
WHERE users.status = 'active'

Multi-Column Conflict Detection: Conflicts can be detected based on combinations of multiple columns:

INSERT INTO user_sessions (user_id, session_id, data)
VALUES (1, 'abc123', '{"key": "value"}')
ON CONFLICT (user_id, session_id) DO UPDATE
SET data = EXCLUDED.data

Performance Considerations and Atomicity Guarantees

PostgreSQL's ON CONFLICT DO UPDATE provides atomic INSERT or UPDATE operation guarantees. This atomicity is particularly important in high-concurrency environments as it prevents race conditions and data inconsistencies.

From a performance perspective, ensuring that columns used for conflict detection have appropriate indexes is crucial. Without proper indexes, conflict detection can become significantly slower.

Practical Application Scenarios

User Session Management: Update user last activity time, creating a new session if none exists:

INSERT INTO user_sessions (user_id, last_activity, session_data)
VALUES (123, NOW(), '{"preferences": {}}')
ON CONFLICT (user_id) DO UPDATE
SET last_activity = EXCLUDED.last_activity,
    session_data = EXCLUDED.session_data

Counter Updates: Atomically increment counter values:

INSERT INTO page_views (page_id, view_count)
VALUES ('homepage', 1)
ON CONFLICT (page_id) DO UPDATE
SET view_count = page_views.view_count + 1

Conclusion

While PostgreSQL's ON CONFLICT DO UPDATE may be more verbose than SQLite's INSERT OR REPLACE in certain scenarios, this design provides unparalleled flexibility and control. Through the EXCLUDED pseudo-table and precise update specifications, developers can build complex data manipulation logic while benefiting from PostgreSQL's robust transaction guarantees and performance optimizations.

For scenarios requiring simple replacement of all column values, although explicit listing of all columns is necessary, this approach promotes code clarity and maintainability. As PostgreSQL continues to evolve, developers can anticipate more optimizations and shorthand syntax, but the current design already offers a powerful and reliable solution for most UPSERT scenarios.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.