이 페이지는 기계 번역되었습니다. 영어 원본을 읽어보세요. English

IBSurgeon 라이브러리

15가지 Firebird 안티패턴

Alexey Kovyazin 작성, 2025년 1월 14일

소개

이 문서는 Firebird 데이터베이스 작업 시 흔히 발생하는 15가지 안티패턴과 각각에 대한 해결책을 설명합니다.

1. MON$에 대한 다중 병렬 쿼리

안티패턴: 매우 흔한 실수 - OnConnect 트리거에서 감사 목적으로 사용자 세부 정보를 선택하거나 라이선스 목적으로 연결 수를 계산하기 위해 MON$ATTACHMENTS를 쿼리하는 것입니다.

왜 문제인가?

  • MON$ 테이블은 fbNN_mon_xx 시스템 파일에 저장된 가상 테이블로, 성능 통계 등을 포함합니다

  • 파일이 1GB를 초과하면 너무 많이 사용하고 있다는 의미입니다

  • 이는 시스템 관리자 전용으로 설계되었습니다 - 즉, 관리자 전용 1-2개의 병렬 쿼리만을 위한 것입니다

  • 200개 이상의 연결에서 MON$에 대한 병렬 쿼리는 Firebird를 크게 느려지게 하며, 500개 이상의 동시 쿼리는 높은 확률로 Firebird를 “멈추게” 합니다

해결책:

  • 감사나 카운트와 같은 비관리 작업에는 MON$를 사용하지 말고, OnConnect에서 사용을 피하세요

  • 감사 목적의 경우:

    • CURRENT_USER, CURRENT_TIMESTAMP 등의 컨텍스트 변수를 사용하세요

    • 트리거보다 훨씬 강력한 Firebird 네이티브 기능인 Audit을 사용하세요

  • 라이선스 목적의 경우 - 사용자의 컨텍스트 변수를 사용하세요

2. 느린 대시보드 로딩

안티패턴: 애플리케이션 시작 시 지난 달 또는 연도의 모든 주문과 청구서를 합산하는 포괄적인 대시보드나 스코어보드를 로딩하거나, 매분 또는 더 자주 일부 지표를 업데이트하는 것입니다.

sql
SELECT
 SUM(total_sales) as yearly_sales,
 COUNT(DISTINCT customers) as customer_count,
 AVG(order_value) as avg_order_value
FROM orders
WHERE order_date BETWEEN '2025-01-01' AND '2025-01-01';

왜 문제인가?

  • 사용자는 실제 작업을 시작하기 전에 회사 전체 통계를 보기 위해 몇 초를 기다려야 합니다

  • Firebird 관점에서 - 많은 병렬 쿼리를 지속적으로 실행하여 대량의 데이터를 검색하고 정렬/그룹화하기 위해 Firebird는 다중 CPU 코어를 집중적으로 사용하고, 디스크, 캐시, 정렬 전용 메모리를 읽습니다 (때로는 정렬이 디스크로 넘어가기도 합니다)

  • 이는 분당 여러 번 보고서를 만드는 것과 같습니다!

해결책:

  1. 대시보드를 볼 사용자 수를 줄이세요:

    • 일반적으로 대시보드는 분석가와 관리자에게만 필요하므로 일반 애플리케이션 로드에서 제외하세요

    • 시작 시 또는 특정 폼에서 대시보드 로딩을 선택 사항으로 만들고 기본적으로 비활성화하세요

    • 시작 시가 아닌 명시적 버튼 클릭으로 대시보드 데이터를 로드하세요 (즉, 보고서처럼 만드세요)

  2. 스케줄에 따라 1개의 프로세스(로봇)로 대시보드 데이터를 계산하고, 간단한 쿼리로 검색할 수 있도록 준비된 단순 테이블에 저장하세요

  3. 트리거를 사용하여 데이터를 집계하고 사용 준비된 상태로 저장하세요

  4. 복제 데이터베이스를 사용하여 대시보드 데이터(및 모든 무거운 보고서)를 계산하세요

3. 불필요한 레코드 로딩

안티패턴: 애플리케이션이나 폼을 열 때 수십만 개의 레코드가 포함되어 있는지 여부와 관계없이 필터링 없이 모든 데이터를 그리드에 로딩하는 것입니다.

delphi
procedure TDataForm.LoadAllRecords;
begin
 FDQuery1.SQL.Text := 'SELECT * FROM large_table';
 FDQuery1.Open;
 // 전체 테이블을 메모리에 로드
 DBGrid1.DataSource.DataSet := FDQuery1;
end;

왜 문제인가?

  • 그리드가 50개의 레코드만 표시함에도 불구하고, 사용자는 검색 기능을 사용하는 대신 수천 개의 레코드를 스크롤해야 합니다

  • 99%의 경우 사용자는 매우 좁은 데이터 하위 집합만 필요합니다: 예를 들어 가장 최근 판매 기록

  • Firebird 관점에서:

    • 열 때마다 수천 개의 레코드를 읽고, 캐시에 저장하고, 네트워크를 통해 전송해야 합니다

    • 데이터셋을 열어 둔 경우(Delphi에서), Firebird는 데이터셋이 닫힐 때까지 버퍼, 임시 공간의 정렬된 레코드(ORDER BY, GROUP BY 등이 있는 경우)를 유지합니다

해결책:

  1. FIRST/SKIP/ROWS로 레코드 수를 제한하세요

  2. 예를 들어 최근 3일 동안 생성/변경된 레코드 표시와 같은 기준으로 레코드 수를 제한하세요

  3. 일반적으로 쿼리는 가능한 한 빨리 닫으세요.

4. 스크롤 시 과도한 쿼리 실행

안티패턴: 스크롤 이벤트에서 쿼리를 실행하는 것입니다. 예를 들어 그리드나 테이블에 데이터를 표시할 때 각 레코드에 대해 별도의 쿼리를 수행하거나, 지연 없이 2개의 그리드에서 마스터-디테일 스크롤의 전형적인 예를 사용하는 경우입니다.

delphi
procedure TForm1.GridScrolled(Sender: TObject);
begin
 // 각 행에 대한 쿼리
 FDQuery2.SQL.Text :=
 'SELECT additional_info FROM details ' +
 'WHERE id = ' + IntToStr(CurrentRowId);
 FDQuery2.Open;
end;

왜 문제인가?

  • 동적 그리드에서 각 레코드에 대해 별도의 쿼리를 수행하면 Firebird가 수천 개의 작은 쿼리를 처리해야 하므로 CPU 리소스를 불필요하게 소비합니다

  • Firebird 관점에서:

    • 초당 수천 개의 작은 쿼리는 상당한 CPU 부하를 생성합니다. 쿼리가 통계에서 0ms로 표시되더라도 준비, 실행, 결과 전송 등이 필요하기 때문입니다

해결책:

  1. 배치 작업을 사용하여 한 번에 여러 행을 로드하세요

  2. 그리드의 기본 쿼리를 향상시켜 상세 쿼리를 그 일부로 실행하세요

  3. 그리드의 보이는 부분에 대한 세부 정보를 로드하는 명시적 버튼을 추가하세요

  4. 스크롤 중 즉시 쿼리가 실행되지 않도록 상세 정보를 받는 쿼리 실행에 지연을 추가하세요

  5. 모든 사용자에 대해 기본적으로 스크롤 시 세부 정보 로딩을 활성화하지 마세요

5. 불필요한 자동 새로고침

안티패턴: 모든 클라이언트 애플리케이션에서 최소 간격으로 그리드 데이터를 자동 새로고침하고, 이 기능을 기본적으로 활성화하는 것입니다.

왜 문제인가?

  • 이로 인해 수백 개의 클라이언트 연결이 거의 동일한 쿼리를 실행하여 동일한 레코드를 검색하게 됩니다

  • 발생 상황: 일정에 대한 자동 새로고침, 대기열 위치 선택, “가장 가까운 슬롯” 검색 등

  • Firebird 관점에서:

    • 대시보드 로딩과 스크롤 이벤트의 조합: 많은 중간 규모 쿼리가 시스템에 부하를 생성합니다

해결책:

  1. 간격을 늘리세요!

  2. 명시적(사용자 트리거) 새로고침을 구현하세요

  3. 실제 데이터 변경에 기반한 선택적 데이터 집합 새로고침을 사용하세요 (스트리밍 또는 트리거 또는 이벤트+스트리밍)

6. 빈번한 레코드 업데이트

안티패턴: 다른 트랜잭션에서 동일한 레코드를 자주 업데이트하여 수많은 레코드 버전을 생성하는 것입니다.

왜 문제인가?

  • 수십 개의 버전이 있는 레코드는 성능을 크게 저하시킬 수 있으며, 수천 개의 버전이 있는 레코드는 블로커가 될 수 있습니다

  • Firebird 관점에서: 특정 트랜잭션의 적절한 버전을 식별하기 위해 레코드 버전 체인을 재구성해야 하며, 이는 많은 읽기 작업이 필요하고 결과적으로 가비지 컬렉션이 훨씬 느려집니다.

해결책:

  1. 중간 가비지 컬렉션이 있는 Firebird 4+로 마이그레이션하세요

  2. 오래 실행되는 쓰기 가능 트랜잭션을 유지하지 말고 적절한 가비지 컬렉션을 수행하세요

  3. Firebird <4의 경우 UPDATE 대신 DELETE+INSERT 사용을 고려하세요

7. 읽기 전용 SELECT에 쓰기 트랜잭션 사용

안티패턴: 읽기 전용 SELECT에 쓰기 트랜잭션을 사용하면 과도한 작업이 발생합니다.

왜 문제인가?

  • 읽기 전용 SELECT에 쓰기 트랜잭션을 사용하면 헤더 페이지의 불필요한 쓰기가 많이 발생합니다

  • 읽기 전용 작업에 쓰기 트랜잭션을 사용하는 것은 비효율적입니다 (커밋 시 큰 TIP가 서버에 추가 부하를 생성)

해결책:

  • 데이터를 변경하지 않는 작업에는 별도의 읽기 전용 트랜잭션을 사용하세요

  • Firebird는 단일 연결 프레임에서 여러 트랜잭션을 열 수 있는 몇 안 되는 데이터베이스 중 하나입니다

  • 전역 임시 테이블은 읽기 전용 트랜잭션에서 사용할 수 있습니다

8. LIKE :param 사용

다음 매개변수가 있는 쿼리는 fieldName에 대한 인덱스를 사용하지 않습니다 (인덱스가 존재하더라도):

sql
SELECT * FROM Table1 WHERE fieldName LIKE :param1

왜 문제인가?

LIKE는 와일드카드 검색(%)을 허용하므로 어떤 수의 기호도 대체할 수 있기 때문에, Firebird는 매개변수 값이 사전에 인덱스 검색에 적합한지 결정할 수 없습니다.

일반적으로 개발자는 매개변수 값을 쿼리 텍스트에 포함하여 해결하려고 합니다:

  • fieldName LIKE «Alex%» - 인덱스 사용 가능

  • fieldName LIKE «%Alex» - 표준 인덱스를 사용할 수 없음

  • fieldName LIKE «%Alex%» - 인덱스를 전혀 사용할 수 없음

이는 다른 문제로 이어집니다 (아래 #10 참조).

해결 방법:

1. 알려진 문자열 접두사에는 STARTING WITH 사용

검색 값이 와일드카드 %로 시작하지 않는 경우 LIKE 대신 STARTING WITH을 사용하세요:

sql
WHERE fieldName STARTING WITH ?param1

2. 양방향 문자열 검색 최적화

알려진 접두사 또는 접미사 패턴이 있는 문자열의 경우 역방향 인덱스를 사용하세요:

sql
-- 역방향 인덱스 생성
CREATE INDEX ixreverse1 ON TABLE1 COMPUTED BY (REVERSE(fieldName));
-- 양방향을 사용한 쿼리
WHERE fieldName STARTING WITH :param1
   OR reverse(fieldName) STARTING WITH reverse(:param2)

3. 점진적 검색 전략 구현

시작/끝/중간에 나타나는 문자열(동시에 나타나지 않는 경우)의 경우:

  • 먼저 STARTING WITH으로 빠른 인덱스 검색 시도

  • 결과가 없으면 느린 LIKE 검색으로 대체

4. 단어 기반 검색 최적화

(공백, 쉼표 등으로 구분된) 완전한 단어를 검색할 때:

  • 별도의 단어-ID 매핑 테이블 생성

  • 원본 텍스트 대신 매핑 테이블을 통해 검색

5. 포괄적인 전문(full-text) 검색 기능의 경우:

  • IBSurgeon Full Text Search UDR 사용 고려

  • 이 오픈소스 솔루션은 고급 텍스트 검색 기능을 제공합니다

9. 읽기 전용 작업에 트랜잭션을 닫지 않음

왜 문제인가?

  • 트랜잭션을 오랫동안 열어두면 Firebird가 잠재적 스냅샷 트랜잭션을 위해 많은 백 버전을 유지해야 할 수 있습니다

해결 방법:

  • 가능한 경우 읽기 전용 트랜잭션을 사용하고, 쓰기 가능한 트랜잭션은 가능한 한 빨리 닫으세요

  • 최신 Firebird 버전(4+)을 사용하여 레코드 버전 체인의 영향을 줄이세요

  • 적절한 스윕(sweep) 구현

10. 쿼리 매개변수화 문제

안티 패턴: 준비된 쿼리와 매개변수화를 피하고, 대신 매개변수 값을 쿼리 텍스트에 직접 포함시키는 것.

delphi
FDQuery1.SQL.Text :=
 'SELECT * FROM users WHERE name = ''' +
 EditUsername.Text + '''';
FDQuery1.Open;

왜 문제인가?

  • 이 방식은 반복 쿼리의 성능을 저하시킵니다

  • 매개변수 값이 포함된 모든 쿼리는 새로 준비되어야 합니다

  • 대형 테이블의 경우 준비 과정이 길고 시간이 많이 걸릴 수 있습니다

  • 문제 분석이 복잡해집니다

  • 텍스트별로 쿼리를 그룹화하기 어렵습니다

  • SQL 인젝션 취약점이 발생합니다

해결 방법:

delphi
FDQuery1.SQL.Text :=
 'SELECT * FROM users WHERE name = :username';
FDQuery1.ParamByName('username').AsString :=
 EditUsername.Text;
FDQuery1.Open;

11. 잘못된 무결성 검사: 기본 키 대신 트리거/CHECK 사용

안티 패턴: 데이터베이스 무결성 검사를 위해 기본 키 대신 트리거나 CHECK를 사용하는 것.

왜 문제인가?

  • 기본 키 검증은 사용자의 트랜잭션 격리 수준과 관계없이 레코드의 현재 버전을 읽는 특수 모드를 사용한다는 점을 무시합니다.

  • 사용자 트랜잭션에서 트리거로 PK 검사를 수행하면 중복 가능성이 높아지고 로직이 불필요하게 복잡해집니다

해결 방법:

  • 기본 키 사용

  • 중복 무결성 검사 방지

  • 데이터베이스 로직을 단순하게 유지

12. MAX()를 사용한 ID 생성

안티 패턴: 새 식별자에 MAX(id)+1을 사용하는 것은 신뢰할 수 없고 비효율적입니다.

sql
INSERT INTO users (id, name)
VALUES ((SELECT MAX(id) + 1 FROM users),
 'John Doe');

왜 문제인가?

  • 새 식별자에 시퀀스(제너레이터) 대신 MAX(id)+1을 사용

  • MAX(id)+1은 일반적인 트랜잭션 매개변수에서 고유성을 보장하지 않습니다 - 두 개의 병렬 트랜잭션이 동일한 MAX() 값을 받을 수 있습니다

  • Max()+1과 CHECK(select if unique)의 조합도 작동하지 않습니다!

해결 방법:

sql
-- 제너레이터/시퀀스 사용!
CREATE GENERATOR gen_user_id;
-- ID 생성에 제너레이터 사용
INSERT INTO users (id, name)
VALUES (
 GEN_ID(gen_user_id, 1),
 'John Doe' );

13. 비효율적인 GUID 사용

왜 문제인가?

  • 시스템 생성 GUID를 gen_uuid() 대신 사용하면 인덱스 성능에 영향을 줄 수 있습니다

  • 시스템 생성 GUID는 매우 무작위적입니다

해결 방법:

  • gen_uuid() 함수 사용

  • BIGINT 사용 고려

  • 버전 6에는 UUID v7이 제공될 예정입니다

14. 비효율적인 계산 필드(Computed Fields)

안티 패턴: 다른 테이블에 대한 SELECT가 포함된 계산 필드를 사용하면 단순 SELECT 작업의 성능이 크게 저하됩니다.

sql
CREATE TABLE orders (
 id INTEGER,
 total_amount COMPUTED BY (
 (SELECT SUM(item_price) FROM order_items
 WHERE order_items.order_id = orders.id)));

왜 문제인가?

  • 계산 필드는 즉시 계산되며 복잡한 로직을 구현하기 위한 것이 아니므로 최적화 노력을 상당히 복잡하게 만들 수 있습니다

  • 테이블 간의 관계를 강화합니다

  • 계산 필드는 테이블 필드의 연결과 같은 가벼운 계산에만 사용하는 것이 합리적입니다

해결 방법:

sql
CREATE TABLE orders (
 id INTEGER PRIMARY KEY,
 cached_total_amount DECIMAL(10,2));

CREATE TRIGGER update_order_total
BEFORE INSERT OR UPDATE ON orders
AS
BEGIN
 NEW.cached_total_amount = (
 SELECT SUM(item_price)
 FROM order_items
 WHERE order_items.order_id = NEW.id
 );
END;

15. 로깅 없는 오류 억제

안티 패턴: Firebird 오류와 경고를 로깅 없이 억제하지 마세요!

delphi
try
 FDQuery1.Open;
except
 // 조용한 실패
end;

왜 문제인가?

  • 오류를 숨기면 적절한 진단과 디버깅이 불가능합니다. 적절한 오류 로깅은 문제를 신속하게 이해하고 해결하는 데 중요합니다.

해결 방법:

delphi
try
 FDQuery1.Open;
except
 on E: Exception do
 begin
 // 포괄적인 로깅
 Logger.Error('데이터베이스 연결 실패: ' + E.Message);
 ShowMessage('데이터베이스에 연결할 수 없습니다. 지원팀에 문의하세요.');
 // 추가 컨텍스트 로깅
 Logger.LogStackTrace(E);
 end;
end;

연락처 정보