2013. 2. 19. 11:06

sqlite의 stmt를 재활용(sqlite3_reset)하여 성능 향상 시키기 (sqlite3 query, insert 예제)

sqlite3를 사용하여 Insert나 Query할 때 아래와 같은 방법으로 사용할 수 있습니다.
(리턴값 체크는 필수이지만, 간략하게 표시하기 위해 제외하였습니다)

(1) sqlite3_exec을 통해 SQL문을 그대로 만들어 전달하고, query는 callback 함수를 통해 전달 받음

int fnCallback(void* arg, int columns, wchar_t** value, wchar_t** name)
{
    ...
}
    ...
    for (...)
    {
        ...
        // DB QUERY
        sqlite3_exec(db, "SELECT ...", fnCallback, pContext, NULL);
        ...
        // DB INSERT
        sqlite3_exec(db, "INSERT ...", NULL, NULL, NULL);
        ...
(2) statement를 prepare하여 callback 함수 없이 각 값들의 bind를 통해 전달하고 전달 받음
    ...
    sqlite3_stmt* pStmt = NULL;
    ...
    for (...)
    {
        // DB INSERT
        sqlite3_finialize(pStmt);
        sqlite3_prepare(db, "INSERT ... (?,?)", -1, &pStmt, NULL);
        sqlite3_bind_int(pStmt, 1, 123);
        sqlite3_bind_int(pStmt, 2, 456);
        sqlite3_step(pStmt);
        sqlite3_finalize(pStmt);
        pStmt = NULL;
        ...
        // DB QUERY
        sqlite3_prepare(db, "SELECT a,b,c ... WHERE a=? AND b=?", -1, &pStmt, NULL);
        sqlite3_bind_int(pStmt, 1, 111);
        sqlite3_bind_int(pStmt, 2, 333);
        for (;;)
        {
            if (SQLITE_ROW == sqlite3_step(pStmt))
            {
                nA = sqlite3_column_int(pStmt, 0);
                nB = sqlite3_column_int(pStmt, 1);
                nC = sqlite3_column_int(pStmt, 2);
            }
            else
            {
                break;
            }
        }
        sqlite3_finalize(pStmt);
        pStmt = NULL;
        ...
(3) statement를 단 한번만 prepare하고 필요시 reset하여 재사용
    ...
    sqlite3_stmt* pStmtInsert = NULL;
    sqlite3_stmt* pStmtQuery  = NULL;
    ...
    sqlite3_prepare(db, "INSERT ... (?,?)", -1, &pStmtInsert, NULL);
    sqlite3_prepare(db, "SELECT a,b,c ... WHERE a=? AND b=?", -1, &pStmtQuery, NULL);
    ...
    for (...)
    {
        // DB INSERT
        sqlite3_reset(pStmtInsert);
        sqlite3_bind_int(pStmtInsert, 1, 123);
        sqlite3_bind_int(pStmtInsert, 2, 456);
        sqlite3_step(pStmtInsert);
        ...
        // DB QUERY
        sqlite3_reset(pStmtQuery);
        sqlite3_bind_int(pStmtQuery, 1, 111);
        sqlite3_bind_int(pStmtQuery, 2, 333);
        for (;;)
        {
            if (SQLITE_ROW == sqlite3_step(pStmtQuery))
            {
                nA = sqlite3_column_int(pStmtQuery, 0);
                nB = sqlite3_column_int(pStmtQuery, 1);
                nC = sqlite3_column_int(pStmtQuery, 2);

            }
            else
            {
                break;
            }
        }
        ...
    }
    ...
    sqlite3_finalize(pStmtInsert);
    pStmtInsert = NULL;
    sqlite3_finalize(pStmtQuery);
    pStmtQuery = NULL;
    ...
겉으로 확인해 보면 방법 (1)이 가장 짧고 명확해 보입니다. 그리고, (3)이 (2)보다 좀더 복잡하고 까다로와 보입니다. 하지만 성능으로 따지자면 (1) < (2) < (3) 입니다. 물론 최근의 computing power가 좋아져서, 큰 차이는 없을 수 있습니다. 하지만, query의 양이 많거나 길이가 길 경우, 혹은 record 수가 많다던지, 여하튼 db의 크기가 커질 수 록 그 성능의 차이는 눈으로 드러날 것입니다.

(2)과 (3)의 차이는 sqlite3_prepare(...)의 호출 빈도라 할 수 있습니다. (3)에서는 그 자리에 대신 sqlite3_reset(...)이 들어가 있습니다. 해당 코드를 보면 sqlite3_reset이 훨씬 부하가 적어 보입니다(단순히 memory free 위주로 되어 있음). 물론 sqlite3_prepare는 btree 호출도 있고 구문 parsing도 있습니다. 위 예에서는 누락되어 있지만, sqlite3_reset의 리턴값 체크는 해주는 것이 중요합니다.