
アプリとDBの間の往復遅延が応答時間に与える影響を TiDB Cloud Starterで計測した
はじめに
TiDB Cloud のようなクラウド DB を使うと、アプリと DB の間にネットワークが入ります。DB の処理が速くても通信の遅延によって応答が遅くなるのではないかと気にする方は多いでしょう。
SQL を 1 回送るたびに、アプリと DB の間の往復の時間が加わります。SQL の処理自体は短くても、1 リクエストの中で SQL を逐次に何度も送る書き方では、往復の時間が回数分だけ積み上がるはずです。
本記事では、アプリと TiDB Cloud Starter の間の往復遅延と 1 リクエストで実行する SQL の回数を変えて、アプリの応答時間の変化を計測します。
応答時間の増分は、往復遅延の大きさよりも、1 リクエストで DB と逐次に往復する回数で決まりました。SQL を 6 回から 2 回に集約すると、往復時間が約 100 ms の条件でも応答時間を 66.9% 削減できました。
TiDB Cloud とは
TiDB Cloud は、MySQL 互換の分散データベース TiDB のマネージドサービスです。Starter は使った分だけ課金されるプランで、条件を満たすインスタンスには無料枠が与えられます。
検証環境
- TiDB Cloud Starter (AWS 東京リージョン)
- TiDB v8.5.3
- macOS
- Python 3.13
- PyMySQL 2.2.8
- Toxiproxy 2.12.0
- 計測実施日: 2026 年 10 月 14 日
対象読者
- クラウド DB の採用や配置を検討している開発者
- DB アクセスの書き方と応答時間の関係を知りたい方
参照
- Connect to Your TiDB Cloud Starter or Essential Instance (PingCAP)
- Limitations and Quotas of TiDB Cloud Starter and Essential (PingCAP)
- Toxiproxy (Shopify)
検証方法
題材は、EC サイトの注文履歴画面とします。
ある顧客の最新の注文 5 件と、各注文の明細を返す処理を、1 リクエストとして扱います。この処理を、SQL の送り方が違う 2 通りで実装しました。
| 実装 | SQL の回数 | 内容 |
|---|---|---|
| 逐次取得 | 6 | 注文 5 件を取得したあと、注文ごとに明細を取得する |
| 一括取得 | 2 | 注文 5 件を取得したあと、5 件分の明細を IN 句で 1 回で取得する |
逐次取得は、いわゆる N+1 の書き方です。2 つの実装が同じ結果を返すことは、計測のたびに確認しています。
テーブルとデータ
注文の orders と、注文明細の order_items を作ります。
顧客 1,000 人がそれぞれ 10 件の注文を持ち、注文ごとに明細が 5 件あります。注文は 10,000 件、明細は 50,000 件です。
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
customer_id INT NOT NULL,
ordered_at DATETIME NOT NULL,
KEY idx_customer (customer_id, ordered_at DESC)
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
line_no TINYINT NOT NULL,
product_name VARCHAR(64) NOT NULL,
qty TINYINT NOT NULL,
unit_price INT NOT NULL,
PRIMARY KEY (order_id, line_no)
);
2 つの実装
逐次取得は、注文を取得したあと、注文の数だけ明細の SQL を送ります。
def order_page_sequential(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、注文ごとに明細を取る (SQL 6 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
page = []
for order_id, ordered_at in orders:
items, sec = timed(cur, ITEMS_ONE_SQL, (order_id,))
sql_sec.append(sec)
page.append({"id": order_id, "ordered_at": str(ordered_at), "items": [list(i[1:]) for i in items]})
return page
一括取得は、注文の ID をまとめて IN 句に渡し、明細を 1 回で取得します。
def order_page_batched(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、明細を IN 句で 1 回で取る (SQL 2 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
ids = [o[0] for o in orders]
items, sec = timed(cur, ITEMS_IN_SQL.format(",".join(["%s"] * len(ids))), ids)
sql_sec.append(sec)
by_order = {}
for item in items:
by_order.setdefault(item[0], []).append(list(item[1:]))
return [{"id": oid, "ordered_at": str(at), "items": by_order.get(oid, [])} for oid, at in orders]
遅延の足し方
アプリと DB の距離を変える代わりに、手元の Mac と TiDB Cloud の間にプロキシの Toxiproxy を置き、往復の遅延を擬似的に足しました。上りと下りに半分ずつ遅延を設定し、足す往復遅延を 0 ms、20 ms、80 ms の 3 段階にしています。
docker run -d --rm --name tidb-latency-proxy \
-p 127.0.0.1:8474:8474 -p 127.0.0.1:13306:13306 \
ghcr.io/shopify/toxiproxy:2.12.0
python latency_bench.py proxy
スクリプトでは TCP の接続先だけをプロキシに向け、TLS の証明書とホスト名は TiDB Cloud のものとして検証しています。
計測の手順
時間はすべて、クライアントで測った経過時間です。1 リクエストの応答時間は、処理の開始から、最後の SQL の結果を受け取って結果を組み立てるまでの時間です。
メインの比較実験では、1 本の接続を使い回し、各遅延でウォームアップを 3 回行ったあと、2 つの実装を 30 回ずつ実行しました。実装の実行順は 1 回ごとに入れ替え、遅延の順序も 10 回ごとに入れ替えています。各遅延の前後では SELECT 1 を 30 回実行して往復時間を確かめました。
これに追加して、一括取得の処理をリクエストのたびに接続を作る場合と、作っておいた接続を使い回す場合とで、30 回ずつ交互に実行する実験も行いました。接続を作る時間には、TCP の接続・TLS のハンドシェイク・認証が含まれます。
python latency_bench.py setup
python latency_bench.py bench 0,20,80 > bench.jsonl
python latency_bench.py conn 0,80 > conn.jsonl
python latency_bench.py summarize bench.jsonl conn.jsonl
結果は 30 回の中央値と四分位 (下位 25% の値と上位 25% の値) で示します。接続情報は TiDB Cloud のコンソールの Connect 画面で確認した値を環境変数で渡します。
latency_bench.py
"""TiDB Cloud Starter で、通信遅延がアプリの 1 リクエストの応答時間に与える影響を計測する。
接続情報は環境変数 TIDB_HOST, TIDB_PORT, TIDB_USER, TIDB_PASSWORD から読む。
遅延は Toxiproxy (管理 API: TOXIPROXY_API、既定 http://127.0.0.1:8474) で加える。
python latency_bench.py setup # テーブル作成とデータ投入
python latency_bench.py proxy # Toxiproxy にプロキシ tidb を登録
python latency_bench.py probe 0,20,80 # 接続時間と SELECT 1 の往復時間
python latency_bench.py check 0,20,80 # 2 つの処理の結果一致と、遅延による増分の確認
python latency_bench.py bench 0,20,80 # 本計測: 遅延 × 逐次取得・一括取得 (1 行 1 試行の JSON)
python latency_bench.py conn 0,80 # 本計測: 接続を毎回作る場合と使い回す場合
python latency_bench.py summarize bench.jsonl conn.jsonl # 中央値と増分の集計
"""
import json
import os
import socket
import ssl
import statistics
import sys
import time
import urllib.request
import pymysql
DB = "latency_lab"
PROXY_NAME = "tidb"
PROXY_LISTEN = ("127.0.0.1", 13306)
API = os.environ.get("TOXIPROXY_API", "http://127.0.0.1:8474")
SETUP = [
f"CREATE DATABASE IF NOT EXISTS {DB}",
f"USE {DB}",
"CREATE TABLE IF NOT EXISTS digit (n INT PRIMARY KEY)",
"INSERT IGNORE INTO digit VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)",
"""CREATE TABLE IF NOT EXISTS orders (
id BIGINT PRIMARY KEY,
customer_id INT NOT NULL,
ordered_at DATETIME NOT NULL,
KEY idx_customer (customer_id, ordered_at DESC)
)""",
"""CREATE TABLE IF NOT EXISTS order_items (
order_id BIGINT NOT NULL,
line_no TINYINT NOT NULL,
product_name VARCHAR(64) NOT NULL,
qty TINYINT NOT NULL,
unit_price INT NOT NULL,
PRIMARY KEY (order_id, line_no)
)""",
]
# 顧客 1,000 人 × 注文 10 件 = 注文 10,000 件、注文ごとに明細 5 件
LOAD = [
"""INSERT INTO orders
SELECT x.id, 1 + MOD(x.id - 1, 1000),
DATE_ADD('2026-09-01', INTERVAL x.id MINUTE)
FROM (
SELECT 1 + a.n + 10*b.n + 100*c.n + 1000*d.n AS id
FROM digit a CROSS JOIN digit b CROSS JOIN digit c CROSS JOIN digit d
) AS x""",
"""INSERT INTO order_items
SELECT o.id, e.n + 1, CONCAT('product-', MOD(o.id * 7 + e.n, 500)),
1 + MOD(e.n, 3), 100 + MOD(o.id + e.n, 900)
FROM orders AS o CROSS JOIN digit AS e WHERE e.n < 5""",
]
ORDERS_SQL = """SELECT id, ordered_at FROM orders
WHERE customer_id = %s ORDER BY ordered_at DESC LIMIT 5"""
ITEMS_ONE_SQL = """SELECT order_id, line_no, product_name, qty, unit_price
FROM order_items WHERE order_id = %s ORDER BY line_no"""
ITEMS_IN_SQL = """SELECT order_id, line_no, product_name, qty, unit_price
FROM order_items WHERE order_id IN ({}) ORDER BY order_id, line_no"""
def tls_context():
ctx = ssl.create_default_context()
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
return ctx
def connect(via_proxy, database=DB):
"""TiDB に接続する。via_proxy なら TCP を Toxiproxy に向け、TLS のホスト名は TiDB のものを検証する。"""
conn = pymysql.connect(
host=os.environ["TIDB_HOST"],
port=int(os.environ.get("TIDB_PORT", "4000")),
user=os.environ["TIDB_USER"],
password=os.environ["TIDB_PASSWORD"],
database=database,
ssl=tls_context(),
autocommit=True,
defer_connect=True,
)
sock = socket.create_connection(PROXY_LISTEN, timeout=30) if via_proxy else None
conn.connect(sock=sock)
return conn
def api(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as res:
return res.read()
def set_delay(rtt_ms):
"""往復で rtt_ms だけ遅れるよう、上り・下りに半分ずつ latency を設定する。0 なら外す。"""
for stream in ("upstream", "downstream"):
name = f"latency_{stream}"
try:
api("DELETE", f"/proxies/{PROXY_NAME}/toxics/{name}")
except urllib.error.HTTPError:
pass
if rtt_ms > 0:
api("POST", f"/proxies/{PROXY_NAME}/toxics",
{"name": name, "type": "latency", "stream": stream,
"attributes": {"latency": rtt_ms // 2, "jitter": 0}})
def setup():
cur = connect(False, None).cursor()
for sql in SETUP:
cur.execute(sql)
cur.execute("SELECT COUNT(*) FROM orders")
if cur.fetchone()[0] == 0:
for sql in LOAD:
cur.execute(sql)
cur.execute("SELECT (SELECT COUNT(*) FROM orders), (SELECT COUNT(*) FROM order_items)")
print("orders, order_items:", cur.fetchone())
def proxy():
try:
api("DELETE", f"/proxies/{PROXY_NAME}")
except urllib.error.HTTPError:
pass
api("POST", "/proxies", {"name": PROXY_NAME, "listen": "0.0.0.0:13306",
"upstream": f"{os.environ['TIDB_HOST']}:4000", "enabled": True})
print(api("GET", f"/proxies/{PROXY_NAME}").decode())
def timed(cur, sql, args=None):
start = time.perf_counter()
cur.execute(sql, args)
rows = cur.fetchall()
return rows, time.perf_counter() - start
def order_page_sequential(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、注文ごとに明細を取る (SQL 6 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
page = []
for order_id, ordered_at in orders:
items, sec = timed(cur, ITEMS_ONE_SQL, (order_id,))
sql_sec.append(sec)
page.append({"id": order_id, "ordered_at": str(ordered_at), "items": [list(i[1:]) for i in items]})
return page
def order_page_batched(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、明細を IN 句で 1 回で取る (SQL 2 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
ids = [o[0] for o in orders]
items, sec = timed(cur, ITEMS_IN_SQL.format(",".join(["%s"] * len(ids))), ids)
sql_sec.append(sec)
by_order = {}
for item in items:
by_order.setdefault(item[0], []).append(list(item[1:]))
return [{"id": oid, "ordered_at": str(at), "items": by_order.get(oid, [])} for oid, at in orders]
def summary(values_ms):
q1, median, q3 = statistics.quantiles(values_ms, n=4, method="inclusive")
return {"median": round(median, 2), "q1": round(q1, 2), "q3": round(q3, 2)}
def probe(delays, n=30):
"""接続確立の時間と SELECT 1 の往復時間を、プロキシなしと各遅延設定で測る。"""
for label, via_proxy, delay in [("direct", False, 0)] + [(f"proxy+{d}", True, d) for d in delays]:
if via_proxy:
set_delay(delay)
connect_ms, select1_ms = [], []
for _ in range(5):
start = time.perf_counter()
conn = connect(via_proxy)
connect_ms.append((time.perf_counter() - start) * 1000)
conn.close()
conn = connect(via_proxy)
cur = conn.cursor()
for _ in range(3):
timed(cur, "SELECT 1")
for _ in range(n):
select1_ms.append(timed(cur, "SELECT 1")[1] * 1000)
conn.close()
print(json.dumps({"path": label, "connect_ms": summary(connect_ms),
"select1_ms": summary(select1_ms)}))
set_delay(0)
def check(delays, n=5):
"""2 つの処理の結果一致と、各遅延設定での応答時間を数回だけ確かめる。"""
for delay in delays:
set_delay(delay)
conn = connect(True)
cur = conn.cursor()
seq_ms, bat_ms = [], []
for i in range(n):
customer_id = 1 + (i * 37) % 1000
start = time.perf_counter()
a = order_page_sequential(cur, customer_id)
seq_ms.append((time.perf_counter() - start) * 1000)
start = time.perf_counter()
b = order_page_batched(cur, customer_id)
bat_ms.append((time.perf_counter() - start) * 1000)
assert a == b and len(a) == 5 and all(len(o["items"]) == 5 for o in a), customer_id
conn.close()
print(json.dumps({"delay_ms": delay, "identical": True,
"sequential_ms": [round(x, 1) for x in seq_ms],
"batched_ms": [round(x, 1) for x in bat_ms]}))
set_delay(0)
def select1_series(cur, n=30):
return summary([timed(cur, "SELECT 1")[1] * 1000 for _ in range(n)])
def emit(row):
print(json.dumps(row), flush=True)
def bench(delays, warmup=3, rounds=30, block=10):
"""遅延 × 逐次取得・一括取得。1 本の接続を使い回し、実装の実行順は周回ごと、遅延の順序はまとまりごとに入れ替える。"""
conn = connect(True)
cur = conn.cursor()
pages = {"sequential": order_page_sequential, "batched": order_page_batched}
for delay in delays:
set_delay(delay)
for i in range(warmup):
for page in pages.values():
page(cur, 1 + i)
emit({"kind": "select1", "delay_ms": delay, "when": "before", **select1_series(cur)})
for start in range(0, rounds, block):
order = delays if (start // block) % 2 == 0 else list(reversed(delays))
for delay in order:
set_delay(delay)
for i in range(start, start + block):
customer_id = 1 + (i * 37) % 1000
names = ["sequential", "batched"] if i % 2 == 0 else ["batched", "sequential"]
for name in names:
sql_sec = []
t0 = time.perf_counter()
pages[name](cur, customer_id, sql_sec)
total = time.perf_counter() - t0
emit({"kind": "page", "delay_ms": delay, "round": i, "version": name,
"customer_id": customer_id, "total_ms": round(total * 1000, 2),
"sql_ms": [round(x * 1000, 2) for x in sql_sec]})
for delay in delays:
set_delay(delay)
emit({"kind": "select1", "delay_ms": delay, "when": "after", **select1_series(cur)})
set_delay(0)
conn.close()
def conn_bench(delays, warmup=3, rounds=30):
"""一括取得の処理を、接続を毎回作る場合 (new) と使い回す場合 (reuse) で交互に実行する。"""
for delay in delays:
set_delay(delay)
reuse = connect(True)
reuse_cur = reuse.cursor()
for i in range(warmup + rounds):
customer_id = 1 + (i * 37) % 1000
for mode in (["new", "reuse"] if i % 2 == 0 else ["reuse", "new"]):
t0 = time.perf_counter()
if mode == "new":
conn = connect(True)
connect_sec = time.perf_counter() - t0
order_page_batched(conn.cursor(), customer_id)
total = time.perf_counter() - t0
conn.close()
else:
connect_sec = 0.0
order_page_batched(reuse_cur, customer_id)
total = time.perf_counter() - t0
if i >= warmup:
emit({"kind": "conn", "delay_ms": delay, "round": i - warmup, "mode": mode,
"total_ms": round(total * 1000, 2), "connect_ms": round(connect_sec * 1000, 2)})
reuse.close()
set_delay(0)
def summarize(bench_path, conn_path):
rows = [json.loads(line) for line in open(bench_path)]
pages = [r for r in rows if r["kind"] == "page"]
delays = sorted({r["delay_ms"] for r in pages})
for version in ("sequential", "batched"):
base = None
for delay in delays:
ms = summary([r["total_ms"] for r in pages if r["version"] == version and r["delay_ms"] == delay])
base = ms["median"] if base is None else base
print(f"{version:10} +{delay:>2} ms: median {ms['median']:7.1f} "
f"(q1 {ms['q1']:6.1f}, q3 {ms['q3']:6.1f}) increase {ms['median'] - base:6.1f}")
rows = [json.loads(line) for line in open(conn_path)]
for delay in sorted({r["delay_ms"] for r in rows}):
for mode in ("new", "reuse"):
target = [r for r in rows if r["delay_ms"] == delay and r["mode"] == mode]
total = summary([r["total_ms"] for r in target])
connect_ms = summary([r["connect_ms"] for r in target])
print(f"conn {mode:5} +{delay:>2} ms: median {total['median']:7.1f} "
f"(q1 {total['q1']:6.1f}, q3 {total['q3']:6.1f}) connect {connect_ms['median']:6.1f}")
if __name__ == "__main__":
command = sys.argv[1]
if command == "summarize":
summarize(sys.argv[2], sys.argv[3])
sys.exit()
if command in ("probe", "check", "bench", "conn"):
{"probe": probe, "check": check, "bench": bench, "conn": conn_bench}[command](
[int(d) for d in sys.argv[2].split(",")])
else:
{"setup": setup, "proxy": proxy}[command]()
検証結果
往復時間の確認
遅延の設定ごとに、SELECT 1 の往復時間を測りました。値は計測の前と後の中央値です。
{"kind": "select1", "delay_ms": 0, "when": "before", "median": 14.83, "q1": 13.86, "q3": 16.69}
{"kind": "select1", "delay_ms": 20, "when": "before", "median": 37.68, "q1": 36.4, "q3": 39.53}
{"kind": "select1", "delay_ms": 80, "when": "before", "median": 100.41, "q1": 98.3, "q3": 103.72}
(中略)
| 足した遅延 (ms) | 計測前 (ms) | 計測後 (ms) |
|---|---|---|
| 0 | 14.83 | 13.97 |
| 20 | 37.68 | 38.20 |
| 80 | 100.41 | 98.79 |
以降の 3 つの条件は、往復時間が約 15 ms・約 38 ms・約 100 ms の場合に当たります。
逐次取得と一括取得
2 つの実装を遅延の設定ごとに 30 回ずつ実行した結果は次のとおりです。※表の削減率とは、逐次取得に対する一括取得の応答時間の削減率
$ python latency_bench.py summarize bench.jsonl conn.jsonl
sequential + 0 ms: median 117.2 (q1 109.5, q3 120.6) increase 0.0
sequential +20 ms: median 254.8 (q1 249.7, q3 273.3) increase 137.6
sequential +80 ms: median 625.7 (q1 619.5, q3 632.2) increase 508.5
batched + 0 ms: median 40.5 (q1 37.1, q3 42.6) increase 0.0
batched +20 ms: median 83.2 (q1 81.1, q3 85.9) increase 42.7
batched +80 ms: median 206.8 (q1 204.3, q3 208.6) increase 166.2
(中略)
| 足した遅延 (ms) | 逐次取得 (ms) | 一括取得 (ms) | 削減率 (%) |
|---|---|---|---|
| 0 | 117.2 | 40.5 | 65.4 |
| 20 | 254.8 | 83.2 | 67.3 |
| 80 | 625.7 | 206.8 | 66.9 |
SQL 1 回あたりの時間は、2 つの実装でほぼ同じでした。
一括取得は、どの遅延でも応答時間を 65% 以上削減できました。
足した遅延 0 ms のときからの応答時間の増分は次のとおりです。
| 足した遅延 (ms) | 逐次取得の増分 (ms) | 一括取得の増分 (ms) |
|---|---|---|
| 20 | 137.6 | 42.7 |
| 80 | 508.5 | 166.2 |
逐次取得の増分は足した遅延の約 6.4〜6.9 倍、一括取得の増分は約 2.1 倍でした。
増分は、足した遅延に SQL の回数をかけた値に近くなりました。
接続の作り方
一括取得の処理を、接続を毎回作る場合と、使い回す場合とで比べた結果は次のとおりです。※表の削減率とは、毎回作る場合に対する、使い回す場合の応答時間の削減率
$ python latency_bench.py summarize bench.jsonl conn.jsonl
(中略)
conn new + 0 ms: median 187.3 (q1 178.3, q3 216.5) connect 143.8
conn reuse + 0 ms: median 38.4 (q1 35.1, q3 41.9) connect 0.0
conn new +80 ms: median 721.0 (q1 709.9, q3 732.9) connect 508.8
conn reuse +80 ms: median 210.4 (q1 205.5, q3 222.3) connect 0.0
| 足した遅延 (ms) | 毎回作る (ms) | うち接続 (ms) | 使い回す (ms) | 削減率 (%) |
|---|---|---|---|---|
| 0 | 187.3 | 143.8 | 38.4 | 79.5 |
| 80 | 721.0 | 508.8 | 210.4 | 70.8 |
接続を毎回作ると、接続を作る時間だけで SQL 2 回の処理本体より長くかかりました。
足した遅延 80 ms では接続を作る時間が 365.0 ms 増えました。足した遅延の 4 倍を超える増え方です。
わかったことと考察
計測の結果から通信遅延による応答時間の増分は次のように見積もれると考えられます。
応答時間の増分 ≒ 往復時間の増分 × 1 リクエストで逐次に行う DB との往復の回数
SQL 1 回の時間が 2 つの実装でほぼ同じだったことから、今回の処理では応答時間の差のほとんどは往復の回数から生じていると思われます。
接続を作る処理では、TCP の接続・TLS のハンドシェイク・認証のために DB …… と何度も往復します。そのため、往復時間が延びたときの影響が SQL 1 回より大きく出たのでしょう。TiDB Cloud Starter のパブリックエンドポイントは TLS での接続が必須なので、接続を作り直す設計ではこの往復がリクエストごとに発生します。
先ほどの見積もりの式に、アプリの 1 リクエストでの往復の回数と往復時間を当てはめると、取るべき対応は次の 3 つの場合に分けられそうです。
| 状況 | 例 | 有効な対応 |
|---|---|---|
| 通信遅延が問題になりにくい | 1 リクエストの DB との往復が少なく、往復時間も小さい | 対応は不要 |
| 設計の改善が有効 | N+1、ループ内の SQL、リクエストごとの接続 | SQL の集約、コネクションプール |
| 配置や構成の見直しを検討 | 往復の回数をこれ以上減らせず、往復時間 × 回数が応答時間の目標を超える | アプリを DB と同じリージョンに置く、プライベートエンドポイント (AWS PrivateLink) を使う |
たとえば往復時間が約 100 ms の場合、DB との往復が 6 回あれば、それだけで応答時間が 0.6 秒を超えます。集約して 2 回にすれば約 0.2 秒に収まりました。往復の回数を減らす改善は配置を変えずにすぐ試せるため、配置を見直す前に検討する価値があるでしょう。
まとめ
アプリと TiDB Cloud Starter の間の往復遅延と 1 リクエストあたりの SQL の回数を変えて、応答時間を計測しました。応答時間の増分は往復時間と逐次の往復回数の積でおおむね決まってくる形です。SQL を 6 回から 2 回に集約すると応答時間を 65% 以上、接続を使い回すと 70% 以上削減できました。通信遅延が気になるときは、配置を変える前に 1 リクエストでの DB との往復の回数を数えてみるとよさそうです。








