* 프로그래머스, 타입스크립트로 함께하는 웹 풀 사이클 개발(React, Node.js) 5기 강의 수강 내용을 정리하는 포스팅.
* 원활한 내용 이해를 위해 수업에서 제시된 자료 이외에, 개인적으로 조사한 자료 등을 덧붙이고 있음.
SELECT COUNT(column_name) FROM table_name WHERE condition;
-- 특정 카테고리에 있는 책의 개수 확인
SELECT COUNT(*) AS book_count
FROM books
WHERE category_id = 1;
-- 조회 결과
book_count
10
데이터의 개수를 세어야 할 때. 특정 조건에 맞는 데이터의 갯수를 알아야 할 때.
예를 들어, 사용자가 얼마나 등록되었는지, 특정 조건을 만족하는 데이터가 몇 개인지 확인하거나 할 때.
SELECT COUNT(*) FROM users;
SELECT COUNT(*)
FROM orders
WHERE status = 'completed';
SELECT category_id, COUNT(*) AS book_count
FROM books
GROUP BY category_id;
별칭(Alias)을 설정하는 데 사용.
열 이름이나 테이블 이름을 가독성 있게 만들거나, 계산된 값을 임시 이름으로 표현할 때 유용.
SELECT column_name AS alias_name FROM table_name;
-- COUNT() 결과에 별칭을 붙임
SELECT COUNT(*) AS total_users FROM users;
-- 복잡한 계산에 별칭 사용
SELECT price * 1.1 AS final_price FROM books;
-- 조회 결과
total_users
100
final_price
110.00
SELECT COUNT(*) AS total_users
FROM users;
SELECT price, price * 1.1 AS final_price
FROM books;
SELECT avg_price_table.avg_price AS category_avg_price
FROM (
SELECT category_id, AVG(price) AS avg_price
FROM books
GROUP BY category_id
) avg_price_table;
쿼리 안에 포함된 또 다른 쿼리.
내부 쿼리의 결과를 사용해 외부 쿼리를 실행하는 구조.
서브쿼리는 () 안에 작성, SELECT, FROM, WHERE, HAVING 등의 절에서 사용할 수 있다.
결과가 단일 값, 다중 행, 또는 테이블일 수 있다.
SELECT (SELECT COUNT(*) FROM books) AS total_books;
SELECT *
FROM (SELECT id, name FROM users WHERE active = 1) AS active_users;
SELECT *
FROM books
WHERE category_id = (SELECT id FROM categories WHERE name = 'Fiction');
-- 특정 카테고리에 포함된 책의 평균 가격이 전체 평균보다 높은 경우 조회
SELECT name, price
FROM books
WHERE price > (
SELECT AVG(price)
FROM books
);
-- 조회 결과
name price
Book A 150
Book B 200
SELECT name, price
FROM books
WHERE price > (
SELECT AVG(price)
FROM books
);
SELECT *
FROM orders
WHERE created_at = (
SELECT MAX(created_at)
FROM orders
);
SELECT *
FROM books
WHERE category_id = (
SELECT id
FROM categories
WHERE name = 'Fiction'
);
SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM orders
);
특정 조건을 만족하는 데이터가 존재하는지 확인.
EXISTS는 서브쿼리의 결과가 존재하면 True를, 존재하지 않으면 False를 반환.
결과의 내용보다 존재 여부에 초점.
서브쿼리는 () 안에 작성되며, 조건절(WHERE, HAVING 등)에서 사용.
SELECT column_name
FROM table_name
WHERE EXISTS (
SELECT 1
FROM another_table
WHERE condition
);
SELECT u.id, u.name
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SELECT u.id, u.name
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SELECT c.id, c.name
FROM categories c
WHERE EXISTS (
SELECT 1
FROM books b
WHERE b.category_id = c.id
);
SELECT *
FROM users
WHERE EXISTS (
SELECT 1
FROM subscriptions s
WHERE s.user_id = users.id AND s.status = 'active'
);
SELECT u.id, u.name
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SELECT u.id, u.name
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SELECT c.name
FROM categories c
WHERE EXISTS (
SELECT 1
FROM books b
WHERE b.category_id = c.id
);
복수의 작업이 순차적으로 진행되었을 때, 만약 중간에 에러가 발생했을 경우.
이전에 진행되었던 작업들을 모두 취소하는 방법.
가령 쇼핑몰에서 결제 기능을 구현했다고 가정해보자.
결제가 이루어짐 -> 결제 정보가 DB에 저장됨의 순서로 진행된다고 했을 때..
만약 결제 정보를 DB에 저장하려는 시점에서 에러가 발생된다면?
결제는 되었는데, 그 정보가 DB에 저장되지 않기 때문에 상품이 발송되지 않는다!
이런 황당한 사태를 방지하기 위해 사용하는 것이 바로 트랜잭션TRANSACTION.
여러 테이블 간의 연관된 작업이 존재하거나, 여러 단계의 작업이 서로 의존성을 갖거나, 데이터의 일관성을 유지해야 하는 경우에 사용해야 한다.
// 좋아요 추가 기능
exports.addLike = async (userId, bookId) => {
// 데이터베이스 연결을 가져옵니다. 트랜잭션을 사용하기 위해 connection 객체를 활용합니다.
const connection = await db.getConnection();
try {
// 트랜잭션 시작
await connection.beginTransaction();
// 1. book_id가 유효한지 확인합니다.
// - EXISTS 서브쿼리를 사용하여 books 테이블에 해당 book_id가 존재하는지 확인합니다.
// - EXISTS는 조건을 만족하는 첫 번째 레코드를 찾으면 즉시 종료하므로 성능이 좋습니다.
const checkQuery = `
SELECT EXISTS (
SELECT 1 FROM books WHERE id = ?
) AS book_exists
`;
const [[{ book_exists }]] = await connection.execute(checkQuery, [bookId]);
if (!book_exists) {
throw new Error('존재하지 않는 도서 ID입니다.'); // 도서 ID가 유효하지 않으면 에러를 던집니다.
}
// 2. 좋아요 추가
// - likes 테이블에 user_id와 book_id를 삽입합니다.
// - FOREIGN KEY 제약 조건으로 인해 user_id와 book_id가 각각 users, books 테이블에 존재해야 합니다.
const likeQuery = `INSERT INTO likes (user_id, book_id) VALUES (?, ?)`;
await connection.execute(likeQuery, [userId, bookId]);
// 3. books 테이블의 좋아요 수 업데이트
// - COUNT()를 사용하여 likes 테이블에서 해당 book_id의 좋아요 개수를 계산합니다.
// - 계산된 값을 books 테이블의 likes 필드에 업데이트합니다.
const updateQuery = `
UPDATE books
SET likes = (
SELECT COUNT(*) FROM likes WHERE book_id = ?
)
WHERE id = ?
`;
await connection.execute(updateQuery, [bookId, bookId]);
// 트랜잭션 커밋: 모든 작업이 성공적으로 수행되었으므로 변경 사항을 확정합니다.
await connection.commit();
} catch (error) {
// 트랜잭션 롤백: 오류 발생 시 모든 변경 사항을 취소합니다.
await connection.rollback();
throw error; // 에러를 호출한 곳으로 다시 전달합니다.
} finally {
// 데이터베이스 연결 해제: 사용한 connection 객체를 반환합니다.
connection.release();
}
};
학습 프로젝트로 구현한 좋아요 추가 기능을 기반으로 설명해보자면..
좋아요 추가 기능은 like 테이블과 book 테이블 양쪽에 데이터를 저장하고 수정하게 된다.
만약에 트랜잭션TRANSACTION을 사용하지 않았는데, 작업 도중에 에러가 발생한다면?
좋아요는 추가됐지만 카운트는 증가하지 않는 상황, like 테이블과 book 테이블의 데이터가 서로 일치하지 않는 상황 등이 발생되면서 전체 시스템 운영에 문제가 발생할 수도 있다.
따라서 작업 수행 전에, connection 객체를 불러온 뒤 이를 기반으로 beginTransaction을 호출하여 트랜잭션 작업을 열어준 뒤..
트랜잭션 내부에서 필요한 작업들을 모두 수행하게 한 뒤, commit()을 호출하여 작업 내용을 반영하게 하거나..
에러가 발생한다면 rollback()을 호출해서 작업 내용을 모두 무효화하면 된다.
어떤 결과가 도출되더라도, release()를 호출하여 사용한 connection 객체는 해제해주어야 한다.
트랜잭션에는 이외에도 1가지 더 유용한 기능이 존재한다.
하나의 트랜잭션이 실행되는 동안 다른 작업이 트랜잭션으로 개입하지 못하게 하는 것.
가령 여러 사용자가 동시에 동일한 도서에 좋아요를 누르는 경우, 트랜잭션이 완료되기 전에는 likes 카운트가 잘못 계산되지 않도록 보호가 이루어진다.
쉽게 말하면 2명 이상의 사용자가 동시에 좋아요 추가를 눌러도, 트랜잭션 덕분에 에러가 발생하지 않고 기능이 잘 동작한다는 뜻.
그 이유는 잠금(Locking) 메커니즘이나 격리 수준(Isolation Level)이라는 개념이 포함되는데.. 이 포스팅에서는 여기까지는 다뤄보진 않겠다.