从一次慢查询说起:Python 服务里怎么把数据库访问做得更稳

# 从一次慢查询说起:Python 服务里怎么把数据库访问做得更稳

 

最近排查一个接口超时,现象很普通:业务代码没有明显的 CPU 热点,应用日志里也没有异常,但 95 线延迟突然从 200ms 涨到 2 秒以上。最后一路追到数据库,发现不是“数据库不行”,而是服务端访问方式太随意:没有稳定的索引命中、连接池参数偏保守、慢查询日志也没有被纳入日常观测。

 

这篇随笔记录一下我现在写 Python Web 服务时处理数据库访问的几个习惯。它们不复杂,但能避免很多线上问题。

 

## 先确认 SQL,而不是先改代码

 

遇到慢接口,我通常先把 ORM 最终生成的 SQL 打出来。很多看似简单的查询,经过条件拼接、排序、分页之后,实际 SQL 可能已经变了形。以 SQLAlchemy 为例,可以在开发环境打开 echo:

 

```python

from sqlalchemy import create_engine

 

engine = create_engine(

    "postgresql+psycopg://app:pwd@127.0.0.1:5432/appdb",

    echo=True,

    pool_size=10,

    max_overflow=20,

    pool_pre_ping=True,

)

```

 

拿到 SQL 后,不要只看语句“感觉对不对”,要直接跑执行计划:

 

```sql

EXPLAIN (ANALYZE, BUFFERS)

SELECT id, user_id, status, created_at

FROM orders

WHERE user_id = 10086 AND status = 'PAID'

ORDER BY created_at DESC

LIMIT 20;

```

 

如果看到 `Seq Scan`,而表数据量又比较大,就要重点检查索引是否覆盖了过滤和排序条件。上面的查询通常需要一个复合索引:

 

```sql

CREATE INDEX CONCURRENTLY idx_orders_user_status_created

ON orders (user_id, status, created_at DESC);

```

 

这里的关键点是顺序。`user_id` 和 `status` 是等值过滤,`created_at` 是排序字段,把它们放在同一个索引里,数据库才更容易少扫描、少排序。很多人只给 `user_id` 单列建索引,数据量小的时候没问题,一旦某个大客户订单很多,延迟就会抖。

 

## 连接池要按并发模型配置

 

Python 服务常见问题是连接池太小。比如 Gunicorn 开 4 个 worker,每个 worker 内部连接池 `pool_size=5`,理论上就是 20 个常驻连接;再加上 `max_overflow`,峰值连接数会更高。这个数既不能拍脑袋设得很大,也不能默认不管。

 

我的做法是先按 worker 数估算上限,然后和数据库的 `max_connections`、其他服务占用一起核对。服务侧配置可以显式写出来,避免不同环境隐式默认:

 

```python

engine = create_engine(

    DB_URL,

    pool_size=8,

    max_overflow=8,

    pool_timeout=3,

    pool_recycle=1800,

    pool_pre_ping=True,

)

```

 

`pool_timeout` 不建议太长。连接拿不到时,让请求尽快失败,通常比把线程/协程一直挂住更容易恢复。`pool_pre_ping=True` 对云数据库、代理层、NAT 闲置连接回收场景也很实用,可以减少“拿到的连接其实已经断了”的偶发错误。

 

## 分页别只会 offset

 

后台列表页常见写法是:

 

```sql

SELECT * FROM orders

WHERE user_id = 10086

ORDER BY created_at DESC

LIMIT 20 OFFSET 100000;

```

 

`OFFSET` 越大,数据库越要跳过大量数据。对深分页接口,我更倾向于用游标分页:

 

```sql

SELECT id, user_id, created_at

FROM orders

WHERE user_id = 10086

  AND created_at < '2026-07-01 10:00:00'

ORDER BY created_at DESC

LIMIT 20;

```

 

前端把上一页最后一条记录的 `created_at` 或 `(created_at, id)` 作为 cursor 传回来,数据库就可以沿着索引继续往后找。这个改动对大表非常明显,尤其是订单、日志、消息这类天然按时间排序的数据。

 

## 把慢查询变成日常指标

 

最后一点是观测。慢查询不应该只在事故时才打开。PostgreSQL 可以启用 `pg_stat_statements`,应用侧也可以在中间件里记录每个请求的 SQL 次数和耗时。一个简单但有效的日志字段是:

 

```text

path=/api/orders sql_count=6 sql_time_ms=183 total_ms=241

```

 

当某个接口从 3 次 SQL 变成 30 次 SQL,或者 SQL 总耗时突然升高,日志和指标会比用户投诉更早发现问题。

 

总结一下:数据库优化不一定上来就调参数,更多时候是把访问路径变清楚。先看真实 SQL,再看执行计划;索引围绕过滤、排序和分页设计;连接池按部署模型配置;最后把慢查询纳入日常观测。做到这些,Python 服务里的数据库访问通常就能稳很

posted @ 2026-07-02 09:06  fitch_liu  阅读(4)  评论(0)    收藏  举报