Node.jsМодуль 5: Базы данных
Оптимизация запросов
Индексы, пагинация, кэширование
Цель урока
В этом уроке ты научишься:
- Создавать индексы
- Реализовывать пагинацию
- Кэшировать данные
Индексы
MongoDB
// В схеме
const userSchema = new mongoose.Schema({
email: { type: String, unique: true, index: true },
name: String,
createdAt: Date
});
// Составной индекс
userSchema.index({ name: 1, createdAt: -1 });
// Текстовый индекс
userSchema.index({ name: 'text', bio: 'text' });
// Программно
await User.collection.createIndex({ email: 1 });PostgreSQL (Prisma)
model User {
id Int @id @default(autoincrement())
email String @unique
name String
@@index([name])
@@index([createdAt(sort: Desc)])
}Когда создавать индексы
- Поля, по которым часто ищут
- Поля для сортировки
- Внешние ключи
Пагинация
Offset-based
// MongoDB
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
const total = await User.countDocuments();
res.json({
data: users,
meta: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});Cursor-based (эффективнее)
// MongoDB
const cursor = req.query.cursor;
const limit = parseInt(req.query.limit) || 10;
const query = cursor
? { _id: { $lt: cursor } }
: {};
const users = await User.find(query)
.sort({ _id: -1 })
.limit(limit + 1);
const hasMore = users.length > limit;
if (hasMore) users.pop();
res.json({
data: users,
meta: {
nextCursor: hasMore ? users[users.length - 1]._id : null,
hasMore
}
});Кэширование с Redis
Установка
npm install redisПодключение
const redis = require('redis');
const client = redis.createClient({
url: process.env.REDIS_URL
});
client.on('error', (err) => console.error('Redis error:', err));
await client.connect();Кэширование запросов
async function getUser(id) {
const cacheKey = `user:${id}`;
// Проверяем кэш
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Запрос к БД
const user = await User.findById(id);
// Сохраняем в кэш (TTL 1 час)
await client.setEx(cacheKey, 3600, JSON.stringify(user));
return user;
}
// Инвалидация
async function updateUser(id, data) {
const user = await User.findByIdAndUpdate(id, data, { new: true });
await client.del(`user:${id}`);
return user;
}Middleware для кэширования
function cache(ttl = 60) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// Перехватываем res.json
const originalJson = res.json.bind(res);
res.json = async (data) => {
await client.setEx(key, ttl, JSON.stringify(data));
return originalJson(data);
};
next();
};
}
// Использование
app.get('/api/users', cache(300), async (req, res) => {
const users = await User.find();
res.json(users);
});Оптимизация запросов
Select нужных полей
// Плохо
const users = await User.find();
// Хорошо
const users = await User.find().select('name email');Lean для чтения
// Возвращает plain объекты (быстрее)
const users = await User.find().lean();Batch операции
// Плохо
for (const id of ids) {
await User.findByIdAndUpdate(id, { status: 'active' });
}
// Хорошо
await User.updateMany(
{ _id: { $in: ids } },
{ status: 'active' }
);Explain для анализа
const explanation = await User.find({ email: 'test@example.com' })
.explain('executionStats');
console.log(explanation.executionStats);Практика
Задание: API с пагинацией и кэшем
Задача: Создай endpoint с пагинацией и кэшированием.
Решение:
app.get('/api/posts', cache(60), async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const [posts, total] = await Promise.all([
Post.find()
.populate('author', 'name')
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(limit)
.lean(),
Post.countDocuments()
]);
res.json({
data: posts,
meta: { page, limit, total, pages: Math.ceil(total / limit) }
});
});Проверь себя
- Когда использовать cursor-based пагинацию?
- Что делает метод
lean()? - Как инвалидировать кэш?