<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>PostgreSQL on Ambertu</title><link>https://ambertu.com/tags/postgresql/</link><description>Recent content in PostgreSQL on Ambertu</description><generator>Hugo</generator><language>zh-CN</language><copyright>2026 Ambertu · 粤ICP备18071259号</copyright><lastBuildDate>Fri, 04 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://ambertu.com/tags/postgresql/index.xml" rel="self" type="application/rss+xml"/><item><title>给 API 中转站选数据库：SQLite / MySQL / PostgreSQL 在小机器上的取舍</title><link>https://ambertu.com/posts/api-gateway-database-choice-small-vps/</link><pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate><guid>https://ambertu.com/posts/api-gateway-database-choice-small-vps/</guid><description>API 中转站的写入量约等于请求量，这决定了数据库选型。本文给出一台 2G 机器上 MariaDB 占 285MB 的实测数据、三种数据库的内存画像与并发模型差异，以及一个比选型更要紧的问题：用量日志表的无限增长。</description><content:encoded><![CDATA[<p>new-api 这类中转程序通常同时支持 SQLite、MySQL 和 PostgreSQL，安装文档也就一句&quot;按需选择&quot;。但在一台 2G 内存的小机器上，这个选择是有真实代价的——<strong>光是数据库进程本身就可能吃掉可用内存的四分之一。</strong></p>
<p>先给一个锚点。我实测一台 2 核 2G 的云主机，上面跑着 nginx、Redis 和两个 Go 服务，MariaDB 的进程 RSS 是：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">mysql  14.5%  291932 KB  /usr/libexec/mariadbd
</span></span></code></pre></div><p><strong>285 MB</strong>，是这台机器上最大的单个进程。而整机 <code>available</code> 只有 1.1 G。换句话说，你还没开始跑业务，数据库已经拿走了四分之一的可用余量。</p>
<h2 id="选型的真正依据写入量--请求量">选型的真正依据：写入量 ≈ 请求量</h2>
<p>在动手比较之前，先想清楚 API 中转站的负载长什么样，否则比较是没有意义的。</p>
<ul>
<li><strong>读</strong>：API key 校验、用户配置、配额查询。数据量小、可缓存、频率高——<strong>三种数据库都轻松应对，不构成差异。</strong></li>
<li><strong>写</strong>：每次请求落一条用量/计费记录。<strong>这是全部的关键：写入速率基本等于请求速率。</strong></li>
</ul>
<p>所以问题可以化简成一句：<strong>你每秒有几次请求？</strong></p>
<table>
	<thead>
			<tr>
					<th>请求量</th>
					<th>每天写入行数</th>
					<th>SQLite 是否够用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1 req/s</td>
					<td>约 8.6 万</td>
					<td>够，且很轻松</td>
			</tr>
			<tr>
					<td>10 req/s</td>
					<td>约 86 万</td>
					<td>勉强够，需调好 WAL 和 busy_timeout</td>
			</tr>
			<tr>
					<td>50 req/s</td>
					<td>约 430 万</td>
					<td>不够，写入会明显排队</td>
			</tr>
			<tr>
					<td>多实例部署</td>
					<td>——</td>
					<td><strong>不行</strong>，SQLite 无法跨进程跨机共享</td>
			</tr>
	</tbody>
</table>
<p>大多数个人自建的中转站落在第一档，很多人却直接上了 MySQL——在小机器上这笔开销是白花的。</p>
<h2 id="三种数据库的内存画像">三种数据库的内存画像</h2>
<figure>
<svg viewBox="0 0 760 280" role="img" aria-label="三种数据库在 2G 机器上的内存占用对比条形图：SQLite 常驻内存接近零，MariaDB 实测 285MB 调优后约 150MB，PostgreSQL 由 shared_buffers 加每连接独立进程构成" style="width:100%;height:auto;color:currentColor">
  <text x="0" y="14" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">数据库常驻内存（2G 机器 · available ≈ 1126 MB）</text>
  <!-- reference line -->
  <line x1="170" y1="26" x2="170" y2="230" stroke="currentColor" stroke-width="1" opacity="0.25"/>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <!-- sqlite -->
    <text x="0" y="56">SQLite</text>
    <rect x="170" y="44" width="6" height="18" fill="currentColor" opacity="0.75"/>
    <text x="186" y="57" opacity="0.8">≈ 0 MB 常驻</text>
    <text x="186" y="74" font-size="9" opacity="0.6">库跑在应用进程里，没有独立进程</text>
    <!-- mariadb -->
    <text x="0" y="112">MariaDB</text>
    <rect x="170" y="100" width="285" height="18" fill="currentColor" opacity="0.75"/>
    <text x="463" y="113" font-weight="bold">285 MB 实测</text>
    <rect x="170" y="122" width="150" height="12" fill="none" stroke="currentColor" stroke-width="1.2" stroke-dasharray="3 3"/>
    <text x="328" y="132" font-size="9" opacity="0.7">约 150 MB（关掉 performance_schema 并调小 buffer pool 后）</text>
    <!-- postgres -->
    <text x="0" y="176">PostgreSQL</text>
    <rect x="170" y="164" width="128" height="18" fill="currentColor" opacity="0.55"/>
    <text x="306" y="177" font-size="9.5" opacity="0.8">shared_buffers 128 MB（默认）</text>
    <rect x="170" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <rect x="214" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <rect x="258" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <rect x="302" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <text x="350" y="196" font-size="9" opacity="0.7">+ 每个连接一个独立进程，随连接数线性增长</text>
  </g>
  <line x1="0" y1="216" x2="740" y2="216" stroke="currentColor" stroke-width="1" opacity="0.2"/>
  <text x="0" y="238" font-size="10" font-family="monospace" fill="currentColor" opacity="0.75">只有 285 MB 那个数是实测值，其余为默认配置下的典型量级。</text>
  <text x="0" y="256" font-size="10" font-family="monospace" fill="currentColor" opacity="0.75">PostgreSQL 的总占用高度依赖 max_connections —— 它是进程模型，不是线程模型。</text>
  <text x="0" y="274" font-size="10" font-family="monospace" fill="currentColor" opacity="0.75">SQLite 仍会用到系统 page cache，但那部分是可回收的，不计入进程 RSS。</text>
</svg>
<figcaption>
PostgreSQL 那一行的画法是有意的：<strong>它的内存不是一个固定数字，而是「基础 + 连接数 × 每连接」</strong>。
在小机器上 <code>max_connections</code> 配错，比 <code>shared_buffers</code> 配错更容易出事。
</figcaption>
</figure>
<h2 id="并发写入模型的差异">并发写入模型的差异</h2>
<figure>
<svg viewBox="0 0 780 250" role="img" aria-label="对比图：SQLite 的写入被序列化为单一写者队列，并发写请求需要排队；MySQL 与 PostgreSQL 支持多个写者通过行级锁并发写入" style="width:100%;height:auto;color:currentColor">
  <defs>
    <marker id="d1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor"/>
    </marker>
  </defs>
  <text x="0" y="14" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">并发写请求的处理方式</text>
  <!-- sqlite -->
  <text x="0" y="42" font-size="10.5" font-family="monospace" fill="currentColor">SQLite</text>
  <g font-family="monospace" font-size="9.5" fill="currentColor">
    <rect x="100" y="30" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="44" text-anchor="middle">写 A</text>
    <rect x="100" y="54" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="68" text-anchor="middle">写 B</text>
    <rect x="100" y="78" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="92" text-anchor="middle">写 C</text>
    <path d="M 170 40 L 250 60" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <path d="M 170 64 L 250 64" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <path d="M 170 88 L 250 68" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <rect x="252" y="46" width="130" height="36" fill="currentColor" fill-opacity="0.12" stroke="currentColor" stroke-width="1.5"/>
    <text x="317" y="62" text-anchor="middle">单写者队列</text>
    <text x="317" y="75" text-anchor="middle" font-size="8.5" opacity="0.7">串行化</text>
    <line x1="382" y1="64" x2="446" y2="64" stroke="currentColor" stroke-width="1.2" marker-end="url(#d1)"/>
    <rect x="448" y="46" width="100" height="36" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="498" y="68" text-anchor="middle">db 文件</text>
    <text x="566" y="54" opacity="0.75">WAL 模式下读不阻塞，</text>
    <text x="566" y="68" opacity="0.75">但写仍然一个一个来。</text>
    <text x="566" y="82" font-weight="bold">busy_timeout 必须配</text>
  </g>
  <line x1="0" y1="116" x2="760" y2="116" stroke="currentColor" stroke-width="1" opacity="0.2"/>
  <!-- mysql/pg -->
  <text x="0" y="146" font-size="10.5" font-family="monospace" fill="currentColor">MySQL</text>
  <text x="0" y="160" font-size="10.5" font-family="monospace" fill="currentColor">PostgreSQL</text>
  <g font-family="monospace" font-size="9.5" fill="currentColor">
    <rect x="100" y="134" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="148" text-anchor="middle">写 A</text>
    <rect x="100" y="158" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="172" text-anchor="middle">写 B</text>
    <rect x="100" y="182" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="196" text-anchor="middle">写 C</text>
    <line x1="170" y1="144" x2="250" y2="144" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="170" y1="168" x2="250" y2="168" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="170" y1="192" x2="250" y2="192" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <rect x="252" y="134" width="130" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="317" y="148" text-anchor="middle" font-size="8.5">连接/工作进程</text>
    <rect x="252" y="158" width="130" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="317" y="172" text-anchor="middle" font-size="8.5">连接/工作进程</text>
    <rect x="252" y="182" width="130" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="317" y="196" text-anchor="middle" font-size="8.5">连接/工作进程</text>
    <line x1="382" y1="144" x2="446" y2="160" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="382" y1="168" x2="446" y2="168" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="382" y1="192" x2="446" y2="176" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <rect x="448" y="150" width="100" height="36" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="498" y="172" text-anchor="middle">行级锁</text>
    <text x="566" y="160" opacity="0.75">真并发写，但代价是</text>
    <text x="566" y="174" opacity="0.75">常驻内存和连接管理。</text>
  </g>
  <line x1="0" y1="216" x2="760" y2="216" stroke="currentColor" stroke-width="1" opacity="0.2"/>
  <text x="0" y="238" font-size="10" font-family="monospace" fill="currentColor" font-weight="bold">选型问题因此变成：你的写入速率会不会撞上 SQLite 的单写者上限。</text>
</svg>
<figcaption>
SQLite 的"单写者"经常被误解成"不能并发"。<strong>WAL 模式下读写是可以并发的</strong>，
受限的只有写与写之间。对写入速率等于请求速率的中转站来说，这个限制什么时候变成问题是可以直接算出来的。
</figcaption>
</figure>
<h2 id="sqlite如果选它这几个-pragma-不能少">SQLite：如果选它，这几个 pragma 不能少</h2>
<p>默认配置的 SQLite 在并发场景下表现很差，而这常被误当成&quot;SQLite 不能上生产&quot;的证据。实际要配的就几行：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">journal_mode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">WAL</span><span class="p">;</span><span class="w">      </span><span class="c1">-- 读写不再互相阻塞。默认的 delete 模式才是性能灾难
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">busy_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">;</span><span class="w">     </span><span class="c1">-- 遇锁等 5 秒而不是立刻报 SQLITE_BUSY
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">synchronous</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">NORMAL</span><span class="p">;</span><span class="w">    </span><span class="c1">-- 配合 WAL 的合理折中
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">       </span><span class="c1">-- 默认是关的，很多人不知道
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">cache_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="mi">32000</span><span class="p">;</span><span class="w">     </span><span class="c1">-- 负数表示 KB，这里约 32 MB
</span></span></span></code></pre></div><p>其中 <strong><code>busy_timeout</code> 是最容易漏、后果最直接的一条</strong>。不配它，两个并发写会有一个立刻收到 <code>SQLITE_BUSY</code> 失败——表现出来就是&quot;偶发写入失败&quot;，而你会以为是 SQLite 不稳定，其实只是它在如实告诉你&quot;锁被占了&quot;，而你没让它等。</p>
<p><code>synchronous = NORMAL</code> 的取舍要说清：配合 WAL 时它能抵御应用崩溃，但<strong>操作系统崩溃或断电可能丢掉最近几个已提交事务</strong>。对用量日志这种损失几条可接受的数据没问题；如果这张表直接用于扣款对账，就该用 <code>FULL</code>。</p>
<p><strong>备份不能直接 <code>cp</code>。</strong> WAL 模式下 <code>-wal</code> 文件里有还没合并进主库的数据，直接复制主文件会拿到一个不一致的快照：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ✓ 正确</span>
</span></span><span class="line"><span class="cl">sqlite3 /path/to/app.db <span class="s2">&#34;VACUUM INTO &#39;/backup/app-</span><span class="k">$(</span>date +%F<span class="k">)</span><span class="s2">.db&#39;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ❌ 错误：WAL 模式下这样拷出来的库可能缺最近的写入</span>
</span></span><span class="line"><span class="cl">cp /path/to/app.db /backup/
</span></span></code></pre></div><h2 id="mysql--mariadb小机器上的调优">MySQL / MariaDB：小机器上的调优</h2>
<p>如果已经在用 MySQL 系，285 MB 是可以压下来的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[mysqld]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 关掉它就能省几十 MB，个人站几乎不需要</span>
</span></span><span class="line"><span class="cl"><span class="na">performance_schema</span> <span class="o">=</span> <span class="s">OFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="na">innodb_buffer_pool_size</span> <span class="o">=</span> <span class="s">96M</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_buffer_pool_instances</span> <span class="o">=</span> <span class="s">1    # 池子小的时候拆成多个实例反而浪费</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_log_file_size</span> <span class="o">=</span> <span class="s">32M</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 每个连接都要分配缓冲区，这个数不是免费的</span>
</span></span><span class="line"><span class="cl"><span class="na">max_connections</span> <span class="o">=</span> <span class="s">30</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="na">key_buffer_size</span> <span class="o">=</span> <span class="s">8M</span>
</span></span><span class="line"><span class="cl"><span class="na">tmp_table_size</span> <span class="o">=</span> <span class="s">16M</span>
</span></span><span class="line"><span class="cl"><span class="na">max_heap_table_size</span> <span class="o">=</span> <span class="s">16M</span>
</span></span></code></pre></div><p><code>performance_schema = OFF</code> 是性价比最高的一条——它默认开启，为性能诊断收集大量数据，而个人站基本用不上。代价是出问题时少了一套诊断视图，需要时再临时打开即可。</p>
<p><code>max_connections = 30</code> 看着很小，但要算清楚：中转程序的连接池通常配 10–20，加上你偶尔用客户端连进去看数据，30 足够。这个数配成 151（默认值）不会立刻占用内存，但<strong>它决定了内存占用的上限</strong>，在没有 swap 的机器上等于把 OOM 的可能性留在了那里。</p>
<h2 id="postgresql注意它是进程模型">PostgreSQL：注意它是进程模型</h2>
<p>PostgreSQL 和 MySQL 最大的运维差异在这里：<strong>每个连接是一个独立的操作系统进程，不是线程。</strong> 所以连接数对内存的影响比 MySQL 显著得多。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">shared_buffers</span> <span class="o">=</span> <span class="s">128MB          # 通用建议是内存的 25%，但共享机器上别照抄</span>
</span></span><span class="line"><span class="cl"><span class="na">max_connections</span> <span class="o">=</span> <span class="s">20            # 进程模型下这个数很值钱</span>
</span></span><span class="line"><span class="cl"><span class="na">work_mem</span> <span class="o">=</span> <span class="s">2MB                  # 注意：单次查询可能同时用掉好几份</span>
</span></span><span class="line"><span class="cl"><span class="na">maintenance_work_mem</span> <span class="o">=</span> <span class="s">32MB</span>
</span></span><span class="line"><span class="cl"><span class="na">effective_cache_size</span> <span class="o">=</span> <span class="s">512MB    # 这只是给优化器的提示，不实际占用</span>
</span></span></code></pre></div><p><code>work_mem</code> 有个容易踩的坑：它是<strong>每个排序/哈希操作</strong>的上限，不是每个连接的上限。一个复杂查询里有多个排序节点时会各分配一份，并发多个这样的查询，内存就会超出直觉。小机器上宁可配小，让它落盘排序。</p>
<p>如果应用的连接池配得比较大，考虑在前面加 pgbouncer 做连接复用——但这又是一个常驻进程，在 2G 机器上要权衡它值不值。</p>
<h2 id="比选型更要紧的问题日志表会无限长大">比选型更要紧的问题：日志表会无限长大</h2>
<p>这是本文最想强调的一点。<strong>无论你选哪个数据库，真正会在半年后咬你的是用量日志表。</strong></p>
<p>机制很简单：每个请求写一行，从不删除。按前面的换算：</p>
<ul>
<li>10 req/s → 每天 86 万行 → <strong>一年约 3.1 亿行</strong></li>
<li>每行假设 300 字节 → 一年约 90 GB</li>
</ul>
<p>后果不是&quot;磁盘满了&quot;这么简单，而是一连串的连带效应：查询变慢 → 管理后台的用量统计页面开始超时 → 备份时间越来越长 → 最后磁盘真的满了，而磁盘满会让数据库直接拒绝写入，<strong>整个中转站停摆</strong>。</p>
<p>所以从第一天就该有保留策略：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 最朴素但有效：定期删除旧数据 + 保留聚合结果
</span></span></span><span class="line"><span class="cl"><span class="k">DELETE</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="mi">90</span><span class="w"> </span><span class="k">DAY</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 删之前先把要长期保留的口径聚合出来
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">usage_daily</span><span class="w"> </span><span class="p">(</span><span class="k">day</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">requests</span><span class="p">,</span><span class="w"> </span><span class="n">tokens</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="nb">DATE</span><span class="p">(</span><span class="n">created_at</span><span class="p">),</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">),</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">total_tokens</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="mi">90</span><span class="w"> </span><span class="k">DAY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>两点提醒：</p>
<ul>
<li><strong>大表上的 <code>DELETE</code> 本身就是个事故来源。</strong> 一次删几千万行会长时间持锁、把 binlog/WAL 撑爆。要分批删（每次几千行、中间 sleep），或者用按时间分区的表直接 <code>DROP PARTITION</code>——后者是从一开始就该做的设计。</li>
<li><strong>SQLite 删完记得 <code>VACUUM</code></strong>，否则文件大小不会回落（空间被标记为可复用但不归还给文件系统）。注意 <code>VACUUM</code> 需要与库等大的临时空间，磁盘快满时反而做不了——所以别等到那时候。</li>
</ul>
<h2 id="决策建议">决策建议</h2>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>选择</th>
					<th>理由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>个人自建、单实例、几 req/s</td>
					<td><strong>SQLite</strong></td>
					<td>零常驻内存，省下的 285 MB 在 2G 机器上是实打实的。配好 WAL 和 busy_timeout 就够</td>
			</tr>
			<tr>
					<td>2G 机器、已有 MySQL 在跑</td>
					<td><strong>复用现有 MySQL</strong></td>
					<td>再起一个数据库进程是纯浪费。调优比换数据库收益大</td>
			</tr>
			<tr>
					<td>请求量大 / 计划多实例</td>
					<td><strong>PostgreSQL 或 MySQL</strong></td>
					<td>SQLite 无法跨进程共享，这是硬限制而非性能问题</td>
			</tr>
			<tr>
					<td>已经在用 Docker 编排</td>
					<td>跟着编排走</td>
					<td>容器化环境里 SQLite 的数据卷管理反而更麻烦</td>
			</tr>
			<tr>
					<td>需要复杂用量分析查询</td>
					<td><strong>PostgreSQL</strong></td>
					<td>窗口函数、CTE、JSON 查询能力更强，统计报表写起来省事</td>
			</tr>
	</tbody>
</table>
<p>最后一条实践建议：<strong>先从 SQLite 开始不是妥协。</strong> 但要知道 SQLite → MySQL 的迁移不是无痛的——SQLite 是动态类型（type affinity），日期存储没有原生类型，<code>AUTOINCREMENT</code> 语义也不同。如果你已经能预见到半年内会上多实例，那一开始就选 MySQL 或 PostgreSQL，省掉一次迁移。</p>
<h2 id="相关阅读">相关阅读</h2>
<ul>
<li><a href="/posts/new-api-binary-vs-docker/">new-api 部署选型：2G 小内存机器上裸二进制比 Docker 省多少</a> —— 那台机器 285 MB 的 MariaDB 实测数据出自这里</li>
<li><a href="/posts/llm-api-timeout-retry-double-billing/">大模型 API 中转的超时与重试：一次 504 可能让你双倍计费</a> —— 用量日志正是核对重复计费的依据</li>
<li><a href="/posts/caddy-vs-nginx-api-gateway-tls/">Caddy 还是 nginx 给 API 中转做反代：证书自动化是决定性差异</a> —— 同一台小机器上的另一项运维取舍</li>
</ul>
]]></content:encoded></item></channel></rss>