데이터 범위 읽기 - LIMIT와 OFFSET

데이터 범위 읽기 - LIMIT와 OFFSET (Read a Range of Data - LIMIT and OFFSET)

이제 여러분은 .one(), .first(), session.get()으로 행 하나를 가져오는 법을 알아요.

그리고 .where()로 필터링하면서 여러 행을 가져오는 법도 알죠.

이제 결과의 범위(range) 만 가져오는 법을 볼게요.

처음 3개 행이 선택된 테이블

출처: 공식문서

데이터 만들기

이전과 같은 코드로 계속하되, 예제를 단순하게 유지하고 여기서 하고 싶은 것에 집중하기 위해 select_heroes() 함수는 조금 바꿀게요.

다시, 조회할 데이터가 있도록 히어로 여러 명을 만들 거예요:

def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
    hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32)
    hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35)
    hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36)
    hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)
        session.add(hero_4)
        session.add(hero_5)
        session.add(hero_6)
        session.add(hero_7)

        session.commit()

모두 조회하는 코드 복습하기

select() 예제들에서 모든 히어로를 조회할 때 썼던 코드는 이거였어요:

def select_heroes():
    with Session(engine) as session:
        statement = select(Hero)
        results = session.exec(statement)
        heroes = results.all()
        print(heroes)

하지만 이건 데이터베이스에 수천 명이 있을 수 있는데 전부 한 번에 가져오는 거라 문제가 될 수 있어요.

LIMIT로 조회하기

지금 데이터베이스에는 히어로가 7명 있어요. 하지만 수천 명일 수도 있으니, 처음 3명만 가져오도록 결과를 제한해 볼게요:

def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).limit(3)
        results = session.exec(statement)
        heroes = results.all()
        print(heroes)

select()가 주는 특별한 select 객체에는 결과를 특정 개수로 제한하는 데 쓸 수 있는 .limit() 메서드도 있어요.

이 경우 7개 행을 전부 가져오는 대신, 처음 3개만 가져오도록 제한했어요.

처음 3개 행이 선택된 테이블

커맨드 라인에서 프로그램 실행하기

커맨드 라인에서 실행하면 이렇게 출력돼요:

$ uv run python app.py

// 이전 출력은 생략했어요 🙈

// LIMIT로 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
 LIMIT ? OFFSET ?
INFO Engine [no key 0.00014s] (3, 0)

// 받은 히어로들을 프린트, 딱 3명
[
    Hero(age=None, secret_name='Dive Wilson', id=1, name='Deadpond'),
    Hero(age=None, secret_name='Pedro Parqueador', id=2, name='Spider-Boy'),
    Hero(age=48, secret_name='Tommy Sharp', id=3, name='Rusty-Man')
]

좋아요! 원하는 대로 히어로 3명만 가져왔네요.

!!! tip "팁"

그 SQL 코드는 잠시 후에 더 자세히 볼게요.

OFFSET과 LIMIT로 조회하기

이제 결과를 제한해서 처음 3명만 가져올 수 있어요.

하지만 사용자 인터페이스가 히어로를 3명씩 묶어서 보여준다고 상상해 볼게요.

!!! tip "팁"

이걸 보통 "**페이지네이션(pagination)**"이라고 불러요. 사용자 인터페이스가 한 번에 미리 정해진 수의 히어로 "페이지"를 보여주기 때문이에요.

그리고 사용자가 인터페이스와 상호작용해서 다음 페이지로 넘어가는 식이죠.

다음 3명은 어떻게 가져올까요?

4번째부터 6번째까지 다음 행들이 선택된 테이블

.offset()을 쓰면 돼요:

def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).offset(3).limit(3)
        results = session.exec(statement)
        heroes = results.all()
        print(heroes)

동작 방식은 이래요. select()가 주는 특별한 select 객체에는 .where(), .offset(), .limit() 같은 메서드들이 있어요.

각 메서드는 내부의 특별한 select 문장 객체에 변경을 적용하면서 같은 객체를 반환해요. 그래서 위 예제처럼 .offset().limit()을 둘 다 쓰는 것처럼, 그 객체에 메서드를 계속 붙일 수 있어요.

Offset은 "이만큼 행을 건너뛰어라"라는 뜻이에요. 이미 봤던 처음 3개를 건너뛰고 싶으니 .offset(3)을 쓰는 거죠.

OFFSET을 써서 커맨드 라인에서 실행하기

이제 커맨드 라인에서 프로그램을 실행하면 이렇게 출력돼요:

$ python app.py

// 이전 출력은 생략했어요 🙈

// LIMIT와 OFFSET으로 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
 LIMIT ? OFFSET ?
INFO Engine [no key 0.00020s] (3, 3)

// 받은 히어로 3명을 프린트, 두 번째 묶음
[
    Hero(age=32, secret_name='Natalia Roman-on', id=4, name='Tarantula'),
    Hero(age=35, secret_name='Trevor Challa', id=5, name='Black Lion'),
    Hero(age=36, secret_name='Steve Weird', id=6, name='Dr. Weird')
]

다음 묶음 조회하기

그다음 3개 행의 묶음을 가져오려면, 이미 봤던 처음 6개를 건너뛰면 돼요:

def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).offset(6).limit(3)
        results = session.exec(statement)
        heroes = results.all()
        print(heroes)

데이터베이스에는 지금 7개 행만 있어서, 이 쿼리는 행 1개만 가져올 수 있어요.

마지막 행(7번째)이 선택된 테이블

하지만 걱정 마세요. 행이 하나뿐인데 3개를 가져오려 해도 데이터베이스가 오류를 내진 않아요(파이썬 리스트였다면 그랬겠지만요).

데이터베이스는 우리가 결과 수를 제한하려 한다는 걸 아는데, 반드시 그만큼의 결과를 찾아야 하는 건 아니에요.

마지막 묶음으로 커맨드 라인에서 실행하기

커맨드 라인에서 실행하면 이렇게 출력돼요:

$ uv run python app.py

// 이전 출력은 생략했어요 🙈

// LIMIT와 OFFSET으로 마지막 묶음 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
 LIMIT ? OFFSET ?
INFO Engine [no key 0.00038s] (3, 6)

// 받은 히어로 묶음을 프린트, 단 한 명
[
    Hero(age=93, secret_name='Esteban Rogelios', id=7, name='Captain North America')
]

SQL의 LIMIT와 OFFSET

아마 새 SQL 키워드 LIMITOFFSET을 눈치채셨을 거예요.

SQL에서 다른 부분 뒤에 이렇게 쓸 수 있어요:

SELECT id, name, secret_name, age
FROM hero
LIMIT 3 OFFSET 6

DB Browser for SQLite에서 시도해 보면 같은 결과를 얻을 수 있어요.

SQL 쿼리의 결과를 보여주는 DB Browser for SQLite

LIMIT와 OFFSET을 WHERE와 조합하기

물론 .limit().offset().where()나 나중에 배울 다른 메서드들과 조합할 수도 있어요:

def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.age > 32).offset(1).limit(2)
        results = session.exec(statement)
        heroes = results.all()
        print(heroes)

LIMIT, OFFSET, WHERE를 다 써서 커맨드 라인에서 실행하기

커맨드 라인에서 실행하면, 데이터베이스에서 나이가 32보다 큰 히어로를 전부 찾아요. 보통 4명이겠죠.

하지만 우리는 offset 1 이후부터 포함하기 시작해서(첫 번째는 세지 않아요) 그 뒤 첫 2개만 가져오도록 제한했어요:

$ uv run python app.py

// 이전 출력은 생략했어요 🙈

// WHERE와 LIMIT, OFFSET으로 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.age > ?
 LIMIT ? OFFSET ?
INFO Engine [no key 0.00022s] (32, 2, 1)

// 받은 히어로를 프린트, 딱 2명
[
    Hero(age=36, id=6, name='Dr. Weird', secret_name='Steve Weird'),
    Hero(age=48, id=3, name='Rusty-Man', secret_name='Tommy Sharp')
]

요약

.where()나 다른 메서드로 데이터를 어떻게 필터링하든, .limit()으로 쿼리를 제한해 기껏해야 N개의 결과만 가져오게 할 수 있어요.

마찬가지로 .offset()으로 앞쪽 결과들을 건너뛸 수도 있어요.

더 알아보기 (Learn more)