조건이 포함된 조회 쿼리
특정 이름이 들어간 유저를 찾고싶다고 하자.
# datasource.py
class DataSource:
...
def select_by_name_exact(self, name):
ix = insert_route(name)
query = f"SELECT * FROM user WHERE name = '{name}'"
cursor = self.cursors[ix]
cursor.execute(query)
result = cursor.fetchall()
return result
삽입과 유사하게 사용하면 된다.
범위 조건이 포함된 조회 쿼리
이제 살짝 복잡해지기 시작한다. aa ~ bb를 찾는다고하면 1번 인스턴스에서 끝나지만 aa ~ zz를 찾는다면 1~4번 인스턴스를 모두 찾게 된다. 범위 조회를 위한 라우터를 만들자.
# router.py
...
def range_route(name_start: str, name_end: str):
start = insert_route(name_start)
end = insert_route(name_end)
return start, end
이제 이것을 기준으로 범위 조회를 하면 된다.
# datasource.py
class DataSource:
...
def select_by_name_range(self, name_start, name_end):
start, end = range_route(name_start, name_end)
result = []
query = f"SELECT * FROM user WHERE name >= '{name_start}' AND name <= '{name_end}'"
for ix in range(start, end+1):
cursor = self.cursors[ix]
cursor.execute(query)
datas = cursor.fetchall()
for data in datas:
result.append(data)
return result
이런식으로 조회하면 된다!
하지만 여기에서 성능의 개선점이 보인다. aa~zz범위를 조회할때 인스턴스를 4개 쓰게되는데 2번째와 3번째 인스턴스는 조회범위에 포함되기 때문에 where문을 걸 필요가 없다. 따라서 쿼리문을 아래와 같이 변경할 수 있다.
# datasource.py
class DataSource:
...
def select_by_name_range(self, name_start, name_end):
start, end = range_route(name_start, name_end)
result = []
for ix in range(start, end+1):
if start < ix < end:
query = "SELECT * FROM user"
else:
query = f"SELECT * FROM user WHERE name >= '{name_start}' AND name <= '{name_end}'"
cursor = self.cursors[ix]
cursor.execute(query)
datas = cursor.fetchall()
for data in datas:
result.append(data)
return result
이렇게 하면 where문을 줄임으로써 성능향상을 기대할 수 있고 대용량 데이터를 처리할 때 샤딩의 장점이기도 하다.
또 다시 개선할 수 있을것 같다. start는 name_end 조건이 필요없고 end일때는 name_start 조건이 필요없다.
# datasource.py
class DataSource:
...
def select_by_name_range(self, name_start, name_end):
start, end = range_route(name_start, name_end)
result = []
for ix in range(start, end+1):
if start < ix < end:
query = "SELECT * FROM user"
elif start == end:
query = f"SELECT * FROM user WHERE name >= '{name_start}' AND name <= '{name_end}'"
elif ix == start:
query = f"SELECT * FROM user WHERE name >= '{name_start}'"
elif ix == end:
query = f"SELECT * FROM user WHERE name <= '{name_end}'"
print(f"{ix}번째 인스턴스가 사용하는 쿼리 -> {query}")
cursor = self.cursors[ix]
cursor.execute(query)
datas = cursor.fetchall()
for data in datas:
result.append(data)
return result
이렇게 수정하고 테스트해보자.
if __name__ == "__main__":
src = DataSource()
src.select_by_name_range("aa", "zz")
# 0번째 인스턴스가 사용하는 쿼리 -> SELECT * FROM user WHERE name >= 'aa'
# 1번째 인스턴스가 사용하는 쿼리 -> SELECT * FROM user
# 2번째 인스턴스가 사용하는 쿼리 -> SELECT * FROM user
# 3번째 인스턴스가 사용하는 쿼리 -> SELECT * FROM user WHERE name <= 'zz'
너무 편안하다. 이렇게 우리는 범위조회 쿼리까지 해결할 수 있다.
샤딩한 컬럼과 상관없는 컬럼의 조회 쿼리
우리는 name기준으로 샤딩했기때문에 이런 방식이 가능했지만 만약 age기준으로 조회하려면 어떻게 해야 할까?
... 모든 인스턴스에 접근해야 한다.
# datasource.py
class DataSource:
...
def select_by_age_exact(self, age):
result = []
query = f"SELECT * FROM user WHERE age = {age}"
for cursor in self.cursors:
cursor.execute(query)
datas = cursor.fetchall()
for data in datas:
result.append(data)
return result
이런식으로 사용하여 쿼리성능이 나빠질 수 있다. 하지만 대용량 데이터를 위해서라면 뭐...
그렇다면 JOIN은?
join도 age조건과 같이 모든 인스턴스에 대해 조인하고 결과를 합치면 된다. 만약 join 대상 테이블도 샤딩되어있다면 좀 끔찍해진다... ㅋㅋ
마무리
이렇게 샤딩을 공부하고 직접 사용해봤다.
mongodb는 내장으로 샤딩을 지원한다고 한다. (좋네...)
그리고 range 샤딩을 사용했지만 modular샤딩이나 dynamic 샤딩같은 다양한 기법이 존재한다고 한다.
언젠가 막대한 데이터를 다루게 될 수 있으니 샤딩은 알고 있으면 좋은 것 같다.
'프로그래밍 > 개발' 카테고리의 다른 글
[개발] 스타크래프트 인공지능을 개발해보자! (2) | 2023.06.04 |
---|---|
[개발] 메시지 큐 (0) | 2023.05.19 |
[개발] 샤딩이란 것을 해보자! (2) (0) | 2023.04.21 |
[개발] 샤딩이란 것을 해보자! (1) (0) | 2023.04.21 |
[개발] nginx에서 https 적용하기 (0) | 2023.02.23 |