出站数据的隐形账单:数据库请求怎么越做越贵越做越慢

2026-05-14 28 预计阅读时间:1 分钟
来源:planetscale.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:9 分钟

每次你的应用向数据库发一条查询,数据就要"出门"——从存储层穿越网络到达应用层。这个过程有两个你可能没认真算过的代价:延迟让应用变慢,流量让账单变贵。而且这两件事是同一根绳上的两个头:请求越频繁、每次带出的数据越胖,绳子就越紧。

这篇文章把"出站问题"拆开看:它藏在哪、长什么样、怎么动手缩减。

出站成本的两个面

云厂商对"数据流出"收费不是秘密。AWS、GCP、Azure 都按 GB 计算出站流量费,单价从 $0.08 到 $0.12 不等。如果你的 API 每天把 100 GB 的查询结果从数据库区域传到应用区域,一个月光出站费就可能超过 $2000。

但钱只是一面。另一面是时间:同样的 100 GB 数据,在典型云内网络(约 10 Gbps 共享带宽)上传输也要几十秒。用户等不起,你也不想等。

关键认知:减少请求的体积和频率,性能和成本同时受益。这不是"要么快要么省"的权衡,而是同一方向的收益。

常见的出站陷阱

以下是几个高频出现的模式,每个都在悄悄放大你的出站流量:

1. SELECT * 的惯性

最省事的写法,也是最浪费的写法。一张 20 列的表,你的业务可能只需要 3 列,但 SELECT * 把 20 列全拉出来。列数膨胀直接乘在每行的体积上。

2. 客户端过滤

先把整张表拉到应用内存,再用 Python/JS 的 filter() 留下你要的行。数据库的 WHERE 子句不是装饰——它是免费的出站压缩器。

3. 重复查询同一数据

同一个用户的 profile,一个请求周期里被查了 5 次。每次都是一次完整的出站往返。短时间内的重复请求,用一层本地缓存就能砍掉大半。

4. 无分页的大结果集

"给我所有订单"。订单表有 80 万行?那就 80 万行全出站。LIMIT + OFFSET 或游标分页不是可选的优雅,是必须的止损。

缩请求、减体积:具体策略

策略不需要多,需要用对。以下四招覆盖大多数场景:

策略 减什么 效果
只选需要的列 单次请求体积 体积降到 1/N(N=总列数/需要列数)
WHERE 下推过滤 返回行数 行数降到过滤比例
短时缓存 请求频率 同一数据重复请求归零
分页/游标 单次请求体积 + 行数 单次出站体积封顶

下面用一个完整的例子把这些策略串起来。

实战:从一条慢查询到低成本方案

假设你有一个订单系统,表结构如下:

CREATE TABLE orders (
    id            BIGSERIAL PRIMARY KEY,
    user_id       BIGINT NOT NULL,
    status        TEXT NOT NULL,        -- 'pending','shipped','delivered','cancelled'
    total_amount  DECIMAL(10,2),
    created_at    TIMESTAMPTZ,
    updated_at    TIMESTAMPTZ,
    shipping_addr TEXT,
    billing_addr  TEXT,
    notes         TEXT,
    metadata      JSONB
);

你的 API 需要返回某用户最近的待处理订单,只展示 ID、金额和创建时间。

❌ 典型的高出站写法

import psycopg2

conn = psycopg2.connect("dbname=shop host=db-region user=app")

def get_pending_orders_bad(user_id: int):
    """全列 + 全行 + 无缓存:出站流量最大化"""
    with conn.cursor() as cur:
        cur.execute("SELECT * FROM orders")  # 全表出站
        all_rows = cur.fetchall()
    # 在应用层过滤——数据库白干了一场大传输
    return [
        r for r in all_rows
        if r["user_id"] == user_id and r["status"] == "pending"
    ]

这张表 10 列,80 万行全拉。假设平均每行 500 字节,单次出站 ≈ 400 MB。每天调用 1000 次?每天 400 GB 出站,月出站费约 $3000–$4000,响应时间也慢到不可用。

✅ 低出站写法

import psycopg2
from functools import lru_cache
from datetime import datetime, timedelta

conn = psycopg2.connect("dbname=shop host=db-region user=app")

# 策略1: 只选需要的列 —— 3列 vs 10列,体积降到约 30%
# 策略2: WHERE 下推过滤 —— 只返回目标用户的 pending 行
# 策略3: 分页封顶 —— 单次最多 50 行,出站体积可控
QUERY = """
    SELECT id, total_amount, created_at
    FROM   orders
    WHERE  user_id = %s AND status = 'pending'
    ORDER  BY created_at DESC
    LIMIT  50
"""

# 策略4: 短时缓存 —— 同一用户 30 秒内的重复查询直接命中内存
@lru_cache(maxsize=2048)
def _cached_query(user_id: int, _cache_key: str):
    """_cache_key 用时间戳实现 TTL,30 秒后自动失效"""
    with conn.cursor() as cur:
        cur.execute(QUERY, (user_id,))
        return cur.fetchall()

def get_pending_orders(user_id: int):
    # 30 秒 TTL:把当前时间截断到 30 秒粒度作为 cache key
    ts = int(datetime.utcnow().timestamp() / 30)
    return _cached_query(user_id, str(ts))

算一下收益:

  • 体积:3 列 × 50 行 × 约 40 字节/行 ≈ 6 KB(对比 400 MB,缩减 >99%)
  • 频率:同一用户 30 秒内重复请求归零
  • 延迟:WHERE + LIMIT 让数据库只扫描少量索引命中行,返回毫秒级完成
  • 月出站费:6 KB × 1000 次/天 × 30 天 ≈ 180 MB,费用 < $2

运行前需要:pip install psycopg2-binary,并确保 db-region 可连通。lru_cache 的 TTL 方案是轻量实现,生产环境建议用 Redis 或 Memcached 做分布式缓存。

用 Prometheus 监控出站量

如果你想知道优化前后到底差多少,可以在应用层加一个简单的指标:

from prometheus_client import Counter, start_http_server

egress_bytes = Counter(
    "db_egress_bytes",
    "Total bytes transferred from DB queries",
    ["query_type"]
)

def get_pending_orders(user_id: int):
    ts = int(datetime.utcnow().timestamp() / 30)
    rows = _cached_query(user_id, str(ts))
    # 估算本次出站字节数
    approx_bytes = len(str(rows).encode("utf-8"))
    egress_bytes.labels(query_type="pending_orders").inc(approx_bytes)
    return rows

# 在主进程启动时调用一次
start_http_server(8000)  # 指标暴露在 :8000/metrics

用 Grafana 或 curl :8000/metrics 观察 db_egress_bytes 的变化,优化效果一目了然。

上线前的出站检查清单

每次新增或修改数据库查询,过一遍这个清单:

  1. 是不是 SELECT *? → 改成只选业务需要的列。
  2. 过滤逻辑在应用层还是数据库层? → 能下推的都下推到 WHERE。
  3. 结果集有没有封顶? → 加 LIMIT,或改用游标分页。
  4. 短时间内会重复查同一数据吗? → 加短时缓存(本地 lru_cache 或 Redis)。
  5. 有没有大字段(TEXT/JSONB/BLOB)被不必要地拉出来? → 单独查或延迟加载。
  6. 跨区域查询? → 检查数据库和应用是否在同一区域;跨区域出站费翻倍。

出站问题不像崩溃那样显眼,但它持续地、安静地拖慢你的应用、膨胀你的账单。把每条查询的体积和频率压下来,是少数能同时让用户和财务都满意的技术决策。


相关推荐