우버 (Uber)

드라이버 및 차량 관리, 실시간 운행 생애 주기 추적, GPS 경로 로깅, 동적 탄력 요금제, 요금 추정, 결제 처리, 상호 평점 시스템, 드라이버 수익 관리, 프로모션 및 고객 지원 티켓 관리를 특징으로 하는 우버 스타일의 승차 공유 플랫폼용 프로덕션급 데이터베이스 스키마입니다.

PostgreSQL14개 테이블웹 앱
ride-sharinggig-economygeolocationpaymentsreal-time
ERD Studio로 제작

이 스키마에 대하여

한 계정이 승객이면서 기사일 수 있어서 users 가 둘을 다 담고, driver_profiles 는 운전할 때만 성립하는 것 — 면허, 상태, 평점, 그리고 현재 위치 — 만 담습니다. 마지막 두 컬럼이 특이합니다. current_latitude·current_longitude 는 계속 덮어써지며, 그래서 추적 테이블이 아니라 기사 행에 있습니다.

운행은 시간이 지나며 채워지는 행입니다. requested_at·accepted_at·started_at·completed_at·cancelled_at 이 상태 이력이 아니라 각각의 시각이라, 한 행에서 생애주기가 읽히고 "기사가 수락까지 얼마나 걸렸나" 가 뺄셈이 됩니다.

돈은 세 번 적히고 셋은 서로 다른 숫자입니다. rides 에는 산정되고 청구된 요금, payments 에는 승객이 실제로 낸 금액, driver_earnings 에는 기사의 총액·수수료·순액이 있습니다. 합치면 차액이 어디로 갔는지 답할 수 없게 됩니다.

눈여겨볼 관계

users → driver_profiles
별도 계정이 아니라 역할입니다. 운전도 하고 타기도 하는 사람이 한 사용자이고, 승객으로서의 평점과 기사로서의 평점이 로그인 둘 없이도 구분됩니다.
rides → ride_tracking
표본마다 한 행인 시계열 경로점입니다. 운행 중에는 끊임없이 쓰이고 끝난 뒤에는 거의 읽히지 않아서 운행 행과 떼어 놓습니다.
rides → payments → driver_earnings
한 번의 운행에 세 행 — 청구한 것, 걷힌 것, 기사가 갖는 것. commission 이 그 차이이고, 바뀌는 요율에서 다시 구하지 않고 저장합니다.
ratings → users (두 번)
rater_id 와 ratee_id. 운행 하나가 서로 반대 방향의 두 행을 만듭니다 — 상호 평가가 늘 갖는 모양입니다.
promotions → promo_usages
사용 이력이 카운터가 아니라 운행에 묶인 행입니다. "이 사용자가 이 코드를 이미 썼나" 에 답할 수 있는 이유가 이것입니다.

설계 판단

기사의 현재 위치는 행이 아니라 컬럼

배차는 지금 가능한 모든 기사의 최신 위치를 지금 알아야 합니다. 몇 초마다 ride_tracking 에 행을 쌓으면 배차 질의마다 기사별 최신 행을 찾아야 합니다. 제자리에서 덮어쓰는 컬럼 둘이면 평범한 인덱스 스캔이 됩니다. 이력을 잃는 것도 아닙니다 — ride_tracking 이 운행 중에만 남기는데, 경로가 필요한 때는 그때뿐입니다.

운행 생애주기는 상태 로그가 아니라 시각들

status 는 지금 어디인지를, 다섯 개의 시각은 어떻게 왔는지를 말합니다. 상태 이력 테이블도 같은 질문에 답하지만 조인과 정렬이 필요합니다. 이렇게 두면 수락 시간·운행 시간·취소율 같은 지표가 전부 컬럼 둘의 뺄셈이 됩니다. 한계는 같은 상태에 두 번 들어갈 수 없다는 것인데, 운행에서는 그게 오히려 맞습니다.

요금 구성은 운행에 저장한다

base_fare·total_fare·surge_multiplier 를 ride_types 에서 다시 구하지 않고 운행에 복사합니다. 요율은 바뀌고 할증은 순간적입니다. 자기 가격을 재현하지 못하는 운행은 고객에게 설명할 수 없는 운행입니다. ride_types 는 현재의 요금표이지 과거의 요금표가 아닙니다.

프로모션 사용은 카운터가 아니라 행

promotions.uses_count 는 max_uses 를 보는 싼 검사이고, promo_usages 는 누가 어느 운행에서 무엇을 썼는지를 적습니다. 이것이 없으면 "1인 1회" 를 강제할 수 없고 분쟁이 생긴 할인을 추적할 수 없습니다. 카운터는 빠른 길, 행이 진실입니다.

이 템플릿의 테이블

users사용자
11 cols
driver_profiles드라이버 프로필
12 cols
vehicles차량
12 cols
vehicle_documents차량 증빙 서류
8 cols
ride_types서비스 등급 (차종)
8 cols
rides운행 내역
22 cols
ride_tracking실시간 위치 정보
6 cols
ratings평점
7 cols
payments결제 정보
10 cols
driver_earnings드라이버 수익금
9 cols
promotions프로모션 (쿠폰)
10 cols
promo_usages프로모션 사용 내역
6 cols
saved_places저장된 장소
8 cols
support_tickets고객 지원 티켓
9 cols

자주 묻는 것

기사 위치를 왜 ride_tracking 에 두지 않나요?
배차가 여러 기사의 현재 위치를 한 번에 묻는데, 그중 대부분은 운행 중이 아니기 때문입니다. 시계열 테이블에서 기사별 최신 행을 읽는 것은 그 질의에 맞는 모양이 아닙니다. driver_profiles 의 컬럼은 덮어써지고, ride_tracking 은 실제 운행의 경로를 남깁니다.
요율이 ride_types 에 있는데 total_fare 를 왜 저장하나요?
요율은 바뀌고 할증은 순간이기 때문입니다. 오늘의 요금표로 가격을 다시 구해야 하는 운행은 지난 영수증을 설명하지 못하고, 이의가 제기된 청구에 답이 없습니다. 요금표는 현재이고 운행은 기록입니다.
승객과 기사를 따로 둬야 하나요?
한 사람이 둘 다 될 수 있다면 아닙니다 — 대부분의 시장에서 그렇습니다. 나누면 전화번호와 인증이 중복되고, 평가 시스템이 이름 하나 찾는 데도 두 곳을 봐야 합니다. driver_profiles 는 운전에만 해당하는 필드만 듭니다.
승객과 기사 매칭은 어떻게 하나요?
이 스키마가 하지 않습니다. 현재 위치 컬럼과 상태 필드가 입력이고, 배차는 그것을 지리 인덱스로 읽습니다 — 로그가 아니라 인덱스 걸린 컬럼인 이유가 그것입니다. 매칭 자체는 질의가 아니라 서비스입니다.
취소 수수료는 어디로 가나요?
모델링돼 있지 않습니다. cancelled_at 은 운행이 일찍 끝났다는 것만 말합니다. 수수료는 청구이므로 그것을 구분하는 타입과 함께 payments 에 들어가고 같은 운행을 가리켜야 합니다. rides 에 수수료 컬럼을 두는 방식은 수수료 종류가 둘이 되는 순간까지만 통합니다.