6P by GN⁺ | ★ favorite | 댓글 3개
  • SQLite는 로컬 저장소와 임베디드 프로젝트에 널리 쓰이는 자체 완결형 RDBMS지만, 하위 호환성 때문에 안전성과 성능에 불리한 기본값을 유지하고 있음
  • 기본적으로 외래 키 제약을 강제하지 않고 ROWID를 재사용할 수 있어, 삭제된 사용자의 게시물이 같은 ID를 받은 다른 사용자에게 연결되는 데이터 무결성 문제가 생길 수 있음
  • 일반 테이블은 선언된 열과 다른 자료형도 저장하며, STRICT 테이블은 이를 막지만 테이블마다 strict를 지정해야 하고 사용자 정의 타입 이름을 활용하던 방식과 충돌함
  • 동시 쓰기에서 즉시 발생하는 SQLITE_BUSY, 비활성화된 WAL, 보수적인 동기화 설정은 각각 busy_timeout, journal_mode, synchronous 프라그마로 개선할 수 있음
  • PRAGMA edition = 2026 같은 연도 기반 에디션으로 안전한 기본값 묶음을 선택하게 하면, 기존 동작을 깨지 않고 SQLite의 기본 설정을 계속 발전시킬 수 있음

SQLite의 강점과 낡은 기본값

  • SQLite는 임베디드 프로젝트의 데이터베이스이자 로컬 데이터 저장의 업계 표준으로 부를 만하며, 일부 서버 소프트웨어인 lobste.rs도 SQLite에서 실행되고 있음
  • 별도 프로세스로 실행되는 전통적인 RDBMS와 달리 라이브러리 형태의 RDBMS여서 소프트웨어를 자체 완결형으로 유지할 수 있음
  • 일반 파일 형식처럼 사용자 정의 직렬화기와 파서를 작성할 필요가 없어, RDBMS와 파일 기반 저장 방식의 장점을 함께 제공함
  • 그러나 데이터 무결성, 동시성, 성능에 영향을 주는 여러 기본값이 부적절하다는 문제가 남아 있음

외래 키를 강제하지 않아 생기는 무결성 문제

  • 외래 키는 데이터베이스의 일관성을 유지하고 끊어진 참조를 방지하는 핵심 수단이지만, SQLite는 기본적으로 이를 강제하지 않음
  • posts.user_idusers.id를 참조하도록 선언해도 기본 설정에서는 다음 동작을 막지 못함
    • 유효한 사용자 ID가 없는 게시물 생성
    • 해당 사용자의 게시물을 남겨둔 채 사용자 삭제
  • INTEGER PRIMARY KEY는 테이블의 ROWID 별칭이 되며, SQLite의 ROWID 할당 알고리듬은 일부 상황에서 ID를 재사용함
    • Bob이 ID 1로 게시물을 작성한 뒤 계정만 삭제해도 외래 키 오류가 발생하지 않음
    • 이후 Alice가 재사용된 ID 1을 받으면 Bob의 기존 게시물이 Alice에게 연결됨
    • 조회 자체는 정상적으로 성공하므로 단순한 끊어진 참조보다 문제를 발견하기 어려움
  • 연결마다 다음 프라그마를 활성화하면 외래 키를 강제할 수 있음
PRAGMA foreign_keys = ON;
  • 이를 처음부터 적용했다면 게시물을 남긴 사용자 삭제는 FOREIGN KEY constraint failed 오류로 중단됨

선언과 다른 자료형을 허용하는 일반 테이블

  • SQLite 값은 NULL, INTEGER, REAL, TEXT, BLOB 가운데 하나지만, 일반 테이블의 열 선언은 저장 가능한 자료형을 엄격히 제한하지 않고 타입 친화도(type affinity) 를 결정함
  • INTEGER 친화도 열에는 다음 저장 규칙이 적용됨
    • 정수로 해석할 수 있는 TEXTINTEGER로 변환함
    • 실수로 해석할 수 있는 TEXT는 배정밀도 부동소수점인 REAL로 변환함
    • 그 밖의 값은 원래 자료형 그대로 저장함
  • 다른 친화도에도 별도 변환 규칙이 있음
    • BLOB 친화도는 값을 그대로 저장함
    • TEXT 친화도는 BLOB, TEXT, NULL을 그대로 두고 숫자를 TEXT로 변환함
    • REAL 친화도는 INTEGER 친화도처럼 동작하지만 정수를 REAL로 변환함
  • 이 때문에 duration_sec INTEGER 열에도 숫자가 아닌 'Way too long, I mean come on' 같은 문자열을 저장할 수 있음
  • 실제 프로젝트에서는 불리언용 숫자 1, 0 대신 문자열 '1', '0'이 기록되어 데이터를 정리하고 디버깅해야 했던 사례도 있었음

STRICT 테이블과 사용자 정의 타입의 충돌

  • STRICT 테이블은 열에 잘못된 자료형을 넣으면 cannot store TEXT value in INTEGER column 같은 타입 오류를 발생시킴
CREATE TABLE music (
  id INTEGER PRIMARY KEY,
  name TEXT,
  duration_sec INTEGER
) strict;
  • 모든 테이블을 전역적으로 엄격하게 만드는 프라그마는 없으므로, 각 CREATE TABLE 문에 strict를 빠짐없이 추가해야 함

  • SQLite는 유연한 타입이 유용하다는 입장이지만, STRICT 테이블에서도 ANY 자료형을 명시하면 모든 종류의 값을 저장하는 열을 만들 수 있음

    • 임의 자료형을 허용할 열과 특정 자료형을 강제할 열을 명시적으로 구분할 수 있음
  • 더 현실적인 호환성 문제는 STRICT 테이블이 자료형을 강제하는 데 그치지 않고 타입 지정자 해석 규칙까지 바꾼다는 점임

  • 일반 테이블이 타입 이름을 해석하는 방식

    • 일반 SQLite 테이블은 선언된 타입 문자열에 따라 친화도를 정함
    • "INT"가 포함되면 INTEGER로 처리함
    • "CHAR", "CLOB", "TEXT" 중 하나가 포함되면 TEXT가 됨
    • "BLOB"이 포함되거나 타입을 생략하면 BLOB으로 처리함
    • "REAL", "FLOA", "DOUB" 중 하나가 포함되면 REAL이 됨
    • 어느 조건에도 해당하지 않으면 NUMERIC으로 처리함
    • 느슨한 타입과 이 규칙을 결합하면 DATETIME, KEY_VALUE_SET, COLOR 같은 사용자 정의 타입 이름을 열에 붙일 수 있음
    • 데이터베이스 커넥터나 래퍼는 타입 이름을 보고 자동으로 직렬화·역직렬화할 수 있음
    • 별도 처리 기능이 없더라도 타입 이름 자체가 열의 예상 데이터를 문서화함
    • STRICT를 그대로 기본값으로 바꾸면 이 활용법을 포기해야 하므로, 명시적인 사용자 정의 타입 별칭이 필요함
    CREATE TYPE KEY_VALUE_SET = TEXT;
    
    • 엄격한 테이블에서 KEY_VALUE_SET 같은 별칭을 사용할 수 있다면, 애플리케이션에서 파싱해야 하는 TEXT 열의 예상 데이터 패턴도 스키마에 기록할 수 있음
    • 사용자 정의 타입에 CHECK 제약을 연결하는 기능도 유용함
    • SQL:1999 표준에는 제약까지 지원하는 타입 별칭 문법인 CREATE DOMAIN 이 이미 있으므로, SQLite가 이 표준 문장을 지원하는 방식으로 구현할 수 있음

동시 쓰기와 SQLITE_BUSY

  • SQLite는 여러 동시 읽기를 허용하지만, 쓰기는 한 번에 하나만 처리함
  • 기본 설정에서는 두 프로세스가 동시에 쓰기 잠금을 얻으려 할 때 하나가 기다리지 않고 즉시 SQLITE_BUSY 오류를 받음
    • 디스크 I/O를 수행하는 쓰기는 원래 느려질 수 있으므로, 일정 시간 잠금 해제를 기다리는 편이 더 자연스러움
    • 실제 시스템이 간헐적으로 중단되어 직접 재시도 루프를 작성해야 했던 사례도 있음
  • 다음 설정은 최대 5초 동안 잠금 획득을 재시도한 뒤에만 SQLITE_BUSY를 반환함
PRAGMA busy_timeout = 5000;
  • 정상 운영에서는 모든 쓰기를 단일 프로세스, 가능하면 단일 스레드에 모으는 편이 적절하며 동시 쓰기가 빠를 수는 없음
  • 다만 동시 쓰기가 필요한 정상적인 운영 사례도 있음
    • sqlite3 명령줄 도구로 데이터베이스를 대화형으로 정리
    • 별도 프런트엔드가 없는 드문 관리 작업용 스크립트 실행
  • 이런 작업은 기본 설정 때문에 예상하지 못한 SQLITE_BUSY를 일으켜 실행 중인 소프트웨어를 중단시킬 수 있음

WAL과 성능 기본값

  • SQLite는 올바르게 설정하면 PostgreSQL이나 MySQL 같은 대형 서버가 맡던 역할도 일부 수행할 만큼 빠를 수 있지만, 기본 성능은 좋지 않음
  • 서버 환경의 세부 튜닝은 Optimizing SQLite for servers에서 더 확인할 수 있음
  • 가장 중요한 기본값 문제는 Write-Ahead Log, 즉 WAL이 비활성화되어 있다는 점임
PRAGMA journal_mode = WAL;
  • WAL은 대부분의 상황에서 쓰기 속도를 크게 높임
  • 다음 설정을 함께 사용하면 데이터 손상 위험 없이 디스크 동기화 횟수를 크게 줄일 수 있음
PRAGMA synchronous = NORMAL;

연도 기반 SQLite 에디션

  • 기존 기본값을 지금 직접 변경하면 오래된 소프트웨어가 깨지고, 사용자가 향후 SQLite 업그레이드를 두려워하게 될 수 있음
  • 하위 호환성을 유지하면서 개선된 기본값을 선택하도록 하나의 상위 프라그마(super pragma) 를 추가할 수 있음
PRAGMA edition = 2026;
  • 2026 에디션은 최소한 다음 설정의 별칭으로 동작함
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
  • 이 에디션에서는 새 테이블의 strict 모드도 기본값으로 적용함
  • Rust editions처럼 기존 코드의 동작을 보존하면서 새로운 기본 동작을 명시적으로 선택하는 방식임
  • JavaScript의 "use strict"; 같은 단일 모드와 달리, 연도 기반 에디션은 시대에 따라 합리적인 기본값 묶음을 바꿀 수 있음
  • 예를 들어 Hctree의 WAL2가 2034년에 메인 브랜치에 들어간다면, 미래의 PRAGMA edition = 2034journal_mode = WAL2를 선택하도록 구성할 수 있음
  • 에디션 체계는 기존 프로그램의 기본 동작을 깨지 않으면서도 SQLite가 과거 설정에 묶이지 않고 안전성과 성능 기본값을 발전시키는 절충안이 될 수 있음

댓글과 토론

Hacker News 의견들
  • 불편한 점만 나열하고 남들이 바뀌길 바라는 대신, 하위 호환성을 유지하는 대안 기본값을 제안한다는 점이 좋음. 동의하지 않으면 PRAGMA edition = 2026을 실행하지 않으면 됨
    SQLITE_BUSY를 약간 변호하자면, busy_timeout은 오류를 받았을 때 지정 시간까지 대기하고 재시도하게 할 뿐임. 기다리는 동안 호출 측에서 다른 일을 할 수도 있으니 라이브러리가 이를 호출 코드에 맡기는 기본값도 합리적이지만, 실제로는 그 처리 논리가 거의 항상 빠짐

    • 이는 단순한 불만 목록이 아니라 SQLite를 진지하게 사용하는 이들이 거의 보편적으로 적용하는 설정에 가까움. 각 제안의 반대 설정은 2026년의 기본값으로 부적절하다고 볼 만함
    • SQLite가 데이터베이스 전체를 잠근다는 건 끈질긴 오해임. 모든 데이터베이스가 쓰기 때 잠금을 사용하며 차이는 잠금 세분성에 있고, 여러 쓰기 작업은 결국 차례로 수행됨
    • 트랜잭션이 읽기에서 쓰기로 승격될 때는 busy_timeout을 우회하고 즉시 SQLITE_BUSY를 반환하는 일이 흔함
      SQLite 트랜잭션은 기본적으로 DEFERRED 모드에서 시작해 실제 쓰기 전까지 읽기 트랜잭션으로 동작함. 그 사이 다른 연결이 쓰기를 시작하면 busy_timeout 설정과 무관하게 즉시 오류가 발생함
    • SQLite는 데이터베이스이므로 에디션이 코드에만 연결되어서는 제대로 작동하지 않음. PRAGMA 설정을 데이터로 저장하고, 수동 설정 여부를 매번 모두 확인한 뒤 다음 open 때 적용하며 대화형 명령줄에서도 표시하는 편이 나아 보임
    • SQLITE_BUSY 처리가 빠지는 일은 “종종”이 아니라 거의 항상에 가까우며, 알려줘도 고쳐지지 않는 경우가 많음. “쓰는 프로세스가 하나뿐이라 처리할 필요 없다”는 말은 데이터베이스를 조회하려 할 때마다 그 프로세스에 SIGSTOP을 보내야 한다는 뜻이 됨
  • 영구 PRAGMA 설정을 SQLite 파일에 저장하고 CLI가 시작할 때 읽어 적용하는 방식도 제안하는 것처럼 보임. 그러면 누군가 데이터 파일을 복사해 가더라도 기본적으로 같은 제약을 적용받게 됨

  • SQLite는 데이터 컨테이너라는 점에서 Rust와 다름. 데이터베이스 파일을 다른 컴퓨터로 옮겨 명령줄 도구로 살펴보는 일이 흔하고, 앱에 내장된 SQLite가 /usr/bin/sqlite3보다 최신인 경우도 많음
    파일에 에디션을 추가하면 새 에디션의 변경 사항을 모르는 구버전이 최신 파일을 읽는 용도가 깨질 수 있음. 큰 문제는 아니며 앱과 동일한 버전의 명령줄 도구를 함께 배포하는 운영 방식이 필요할 듯함

    • 일부 PRAGMA는 에디션 유무와 상관없이 같은 문제가 있음. busy_timeout은 연결별 설정이고, WAL 모드에서는 .db 파일만 복사하면 데이터베이스를 망가뜨릴 수 있으므로 VACUUM INTO가 필요하다는 사실도 사용자가 알아야 함
    • SQLite의 메타데이터로 이전 에디션이 마지막 쓰기 이후 데이터베이스를 수정했는지 확인하면 완전한 하위 호환성을 유지할 수 있음. 새 에디션의 엄격한 규칙을 적용하되 구버전이 중간에 수정했다면 다시 준수 여부가 검증될 때까지 기존 규칙으로 돌아가면 됨
    • 에디션이 의미 변화 자체를 기술하도록 파일에 포함할 수도 있음. 구버전은 안전하게 무시하고 신버전은 해석해 실행하며, 에디션을 초기에 선언하고 한 방향으로만 변경하도록 강제하면 도입 안전성을 어느 정도 확보할 수 있음
    • SQLite 실행 파일이 1MB 미만이니 데이터 파일 안에 SQLite 자체를 통째로 넣는 방식도 생각해볼 수 있음. 데이터베이스를 저장할 환경이라면 대개 그 정도 추가 용량은 감당할 수 있는데, 이런 구현이 실제로 있는지 또는 불가능한 이유가 있는지 궁금함
    • SQLite가 데이터 컨테이너라는 점이 핵심임. sqlite.org에 따르면 2004년부터 파일 형식은 완전한 하위 호환성을 유지했고, 신버전은 구버전 파일을 항상 읽고 쓸 수 있음. 구버전이 신버전 파일을 다루는 상위 호환성도 흔하지만 새 파일 형식 기능 때문에 가끔 깨짐
      에디션 B가 A와 하위 호환되어야 한다면 현재 상태와 크게 다르지 않음. 그렇지 않다면 에디션별 파일 형식이 갈라져 A부터 Z까지 각각의 호환성을 관리해야 하며, Y 에디션 파일을 가져올 때 어떻게 할지도 정해야 함. 참고: https://www.sqlite.org/formatchng.html, sqlite.org의 관련 표현 검색 결과는 https://pastebin.com/Q7b7h4eM에 있음
  • 새 에디션이 나올 때마다 과거 에디션의 PRAGMA 집합을 하위 호환성 때문에 유지하면 내장 환경의 코드 비대화가 생김
    PRAGMA edition은 데이터베이스 파일에 기록하지 않고 라이브러리 수준에서 여러 PRAGMA 명령으로 확장되는 단축어여야 함. 기본 빌드는 폐기 정책에 따라 제한된 에디션만 지원하고, 2046년에도 오래된 에디션이 필요하다면 시스템 테이블 등으로 런타임에 동적으로 불러오게 할 수 있음

    • 그게 바로 글쓴이의 제안처럼 보임. 에디션은 이미 구현된 설정들의 묶음이므로 추가되는 코드는 수백 바이트 정도에 불과할 것임
  • 에디션이 실제로 크게 유용하지는 않을 듯함. 개발자는 에디션 PRAGMA 하나든 여러 PRAGMA든 결국 설정을 빠짐없이 적용해야 하고, 사용하는 선택지를 이해해 신중히 골라야 함. 에디션이 직접 이해하고 드러내야 할 설정을 추상화해 숨기면 오히려 더 어려워질 수 있음
    제안된 기본값도 완전한 해결책보다는 점진적인 편의 개선에 가까움. 기본 타입 선호 규칙이 이상하더라도 STRICT 테이블은 포괄적인 검증 수단이 아니므로 강한 검증이 필요하면 상위 계층에서 별도로 구현해야 함
    busy_timeout도 편리할 뿐 시간 초과 자체는 여전히 처리해야 하며, “시간 초과가 생길 줄 몰라 앱이 잘못 처리했다”는 문제를 PRAGMA가 해결해주지는 못함

  • JavaScript의 "use strict"는 흥미로운 선례임. 하위 호환성 때문에 이상한 동작을 고칠 수 없다고들 하지만 이미 한 번 선택적 엄격 모드로 해결했으니 다시 못 할 이유가 없음

    • "use strict;"는 Perl에도 있으며, 실험적 기능을 켜거나 특정 버전의 기능 집합을 요구할 수도 있음
  • SQLite가 ROWID를 재사용하는 부분에서 “끊어진 참조가 잘못된 열을 가리킨다”는 문장은 잘못된 행을 가리킨다고 고쳐야 할 듯함

  • 이 제안은 https://sqlite.org/forum/forum에도 올려볼 만함. SQLite 팀이 해당 포럼을 면밀히 보고 있으며 질문이나 제안에 훌륭한 답변을 해준 적이 있음

  • 이 방식이 마음에 들지만 C++처럼 에디션이 많아지면 추적하기 혼란스러울 수 있음. 매년 하나씩 고정하기보다는 3년이나 5년 주기가 SQLite 같은 장기 프로젝트에 더 적합해 보임

  • 문자열 '1''0'이 들어갔다면 해당 열에 INTEGER 타입 선호 규칙이 없었던 것으로 보임. 불리언 용도라면 INTEGER로 지정했어야 함
    INTEGER 열에 정수나 실수처럼 보이는 문자열을 문자열 그대로 넣으려 여러 번 시도했지만, BLOB으로 넣거나 앞에 '\' 같은 문자를 붙인 뒤 애플리케이션에서 검사하고 제거하는 방법밖에 찾지 못했음. 이는 EAttribute.eatvalue가 어떤 타입이든 담아야 하는 온톨로지형 데이터베이스에서 필요했던 작업임

  • 첫 예시에서 엔터티를 삭제한 뒤 고유 ID가 재사용된다는 점도 놀라움. 외래 키가 올바르게 처리된다면 정의상 문제가 없겠지만 직관적으로는 잘못된 설계처럼 느껴짐

    • 부모 테이블의 ID가 재사용되면 엉뚱한 사용자에게 데이터가 노출될 가능성이 있으므로 보안 취약점이 될 수 있음
    • 이것이 기본 동작이지만 테이블을 만들 때 바꿀 수 있음. 자세한 내용은 https://sqlite.org/autoinc.html에서 확인 가능함

이론은 좋아 보이지만 호환성 충돌 및 오버헤드를 생각하면 손해일 것 같아요.
SQLite 는 이지스 같은 임베디드 환경에서 동작하기 위해 탄생한 DBMS 인데요,
void** 뻥뻥 때리고 weak 선언후에 함수 덮어씌우기 하는게 임베디드 세상이죠. 그런세상에서 저걸 굳이? 싶은 생각이 드네요

Lobste.rs 의견들
  • 어제 lobste.rs가 SQLite로 전환했다는 소식(https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite)을 보고 이 글을 썼음
    당시 대화를 읽으며 SQLite 기본값의 사소한 불편들이 떠올라 블로그 글로 정리함

    • 나도 그 대화를 읽고 같은 결론에 도달함
      여력이 있다면 이 글을 SQLite 포럼에도 공유할 만함 : https://sqlite.org/forum/forum
  • SQL에서 CREATE TYPE 은 복합 타입을 만들 때 사용하며, Postgres는 여기에 범위, 열거, 배열, 상당히 복잡한 기반 타입까지 지원함
    원하는 기능은 CREATE DOMAIN 에 가깝다. 기반 구체 타입으로 새타입(newtype)을 만들고 기본값과 제약 조건도 지정할 수 있어, 여러 테이블에서 같은 CHECK 제약 조건을 반복하는 스키마가 훨씬 간결해짐. 예를 들면 create domain mything as text; 또는 create domain mything as integer not null check (VALUE < 5);처럼 쓸 수 있음

    • 기존 타입 별칭 기능이 있는지 먼저 확인했어야 했음. 글에 관련 내용을 추가하겠음
  • busy_timeout만으로는 즉시 발생하는 SQLITE_BUSY 오류를 막을 수 없다는 꽤 위험한 함정도 다룰 필요가 있음
    읽기 전용 쿼리로 트랜잭션을 시작한 뒤 쓰기 쿼리를 실행해 보유한 잠금을 승격하려 할 때 다른 쓰기 작업이 있으면, busy_timeout 설정과 무관하게 즉시 실패함. 해결하려면 begin immediate 를 사용해야 함

    • 실제 버그가 아니라는 건 알지만, 솔직히 거의 버그처럼 느껴지는 동작
  • 제안한 에디션에 묶인 설정 중 일부는 SQLite 모델에서 연결 속성이고, 나머지는 데이터베이스 파일이나 개별 테이블의 속성이라는 점을 고려해야 함
    PRAGMA journal_mode = WAL은 데이터베이스 파일에 계속 유지되지만 PRAGMA foreign_keys = ON은 연결마다 매번 설정해야 함. STRICT는 개별 테이블의 속성이어서 한 데이터베이스 안에 엄격한 테이블과 그렇지 않은 테이블을 섞을 수도 있음. 따라서 이 통합 PRAGMA를 데이터베이스 파일과 연결 중 어디의 속성으로 정의할지 정해야 함. 구버전 라이브러리가 자신이 모르는 기능이 포함된 파일을 수정해 망가뜨리지 않도록 경고하는 내장 장치가 있는지도 궁금

  • 에디션 방식은 기본적으로 하위 호환성을 유지하려는 SQLite 개발진의 목표와, 최신 권장 설정을 원하는 사용자를 위한 덜 혼란스러운 기본값 사이의 좋은 절충안으로 보임

  • 최근 RocksDB에 푹 빠졌음. SQLite처럼 내장형 데이터베이스 라이브러리지만 SQL이 없다는 점이 오히려 큰 장점이라고 봄
    모든 작업을 SQL로 표현해야 하는 제약은 데이터베이스를 다루는 데 방해만 된다고 느낌

    • RocksDB는 키-값 데이터베이스이고 SQLite는 관계형 데이터베이스라서, 서로 다른 목적을 위해 만들어진 별개의 개념이라고 좀
      lobste.rs처럼 SQL을 사용하는 시스템을 RocksDB나 다른 키-값 데이터베이스로 어떻게 옮길지 상상하기 어려움
  • busy_timeout을 설정하는 것이 해결책이라는 데 동의하지 않음. SQLite는 단일 쓰기 스레드를 두고 서로 무관한 논리적 트랜잭션을 일괄 처리하면 성능이 매우 뛰어남
    구체적인 방식은 https://andersmurphy.com/2025/12/… 에 설명돼 있음

    • 일반적인 작업 부하에서는 쓰기 작업을 하나로 제한하고, 여러 쓰기 작업이 상시 경쟁하는 구조를 만들지 않는 편이 맞음
      하지만 정리 작업을 위해 데이터베이스를 직접 수정하거나, 별도 관리 화면까지 만들 필요는 없는 간헐적인 관리 작업을 스크립트로 실행하는 일도 흔함. 이때 운 나쁘게 실행 시점이 겹쳤다는 이유로 서버나 애플리케이션이 무작위로 중단될 수 있다는 건 좋지 않음