PostgreSQL
JSONB, CTE-ներ, խիստ constraint-ներ, MVCC, row-level security և Prisma production-ում։
30 վայրկյանում
PostgreSQL-ը առաջադեմ open-source ռելյացիոն տվյալների բազա է, որը հայտնի է իր ճշգրտությամբ, հարուստ SQL-ով և ընդլայնելիությամբ։ Այն մեկ համակարգում մշակում է ռելյացիոն տվյալներ, JSON փաստաթղթեր, full-text search և աշխարհագրական տվյալներ, և թույլ է տալիս, որ տվյալների բազան ինքը պարտադրի բարդ կանոններ։ Բարդ պրոդուկտների, հատկապես multi-tenant SaaS-ի համար այն հաճախ ամենաանվտանգ լռելյայն ընտրությունն է։
Ինչու եմ օգտագործում
- PostgreSQL օգտագործում եմ բարդ պրոդուկտների համար, որովհետև խիստ constraint-ները, transactional DDL-ը և հարուստ SQL-ը թույլ են տալիս, որ տվյալների բազան պաշտպանի տվյալների ամբողջականությունը, ոչ թե պարզապես պահի դրանք։
- Ինձ դուր է գալիս, որ JSONB-ն ինձ ճկուն դաշտեր է տալիս այնտեղ, որտեղ դրանք պետք են, առանց մնացածի համար ռելյացիոն մոդելավորումից հրաժարվելու։
- Ընտրել եմ այն Prisma-ի հետ մեծ multi-tenant SaaS-ի համար, որտեղ խիստ տիպավորված սխեման և վերանայված SQL migration-ները մեծ domain-ը կառավարելի են պահում։
- Tenancy-ի համար նրա հնարավորությունները, ինչպես row-level security-ն և partial ինդեքսները, տալիս են պաշտպանության երկրորդ գիծ՝ հավելվածի կոդից դուրս։
Ինչպես է աշխատում
01Ինչու Postgres. կարևոր հնարավորություններ
PostgreSQL-ը սերտորեն հետևում է SQL ստանդարտին և դրա վրա շատ բան է ավելացնում։ Constraint-ները ուժեղ են՝ CHECK, foreign key-եր, որոնք կարելի է հետաձգել մինչև commit-ի պահը, և EXCLUDE constraint-ներ, որոնք կարող են արգելել համընկնող ժամանակային միջակայքեր, օրինակ՝ կրկնակի ամրագրումներ։ DDL-ը transactional է, ուստի ձախողված migration-ը ամբողջությամբ rollback է լինում։ Common table expression-ները (WITH հարցումներ), ներառյալ ռեկուրսիվները՝ ծառերի համար, բարդ հարցումները ընթեռնելի են պահում։ Window function-ները, upsert-ները (INSERT ... ON CONFLICT), RETURNING-ը, array-ները, range-երը, enum-ները և generated սյունակները բոլորը ներկառուցված են։ Extension-ները ամբողջական հնարավորություններ են ավելացնում՝ pg_trgm՝ fuzzy search-ի համար, PostGIS՝ քարտեզների համար, pgvector՝ embedding-ների համար, pg_stat_statements՝ հարցումների վիճակագրության համար։ PostgreSQL 18-ը (սեպտեմբեր 2025) ավելացրեց ասինխրոն I/O և native uuidv7() ֆունկցիա։
-- recursive CTE: all subcategories of category 1
WITH RECURSIVE tree AS (
SELECT id, parent_id, name FROM categories WHERE id = 1
UNION ALL
SELECT c.id, c.parent_id, c.name
FROM categories c JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree;
-- forbid overlapping bookings for the same room
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE bookings ADD CONSTRAINT no_overlap
EXCLUDE USING gist (room_id WITH =, period WITH &&);
02JSONB՝ ճիշտ ձևով
JSONB-ն JSON-ը պահում է parse արված բինար ֆորմատով, որը կարելի է ինդեքսավորել և հարցումներ անել։ -> և ->> օպերատորները կարդում են դաշտերը, @>-ը ստուգում է պարունակումը, իսկ jsonb_path_query-ն աջակցում է JSONPath։ Այն իդեալական է իսկապես փոփոխական տվյալների համար՝ ապրանքի ատրիբուտներ, որոնք տարբերվում են ըստ կատեգորիայի, ինտեգրացիաների payload-ներ, կարգավորումներ կամ CMS բլոկներ։ Այն վատ ընտրություն է այն տվյալների համար, որոնցով հաճախ filter, join կամ constraint եք անում. դրանց տեղը իրական սյունակներում է՝ տիպերով և foreign key-երով։ Գործնական pattern է հիմնական դաշտերը պահել ռելյացիոն, իսկ «երկար պոչը» դնել JSONB սյունակում՝ GIN-ով ինդեքսավորված։ Եթե JSON-ի որևէ դաշտ կարևոր է դառնում, բարձրացրեք այն generated սյունակի կամ իրական սյունակի։ Հիշեք, որ JSONB արժեքի ցանկացած մասի թարմացումը վերագրում է ամբողջ արժեքը։
ALTER TABLE products ADD COLUMN attrs jsonb NOT NULL DEFAULT '{}';
CREATE INDEX products_attrs_gin ON products USING gin (attrs jsonb_path_ops);
-- all products with colour "black" and size "M"
SELECT id, name
FROM products
WHERE attrs @> '{"colour": "black", "size": "M"}';
03Ինդեքսներ. B-tree, GIN և մյուսները
B-tree-ն լռելյայն է և մշակում է հավասարություն, միջակայքեր և դասավորում. օգտագործեք այն id-ների, foreign key-երի, ամսաթվերի և սյունակների մեծ մասի համար։ GIN-ը (generalised inverted index) ինդեքսավորում է արժեքի ներսում գտնվող տարրերը, ինչը այն ճիշտ ընտրություն է դարձնում JSONB containment-ի, array-ների և full-text search-ի համար. այն ավելի մեծ է և ավելի դանդաղ է թարմացվում, քան B-tree-ն։ GiST-ը աջակցում է range-եր, երկրաչափություն և exclusion constraint-ներ։ BRIN-ը փոքրիկ է և աշխատում է հսկայական, բնականորեն դասավորված աղյուսակների համար, ինչպես log-երը։ Postgres-ն ունի նաև partial ինդեքսներ (միայն WHERE պայմանին համապատասխանող տողեր, օրինակ՝ ակտիվ տողերը), expression ինդեքսներ (lower(email)-ի վրա) և INCLUDE սյունակներ՝ covering ինդեքսների համար։ Live աղյուսակների վրա ինդեքսները ստեղծեք CREATE INDEX CONCURRENTLY-ով, իսկ plan-երը ստուգեք EXPLAIN (ANALYZE, BUFFERS)-ով։
- B-tree. հավասարություն, միջակայքեր, ORDER BY։
- GIN. JSONB, array-ներ, full-text, trigram search։
- GiST. range-եր, երկրաչափություն, exclusion constraint-ներ։
- Partial և expression ինդեքսներ. ավելի փոքր և ավելի ճշգրիտ։
04MVCC, VACUUM և միացումներ
PostgreSQL-ը MVCC-ն (multi-version concurrency control) իրականացնում է՝ երբեք տողերը տեղում չթարմացնելով։ UPDATE-ը գրում է տողի նոր տարբերակ և հինը նշում որպես dead. յուրաքանչյուր transaction տեսնում է այն տարբերակները, որոնք տեսանելի էին իր snapshot-ը վերցնելու պահին։ Կարդացողները երբեք չեն արգելափակում գրողներին։ Գինը մաքրումն է. dead տողերը մնում են, մինչև VACUUM-ը հեռացնի դրանք, և autovacuum-ը դա անում է background-ում։ Եթե autovacuum-ը հետ է մնում, աղյուսակները ուռչում են, և հարցումները դանդաղում. երկար աշխատող transaction-ները վիճակն ավելի են վատացնում, որովհետև խանգարում են մաքրմանը։ VACUUM-ը նաև կանխում է transaction ID wraparound-ը, ուստի երբեք մի անջատեք այն։ Լռելյայն isolation level-ը READ COMMITTED-ն է։ Յուրաքանչյուր միացում առանձին սերվերային պրոցես է, ուստի օգտագործեք pooler, օրինակ՝ PgBouncer transaction mode-ում, երբ միանում են հավելվածի բազմաթիվ instance-ներ կամ serverless function-ներ։
- Հետևեք dead tuple-ներին և autovacuum-ին pg_stat_user_tables-ում։
- Խուսափեք երկար idle-in-transaction session-ներից. սահմանեք idle_in_transaction_session_timeout։
- Միացումները pool արեք. հարյուրավոր ուղիղ միացումներ իզուր հիշողություն են վատնում։
05Row-level security multi-tenancy-ի համար
Row-level security-ն (RLS) թույլ է տալիս, որ տվյալների բազան տողերը filter անի յուրաքանչյուր հարցման համար։ Միացնում եք այն աղյուսակի վրա և գրում policy, օրինակ՝ «տողը տեսանելի է միայն եթե նրա company_id-ն հավասար է ընթացիկ tenant կարգավորմանը»։ Հավելվածը այդ կարգավորումը սահմանում է յուրաքանչյուր transaction-ի սկզբում, և հետո նույնիսկ այն հարցումը, որը մոռացել է իր WHERE պայմանը, չի կարող տեսնել մեկ այլ tenant-ի տվյալները։ Կարևոր մանրամասներ. աղյուսակի owner-ները և superuser-ները շրջանցում են RLS-ը, եթե չեք օգտագործում FORCE ROW LEVEL SECURITY և չեք միանում որպես առանձին non-owner role. connection pooling-ի դեպքում tenant-ը սահմանեք set_config(..., true)-ով, որպեսզի այն լոկալ լինի transaction-ի համար. և ինդեքսավորեք tenant սյունակը, որովհետև այժմ յուրաքանչյուր հարցում filter է անում ըստ դրա։ RLS-ը լավագույնս աշխատում է որպես երկրորդ շերտ՝ հավելվածում բացահայտ tenant filter-ների վրա։
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (company_id = current_setting('app.company_id')::uuid)
WITH CHECK (company_id = current_setting('app.company_id')::uuid);
-- per request, inside a transaction:
BEGIN;
SELECT set_config('app.company_id', '0193a1b2-0000-7000-8000-000000000001', true);
SELECT id, status FROM orders; -- only this tenant's rows
COMMIT;
06Prisma PostgreSQL-ի հետ
Prisma-ն TypeScript ORM է. մոդելները նկարագրում եք schema.prisma-ում, և այն գեներացնում է ամբողջությամբ տիպավորված client և SQL migration-ներ։ Development-ում prisma migrate dev-ը համեմատում է սխեման տվյալների բազայի հետ, գրում migration ֆայլ և կիրառում այն։ CI-ում և production-ում աշխատեցրեք prisma migrate deploy, որը կիրառում է միայն commit արված migration-ները և երբեք չի reset անում տվյալները։ Օգտագործեք --create-only՝ migration գեներացնելու և SQL-ը նախքան կիրառելը խմբագրելու համար. այսպես եք ավելացնում այն, ինչ Prisma-ի սխեման չի կարող լիովին արտահայտել, օրինակ՝ RLS policy-ներ, CHECK և exclusion constraint-ներ կամ CREATE INDEX CONCURRENTLY։ Client extension-ները կարող են ավելացնել cross-cutting կանոններ, օրինակ՝ յուրաքանչյուր հարցման ավտոմատ սահմանափակում tenant-ով։ Prisma 7-ը անցավ առանց Rust-ի client-ի՝ driver adapter-ներով և prisma.config.ts ֆայլով, ուստի 6-րդ տարբերակից անցնելիս ստուգեք upgrade guide-ը։
model Order {
id String @id @default(uuid(7)) @db.Uuid
companyId String @db.Uuid
status String
totalMinor BigInt
createdAt DateTime @default(now()) @db.Timestamptz
items OrderItem[]
@@index([companyId, createdAt(sort: Desc)])
}
// npx prisma migrate dev --name add_orders --create-only
// review/edit SQL, then: npx prisma migrate dev
// production: npx prisma migrate deploy
07Backup-ներ, և Postgres ընդդեմ MySQL
pg_dump-ը մեկ տվյալների բազայի logical backup է անում. custom ֆորմատը (-Fc) սեղմված է և թույլ է տալիս pg_restore-ին վերականգնել ընտրովի և զուգահեռ։ Ավելի մեծ համակարգերի և point-in-time recovery-ի համար համադրեք physical base backup-ը (pg_basebackup) անընդհատ WAL archiving-ի հետ, իդեալում՝ pgBackRest-ի կամ WAL-G-ի նման գործիքով, որը կառավարում է retention-ը, սեղմումը և off-site պահպանումը։ Պարբերաբար ստուգեք վերականգնումը։ Postgres-ի և MySQL-ի միջև ընտրություն. երկուսն էլ հիանալի են ստանդարտ վեբ հավելվածների համար։ Ես թեքվում եմ դեպի Postgres բարդ domain-ների, տվյալների խիստ ամբողջականության, JSONB-ի, առաջադեմ SQL-ի, RLS-ով multi-tenancy-ի դեպքում, կամ երբ պետք են extension-ներ, ինչպես PostGIS-ը կամ pgvector-ը։ Մնում եմ MySQL-ի կամ MariaDB-ի հետ, երբ hosting-ը, թիմը կամ framework-ի էկոհամակարգը կառուցված են դրա շուրջ, և ծանրաբեռնվածությունը պարզ read-եր ու write-եր են։
# logical backup, compressed custom format
pg_dump -Fc -U backup_user -h localhost -d shop -f shop-$(date +%F).dump
# restore into a fresh database with 4 parallel jobs
createdb -U postgres shop_restore_test
pg_restore -U postgres -d shop_restore_test -j 4 shop-2026-11-05.dump
Օգտագործեք, երբ
- Բարդ domain-ներ՝ բազմաթիվ կապերով և ամբողջականության խիստ կանոններով։
- Multi-tenant SaaS, որտեղ RLS-ը և partial ինդեքսները օգնում են մեկուսացմանը։
- Խառը տվյալներ՝ ռելյացիոն միջուկ գումարած JSONB, full-text, աշխարհագրական կամ vector search։
- TypeScript backend-ներ Prisma-ով, Drizzle-ով կամ մեկ այլ տիպավորված ORM-ով։
Մի՛ օգտագործեք, երբ
- Hosting միջավայրեր կամ թիմեր, որոնք ամբողջությամբ կառուցված են MySQL-ի շուրջ, որտեղ անցումը ռիսկ է ավելացնում՝ առանց հստակ օգուտի։
- Ծանրաբեռնվածություն՝ կարճատև միացումների հսկայական քանակով և առանց pooler-ի։
- Մաքուր analytics շատ մեծ dataset-ների վրա, որտեղ ավելի լավ է համապատասխանում սյունակային warehouse-ը։
Այլընտրանքներ
- MySQL / MariaDB
Ավելի պարզ շահագործում և ամենուրեք hosting. հիանալի է ստանդարտ վեբ ծանրաբեռնվածության համար։
- Managed Postgres (RDS, Neon, Supabase)
Backup-ները, replica-ները և pooling-ը ձեր փոխարեն են արվում. Supabase-ը նաև RLS-ը ուղղակիորեն բացում է կլիենտների համար։
- SQLite
Ներդրված, zero-ops տվյալների բազա. իդեալական է լոկալ, offline կամ edge դեպքերի համար։
Լավագույն պրակտիկաներ
- Թույլ տվեք, որ տվյալների բազան պարտադրի կանոնները constraint-ներով, ներառյալ CHECK-ը և EXCLUDE-ը։
- Ժամանակի պահերի համար օգտագործեք timestamptz, իսկ գումարների համար՝ NUMERIC կամ bigint minor միավորներ։
- JSONB-ն օգտագործեք միայն փոփոխական տվյալների համար՝ GIN ինդեքսով, երբ հարցումներ եք անում դրա վրա։
- Live աղյուսակների վրա ինդեքսները ստեղծեք CONCURRENTLY և migration-ներում սահմանեք lock timeout-ներ։
- Autovacuum-ը առողջ պահեք և խուսափեք երկար աշխատող transaction-ներից։
- Օգտագործեք connection pooler և tenant կարգավորումները պահեք transaction-ի մակարդակում լոկալ։
- Production-ում աշխատեցրեք prisma migrate deploy և վերանայեք գեներացված SQL-ը։
Տարածված սխալներ
- Հիմնական ռելյացիոն տվյալները դնել մեկ մեծ JSONB սյունակի մեջ։
- Ենթադրել, որ RLS-ը պաշտպանում է ձեզ, մինչդեռ միանում եք որպես աղյուսակի owner կամ superuser։
- prisma migrate dev կամ db push աշխատեցնել production-ի դեմ։
- Անտեսել autovacuum-ի bloat-ի և wraparound-ի զգուշացումները։
- Serverless function-ներից հարյուրավոր ուղիղ միացումներ բացել առանց pooling-ի։
Խորը վերլուծություն
08Պլանների ընթերցում՝ EXPLAIN, վիճակագրություն և գնահատականներ
Planner-ը պլանն ընտրում է տողերի գնահատված քանակից, և վատ պլանների մեծ մասը գալիս է վատ գնահատականներից։ EXPLAIN (ANALYZE, BUFFERS)-ը գործարկում է հարցումը և յուրաքանչյուր հանգույցի համար ցույց է տալիս գնահատված և իրական տողերը, ժամանակը, ինչպես նաև քանի էջ է եկել shared buffers-ից (hit) և քանիսը՝ սկավառակից (read)․ PostgreSQL 18-ից սկսած BUFFERS-ը ավտոմատ ներառվում է ANALYZE-ի հետ։ Երբ գնահատականները տասն անգամ կամ ավելի են սխալ, planner-ը կարող է ընտրել nested loop այնտեղ, որտեղ hash join էր պետք։ Գնահատականները գալիս են ANALYZE-ի հավաքած վիճակագրությունից, որը գործարկում է autovacuum-ը, բայց դրանք ենթադրում են, որ սյունակներն անկախ են։ Կոռելացված սյունակների համար, ինչպես քաղաքը և փոստային ինդեքսը կամ tenant-ը և status-ը, ստեղծեք extended statistics, որպեսզի planner-ը իմանա նրանց կապը։ Անհավասարաչափ բաշխված սյունակների համար բարձրացրեք այդ սյունակի statistics target-ը։ Prepared statement-ները լրացուցիչ նրբություն ունեն․ հինգ կատարումից հետո Postgres-ը կարող է անցնել generic պլանի, որն անտեսում է պարամետրերի արժեքները, և դա վատ է, երբ մեկ tenant ունի միլիոնավոր տողեր, իսկ մյուսները՝ տասը․ plan_cache_mode-ը թույլ է տալիս դա կառավարել։
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE company_id = $1 AND status = 'open';
-- planner thinks company and status are independent; teach it otherwise
CREATE STATISTICS st_orders_company_status (dependencies, mcv)
ON company_id, status FROM orders;
ALTER TABLE orders ALTER COLUMN company_id SET STATISTICS 1000;
ANALYZE orders;
-- top queries by total time (needs pg_stat_statements)
SELECT calls, round(total_exec_time) AS total_ms, round(mean_exec_time, 1) AS mean_ms, query
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
09Lock-եր և zero-downtime DDL
ALTER TABLE-ի ձևերի մեծ մասը վերցնում է ACCESS EXCLUSIVE lock, որը կոնֆլիկտում է ամեն ինչի հետ, այդ թվում՝ սովորական SELECT-ների։ Lock-ը կարող է պահվել ընդամենը միլիվայրկյաններ, բայց իրական վտանգը հերթն է․ եթե երկար transaction-ը աղյուսակի վրա որևէ lock է պահում, ALTER-ը սպասում է, և դրանից հետո եկող ամեն հարցում հերթ է կանգնում ALTER-ի հետևում։ Հավելվածը սառչում է, թեև տեխնիկապես ոչինչ դանդաղ չէ։ Լուծումը migration-ներում lock_timeout-ը մի քանի վայրկյան սահմանելն ու կրկնելն է, որպեսզի արգելափակված ALTER-ը հրաժարվի, այլ ոչ թե կանգնեցնի տրաֆիկը։ Իմացեք, թե որ գործողություններն են էժան։ Հաստատուն default-ով սյունակ ավելացնելը PostgreSQL 11-ից սկսած միայն metadata է փոխում։ Foreign key կամ CHECK constraint ավելացնելը կարելի է բաժանել․ ավելացրեք այն NOT VALID-ով, ինչն արագ է, հետո VALIDATE CONSTRAINT, որը սկանավորում է աղյուսակը ավելի թույլ lock-ով, որը թույլ է տալիս ընթերցումներ և գրառումներ։ SET NOT NULL-ը կարող է բաց թողնել աղյուսակի սկանավորումը, եթե արդեն կա validated CHECK (col IS NOT NULL)։ Սյունակի տիպը փոխելը սովորաբար վերագրում է ամբողջ աղյուսակը, ուստի նախընտրեք նոր սյունակ և batch-երով backfill։
SET lock_timeout = '5s';
-- foreign key without blocking writes for the whole scan
ALTER TABLE orders ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers (id) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_customer;
-- NOT NULL without a long exclusive scan
ALTER TABLE orders ADD CONSTRAINT orders_currency_nn CHECK (currency IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_currency_nn;
ALTER TABLE orders ALTER COLUMN currency SET NOT NULL; -- uses the check, no scan
ALTER TABLE orders DROP CONSTRAINT orders_currency_nn;
10Connection pooling-ի ռեժիմները և դրանց կողմնակի ազդեցությունները
PostgreSQL-ի յուրաքանչյուր կապ սերվերային պրոցես է իր սեփական հիշողությամբ, իսկ sort-երի և hash-երի հիշողությունը (work_mem) հատկացվում է ըստ գործողության, ուստի մեկ բարդ հարցումը կարող է օգտագործել դրա մի քանի բազմապատիկը։ Հետևաբար հարյուրավոր ուղիղ կապերը վատնում են հիշողություն և ավելացնում մրցակցություն․ սովորաբար օպտիմալ են մի քանի տասնյակ ակտիվ կապեր։ PgBouncer-ը transaction ռեժիմում թույլ է տալիս հազարավոր կլիենտների կիսել փոքր pool՝ սերվերային կապը տրամադրելով միայն transaction-ի տևողությամբ։ Դա կոտրում է այն ամենը, ինչը հենվում է transaction-ների միջև session-ի վիճակի վրա՝ SET առանց LOCAL-ի, session-level advisory lock-եր, LISTEN և NOTIFY, ժամանակավոր աղյուսակներ և PgBouncer-ի հին տարբերակներում՝ server-side prepared statement-ներ (1.21 տարբերակը ավելացրեց պրոտոկոլի մակարդակի prepared statement-ների աջակցություն max_prepared_statements-ի միջոցով)։ Ահա թե ինչու row-level security-ի tenant կարգավորումները պետք է լինեն transaction-local։ Սխեմայի migration-ները գործարկեք ուղիղ կապով, ոչ թե transaction pooler-ի միջոցով, քանի որ migration գործիքները, այդ թվում Prisma Migrate-ը, հենվում են session-level advisory lock-երի վրա։ Serverless հարթակները pooling-ը դարձնում են պարտադիր, քանի որ յուրաքանչյուր function instance բացում է իր սեփական կապերը։
- Pool-ի չափը որոշեք ըստ միջուկների և պահոցի արագության, ոչ թե ըստ հավելվածի instance-ների քանակի։
- Transaction pooler-ի դեպքում transaction-ների ներսում օգտագործեք set_config(name, value, true) կամ SET LOCAL։
- Migration-ների և սպասարկման համար պահեք առանձին ուղիղ connection string։
11Replication, failover և major upgrade-ներ
Streaming replication-ը write-ahead log-ը (WAL) ուղարկում է standby-ներին, որոնք այն վերարտադրում են՝ տալով բայթ առ բայթ նույնական պատճեն, որը կարելի է օգտագործել ընթերցումների և failover-ի համար։ synchronous_standby_names-ով և synchronous_commit-ով կարող եք պահանջել, որ standby-ը հաստատի յուրաքանչյուր commit՝ latency-ն փոխանակելով failover-ի ժամանակ տվյալների զրո կորստի հետ։ Postgres-ն ինքը նոր primary չի ընտրում․ Patroni-ի նման գործիքները կամ managed ծառայությունը կատարում են health check-երը, promotion-ը և fencing-ը։ Logical replication-ը WAL բայթերի փոխարեն ուղարկում է տողերի փոփոխությունները ըստ աղյուսակների, ուստի աշխատում է major տարբերակների միջև և կարող է պատճենել ընտրված աղյուսակներ, ինչը այն դարձնում է գրեթե զրո downtime-ով upgrade-ների և migration-ների ստանդարտ ուղի․ այն չի replicate անում DDL-ը և sequence-ների արժեքները, ուստի դրանք պետք է ինքներդ կարգավորեք։ Replication slot-երը ստիպում են primary-ին պահել WAL-ը, մինչև consumer-ը այն ստանա․ լքված slot-ը լցնում է սկավառակը, ուստի սահմանեք max_slot_wal_keep_size և alert դրեք անգործ slot-երի վրա։ Տեղում upgrade-ի համար pg_upgrade --link-ը նույնիսկ մեծ բազաների համար րոպեներ է տևում, և PostgreSQL 18-ից սկսած այն պահպանում է planner-ի վիճակագրությունը, ուստի upgrade-ից անմիջապես հետո արտադրողականությունը նորմալ է։
-- is any slot holding back WAL?
SELECT slot_name, slot_type, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
-- replication lag per standby (run on the primary)
SELECT application_name, state, replay_lag FROM pg_stat_replication;
Իրական իրավիճակներ
Երկու վայրկյանանոց migration-ը երկու րոպե սառեցրեց հավելվածը
Խնդիր
Migration-ը ALTER TABLE orders-ով սյունակ ավելացրեց։ Սովորաբար դա միլիվայրկյաններ է տևում, բայց այս անգամ orders-ին դիպչող ամեն հարցում կախվեց, մինչև deploy-ի timeout-ը։
Ինչպես լուծել
ALTER-ը սպասում էր ACCESS EXCLUSIVE lock-ի երկար աշխատող transaction-ի հետևում, և բոլոր հաջորդ հարցումները հերթ կանգնեցին ALTER-ի հետևում։ Հաստատեք pg_stat_activity-ով և pg_blocking_pids()-ով․ փնտրեք 'idle in transaction' վիճակում session-ներ կամ երկար հաշվետվության հարցում։ Վերականգնեք՝ արգելափակողը չեղարկելով pg_cancel_backend()-ով կամ դադարեցնելով pg_terminate_backend()-ով։ Հաջորդ անգամ կանխեք․ migration-ները գործարկեք SET lock_timeout = '5s'-ով և կրկնեք backoff-ով, սահմանեք idle_in_transaction_session_timeout, որպեսզի մոռացված session-ներն ինքնուրույն փակվեն, հաշվետվության հարցումները պահեք replica-ի վրա և alert դրեք մի քանի րոպեից հին transaction-ների վրա։ Ռիսկային փոփոխությունները բաժանեք NOT VALID և VALIDATE քայլերի, որպեսզի երկար սկանավորումները երբեք ուժեղ lock-եր չպահեն։
Primary-ի սկավառակը լցվում է առանց ակնհայտ պատճառի
Խնդիր
Primary-ի սկավառակի օգտագործումն օրական գիգաբայթներով աճում է, թեև աղյուսակների չափերը կայուն են։ pg_wal թղթապանակը հսկայական է։
Ինչպես լուծել
Ստուգեք pg_replication_slots-ը անգործ slot-ի համար․ հեռացված replica-ն կամ կանգ առած change-data-capture connector-ը թողնում են slot, որը ստիպում է primary-ին պահել այդ պահից ի վեր բոլոր WAL segment-ները։ Եթե consumer-ը վերջնականապես չկա, հեռացրեք այն pg_drop_replication_slot()-ով։ Ստուգեք նաև, որ WAL archiving-ը հաջողվում է, քանի որ ձախողված archive_command-ները նույնպես պահում են WAL-ը․ pg_stat_archiver-ը ցույց է տալիս ձախողումները։ Հետո սահմանեք max_slot_wal_keep_size, որպեսզի խրված slot-ը invalidate արվի մինչև սկավառակի լցվելը, և ավելացրեք alert-ներ անգործ slot-երի և 80 տոկոսից բարձր սկավառակի օգտագործման համար։ Եթե WAL-ը կարգին է, նայեք աղյուսակների և ինդեքսների bloat-ին, որն առաջանում է vacuum-ը արգելափակող երկար transaction-ներից՝ օգտագործելով pg_stat_user_tables-ի dead tuple-ների քանակները։
Հարցումը staging-ում արագ է, իսկ production-ում՝ դանդաղ
Խնդիր
Tenant-ի dashboard-ի հարցումը staging-ում տևում է 20 ms, բայց production-ում մեկ մեծ հաճախորդի համար՝ 4 վայրկյան։ Նույն սխեման, նույն ինդեքսները։
Ինչպես լուծել
Համեմատեք EXPLAIN (ANALYZE, BUFFERS)-ը production-ում հենց այդ tenant-ի համար staging-ի հետ։ Տիպիկ արդյունքներ․ տողերի գնահատականները շատ են շեղված, քանի որ company_id-ը և status-ը կոռելացված են, կամ տվյալներն անհավասարաչափ են բաշխված, և մեկ tenant-ին է պատկանում տողերի մեծ մասը, ինչպես նաև prepared statement, որն անցել է փոքր tenant-ների համար լավացված generic պլանի։ Գնահատականներն ուղղեք կոռելացված սյունակների վրա CREATE STATISTICS-ով, tenant սյունակի ավելի բարձր statistics target-ով և նոր ANALYZE-ով։ Եթե խնդիրը generic պլաններն են, այդ role-ի կամ հարցման ուղու համար սահմանեք plan_cache_mode = force_custom_plan։ Ավելացրեք company_id-ով սկսվող composite ինդեքս, որը համապատասխանում է ֆիլտրին և դասավորմանը։ Դանդաղ հարցումների համար պահեք auto_explain-ը միացված, որպեսզի ապագա դեպքերը գան արդեն log արված պլաններով։
Արագ ուղեցույց
- Ամենածանր հարցումներն ըստ ընդհանուր ժամանակի
SELECT calls, total_exec_time, query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;- Իրական պլան I/O մանրամասներով
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;- Ստիպել DDL-ին հրաժարվել՝ հերթ կանգնելու փոխարեն
SET lock_timeout = '5s';- Ով է արգելափակված և ում կողմից
SELECT pid, pg_blocking_pids(pid), state, query FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0;- Կանգնեցնել հարցումը կամ session-ը
SELECT pg_cancel_backend(pid); / SELECT pg_terminate_backend(pid);- Constraint ավելացնել առանց երկար lock-ի
ALTER TABLE t ADD CONSTRAINT c ... NOT VALID; ALTER TABLE t VALIDATE CONSTRAINT c;- Planner-ին սովորեցնել կոռելացված սյունակների մասին
CREATE STATISTICS s ON city, zip FROM addresses; ANALYZE addresses;- Ամենամեծ աղյուսակները՝ ինդեքսներով
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;- Փակել մոռացված բաց transaction-ները
ALTER ROLE app SET idle_in_transaction_session_timeout = '60s';- Սահմանափակել replication slot-երի պահած WAL-ը
max_slot_wal_keep_size = '50GB'
Իմ իրական նախագծերում
- Համատեղ ղեկավարել եմ ռեստորանների multi-tenant POS SaaS, որը կառուցված է NestJS-ի, Prisma 6-ի և PostgreSQL-ի վրա՝ 181 Prisma մոդելով, մոտ 4,800 տողանոց սխեմայում։
- Այդ համակարգում tenant-ների մեկուսացումը պարտադրվում է երկու անգամ՝ բացահայտ company id յուրաքանչյուր հարցման մեջ գումարած Prisma extension, որը սխալ է նետում, եթե scope-ը բացակայում է, allow-list արված բացառություններով և «սխալ tenant» test case-ով յուրաքանչյուր մոդուլի թեստում։
- SaaS-ը աշխատում է dedicated սերվերի վրա Docker Compose-ով, PostgreSQL-ով, Redis-ով և BullMQ worker-ներով, backup script-ներով և runbook-ներով՝ observability stack-ի կողքին։
- Նրա offline մասնաճյուղային hub-ը օգտագործում է լոկալ SQLite տվյալների բազա՝ checksum-ով ամրագրված versioned migration-ներով, որպեսզի մասնաճյուղերը շարունակեն վաճառել, երբ ինտերնետն անջատվում է։
Հարցեր և պատասխաններ
Q1Ինչու՞ կընտրեիք PostgreSQL-ը MySQL-ի փոխարեն։
Բարդ domain-ների համար գնահատում եմ նրա ճշգրտությունն ու արտահայտչականությունը։ Constraint-ներն ավելի հարուստ են, ներառյալ CHECK-ը, deferrable foreign key-երը և exclusion constraint-ները, օրինակ՝ համընկնող ամրագրումների համար։ DDL-ը transactional է, ուստի ձախողված migration-ները մաքուր rollback են լինում։ JSONB-ն GIN ինդեքսներով լավ է մշակում ճկուն տվյալները, իսկ CTE-ները, window function-ները և upsert-ները բարդ հարցումները մաքուր են դարձնում։ Row-level security-ն, partial և expression ինդեքսները, ինչպես նաև PostGIS-ի, pg_trgm-ի և pgvector-ի նման extension-ները հնարավորություններ են ավելացնում առանց մեկ այլ համակարգի։ MySQL-ը դեռ հիանալի է ավելի պարզ վեբ ծանրաբեռնվածության համար և ունի shared hosting-ի ավելի լայն աջակցություն, ուստի գոյություն ունեցող MySQL ենթակառուցվածքի վրա ստանդարտ Laravel կայքի համար չէի անցնի պարզապես անցնելու համար։
Q2Բացատրեք MVCC-ն PostgreSQL-ում և ինչու է VACUUM-ը կարևոր։
MVCC-ն նշանակում է, որ յուրաքանչյուր transaction տեսնում է տվյալների համահունչ snapshot՝ առանց կարդացողներին lock անելու։ Postgres-ը դրան հասնում է՝ պահելով տողի մի քանի տարբերակ. UPDATE-ը insert է անում նոր տարբերակ և հինը նշում որպես dead, իսկ տեսանելիությունը որոշվում է յուրաքանչյուր տարբերակի վրա պահված transaction id-ներով։ Արդյունքում read-երը երբեք չեն արգելափակում write-երը։ Գինն այն է, որ dead տարբերակները մնում են դիսկի վրա, մինչև VACUUM-ը վերադարձնի այդ տարածքը։ Autovacuum-ը դա անում է ավտոմատ և նաև «սառեցնում» է հին տողերը՝ transaction ID wraparound-ը կանխելու համար։ Եթե այն հետ է մնում, հաճախ երկար աշխատող կամ idle-in-transaction session-ների պատճառով, աղյուսակներն ուռչում են, և performance-ը ընկնում է, ուստի հետևում եմ դրան։
Q3Ե՞րբ եք GIN ինդեքս օգտագործում B-tree-ի փոխարեն։
Երբ արժեքը պարունակում է բազմաթիվ որոնելի մասեր։ B-tree-ն ինդեքսավորում է սյունակի ամբողջ արժեքը դասավորված կարգով, ինչը կատարյալ է հավասարության, միջակայքերի և դասավորման համար scalar-ների վրա, ինչպես id-ները, ամսաթվերը և email-ները։ GIN ինդեքսը inverted է. այն արժեքի ներսում յուրաքանչյուր տարր քարտեզագրում է այն պարունակող տողերին, ուստի հարմար է JSONB containment հարցումների համար @>-ով, array-ների overlap-ի, tsvector-ով full-text search-ի և pg_trgm-ով trigram fuzzy search-ի համար։ GIN ինդեքսներն ավելի մեծ են և ավելի դանդաղ են թարմացվում, ուստի դրանք ավելացնում եմ միայն այն սյունակներին, որոնց վրա իրականում այդպիսի հարցումներ են արվում, իսկ JSONB-ի համար հաճախ օգտագործում եմ jsonb_path_ops, որն ավելի փոքր է, երբ պետք է միայն containment։
Q4Ինչպե՞ս է աշխատում row-level security-ն, և որո՞նք են թակարդները։
RLS-ը միացնում եք աղյուսակի վրա և սահմանում policy-ներ՝ USING-ով, թե որ տողերը կարելի է կարդալ, և WITH CHECK-ով, թե որ տողերը կարելի է գրել։ Policy-ները սովորաբար համեմատում են tenant սյունակը session-ի կարգավորման հետ, որը հավելվածը սահմանում է յուրաքանչյուր transaction-ի համար։ Թակարդներ. superuser-ները և BYPASSRLS ունեցող role-երը անտեսում են policy-ները, աղյուսակի owner-ները նույնպես, եթե FORCE ROW LEVEL SECURITY-ն սահմանված չէ, ուստի հավելվածը պետք է միանա որպես non-owner role։ PgBouncer-ի նման pooler-ների դեպքում կարգավորումները պետք է transaction-ի մակարդակում լոկալ լինեն, այլապես մեկ tenant-ի կարգավորումը կարող է «արտահոսել» հաջորդ հարցմանը։ Policy-ները filter են ավելացնում յուրաքանչյուր հարցման, ուստի tenant սյունակին ինդեքսներ են պետք։ RLS-ին վերաբերվում եմ որպես defence in depth, ոչ թե հավելվածի մակարդակի ստուգումների փոխարինող։
Q5Ինչպե՞ս եք անվտանգ կերպով կառավարում Prisma migration-ները production-ում։
Migration-ները ստեղծվում են development-ում prisma migrate dev-ով, վերանայվում են որպես SQL pull request-ում և commit արվում։ Production-ում և staging-ում միշտ աշխատում է միայն prisma migrate deploy, սովորաբար որպես release քայլ՝ նախքան նոր կոդի գործարկումը, և այն կիրառում է սպասող migration-ները՝ առանց որևէ բան reset անելու։ Ռիսկային փոփոխությունների համար օգտագործում եմ --create-only և խմբագրում SQL-ը. սյունակներն ավելացնում եմ որպես nullable, backfill եմ անում batch-երով, ինդեքսները ստեղծում եմ concurrently, ինչը պետք է աշխատի transaction-ից դուրս, և ավելացնում եմ constraint-ներ, որոնք Prisma-ն չի կարող արտահայտել, ինչպես RLS policy-ները կամ partial unique ինդեքսները։ Երբեք չեմ օգտագործում db push կամ migrate reset լոկալ development-ից դուրս, իսկ destructive փոփոխությունները թողարկվում են ավելի ուշ release-ում, քան այն կոդը, որը դադարում է օգտագործել սյունակը։
Q6Ինչպե՞ս եք backup անում PostgreSQL-ը։
Կախված է չափից և վերականգնման նպատակներից։ Փոքր և միջին տվյալների բազաների համար custom ֆորմատով գիշերային pg_dump-ը պարզ է և թույլ է տալիս pg_restore-ով վերականգնել առանձին աղյուսակներ։ Production համակարգերի համար, որոնց պետք է point-in-time recovery, օգտագործում եմ physical base backup գումարած անընդհատ WAL archiving՝ կառավարվող pgBackRest-ի կամ WAL-G-ի նման գործիքով, որը մշակում է սեղմումը, գաղտնագրումը, retention-ը և off-site պահպանումը։ Managed ծառայությունները դա անում են ձեր փոխարեն։ Անկախ մեթոդից, backup-ները պետք է պահվեն database սերվերից դուրս, և ես պլանավորում եմ պարբերական test վերականգնումներ առանձին instance-ի մեջ ու չափում, թե որքան են դրանք տևում, որովհետև հենց դա է այն վերականգնման ժամանակը, որը բիզնեսն իրականում կստանա։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս կթարմացնեիք ծանրաբեռնված PostgreSQL բազան նոր major տարբերակի՝ նվազագույն downtime-ով։
Կա երկու հիմնական ուղի։ pg_upgrade-ը --link-ով պարզ և արագ է, սովորաբար րոպեներ, քանի որ վերաօգտագործում է տվյալների ֆայլերը, բայց գործարկման ընթացքում բազան անհասանելի է, իսկ նոր cluster-ը միացնելուց հետո rollback-ը նշանակում է վերականգնում backup-ից, ուստի նախ թարմ backup եմ անում և փորձարկում պատճենի վրա։ PostgreSQL 18-ից սկսած planner-ի վիճակագրությունը պահպանվում է upgrade-ից հետո, ինչը վերացնում է նախկինում հաջորդող դանդաղ շրջանը։ Գրեթե զրո downtime-ի համար օգտագործում եմ logical replication․ ստեղծում եմ նոր cluster նոր տարբերակով, պատճենում սխեման, subscribe անում բոլոր աղյուսակներին, թողնում, որ հասնի, հետո հավելվածը փոխարկում եմ գրառումների կարճ դադարի ընթացքում։ Փոխզիջումներն ավելի շատ աշխատանքն ու ինքնուրույն լուծելիք բացերն են․ DDL-ը չի replicate արվում, sequence-ները պետք է ձեռքով առաջ տանել մինչև cut-over-ը, և աղյուսակներին պետք են primary key-եր կամ replica identity։ Managed ծառայությունները հաճախ երկրորդ մոտեցումը փաթեթավորում են որպես blue-green deployment։
S2Postgres-ը կարող է լինել queue, search engine և vector store։ Ե՞րբ չպետք է լինի։
Postgres-ը հարակից գործերի համար օգտագործելը հաճախ ճիշտ որոշում է, քանի որ վերացնում է ևս մեկ համակարգ սպասարկելու անհրաժեշտությունը և transaction-ները պահում մեկ տեղում։ SELECT ... FOR UPDATE SKIP LOCKED-ով job queue-ն լավ է աշխատում մինչև վայրկյանում հազարավոր job-եր, և job-ը հերթ է դրվում բիզնես փոփոխության հետ ատոմար։ tsvector-ով և pg_trgm-ով full-text search-ը ծածկում է կայքի որոնումը ֆիլտրերով․ նվիրված engine-ի եմ անցնում, երբ պետք են տառասխալների հանդուրժողականություն, relevance-ի կարգավորում, բազմաթիվ facet-ներ կամ բազմաթիվ լեզուներ մեծ մասշտաբով։ pgvector-ը HNSW ինդեքսներով մշակում է միլիոնավոր embedding-ներ, հատկապես երբ vector-ները պետք է ֆիլտրվեն ըստ tenant-ի կամ թույլտվությունների։ Նվիրված գործիքները հաղթում են, երբ բեռի մասշտաբը կամ հնարավորությունները գերիշխում են․ շատ բարձր throughput-ով event stream-եր, միլիարդավոր տողերի անալիտիկա (columnar պահոցներ) կամ որոնում՝ որպես հիմնական պրոդուկտ։ Չափանիշն այն է, թե արդյոք լրացուցիչ բեռը սկսում է վնասել primary-ի հիմնական գործին՝ transaction-ների հուսալիությանը։
S3Ինչպե՞ս եք նախագծում PostgreSQL-ը գրառումների բարձր արագության համար, օրինակ՝ event-ների կամ telemetry-ի ընդունման։
Նախ կրճատում եմ յուրաքանչյուր տողի աշխատանքը։ Insert-ները խմբավորում եմ բազմատող INSERT-ով կամ COPY-ով՝ ամեն event-ի համար մեկ transaction-ի փոխարեն։ «Տաք» աղյուսակի վրա ինդեքսները պահում եմ նվազագույն, քանի որ յուրաքանչյուր ինդեքս բազմապատկում է գրելու արժեքը․ timestamp-ի վրա BRIN ինդեքսը փոքրիկ է և հարմար append-only տվյալների համար։ Partitioning եմ անում ըստ ժամանակի, որպեսզի ինդեքսները մնան փոքր, և պահպանման ժամկետը լինի էժան partition drop՝ DELETE-ի և vacuum-ի փոխարեն։ Հաճախակի թարմացվող աղյուսակների համար ավելի ցածր fillfactor-ը տեղ է թողնում HOT update-ների համար, որոնք չեն դիպչում ինդեքսներին։ Կարգավորում եմ checkpoint-ները ավելի մեծ max_wal_size-ով, որպեսզի սերվերը անընդհատ flush չանի։ Այն տվյալների համար, որոնք կարող են հանդուրժել crash-ի ժամանակ վերջին պահերի կորուստը, session-ի մակարդակով synchronous_commit = off-ը մեծ արագացում է տալիս՝ առանց corruption-ի ռիսկի․ unlogged աղյուսակներն ավելի հեռու են գնում, բայց crash-ից հետո դատարկվում են։ Եթե հում ծավալը շարունակում է աճել, ագրեգացնում եմ rollup աղյուսակներում կամ անալիտիկան տեղափոխում columnar պահոց։
Բառարան
- WAL
- Write-ahead log՝ ամեն փոփոխություն նախ գրվում է այստեղ․ օգտագործվում է crash recovery-ի, replication-ի և point-in-time restore-ի համար։
- ACCESS EXCLUSIVE lock
- Աղյուսակի ամենաուժեղ lock-ը․ արգելափակում է բոլոր ընթերցումներն ու գրառումները, և այն վերցնում են ALTER TABLE-ի ձևերի մեծ մասը։
- Lock queue
- Lock-ի սպասող հարցումները հերթականությամբ․ սպասող ALTER-ը ստիպում է սպասել այդ աղյուսակի բոլոր հաջորդ հարցումներին։
- NOT VALID constraint
- Constraint, որը կիրառվում է միայն նոր տողերի նկատմամբ և ավելի ուշ validate է արվում տրաֆիկը թույլատրող ավելի թույլ lock-ով։
- Extended statistics
- Planner-ի վիճակագրություն մի քանի սյունակների վրա, ստեղծվում է CREATE STATISTICS-ով՝ կոռելացված տվյալների գնահատականներն ուղղելու համար։
- Generic plan
- Prepared statement-ի cache արված պլան, որն անտեսում է պարամետրերի իրական արժեքները․ կարող է վատ լինել անհավասարաչափ տվյալների դեպքում։
- Replication slot
- Նշիչ, որը ստիպում է primary-ին պահել WAL-ը, մինչև կոնկրետ consumer-ը այն ստանա։
- Logical replication
- Տողերի փոփոխությունների replication ըստ աղյուսակների՝ WAL բայթերի փոխարեն․ աշխատում է major տարբերակների միջև։
- Patroni
- Open-source գործիք PostgreSQL-ի high availability-ն կառավարելու համար՝ leader election, failover և replica-ների կարգավորում։
- pg_upgrade
- Major տարբերակի տեղում upgrade-ի գործիքը․ --link-ով այն վերաօգտագործում է տվյալների ֆայլերը և արագ ավարտվում։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։