Puri์ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ํ์ฉํ์ฌ ๋ณต์กํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ์ํํ ์ ์์ต๋๋ค.
๊ณ ๊ธ ๊ธฐ๋ฅ ๊ฐ์
Transaction ํธ๋์ญ์
์ผ๋ก ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ ACID ์์ฑ
์๋ธ์ฟผ๋ฆฌ ์ค์ฒฉ ์ฟผ๋ฆฌ๋ก ๋ณต์กํ ์กฐ๊ฑด ํํ FROM, WHERE, SELECT
Upsert INSERT OR UPDATE ํ๋ฒ์ ON CONFLICT
Vector Search AI ์๋ฒ ๋ฉ ์ ์ฌ๋ ๊ฒ์ pgvector
Transaction - ํธ๋์ญ์
ํธ๋์ญ์
์ ์ฌ๋ฌ ์ฟผ๋ฆฌ๋ฅผ ํ๋์ ๋จ์๋ก ๋ฌถ์ด ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ๋ณด์ฅํฉ๋๋ค.
๊ธฐ๋ณธ ํธ๋์ญ์
await db . transaction ( async ( trx ) => {
// 1. ์ฌ์ฉ์ ์์ฑ
const [ userId ] = await trx
. table ( "users" )
. insert ({
username: "john" ,
email: "john@test.com" ,
password: "hashed" ,
role: "normal" ,
})
. returning ( "id" );
// 2. ํ๋กํ ์์ฑ
await trx . table ( "profiles" ). insert ({
user_id: userId . id ,
bio: "Hello world" ,
});
// 3. ์ด๊ธฐ ์ค์ ์์ฑ
await trx . table ( "user_settings" ). insert ({
user_id: userId . id ,
theme: "dark" ,
language: "ko" ,
});
// ๋ชจ๋ ์์
์ด ์ฑ๊ณตํ๋ฉด ์๋ ์ปค๋ฐ
// ํ๋๋ผ๋ ์คํจํ๋ฉด ์๋ ๋กค๋ฐฑ
});
ํธ๋์ญ์
์ ACID ์์ฑ : - Atomicity : ๋ชจ๋ ์ฑ๊ณต ๋๋ ๋ชจ๋ ์คํจ - Consistency : ๋ฐ์ดํฐ
์ผ๊ด์ฑ ์ ์ง - Isolation : ๋์ ์คํ ๊ฒฉ๋ฆฌ - Durability : ์๊ตฌ ์ ์ฅ
๋ช
์์ ๋กค๋ฐฑ
await db . transaction ( async ( trx ) => {
// ๋ฐ์ดํฐ ์ฝ์
await trx . table ( "users" ). insert ({
username: "test" ,
email: "test@test.com" ,
password: "pass" ,
role: "normal" ,
});
// ์กฐ๊ฑด์ ๋ฐ๋ผ ๋กค๋ฐฑ
const existingUser = await trx . table ( "users" ). where ( "email" , "test@test.com" ). first ();
if ( existingUser ) {
// ์๋ ๋กค๋ฐฑ
await trx . rollback ();
return ;
}
// ๋กค๋ฐฑํ์ง ์์ผ๋ฉด ์๋ ์ปค๋ฐ
});
ํธ๋์ญ์
์๋ฌ ์ฒ๋ฆฌ
try {
await db . transaction ( async ( trx ) => {
await trx . table ( "users" ). insert ({
username: "john" ,
email: "john@test.com" ,
password: "pass" ,
role: "normal" ,
});
// ์๋ฌ ๋ฐ์ ์ ์๋ ๋กค๋ฐฑ
throw new Error ( "Something went wrong" );
});
} catch ( error ) {
console . error ( "Transaction failed:" , error );
// ๋ชจ๋ ๋ณ๊ฒฝ์ฌํญ์ด ๋กค๋ฐฑ๋จ
}
์๋ธ์ฟผ๋ฆฌ
์๋ธ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
FROM ์ ์๋ธ์ฟผ๋ฆฌ
// ์๋ธ์ฟผ๋ฆฌ ์ ์
const activeUsers = db . table ( "users" ). where ( "is_active" , true ). select ({
id: "id" ,
username: "username" ,
email: "email" ,
});
// ์๋ธ์ฟผ๋ฆฌ๋ฅผ ํ
์ด๋ธ์ฒ๋ผ ์ฌ์ฉ
const results = await db
. table ({ active: activeUsers })
. join ( "posts" , "active.id" , "posts.user_id" )
. select ({
userId: "active.id" ,
username: "active.username" ,
postCount: Puri . count ( "posts.id" ),
})
. groupBy ( "active.id" , "active.username" );
WHERE ์ ์๋ธ์ฟผ๋ฆฌ
// ํ๊ท ๊ธ์ฌ๋ณด๋ค ๋์ ์ง์
const results = await db . table ( "employees" ). select ({
id: "id" ,
name: "username" ,
salary: "salary" ,
}). whereRaw ( `
salary > (SELECT AVG(salary) FROM employees)
` );
SELECT ์ ์๋ธ์ฟผ๋ฆฌ
const results = await db . table ( "users" ). select ({
id: "id" ,
username: "username" ,
postCount: Puri . rawNumber ( `
(SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
` ),
commentCount: Puri . rawNumber ( `
(SELECT COUNT(*) FROM comments WHERE comments.user_id = users.id)
` ),
});
WHERE ๊ทธ๋ฃน - ๋ณต์กํ ์กฐ๊ฑด
whereGroup - AND/OR ์กฐํฉ
const results = await db
. table ( "users" )
. select ({ id: "id" , name: "username" })
. whereGroup (( g ) => g . where ( "role" , "admin" ). orWhere ( "role" , "moderator" ))
. where ( "is_active" , true );
// SQL: WHERE (role = 'admin' OR role = 'moderator') AND is_active = true
์ค์ฒฉ ๊ทธ๋ฃน
const results = await db
. table ( "employees" )
. select ({ id: "id" , name: "username" })
. whereGroup (( g ) =>
g
. whereGroup (( g2 ) => g2 . where ( "department_id" , 1 ). where ( "salary" , ">" , 50000 ))
. orWhereGroup (( g2 ) => g2 . where ( "department_id" , 2 ). where ( "salary" , ">" , 60000 )),
);
// SQL: WHERE (
// (department_id = 1 AND salary > 50000)
// OR
// (department_id = 2 AND salary > 60000)
// )
์ค์ ์์ - ๊ฒ์ ํํฐ
async searchUsers ( params : {
search? : string ;
role ?: string [];
isActive ?: boolean ;
minAge ?: number ;
}) {
let query = this . getPuri ( "r" )
. table ( "users" )
. select ({
id: "id" ,
username: "username" ,
email: "email" ,
});
// ๊ฒ์์ด (์ฌ๋ฌ ํ๋)
if ( params . search ) {
query = query . whereGroup (( g ) =>
g
. where ( "username" , "like" , `% ${ params . search } %` )
. orWhere ( "email" , "like" , `% ${ params . search } %` )
. orWhere ( "bio" , "like" , `% ${ params . search } %` )
);
}
// ์ญํ ํํฐ
if ( params . role && params . role . length > 0 ) {
query = query . whereIn ( "role" , params . role );
}
// ํ์ฑ ์ํ
if ( params . isActive !== undefined ) {
query = query . where ( "is_active" , params . isActive );
}
// ๋์ด ํํฐ
if ( params . minAge !== undefined ) {
query = query . whereRaw (
"EXTRACT(YEAR FROM AGE(birth_date)) >= ?" ,
[ params . minAge ]
);
}
return await query . orderBy ( "created_at" , "desc" );
}
UPSERT - ON CONFLICT
INSERT์ UPDATE๋ฅผ ํ ๋ฒ์ ์ํํฉ๋๋ค.
๊ธฐ๋ณธ UPSERT
await db
. table ( "users" )
. insert ({
email: "john@test.com" ,
username: "john" ,
password: "hashed" ,
role: "normal" ,
})
. onConflict ( "email" , {
update: [ "username" , "password" ],
});
// email์ด ์ด๋ฏธ ์กด์ฌํ๋ฉด username๊ณผ password๋ง ์
๋ฐ์ดํธ
// ์กด์ฌํ์ง ์์ผ๋ฉด ์๋ก ์ฝ์
์ ํ์ ์
๋ฐ์ดํธ
await db
. table ( "user_stats" )
. insert ({
user_id: 1 ,
login_count: 1 ,
last_login: new Date (),
})
. onConflict ( "user_id" , {
update: {
login_count: Puri . rawNumber ( "user_stats.login_count + 1" ),
last_login: new Date (),
},
});
// login_count๋ ๊ธฐ์กด ๊ฐ + 1
// last_login์ ์ ๊ฐ์ผ๋ก ์
๋ฐ์ดํธ
DO NOTHING
await db
. table ( "users" )
. insert ({
email: "john@test.com" ,
username: "john" ,
password: "pass" ,
role: "normal" ,
})
. onConflict ( "email" , "nothing" );
// email์ด ์ด๋ฏธ ์กด์ฌํ๋ฉด ์๋ฌด๊ฒ๋ ํ์ง ์์
๋ณต์ ์ปฌ๋ผ UNIQUE
await db
. table ( "user_preferences" )
. insert ({
user_id: 1 ,
key: "theme" ,
value: "dark" ,
})
. onConflict ([ "user_id" , "key" ], {
update: [ "value" ],
});
// (user_id, key) ์กฐํฉ์ด ์ด๋ฏธ ์กด์ฌํ๋ฉด value๋ง ์
๋ฐ์ดํธ
UpsertBuilder - ๋ณต์กํ ๊ด๊ณ ์ ์ฅ
์ฌ๋ฌ ํ
์ด๋ธ์ ๊ด๊ณ๋ฅผ ํ ๋ฒ์ ์ ์ฅํ ๋ ์ฌ์ฉํฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
await db . transaction ( async ( trx ) => {
// 1. Company ๋ฑ๋ก
const companyRef = trx . ubRegister ( "companies" , {
name: "Tech Corp" ,
});
// 2. Department ๋ฑ๋ก (Company ์ฐธ์กฐ)
const deptRef = trx . ubRegister ( "departments" , {
name: "Engineering" ,
company_id: companyRef , // UBRef ์ฌ์ฉ
});
// 3. User ๋ฑ๋ก
const userRef = trx . ubRegister ( "users" , {
email: "dev@tech.com" ,
username: "developer" ,
password: "hashed" ,
role: "normal" ,
});
// 4. Employee ๋ฑ๋ก (User, Department ์ฐธ์กฐ)
trx . ubRegister ( "employees" , {
user_id: userRef ,
department_id: deptRef ,
employee_number: "E001" ,
salary: "70000" ,
});
// 5. ์์๋๋ก ์ ์ฅ
await trx . ubUpsert ( "companies" );
await trx . ubUpsert ( "departments" );
await trx . ubUpsert ( "users" );
await trx . ubUpsert ( "employees" );
});
UBRef์ ์ฅ์ : - ์ธ๋ ํค๋ฅผ ์ ๊ฒฝ ์ฐ์ง ์๊ณ ๊ด๊ณ ์ ์ - ์๋์ผ๋ก ์์กด ์์ ํด๊ฒฐ - ํธ๋์ญ์
๋ด์์
ID ์๋ ํ ๋น
Many-to-Many ๊ด๊ณ
await db . transaction ( async ( trx ) => {
// Project ์์ฑ
const projectRef = trx . ubRegister ( "projects" , {
name: "New Feature" ,
status: "in_progress" ,
});
// ์ฌ๋ฌ Employee ์์ฑ
const emp1Ref = trx . ubRegister ( "employees" , {
employee_number: "E100" ,
salary: "60000" ,
});
const emp2Ref = trx . ubRegister ( "employees" , {
employee_number: "E101" ,
salary: "65000" ,
});
// M:N ๊ด๊ณ ์ค์
trx . ubRegister ( "projects__employees" , {
project_id: projectRef ,
employee_id: emp1Ref ,
});
trx . ubRegister ( "projects__employees" , {
project_id: projectRef ,
employee_id: emp2Ref ,
});
// ์ ์ฅ
await trx . ubUpsert ( "projects" );
await trx . ubUpsert ( "employees" );
await trx . ubUpsert ( "projects__employees" );
});
๋๋ ๋ฑ๋ก
await db . transaction ( async ( trx ) => {
const companyRef = trx . ubRegister ( "companies" , {
name: "Startup Inc" ,
});
// 10๋ช
์ ์ง์ ๋ฑ๋ก
for ( let i = 0 ; i < 10 ; i ++ ) {
const userRef = trx . ubRegister ( "users" , {
email: `user ${ i } @startup.com` ,
username: `user ${ i } ` ,
password: "pass" ,
role: "normal" ,
});
trx . ubRegister ( "employees" , {
user_id: userRef ,
employee_number: `E ${ 1000 + i } ` ,
salary: String ( 50000 + i * 5000 ),
});
}
// ์ผ๊ด ์ ์ฅ
await trx . ubUpsert ( "companies" );
await trx . ubUpsert ( "users" );
await trx . ubUpsert ( "employees" );
});
Vector Search - ๋ฒกํฐ ์ ์ฌ๋ ๊ฒ์
AI ์๋ฒ ๋ฉ์ ์ฌ์ฉํ ์๋ฏธ๋ก ์ ๊ฒ์์
๋๋ค.
Cosine Similarity (๊ธฐ๋ณธ)
const queryEmbedding = [ 0.1 , 0.2 , 0.3 , ... ]; // 1536์ฐจ์ ๋ฒกํฐ
const results = await db
. table ( "documents" )
. vectorSimilarity ( "embedding" , queryEmbedding , {
method: "cosine" ,
threshold: 0.7 , // 70% ์ด์ ์ ์ฌ๋
})
. select ({
id: "id" ,
title: "title" ,
similarity: "similarity" , // ์๋ ์ถ๊ฐ๋จ
})
. limit ( 10 );
// ๊ฒฐ๊ณผ๋ similarity ๋ด๋ฆผ์ฐจ์ ์ ๋ ฌ
results [ 0 ]. similarity ; // 0.95 (๋์์๋ก ์ ์ฌ)
L2 Distance
const results = await db
. table ( "documents" )
. vectorSimilarity ( "embedding" , queryEmbedding , {
method: "l2" ,
threshold: 1.0 , // ๊ฑฐ๋ฆฌ 1.0 ์ดํ
})
. select ({
id: "id" ,
title: "title" ,
similarity: "similarity" , // ๊ฑฐ๋ฆฌ๊ฐ (๋ฎ์์๋ก ์ ์ฌ)
})
. limit ( 10 );
Inner Product
const results = await db
. table ( "documents" )
. vectorSimilarity ( "embedding" , queryEmbedding , {
method: "inner_product" ,
threshold: 0.5 ,
})
. select ({
id: "id" ,
content: "content" ,
similarity: "similarity" ,
})
. limit ( 20 );
Vector Search ๋ฉ์๋ ์ ํ : - cosine : ์ผ๋ฐ์ ์ธ ์๋ฒ ๋ฉ ๊ฒ์ (OpenAI, Cohere ๋ฑ) - l2 :
๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ ๊ฒ์ - inner_product : ์ ๊ทํ๋ ๋ฒกํฐ์ ์ ํฉ
Full-text Search
PGroonga (ํ๊ธ ์ง์ ์ฐ์)
// ๋จ์ผ ์ปฌ๋ผ ๊ฒ์
const results = await db
. table ( "posts" )
. whereSearch ( "title" , "๊ฒ์์ด" )
. select ({
id: "id" ,
title: "title" ,
score: Puri . score (), // ๊ฒ์ ์ ์
})
. orderBy ( "score" , "desc" );
// ๋ณตํฉ ์ปฌ๋ผ ๊ฒ์ (๊ฐ์ค์น)
const results = await db
. table ( "posts" )
. whereSearch ([ "title" , "content" ], "๊ฒ์์ด" , {
weights: [ 10 , 1 ], // title์ด 10๋ฐฐ ์ค์
})
. select ({
id: "id" ,
title: "title" ,
highlightedTitle: Puri . highlight ( "title" , "๊ฒ์์ด" ),
score: Puri . score (),
})
. orderBy ( "score" , "desc" );
PostgreSQL tsvector
// ๊ธฐ๋ณธ ๊ฒ์
const results = await db
. table ( "documents" )
. whereTsSearch ( "title" , "search query" , "simple" )
. select ({
id: "id" ,
title: "title" ,
highlighted: Puri . tsHighlight ( "title" , "search query" ),
rank: Puri . tsRank ( Puri . toTsVector ( "documents.title" , "simple" ), "search query" ),
})
. orderBy ( "rank" , "desc" );
// ๊ณ ๊ธ ๊ฒ์ ์ต์
const results = await db
. table ( "documents" )
. whereTsSearch ( "content" , "important keywords" , {
config: "english" ,
parser: "plainto_tsquery" ,
})
. select ({
id: "id" ,
content: Puri . tsHighlight ( "content" , "important keywords" , {
startSel: "<mark>" ,
stopSel: "</mark>" ,
maxFragments: 3 ,
}),
rank: Puri . tsRankCd ( Puri . toTsVector ( "documents.content" , "english" ), "important keywords" ),
})
. orderBy ( "rank" , "desc" );
๋ฐฐ์น ์์
Batch Insert
const users = [
{ username: "user1" , email: "user1@test.com" , password: "pass" , role: "normal" },
{ username: "user2" , email: "user2@test.com" , password: "pass" , role: "normal" },
{ username: "user3" , email: "user3@test.com" , password: "pass" , role: "normal" },
];
await db . table ( "users" ). insert ( users );
Batch Update
await db . transaction ( async ( trx ) => {
for ( const userId of userIds ) {
await trx . table ( "users" ). where ( "id" , userId ). update ({ is_active: true });
}
});
Batch Delete
// IN ์ฌ์ฉ
await db . table ( "users" ). whereIn ( "id" , [ 1 , 2 , 3 , 4 , 5 ]). delete ();
// ์กฐ๊ฑด
await db . table ( "users" ). where ( "last_login" , "<" , "2023-01-01" ). delete ();
์ฑ๋ฅ ์ต์ ํ
์ธ๋ฑ์ค ํ์ฉ
-- ์์ฃผ ์กฐํํ๋ ์ปฌ๋ผ์ ์ธ๋ฑ์ค
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- ๋ณตํฉ ์ธ๋ฑ์ค
CREATE INDEX idx_users_role_active ON users( role , is_active);
-- ๋ฒกํฐ ์ธ๋ฑ์ค (HNSW)
CREATE INDEX idx_documents_embedding ON documents
USING hnsw (embedding vector_cosine_ops);
-- Full-text ์ธ๋ฑ์ค
CREATE INDEX idx_posts_content_pgroonga ON posts
USING pgroonga (content);
SELECT ์ต์ ํ
// โ ๋์จ: ๋ถํ์ํ ์ปฌ๋ผ ์กฐํ
await db . table ( "users" ). selectAll ();
// โ
์ข์: ํ์ํ ์ปฌ๋ผ๋ง
await db . table ( "users" ). select ({
id: "id" ,
name: "username" ,
});
JOIN ์ต์ ํ
// โ
์์ ํ
์ด๋ธ โ ํฐ ํ
์ด๋ธ ์์
await db
. table ( "departments" ) // 10๊ฐ
. join ( "employees" , ... ) // 100๊ฐ
. join ( "user_logs" , ... ); // 10000๊ฐ
ํ์ด์ง๋ค์ด์
์ต์ ํ
// โ
Cursor ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์
(๋๋ ๋ฐ์ดํฐ)
async function getCursorPage ( lastId : number , limit : number ) {
return await db
. table ( "posts" )
. select ({ id: "id" , title: "title" })
. where ( "id" , ">" , lastId )
. orderBy ( "id" , "asc" )
. limit ( limit );
}
// ์ฌ์ฉ
const page1 = await getCursorPage ( 0 , 20 );
const page2 = await getCursorPage ( page1 [ page1 . length - 1 ]. id , 20 );
๋ค์ ๋จ๊ณ
Model Model์์ Puri ํ์ฉํ๊ธฐ
API Development API์์ Puri ์ฌ์ฉํ๊ธฐ
Testing Puri ์ฟผ๋ฆฌ ํ
์คํธํ๊ธฐ
Basic Queries ๊ธฐ๋ณธ ์ฟผ๋ฆฌ๋ก ๋์๊ฐ๊ธฐ