Skip to content

PostgreSQL Speed Optimization for Advanced Programmers

When it comes to optimizing PostgreSQL, understanding the nuances of configuration, storage, and query structures is essential. This tutorial delves into some advanced tips and tricks to enhance your PostgreSQL's performance.

1. Connection Optimization

Use Unix Socket Instead of TCP/IP Connection

By default, local connections in PostgreSQL are made using a Unix-domain socket. If you're connecting to a server on the same machine, a Unix socket can be faster than a TCP/IP connection.

Example: To benchmark the difference, you can use pg_bench.

1
2
3
4
5
# Using TCP/IP
pg_bench -h localhost -U your_username your_database

# Using Unix socket
pg_bench -h /var/run/postgresql -U your_username your_database

2. Store Data Effectively

Storing data in an optimized manner can significantly speed up your queries.

Example: Your initial table creation:

1
2
3
4
5
6
CREATE TABLE t_test(
    v1 varchar(100),
    i1 int,
    v2 varchar(100),
    i2 int
);

A better way is to group similar data types together:

1
2
3
4
5
6
CREATE TABLE t_test(
    v1 varchar(100),
    v2 varchar(100),
    i1 int,
    i2 int
);
This can lead to better data locality and cache utilization.

3. Indexing Strategies

Add Indexes

Adding indexes can greatly speed up data retrieval times. However, they also add overhead to write operations. Thus, use them judiciously.

Tip: If the text column is long, consider using the hashtext function to speed up operations.

Use Full Text Indexes

Full-text search is a technique to search a full-text database against user queries. PostgreSQL provides a way to both store and efficiently search through large volumes of text data.

Example:

1
2
3
4
5
CREATE INDEX idx_gin ON t_test USING gin(to_tsvector('english', v1));

-- Remember to adjust autovacuum settings for performance
ALTER TABLE t_test SET (autovacuum_vacuum_scale_factor = 0.02);
ALTER TABLE t_test SET (autovacuum_analyze_scale_factor = 0.01);

4. Query Optimization

Composite Time Trickery

Rather than unpacking the composite type in the SELECT clause, do it in the FROM clause for better readability and sometimes performance.

Example:

Instead of:

SELECT (pgstattuple('t_email')).* as x;

Use:

SELECT (x).* FROM pgstattuple('t_email') AS x;

Use Fetch Size

When querying large datasets, consider adjusting the fetch size to improve retrieval performance.

Example:

ALTER FOREIGN TABLE t_email OPTIONS (fetch_size '10000');

This will retrieve 10,000 rows in each batch from the foreign server, reducing the number of network round-trips required.

5. Regular Maintenance

Regular maintenance activities like running VACUUM, ANALYZE, and REINDEX can help in keeping your database optimized. Set up autovacuum processes, so these tasks are done automatically.

Example:

1
2
3
-- Adjust autovacuum settings for a particular table
ALTER TABLE t_test SET (autovacuum_vacuum_scale_factor = 0.05);
ALTER TABLE t_test SET (autovacuum_analyze_scale_factor = 0.025);

6. Partitioning Large Tables

Partitioning can be particularly useful for tables with a large amount of data. It allows the data to be broken down into smaller, more manageable pieces, and can improve query performance.

Example: Using range partitioning on a date column:

1
2
3
4
5
6
7
8
CREATE TABLE t_orders (
    order_id int,
    order_date date,
    ...
) PARTITION BY RANGE (order_date);

CREATE TABLE t_orders_2022 PARTITION OF t_orders FOR VALUES FROM ('2022-01-01') TO ('2023-01-01');
CREATE TABLE t_orders_2023 PARTITION OF t_orders FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

7. Using Connection Pooling

Maintaining a large number of connections can be resource-intensive. Connection pooling can be a solution to manage connections and reduce overhead.

Tip: Consider using pgBouncer or similar tools for connection pooling.

8. Offload Read Queries

If you have read-intensive workloads, consider using read replicas. They can offload the main database and lead to faster query executions.

9. Efficient Use of JSON Data

PostgreSQL has robust support for JSON and JSONB data types. Using the right functions and operators can help in optimizing queries on JSON data.

Example: Create an index on a JSONB column:

CREATE INDEX idx_jsonb_data ON t_test USING gin(data jsonb_path_ops);

10. Caching Strategy

The effective use of caching mechanisms like pg_stat_statements can help in identifying and optimizing frequently executed queries.

Example: To view the most frequently executed queries:

SELECT * FROM pg_stat_statements ORDER BY calls DESC;

11. Use Materialized Views

Materialized views are a way to cache the result of a query physically and can be refreshed periodically. They can improve performance for repetitive and complex queries.

Example:

1
2
3
4
5
6
7
CREATE MATERIALIZED VIEW mat_view_sales AS 
SELECT product_id, SUM(sales) 
FROM sales_data 
GROUP BY product_id;

-- Refresh the view periodically
REFRESH MATERIALIZED VIEW mat_view_sales;

12. Monitoring and Logging

Keeping an eye on the logs and using tools like pg_stat_activity and pgBadger can provide insights into slow queries and other performance issues.

More Tricks

Slow Query 1:

1
2
3
4
SELECT developper.name
FROM developper
LEFT JOIN talent ON developper.id = talent.foreign_id
WHERE talent.id IS NULL;

Fast Query 1:

1
2
3
SELECT developper.name
FROM developper
WHERE NOT EXISTS (SELECT 1 FROM talent WHERE developper.id = talent.foreign_id);

Slow Query 2:

1
2
3
SELECT DISTINCT developper.name
FROM developper, project
WHERE developper.id = project.developer_id;

Fast Query 2:

1
2
3
SELECT developper.name
FROM developper
JOIN project ON developper.id = project.developer_id;

Slow Query 3:

1
2
3
4
SELECT * 
FROM developper
ORDER BY name DESC 
LIMIT 10;

Fast Query 3:

1
2
3
4
SELECT * 
FROM developper 
ORDER BY name 
LIMIT 10 OFFSET (SELECT COUNT(*) - 10 FROM developper);

Slow Query 4:

1
2
3
SELECT developper.name
FROM developper
WHERE age >= 20 AND age <= 30;

Fast Query 4:

1
2
3
SELECT developper.name
FROM developper
WHERE age BETWEEN 20 AND 30;

Slow Query 5:

1
2
3
SELECT COUNT(*)
FROM developper
WHERE name IS NULL;

Fast Query 5:

SELECT COUNT(name) - COUNT(*)
FROM developper;

Slow Query 6:

1
2
3
SELECT developper.name
FROM developper
WHERE name LIKE 'John%';

Fast Query 6:

1
2
3
SELECT developper.name
FROM developper
WHERE name >= 'John' AND name < 'Joho';

Slow Query 7:

1
2
3
SELECT *
FROM developper
WHERE id IN (SELECT developer_id FROM project WHERE status = 'completed');

Fast Query 7:

1
2
3
4
SELECT developper.*
FROM developper
JOIN project ON developper.id = project.developer_id
WHERE project.status = 'completed';

Slow Query 8:

1
2
3
4
SELECT SUM(salary)
FROM developper
GROUP BY department_id
HAVING SUM(salary) > 10000;

Fast Query 8:

1
2
3
4
SELECT department_id, SUM(salary) as total_salary
FROM developper
GROUP BY department_id
HAVING total_salary > 10000;

Slow Query 9:

1
2
3
SELECT developper.name
FROM developper, project
WHERE developper.id = project.developer_id AND project.status = 'active';

Fast Query 9:

1
2
3
4
SELECT developper.name
FROM developper
JOIN project ON developper.id = project.developer_id
WHERE project.status = 'active';

Slow Query 10:

1
2
3
4
SELECT developper.name
FROM developper
LEFT JOIN project ON developper.id = project.developer_id
WHERE project.id IS NULL;

Fast Query 10:

1
2
3
SELECT developper.name
FROM developper
WHERE NOT EXISTS (SELECT 1 FROM project WHERE developper.id = project.developer_id);

Slow Query 11:

1
2
3
SELECT developper.name
FROM developper
WHERE UPPER(name) = 'JOHN';

Fast Query 11:

1
2
3
4
-- Assuming there is an index on the `name` column.
SELECT developper.name
FROM developper
WHERE name = 'John';

Slow Query 12:

1
2
3
4
SELECT name, SUM(salary) 
FROM developper
GROUP BY name
ORDER BY SUM(salary) DESC;

Fast Query 12:

1
2
3
4
5
-- Use an alias to avoid computing SUM(salary) twice.
SELECT name, SUM(salary) as total_salary 
FROM developper
GROUP BY name
ORDER BY total_salary DESC;

Slow Query 13:

1
2
3
SELECT developper.name
FROM developper, skills
WHERE developper.id = skills.developer_id AND skills.name = 'Python';

Fast Query 13:

1
2
3
4
SELECT developper.name
FROM developper
JOIN skills ON developper.id = skills.developer_id
WHERE skills.name = 'Python';

Slow Query 14:

1
2
3
SELECT developper.name
FROM developper
WHERE id IN (SELECT developer_id FROM project WHERE status NOT IN ('completed', 'active'));

Fast Query 14:

1
2
3
4
SELECT developper.name
FROM developper
JOIN project ON developper.id = project.developer_id
WHERE project.status NOT IN ('completed', 'active');

Slow Query 15:

1
2
3
4
SELECT developper.name
FROM developper
LEFT JOIN project ON developper.id = project.developer_id
WHERE project.name IS NULL;

Fast Query 15:

1
2
3
SELECT developper.name
FROM developper
WHERE NOT EXISTS (SELECT 1 FROM project WHERE developper.id = project.developer_id);

Slow Query 16:

1
2
3
4
SELECT developper.name, COUNT(project.id)
FROM developper, project
WHERE developper.id = project.developer_id
GROUP BY developper.name;

Fast Query 16:

1
2
3
4
SELECT developper.name, COUNT(project.id)
FROM developper
JOIN project ON developper.id = project.developer_id
GROUP BY developper.name;

Slow Query 17:

1
2
3
SELECT developper.name
FROM developper
WHERE CHAR_LENGTH(name) > 5;

Fast Query 17:

1
2
3
4
-- Assuming there is an index on the `name` column.
SELECT developper.name
FROM developper
WHERE LENGTH(name) > 5;

Slow Query 18:

1
2
3
SELECT developper.name
FROM developper, project
WHERE developper.id = project.developer_id AND project.status LIKE 'act%';

Fast Query 18:

1
2
3
4
SELECT developper.name
FROM developper
JOIN project ON developper.id = project.developer_id
WHERE project.status LIKE 'act%';

Slow Query 19:

1
2
3
4
SELECT developper.name
FROM developper
WHERE age > 20
ORDER BY age;

Fast Query 19:

1
2
3
4
5
-- If there's an index on age, this will be faster.
SELECT developper.name
FROM developper
WHERE age > 20
ORDER BY age ASC;

Slow Query 20:

1
2
3
SELECT developper.name
FROM developper
WHERE name <> '';

Fast Query 20:

1
2
3
SELECT developper.name
FROM developper
WHERE name IS NOT NULL AND name != '';