アンチパターン@RDB¶
はじめに¶
本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。
01. DB接続を再利用しない¶
問題¶
DB処理のたびにDB接続を確立すると、DB処理に時間がかかる。
実装例(TypeScript)¶
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
export async function handler() {
// 毎回、PrismaClientを作成してしまっている
const prisma = new PrismaClient();
return prisma.user.findMany();
}
02. 不要なDBレコードやカラムを取得する¶
問題¶
不要なDBレコードやカラムを取得すると、DB処理に時間がかかる。
実装例(TypeScript)¶
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
const user = await prisma.user.findUnique({
where: {id},
// 不要なカラムも取得してしまっている
include: {team: true, bans: true, logs: true, profiles: true},
});
03. N+1問題を起こす¶
N+1問題とは¶
親テーブルを経由して子テーブルにアクセスするとき、親テーブルのレコード数分のSQLを発行してしまうアンチパターンのこと。
実装例(PHP)¶
▼ 問題がある実装¶
反復処理の中で子テーブルのレコードにアクセスしてしまう場合、N+1問題が起こる。
内部的には、親テーブルへのSQLと、Where句を持つSQLが親テーブルのレコード数分だけ発行される。
<?php
// 親テーブルにSQLを発行 (1回)
$departments = Department::all();
foreach($departments as $department) {
// 親テーブルのレコード数分のWhere句SQLを発行する (N回)
$department->employees;
}
# 1回
select * from `departments`
# N回
select * from `employees` where `department_id` = 1
select * from `employees` where `department_id` = 2
select * from `employees` where `department_id` = 3
...
▼ 解決方法¶
反復処理の前に、子テーブルへアクセスしておく。
データアクセス時にIN句やJOIN句を使用すると、N+1問題を解消できる。
Laravelでは with() 関数を使用すると内部的には、親テーブルへのSQLと、IN句を使用したSQLが発行され、最終的に2回で済む。
<?php
// SQL発行 (2回)
// 内部的ににIN句
$departments = Department::with('employees')->get();
foreach($departments as $department) {
// キャッシュを使用するのでSQLの発行はされない (0回)
$department->employees;
}
# 2回
select * from `departments`
select * from `employees` where `department_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ... 100)
実装例(TypeScript)¶
▼ 問題がある実装(1)¶
反復処理の中で子テーブルのレコードにアクセスしてしまう場合、N+1問題が起こる。
内部的には、親テーブルへのSQLと、Where句を持つSQLが親テーブルのレコード数分だけ発行される。
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
// 親テーブルにSQLを発行 (1回)
const users = await prisma.user.findMany({where: {teamId}});
for (const u of users) {
// 親テーブルのレコード数分のWhere句SQLを発行する (N回)
const logs = await prisma.log.findMany({
where: {userId: u.id},
});
}
▼ 問題がある実装(2)¶
map() 関数を使用した反復処理でも、もちろんN+1問題が起こる。
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
// 親テーブルにSQLを発行 (1回)
const users = await prisma.user.findMany({where: {teamId}});
// 親テーブルのレコード数分のWhere句SQLを発行する (N回)
const logs = await Promise.all(
users.map(async (u) => {
const logs = await prisma.log.findMany({
where: {userId: u.id},
});
return {user: u, logs};
}),
);
▼ 解決方法¶
データアクセス時にIN句やJOIN句を使用すると、N+1問題を解消できる。
Prismaでは、in プロパティをIN句を使用したSQLが発行され、最終的に2回で済む。
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
// 親テーブルにSQLを発行 (1回)
const users = await prisma.user.findMany({where: {teamId}});
// 親テーブルのレコード数分のWhere句SQLを発行する (1回)
const logs = await prisma.log.findMany({
where: {
// 内部的にIN句
userId: {in: users.map((u) => u.id)},
},
});
04. 一覧取得でページング(取得数指定)がない¶
問題¶
DBからレコードの一覧を取得する場合、ページング(取得数指定)がないと、DB処理に時間がかかる。
実装例(TypeScript)¶
▼ 解決方法¶
const page = 1;
const limit = 50;
const users = await prisma.user.findMany({
where: {teamId},
skip: (page - 1) * limit,
take: limit,
orderBy: {createdAt: "desc"},
});
05. 検索するレコード数をむやみに増やす¶
問題¶
集計結果を一覧で表示するような機能では、テナント内のデータを横断的に取得する必要がある。
しかし、必要なデータを大量にRead処理で取得し、これをその都度集計すると、負荷もかかるし、処理時間も長くなる。
実装例(TypeScript)¶
▼ 問題がある実装¶
const events = await prisma.event.findMany({
where: {
tenantId,
occurredAt: {
gte: startAt,
lt: endAt,
},
},
});
const eventCount = events.length;
▼ 解決方法¶
高負荷の処理をバッチマイクロサービスに分割する。
バックエンドでは、バッチマイクロサービスと共有のテーブルか、事前に集計されたデータを取得する。
バッチマイクロサービスでは、共有の集計テーブルに集計結果を書き込む。
await prisma.eventHourlySummary.upsert({
where: {
tenantId_hour: {
tenantId,
hour: startAt,
},
},
update: {
count: eventCount,
},
create: {
tenantId,
hour: startAt,
count: eventCount,
},
});
// EventHourlySummaryテーブル
model EventHourlySummary {
tenantId String
hour DateTime
count Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@id([tenantId, hour])
@@map("event_hourly_summaries")
}
バックエンドでは、共有の集計テーブルから集計結果を取得する。
const eventSummary = await prisma.eventHourlySummary.findUnique({
where: {
tenantId_hour: {
tenantId,
hour: startAt,
},
},
});
const eventCount = eventSummary?.count ?? 0;