Зменшыце навантажэнне на Prisma і PostgreSQL прытым, перш чым плаціце за большую базу дадзеных.
Пятнаццац практычных методаў — ад EXPLAIN ANALYZE і кампазітных індэксаў да рашэння проблемы N+1, выкарыстоўвання лічыльнікаў, пулінацыі і процедуры vacuum — для змены колькасці операцый у базе дадзенаў Prisma.
Крыніцы: I Reduced Prisma Database Operations by 60% Without Upgrading My Database
У прыграмах на Prisma і PostgreSQL база дадзеных часта становіцца першай справжней прыградой. Запит, які виконваецца за 5 мс для калькольвання каля кількох соткаў рэкордаў, можа займаць секунды, калі табліца мае мільйоны рэкордаў, і першым падсказкам є купіць большую інстанцыю або дорожэйшы пакет. Узглянем прыклад маркетплейс-прыграмы, дзе навантажэння на базу дадзеных доходзіць да 5 000–6 000 операцыяў на дзень, што достатна, каб выкарыстаць безкоштовны тариф на адпаведнае навантажэнне ўсё роўным сабой. Аналіз і оптымацыя запытаў у такім случае можу скасаваць больш чым 60 процэнтаў операцыяў, прышвыдзіць адпаведныя рэспансы API і зменшыць суму рахунку, не чынячы нічога з апаратным забезпечэнням.
Тактыкі, прыведзены нижэй і аррангаваны ад діагназы до проектавання схемы і ўтримання, зазвычай і яшчэ ўлучаюць разлік.
Змяркуйце прычыны перш чым што-небудзь зменіць
Не прыпускайце, што запыт медленны; змяркуйце яго. Два інструмента падходзяць для большасці задач:
EXPLAIN ANALYZEпаказвае план выконання PostgreSQL, а таксама колькість сканаваных рэядоў, чы былі выкарыстаны індэксы, і фактычны час выконання.- Логаванне запытак у Prisma апкрывае точны SQL-код, які генеруе кліент.
Выконанне EXPLAIN ANALYZE для фільтраў па продавцу выглядае так. Значыць, ANALYZE дзе-жа выконвае запыт, таму запыты, якія мяняюць данні, трэба пакласты ў транзакцыю, якую можна будзе анулюваць.
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE seller_id = 123;
Нават пры чыстам выглядзе запыту Prisma можа адбыцца секвэнцыйнае сканаванне всіх рэядоў таблы; толькі план выконання паказвае гэта. pg_stat_statements (які ранжуе запыты па загальным часе), аналізы запытак у Prisma і панелі керування, такія як у Vercel, дапамагаюць выбраць, што паспелі вылечыць перша. Для структураванага падходу адзірніце як знайсці справжню прычыну медленнае выконання запыту ў Node.js.
Індексавацыя запитоў, якія вы насправдэ працоўвайце
Індэксы зазвычай ёсць самой значным фактарам паўпрынцыпнасці. Без іх запит такога типу можа змусіць PostgreSQL пераглядаць кожны рядок у orders:
SELECT *
FROM orders
WHERE seller_id = 123;
Просты індэкс типу B-tree на столбце, які выкарыстоўваецца для фільтрацыі, дазволяе планавальніку адразу перайсці да адпаведных рядкоў:
CREATE INDEX idx_orders_seller
ON orders(seller_id);
Калі прыложэнне регулярна фільтруе за колькама столбцамі адночасна, компазытны індэкс дапамагае працаваць з гэтай комбінацыяй безпосередньа:
CREATE INDEX idx_orders_seller_status
ON orders(seller_id, status);
За дапамогою такога індэксу умовы, якія стосуюцца як продавца, так і статусу, перетвараюцца на прымітны пошук у індэксе:
WHERE seller_id = 123
AND status = 'PAID'
У складнам індексе значны рэхтары столбцоў. (seller_id, status) таксама дапамагае запытам, якія фільтруюць толькі па seller_id, але не запытам, якія фільтруюць толькі па status. Загальны прынцып — індексаваць тыя столбцы, па якіх вы насправдэўна фільтруеце, сортаваеце або з’едынаеце данні, а не всё; кожны індекс займае прыстановую памяць і спамляе кожную запіс у таблыцу.
Адзёрваць запыты N+1
Шаблон N+1 — это класычная проблема ORM. Вы завантажваеце спіс з 100 замовленняў:
const orders = await prisma.order.findMany();
а потым ў цікле завантажваеце корыстніка кожнага замовлення по аднаму:
for (const order of orders) {
await prisma.user.findUnique({
where: {
id: order.userId
}
});
}
Это 101 запыт для адной сторонкі дадзенняў. Якшчы папросіць Prisma завантажыць асоціяцыю разам з замовленнямі, цікл не будзе працаваць:
const orders = await prisma.order.findMany({
include: {
user: true
}
});
Адзін вызов заменяе цыкл, пры чым спадае латэнцыя і навантажэння. Залежна ад версіі та настройкі, Prisma выкананае include можа быць як спраўа з’яеднання, так і як адна дадатковая запытка пакетам; у будзь-якім случае зникаюць перавыплывы дадзеных за кожныя рэкорды.
Выберыце толькі тыя столбцы, якія вам патрэбны
Скарычанне колькасці перадаваемых дадзеных — адна з найбюджэтнейшых мераў пахамлення. Простая функцыя findMany вяртае кожны столбець кожнага рэкорду:
await prisma.user.findMany();
Функцыя select обмежвае рэзультат тым, што фактычна выкарыстоўваець вызываючы код:
await prisma.user.findMany({
select: {
id: true,
name: true
}
});
Калі ў таблы є JSON-даны, Markdown, атракцыі чы дугі текст, гэта сама по сабе можа значна зменшыць час адпаведзі, а таксама не дазволяе чутлівым столбцам прабрацца ў адпаведзі API.
Заменіце паўтаральныя агрэгаты на збераганыя лічыльнікі
Панелі аналітыки часта перасчыляюць агрэгаты занова праз кожнае завантажэнне сторонні:
COUNT(*)
SUM(amount)
AVG(...)
Это прыемна для 100 рэядоў, але крываючая для мільйонаў. Напрыклад, вычысленне аб’ёму продажаў за кожны запит значыць сумаванне всей табліцы:
SELECT SUM(amount)
FROM orders;
Альтэрнатыва — падтрымліваць актуальныя агрегатныя значэння пад час зберагання, зберагаючы такія показнікі, як аб’ёмы, безпасцерэдна ў записе выкарыстоўвача:
User
-------------
totalSales
totalOrders
totalRevenue
Апдэйтуйце гэтыя поля ўсё час, калі завершаецца пакупка, і тады чытанне стане працэсам зішчыткі O(1) уместо сканавання мільйонаў рэядоў; у вышэазваным сценарыі гэта была адна з найбольшых аптымізацый. Апдэйтуйце лічыльнікі атамарна (напрыклад, за дапамогою increment у Prisma ў той жа транзакцыі, што і вставка замовлення), і час а час разабяжыце ўсі показнікі, адтак як дэнормалізаваныя аб’ёмы могу зменшвацца.
Пагінацыя кожнага спісу
Табліцы не застаюцца маленькімі, таму ніколі не вяртайце необмежаны спіс у такім формате:
await prisma.product.findMany();
Ўзайце take і skip для запрашоўкі абмежанай сторанцы; гэты прыклад вяртае другую з 20 сторанцаў:
await prisma.product.findMany({
take: 20,
skip: 20
});
Для большых набораў дадзеных або безканцовага прасування лепш выкарыстоўваць пагінацыю за дапамогою курсара: афсэт прыводзіць PostgreSQL да перагляду кожнай прыпуснутай строчкі, тады як курсар на індексаванай колонце запускае працэс далей ад самага канца пяршней сторанцы.
Кэшаванне чытанняў, якія рэдка змінююцца
Найшырэйшы запит — гэты, які ніколі не выкананы. Дадзеныя, якія змінююцца рэдка, ўзгодныя для кэшавання: сторанцы продуктам, публічныя вітрины, настройкі прыемлівання і спісы катэгорый. Частыя варыянты включаюць Upstash Redis, кэш Next.js і LRU-кэш унутроць прыемлівання. Выведаўчы часта выкарыстоўваныя запиты з PostgreSQL скасоввае велікі тлак; ценаю гэтага є неяўнасць дадзеных, таму трэба выявіць, насколькі старым можа быць дадзены, і прабавіць яго зноў апрацаваць, калі строчкі змянююцца.
Блакатнае запісванне заместо цыклаў
Кожны адзін чаканы вызов — это сецесія па сети, і ўсе яны быстра нарабляюцца. Стварэнне записей по адной у цікле — это класычны прыклад:
for (const item of items) {
await prisma.item.create({
data: item
});
}
createMany вставляе цэлы массив за адну заповедзь:
await prisma.item.createMany({
data: items
});
Адна сецесія краща за сотні. Згадайце, што createMany не стварае вялізных асоцыяцый і вяртае толькі кальку, а не самыя рядкі; якщо вам патрэбны записі, пераканайцеся ў документах вашай версіі Prisma.
Агрупаваць сувязаные змены ў транзакцыі
Стварэнне замовлення часта павярожана іншымі зменамі, такімі як корэктаванне запасоў, апдэйт балансаў і стварэнне квітанцыі. Штыры змены належаць адно да другога, і $transaction гарантуе, што яны або ўсе будуць выкананы, або ўсе будуць анульваны:
await prisma.$transaction([
prisma.order.create(...),
prisma.user.update(...),
prisma.balance.update(...)
]);
Асалівая выгода — это адэкватнае функцыянаванне, а не швальнасць: без актуалізацыі балансу немагчыма выконваць які-лебе запуск, якщо ў процесе выйдзе праця. Трэба робіць транзакцыі короткімі, адказнае ў яных знаходзяцца блокіранні і з’ѐеднання.
Павторны выкарыстоўванне аднаго Prisma Client
Стварэнне новага PrismaClient за кожны запит є дорогім і можа выкарыстаць усе з’ѐеднання; калі працюеш у режыме развіцця, гарячая перзагрузка можа ствараць новы кліент пасля кожных змян у файле. Сінглтон памагае ужыцца ад гэтых проблем. Імпортуй кліент:
import { PrismaClient } from "@prisma/client";
Потым зберагчы інстанцую на globalThis параду ў режыме не-продакшн, каб перзагружаныя модулі выкарыстоўвалі вялікі кліент уместо стварэння новага:
const globalForPrisma =
globalThis as unknown as {
prisma: PrismaClient | undefined;
};export const prisma =
globalForPrisma.prisma ??
new PrismaClient();if (process.env.NODE_ENV !== "production")
globalForPrisma.prisma = prisma;
Каст as unknown as дае globalThis атрыбут prisma з пазначаным типам; у режыме продакшн модуль запускаецца толькі раз, таму глобальны кэш не патрэбен.
Адмастыраванне з’ѐеднанняў, ў чым случку на серверлессе
Платформы без сервера можу запускаць тыячы паралельных экземпляраў функцый, кожны з якіх патрабуе сабе аднальнага з’яўлення да базы дадзеных. Без пулінацыі PostgreSQL можа выкарыстоўваць усе з’яўлення задаўна перш чым пачнуцца проблемы з CPU. Prisma Accelerate і PgBouncer аба стояць між прыемнікам і базай дадзеных, дазваляючы многім запитам дзеліцься значна меншым наборам рэальных з’яўленняў PostgreSQL.
Створыце індэкс для полей JSONB, па якіх вы фільтруеце
Тып JSONB у PostgreSQL ўсё можа, але фільтрацыя па вкладзеных полях без індэкса зазвычай значыць павальны скан всіх рэкордаў табелі. Якщо вы регулярна выкарыстоўваете запиты ўнутар дакументаў JSON, створыце GIN-індэкс на гэтым столбцы (або выразовы індэкс на конкрэтным ключы, па якім часта фільтруеце), тады медленныя пошукі дакументаў будуць ператварацца на шырокія пошукі за дапамогою індэксаў.
Для пошуку унікальных элементаў вярніцеся да методу findUnique
Калі вы выкарыстоўваете первычны ключ або інша унікальная палату для запитаў, наступны спосаб є найкращы:
findUnique()
Гэта паведамляе Prisma, што пошук нацелены на адну строчку, што дазволяе Prisma аб’еднаваць калькі findUnique з таго ж момента ў аднай запытце, што ўжоць карыстаецца сабой у рэзалверах GraphQL. Напрыклад:
await prisma.user.findUnique({
where: {
id
}
});
Выбірайце гэты варыянт замест наступнага, калі поле є унікальным:
findFirst()
findFirst застаецца правым выборам для фільтраў, дзе не патрэбна унікальнае значэнне, калі вам насправды трэба „першая падходячая строчка“.
Архіваваць старыя строчкі замест таго, каб вечна выкасываць іх
Мяккае выкасыванне є зручным; проста пазначайце строчку:
isDeleted = true
Аднак з годама кожная запытка павінна фільтраваць выкасываныя строчкі, а таксама запобегаць збільшэнню розмеру таблей і індексаў. У системах з вялікым об’ёмам дадзеных періядычна перакладвайце выкасываныя записы ў таблі-архіў: меньшыя таблі означаюць меншыя індэксы і быстрэйшыя запыткі. Частковы індекс над невыкасыванымі строчкамі є проміжным рашэнням.
Зберагачыце автавакуум здаровым
PostgreSQL выкарыстоўвае MVCC (контроль канкуранцыі з мнагаю версіяй), таму апডейтаваныя і выдаленыя рядкі не знімаюцца негаразду; яны застаюцца у вялікай колькасцы як „мертвыя туплы“. Автавакуум — это фоновы процес, який іх чыстіць. Якша налаштаваны автавакуум:
- вызволяе прастор, які займаюць „мертвыя туплы“;
- апдэйтуе статыстыку планавальніка запытоў;
- дзеяўна падтрымвае індэксы;
- забезпечвае точнасць планаў запытоў.
Ігнораванне яго ведае да збільшэння розмеру базы дадзеных і парадоксальных планаў запытоў, ў частычна на больш заўважлівых табелях, такіх як вышэўказаныя лічыльнікі.
Ключовыя выводы
Большасц проблем з аддачай баз дадзеных не рашаюцца за дапамогою большых сервероў; яны рашаюцца за дапамогою кращых патэранаў доступу да дадзеных. У вышэўказаным сцэнарыі прадаўцу хостынгу і розмеру базы дадзеных не змяніліся. Прыбуткі былі атрыбутам:
- аналізу дорогіх запытоў заместа адгадвання;
Разам яны далі магчымасць адбывацца на 60 процэнтоў меншых операцый, шырэйшых API і большай кантэнцыі трафіку. Перад расшырэнням выявіце, як на самай працоўвае ваша база дадзеных; існуючыя запиты часта маскуюць большую частку працэснае моці, якая вам патрэбна.