[수정]
- 파티션 관리 job 추가 (+3일 미리 생성, 14일 이전 파티션 자동drop 설정) - (임시) GPU 운영 포트 9000번 변경 - ais_target 테이블 일일 파티션구조로 변경 (1일 데이터 약 20GB)
This commit is contained in:
부모
5857a4a822
커밋
55d4dd5886
@ -30,23 +30,29 @@ public class SwaggerConfig {
|
|||||||
@Value("${server.port:8081}")
|
@Value("${server.port:8081}")
|
||||||
private int serverPort;
|
private int serverPort;
|
||||||
|
|
||||||
|
@Value("${server.servlet.context-path:}")
|
||||||
|
private String contextPath;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OpenAPI openAPI() {
|
public OpenAPI openAPI() {
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(apiInfo())
|
.info(apiInfo())
|
||||||
.servers(List.of(
|
.servers(List.of(
|
||||||
new Server()
|
new Server()
|
||||||
.url("http://localhost:" + serverPort)
|
.url("http://localhost:" + serverPort + contextPath)
|
||||||
.description("로컬 개발 서버"),
|
.description("로컬 개발 서버"),
|
||||||
new Server()
|
new Server()
|
||||||
.url("http://10.26.252.39:" + serverPort)
|
.url("http://10.26.252.39:" + serverPort + contextPath)
|
||||||
.description("로컬 개발 서버"),
|
.description("로컬 개발 서버"),
|
||||||
new Server()
|
new Server()
|
||||||
.url("http://211.208.115.83:" + serverPort)
|
.url("http://211.208.115.83:" + serverPort + contextPath)
|
||||||
.description("중계 서버"),
|
.description("중계 서버"),
|
||||||
new Server()
|
new Server()
|
||||||
.url("http://10.187.58.58:" + serverPort)
|
.url("http://10.187.58.58:" + serverPort + contextPath)
|
||||||
.description("운영 서버")
|
.description("운영 서버"),
|
||||||
|
new Server()
|
||||||
|
.url("https://mda.kcg.go.kr" + contextPath)
|
||||||
|
.description("운영 서버 프록시")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,50 +1,133 @@
|
|||||||
package com.snp.batch.global.partition;
|
package com.snp.batch.global.partition;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 파티션 관리 대상 테이블 설정
|
* 파티션 관리 설정 (application.yml 기반)
|
||||||
*
|
*
|
||||||
* Daily 파티션: 매일 실행
|
* 설정 예시:
|
||||||
* Monthly 파티션: 매월 말일에만 실행
|
* app.batch.partition:
|
||||||
|
* daily-tables:
|
||||||
|
* - schema: snp_data
|
||||||
|
* table-name: ais_target
|
||||||
|
* partition-column: message_timestamp
|
||||||
|
* periods-ahead: 3
|
||||||
|
* monthly-tables:
|
||||||
|
* - schema: snp_data
|
||||||
|
* table-name: some_table
|
||||||
|
* partition-column: created_at
|
||||||
|
* periods-ahead: 2
|
||||||
|
* retention:
|
||||||
|
* daily-default-days: 14
|
||||||
|
* monthly-default-months: 1
|
||||||
|
* custom:
|
||||||
|
* - table-name: ais_target
|
||||||
|
* retention-days: 30
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
@Component
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "app.batch.partition")
|
||||||
public class PartitionConfig {
|
public class PartitionConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daily 파티션 대상 테이블 (파티션 네이밍: {table}_YYYY_MM_DD)
|
* 일별 파티션 테이블 목록 (파티션 네이밍: {table}_YYMMDD)
|
||||||
*/
|
*/
|
||||||
private final List<PartitionTableInfo> dailyPartitionTables = List.of(
|
private List<PartitionTableConfig> dailyTables = new ArrayList<>();
|
||||||
// 추후 daily 파티션 테이블 추가
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monthly 파티션 대상 테이블 (파티션 네이밍: {table}_YYYY_MM)
|
* 월별 파티션 테이블 목록 (파티션 네이밍: {table}_YYYY_MM)
|
||||||
*/
|
*/
|
||||||
private final List<PartitionTableInfo> monthlyPartitionTables = List.of(
|
private List<PartitionTableConfig> monthlyTables = new ArrayList<>();
|
||||||
new PartitionTableInfo(
|
|
||||||
"snp_data",
|
|
||||||
"ais_target",
|
|
||||||
"message_timestamp",
|
|
||||||
2 // 미리 생성할 개월 수
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 파티션 테이블 정보
|
* 보관기간 설정
|
||||||
*/
|
*/
|
||||||
public record PartitionTableInfo(
|
private RetentionConfig retention = new RetentionConfig();
|
||||||
String schema,
|
|
||||||
String tableName,
|
/**
|
||||||
String partitionColumn,
|
* 파티션 테이블 설정
|
||||||
int periodsAhead // 미리 생성할 기간 수 (daily: 일, monthly: 월)
|
*/
|
||||||
) {
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class PartitionTableConfig {
|
||||||
|
private String schema = "snp_data";
|
||||||
|
private String tableName;
|
||||||
|
private String partitionColumn;
|
||||||
|
private int periodsAhead = 3; // 미리 생성할 기간 수 (daily: 일, monthly: 월)
|
||||||
|
|
||||||
public String getFullTableName() {
|
public String getFullTableName() {
|
||||||
return schema + "." + tableName;
|
return schema + "." + tableName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 보관기간 설정
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class RetentionConfig {
|
||||||
|
/**
|
||||||
|
* 일별 파티션 기본 보관기간 (일)
|
||||||
|
*/
|
||||||
|
private int dailyDefaultDays = 14;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 월별 파티션 기본 보관기간 (개월)
|
||||||
|
*/
|
||||||
|
private int monthlyDefaultMonths = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 테이블 보관기간 설정
|
||||||
|
*/
|
||||||
|
private List<CustomRetention> custom = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 테이블 보관기간 설정
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class CustomRetention {
|
||||||
|
private String tableName;
|
||||||
|
private Integer retentionDays; // 일 단위 보관기간 (일별 파티션용)
|
||||||
|
private Integer retentionMonths; // 월 단위 보관기간 (월별 파티션용)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일별 파티션 테이블의 보관기간 조회 (일 단위)
|
||||||
|
*/
|
||||||
|
public int getDailyRetentionDays(String tableName) {
|
||||||
|
return getCustomRetention(tableName)
|
||||||
|
.map(c -> c.getRetentionDays() != null ? c.getRetentionDays() : retention.getDailyDefaultDays())
|
||||||
|
.orElse(retention.getDailyDefaultDays());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 월별 파티션 테이블의 보관기간 조회 (월 단위)
|
||||||
|
*/
|
||||||
|
public int getMonthlyRetentionMonths(String tableName) {
|
||||||
|
return getCustomRetention(tableName)
|
||||||
|
.map(c -> c.getRetentionMonths() != null ? c.getRetentionMonths() : retention.getMonthlyDefaultMonths())
|
||||||
|
.orElse(retention.getMonthlyDefaultMonths());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 테이블 보관기간 설정 조회
|
||||||
|
*/
|
||||||
|
private Optional<CustomRetention> getCustomRetention(String tableName) {
|
||||||
|
if (retention.getCustom() == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return retention.getCustom().stream()
|
||||||
|
.filter(c -> tableName.equals(c.getTableName()))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.snp.batch.global.partition;
|
package com.snp.batch.global.partition;
|
||||||
|
|
||||||
import com.snp.batch.global.partition.PartitionConfig.PartitionTableInfo;
|
import com.snp.batch.global.partition.PartitionConfig.PartitionTableConfig;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.batch.core.StepContribution;
|
import org.springframework.batch.core.StepContribution;
|
||||||
@ -20,8 +20,12 @@ import java.util.List;
|
|||||||
* 파티션 관리 Tasklet
|
* 파티션 관리 Tasklet
|
||||||
*
|
*
|
||||||
* 스케줄: 매일 실행
|
* 스케줄: 매일 실행
|
||||||
* - Daily 파티션: 매일 생성
|
* - Daily 파티션: 매일 생성/삭제 (네이밍: {table}_YYMMDD)
|
||||||
* - Monthly 파티션: 매월 말일에만 생성
|
* - Monthly 파티션: 매월 말일에만 생성/삭제 (네이밍: {table}_YYYY_MM)
|
||||||
|
*
|
||||||
|
* 보관기간:
|
||||||
|
* - 기본값: 일별 14일, 월별 1개월
|
||||||
|
* - 개별 테이블별 보관기간 설정 가능 (application.yml)
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ -31,6 +35,9 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final PartitionConfig partitionConfig;
|
private final PartitionConfig partitionConfig;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DAILY_PARTITION_FORMAT = DateTimeFormatter.ofPattern("yyMMdd");
|
||||||
|
private static final DateTimeFormatter MONTHLY_PARTITION_FORMAT = DateTimeFormatter.ofPattern("yyyy_MM");
|
||||||
|
|
||||||
private static final String PARTITION_EXISTS_SQL = """
|
private static final String PARTITION_EXISTS_SQL = """
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1 FROM pg_class c
|
SELECT 1 FROM pg_class c
|
||||||
@ -41,6 +48,17 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
)
|
)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
private static final String FIND_PARTITIONS_SQL = """
|
||||||
|
SELECT c.relname
|
||||||
|
FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
JOIN pg_inherits i ON i.inhrelid = c.oid
|
||||||
|
WHERE n.nspname = ?
|
||||||
|
AND c.relname LIKE ?
|
||||||
|
AND c.relkind = 'r'
|
||||||
|
ORDER BY c.relname
|
||||||
|
""";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
@ -52,14 +70,24 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
log.info("월 말일 여부: {}", isLastDayOfMonth);
|
log.info("월 말일 여부: {}", isLastDayOfMonth);
|
||||||
log.info("========================================");
|
log.info("========================================");
|
||||||
|
|
||||||
// Daily 파티션 처리 (매일)
|
// 1. Daily 파티션 생성 (매일)
|
||||||
processDailyPartitions(today);
|
createDailyPartitions(today);
|
||||||
|
|
||||||
// Monthly 파티션 처리 (매월 말일만)
|
// 2. Daily 파티션 삭제 (보관기간 초과분)
|
||||||
|
deleteDailyPartitions(today);
|
||||||
|
|
||||||
|
// 3. Monthly 파티션 생성 (매월 말일만)
|
||||||
if (isLastDayOfMonth) {
|
if (isLastDayOfMonth) {
|
||||||
processMonthlyPartitions(today);
|
createMonthlyPartitions(today);
|
||||||
} else {
|
} else {
|
||||||
log.info("Monthly 파티션: 말일이 아니므로 스킵");
|
log.info("Monthly 파티션 생성: 말일이 아니므로 스킵");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Monthly 파티션 삭제 (매월 1일에만, 보관기간 초과분)
|
||||||
|
if (today.getDayOfMonth() == 1) {
|
||||||
|
deleteMonthlyPartitions(today);
|
||||||
|
} else {
|
||||||
|
log.info("Monthly 파티션 삭제: 1일이 아니므로 스킵");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("========================================");
|
log.info("========================================");
|
||||||
@ -76,36 +104,38 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
return date.getDayOfMonth() == YearMonth.from(date).lengthOfMonth();
|
return date.getDayOfMonth() == YearMonth.from(date).lengthOfMonth();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ==================== Daily 파티션 생성 ====================
|
||||||
* Daily 파티션 처리
|
|
||||||
*/
|
|
||||||
private void processDailyPartitions(LocalDate today) {
|
|
||||||
List<PartitionTableInfo> tables = partitionConfig.getDailyPartitionTables();
|
|
||||||
|
|
||||||
if (tables.isEmpty()) {
|
/**
|
||||||
log.info("Daily 파티션: 대상 테이블 없음");
|
* Daily 파티션 생성
|
||||||
|
*/
|
||||||
|
private void createDailyPartitions(LocalDate today) {
|
||||||
|
List<PartitionTableConfig> tables = partitionConfig.getDailyTables();
|
||||||
|
|
||||||
|
if (tables == null || tables.isEmpty()) {
|
||||||
|
log.info("Daily 파티션 생성: 대상 테이블 없음");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Daily 파티션 처리 시작: {} 개 테이블", tables.size());
|
log.info("Daily 파티션 생성 시작: {} 개 테이블", tables.size());
|
||||||
|
|
||||||
for (PartitionTableInfo table : tables) {
|
for (PartitionTableConfig table : tables) {
|
||||||
processDailyPartition(table, today);
|
createDailyPartitionsForTable(table, today);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 개별 Daily 파티션 생성
|
* 개별 테이블 Daily 파티션 생성
|
||||||
*/
|
*/
|
||||||
private void processDailyPartition(PartitionTableInfo table, LocalDate today) {
|
private void createDailyPartitionsForTable(PartitionTableConfig table, LocalDate today) {
|
||||||
List<String> created = new ArrayList<>();
|
List<String> created = new ArrayList<>();
|
||||||
List<String> skipped = new ArrayList<>();
|
List<String> skipped = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0; i <= table.periodsAhead(); i++) {
|
for (int i = 0; i <= table.getPeriodsAhead(); i++) {
|
||||||
LocalDate targetDate = today.plusDays(i);
|
LocalDate targetDate = today.plusDays(i);
|
||||||
String partitionName = getDailyPartitionName(table.tableName(), targetDate);
|
String partitionName = getDailyPartitionName(table.getTableName(), targetDate);
|
||||||
|
|
||||||
if (partitionExists(table.schema(), partitionName)) {
|
if (partitionExists(table.getSchema(), partitionName)) {
|
||||||
skipped.add(partitionName);
|
skipped.add(partitionName);
|
||||||
} else {
|
} else {
|
||||||
createDailyPartition(table, targetDate, partitionName);
|
createDailyPartition(table, targetDate, partitionName);
|
||||||
@ -113,40 +143,97 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[{}] Daily 파티션 - 생성: {}, 스킵: {}",
|
log.info("[{}] Daily 파티션 생성 - 생성: {}, 스킵: {}",
|
||||||
table.tableName(), created.size(), skipped.size());
|
table.getTableName(), created.size(), skipped.size());
|
||||||
|
if (!created.isEmpty()) {
|
||||||
|
log.info("[{}] 생성된 파티션: {}", table.getTableName(), created);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ==================== Daily 파티션 삭제 ====================
|
||||||
* Monthly 파티션 처리
|
|
||||||
*/
|
|
||||||
private void processMonthlyPartitions(LocalDate today) {
|
|
||||||
List<PartitionTableInfo> tables = partitionConfig.getMonthlyPartitionTables();
|
|
||||||
|
|
||||||
if (tables.isEmpty()) {
|
/**
|
||||||
log.info("Monthly 파티션: 대상 테이블 없음");
|
* Daily 파티션 삭제 (보관기간 초과분)
|
||||||
|
*/
|
||||||
|
private void deleteDailyPartitions(LocalDate today) {
|
||||||
|
List<PartitionTableConfig> tables = partitionConfig.getDailyTables();
|
||||||
|
|
||||||
|
if (tables == null || tables.isEmpty()) {
|
||||||
|
log.info("Daily 파티션 삭제: 대상 테이블 없음");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Monthly 파티션 처리 시작: {} 개 테이블", tables.size());
|
log.info("Daily 파티션 삭제 시작: {} 개 테이블", tables.size());
|
||||||
|
|
||||||
for (PartitionTableInfo table : tables) {
|
for (PartitionTableConfig table : tables) {
|
||||||
processMonthlyPartition(table, today);
|
int retentionDays = partitionConfig.getDailyRetentionDays(table.getTableName());
|
||||||
|
deleteDailyPartitionsForTable(table, today, retentionDays);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 개별 Monthly 파티션 생성
|
* 개별 테이블 Daily 파티션 삭제
|
||||||
*/
|
*/
|
||||||
private void processMonthlyPartition(PartitionTableInfo table, LocalDate today) {
|
private void deleteDailyPartitionsForTable(PartitionTableConfig table, LocalDate today, int retentionDays) {
|
||||||
|
LocalDate cutoffDate = today.minusDays(retentionDays);
|
||||||
|
String likePattern = table.getTableName() + "_%";
|
||||||
|
|
||||||
|
List<String> partitions = jdbcTemplate.queryForList(
|
||||||
|
FIND_PARTITIONS_SQL, String.class, table.getSchema(), likePattern);
|
||||||
|
|
||||||
|
List<String> deleted = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String partitionName : partitions) {
|
||||||
|
// 파티션 이름에서 날짜 추출 (table_YYMMDD)
|
||||||
|
LocalDate partitionDate = parseDailyPartitionDate(table.getTableName(), partitionName);
|
||||||
|
if (partitionDate != null && partitionDate.isBefore(cutoffDate)) {
|
||||||
|
dropPartition(table.getSchema(), partitionName);
|
||||||
|
deleted.add(partitionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deleted.isEmpty()) {
|
||||||
|
log.info("[{}] Daily 파티션 삭제 - 보관기간: {}일, 삭제: {} 개",
|
||||||
|
table.getTableName(), retentionDays, deleted.size());
|
||||||
|
log.info("[{}] 삭제된 파티션: {}", table.getTableName(), deleted);
|
||||||
|
} else {
|
||||||
|
log.info("[{}] Daily 파티션 삭제 - 보관기간: {}일, 삭제할 파티션 없음",
|
||||||
|
table.getTableName(), retentionDays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Monthly 파티션 생성 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monthly 파티션 생성
|
||||||
|
*/
|
||||||
|
private void createMonthlyPartitions(LocalDate today) {
|
||||||
|
List<PartitionTableConfig> tables = partitionConfig.getMonthlyTables();
|
||||||
|
|
||||||
|
if (tables == null || tables.isEmpty()) {
|
||||||
|
log.info("Monthly 파티션 생성: 대상 테이블 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Monthly 파티션 생성 시작: {} 개 테이블", tables.size());
|
||||||
|
|
||||||
|
for (PartitionTableConfig table : tables) {
|
||||||
|
createMonthlyPartitionsForTable(table, today);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 테이블 Monthly 파티션 생성
|
||||||
|
*/
|
||||||
|
private void createMonthlyPartitionsForTable(PartitionTableConfig table, LocalDate today) {
|
||||||
List<String> created = new ArrayList<>();
|
List<String> created = new ArrayList<>();
|
||||||
List<String> skipped = new ArrayList<>();
|
List<String> skipped = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0; i <= table.periodsAhead(); i++) {
|
for (int i = 0; i <= table.getPeriodsAhead(); i++) {
|
||||||
LocalDate targetDate = today.plusMonths(i).withDayOfMonth(1);
|
LocalDate targetDate = today.plusMonths(i).withDayOfMonth(1);
|
||||||
String partitionName = getMonthlyPartitionName(table.tableName(), targetDate);
|
String partitionName = getMonthlyPartitionName(table.getTableName(), targetDate);
|
||||||
|
|
||||||
if (partitionExists(table.schema(), partitionName)) {
|
if (partitionExists(table.getSchema(), partitionName)) {
|
||||||
skipped.add(partitionName);
|
skipped.add(partitionName);
|
||||||
} else {
|
} else {
|
||||||
createMonthlyPartition(table, targetDate, partitionName);
|
createMonthlyPartition(table, targetDate, partitionName);
|
||||||
@ -154,27 +241,127 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[{}] Monthly 파티션 - 생성: {}, 스킵: {}",
|
log.info("[{}] Monthly 파티션 생성 - 생성: {}, 스킵: {}",
|
||||||
table.tableName(), created.size(), skipped.size());
|
table.getTableName(), created.size(), skipped.size());
|
||||||
if (!created.isEmpty()) {
|
if (!created.isEmpty()) {
|
||||||
log.info("[{}] 생성된 파티션: {}", table.tableName(), created);
|
log.info("[{}] 생성된 파티션: {}", table.getTableName(), created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Monthly 파티션 삭제 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monthly 파티션 삭제 (보관기간 초과분)
|
||||||
|
*/
|
||||||
|
private void deleteMonthlyPartitions(LocalDate today) {
|
||||||
|
List<PartitionTableConfig> tables = partitionConfig.getMonthlyTables();
|
||||||
|
|
||||||
|
if (tables == null || tables.isEmpty()) {
|
||||||
|
log.info("Monthly 파티션 삭제: 대상 테이블 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Monthly 파티션 삭제 시작: {} 개 테이블", tables.size());
|
||||||
|
|
||||||
|
for (PartitionTableConfig table : tables) {
|
||||||
|
int retentionMonths = partitionConfig.getMonthlyRetentionMonths(table.getTableName());
|
||||||
|
deleteMonthlyPartitionsForTable(table, today, retentionMonths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daily 파티션 이름 생성 (table_YYYY_MM_DD)
|
* 개별 테이블 Monthly 파티션 삭제
|
||||||
|
*/
|
||||||
|
private void deleteMonthlyPartitionsForTable(PartitionTableConfig table, LocalDate today, int retentionMonths) {
|
||||||
|
LocalDate cutoffDate = today.minusMonths(retentionMonths).withDayOfMonth(1);
|
||||||
|
String likePattern = table.getTableName() + "_%";
|
||||||
|
|
||||||
|
List<String> partitions = jdbcTemplate.queryForList(
|
||||||
|
FIND_PARTITIONS_SQL, String.class, table.getSchema(), likePattern);
|
||||||
|
|
||||||
|
List<String> deleted = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String partitionName : partitions) {
|
||||||
|
// 파티션 이름에서 날짜 추출 (table_YYYY_MM)
|
||||||
|
LocalDate partitionDate = parseMonthlyPartitionDate(table.getTableName(), partitionName);
|
||||||
|
if (partitionDate != null && partitionDate.isBefore(cutoffDate)) {
|
||||||
|
dropPartition(table.getSchema(), partitionName);
|
||||||
|
deleted.add(partitionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deleted.isEmpty()) {
|
||||||
|
log.info("[{}] Monthly 파티션 삭제 - 보관기간: {}개월, 삭제: {} 개",
|
||||||
|
table.getTableName(), retentionMonths, deleted.size());
|
||||||
|
log.info("[{}] 삭제된 파티션: {}", table.getTableName(), deleted);
|
||||||
|
} else {
|
||||||
|
log.info("[{}] Monthly 파티션 삭제 - 보관기간: {}개월, 삭제할 파티션 없음",
|
||||||
|
table.getTableName(), retentionMonths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 파티션 이름 생성 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily 파티션 이름 생성 (table_YYMMDD)
|
||||||
*/
|
*/
|
||||||
private String getDailyPartitionName(String tableName, LocalDate date) {
|
private String getDailyPartitionName(String tableName, LocalDate date) {
|
||||||
return tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyy_MM_dd"));
|
return tableName + "_" + date.format(DAILY_PARTITION_FORMAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monthly 파티션 이름 생성 (table_YYYY_MM)
|
* Monthly 파티션 이름 생성 (table_YYYY_MM)
|
||||||
*/
|
*/
|
||||||
private String getMonthlyPartitionName(String tableName, LocalDate date) {
|
private String getMonthlyPartitionName(String tableName, LocalDate date) {
|
||||||
return tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyy_MM"));
|
return tableName + "_" + date.format(MONTHLY_PARTITION_FORMAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 파티션 이름에서 날짜 추출 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily 파티션 이름에서 날짜 추출 (table_YYMMDD -> LocalDate)
|
||||||
|
*/
|
||||||
|
private LocalDate parseDailyPartitionDate(String tableName, String partitionName) {
|
||||||
|
try {
|
||||||
|
String prefix = tableName + "_";
|
||||||
|
if (!partitionName.startsWith(prefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String dateStr = partitionName.substring(prefix.length());
|
||||||
|
// YYMMDD 형식 (6자리)
|
||||||
|
if (dateStr.length() == 6 && dateStr.matches("\\d{6}")) {
|
||||||
|
return LocalDate.parse(dateStr, DAILY_PARTITION_FORMAT);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.trace("파티션 날짜 파싱 실패: {}", partitionName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monthly 파티션 이름에서 날짜 추출 (table_YYYY_MM -> LocalDate)
|
||||||
|
*/
|
||||||
|
private LocalDate parseMonthlyPartitionDate(String tableName, String partitionName) {
|
||||||
|
try {
|
||||||
|
String prefix = tableName + "_";
|
||||||
|
if (!partitionName.startsWith(prefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String dateStr = partitionName.substring(prefix.length());
|
||||||
|
// YYYY_MM 형식 (7자리)
|
||||||
|
if (dateStr.length() == 7 && dateStr.matches("\\d{4}_\\d{2}")) {
|
||||||
|
return LocalDate.parse(dateStr + "_01", DateTimeFormatter.ofPattern("yyyy_MM_dd"));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.trace("파티션 날짜 파싱 실패: {}", partitionName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DB 작업 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 파티션 존재 여부 확인
|
* 파티션 존재 여부 확인
|
||||||
*/
|
*/
|
||||||
@ -186,14 +373,14 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
/**
|
/**
|
||||||
* Daily 파티션 생성
|
* Daily 파티션 생성
|
||||||
*/
|
*/
|
||||||
private void createDailyPartition(PartitionTableInfo table, LocalDate targetDate, String partitionName) {
|
private void createDailyPartition(PartitionTableConfig table, LocalDate targetDate, String partitionName) {
|
||||||
LocalDate endDate = targetDate.plusDays(1);
|
LocalDate endDate = targetDate.plusDays(1);
|
||||||
|
|
||||||
String sql = String.format("""
|
String sql = String.format("""
|
||||||
CREATE TABLE %s.%s PARTITION OF %s
|
CREATE TABLE %s.%s PARTITION OF %s
|
||||||
FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00')
|
FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00')
|
||||||
""",
|
""",
|
||||||
table.schema(), partitionName, table.getFullTableName(),
|
table.getSchema(), partitionName, table.getFullTableName(),
|
||||||
targetDate, endDate);
|
targetDate, endDate);
|
||||||
|
|
||||||
jdbcTemplate.execute(sql);
|
jdbcTemplate.execute(sql);
|
||||||
@ -203,7 +390,7 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
/**
|
/**
|
||||||
* Monthly 파티션 생성
|
* Monthly 파티션 생성
|
||||||
*/
|
*/
|
||||||
private void createMonthlyPartition(PartitionTableInfo table, LocalDate targetDate, String partitionName) {
|
private void createMonthlyPartition(PartitionTableConfig table, LocalDate targetDate, String partitionName) {
|
||||||
LocalDate startDate = targetDate.withDayOfMonth(1);
|
LocalDate startDate = targetDate.withDayOfMonth(1);
|
||||||
LocalDate endDate = startDate.plusMonths(1);
|
LocalDate endDate = startDate.plusMonths(1);
|
||||||
|
|
||||||
@ -211,10 +398,19 @@ public class PartitionManagerTasklet implements Tasklet {
|
|||||||
CREATE TABLE %s.%s PARTITION OF %s
|
CREATE TABLE %s.%s PARTITION OF %s
|
||||||
FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00')
|
FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00')
|
||||||
""",
|
""",
|
||||||
table.schema(), partitionName, table.getFullTableName(),
|
table.getSchema(), partitionName, table.getFullTableName(),
|
||||||
startDate, endDate);
|
startDate, endDate);
|
||||||
|
|
||||||
jdbcTemplate.execute(sql);
|
jdbcTemplate.execute(sql);
|
||||||
log.debug("Monthly 파티션 생성: {}", partitionName);
|
log.debug("Monthly 파티션 생성: {}", partitionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파티션 삭제
|
||||||
|
*/
|
||||||
|
private void dropPartition(String schema, String partitionName) {
|
||||||
|
String sql = String.format("DROP TABLE IF EXISTS %s.%s", schema, partitionName);
|
||||||
|
jdbcTemplate.execute(sql);
|
||||||
|
log.debug("파티션 삭제: {}", partitionName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ public class AisTargetRepositoryImpl implements AisTargetRepository {
|
|||||||
received_date, collected_at, created_at, updated_at
|
received_date, collected_at, created_at, updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?, ?,
|
?, ?, ?, ?, ?, ?, ?,
|
||||||
?, ?, ST_SetSRID(ST_MakePoint(?, ?), 4326),
|
?, ?, public.ST_SetSRID(public.ST_MakePoint(?, ?), 4326),
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?, ?, ?, ?,
|
?, ?, ?, ?, ?, ?, ?,
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
@ -203,9 +203,9 @@ public class AisTargetRepositoryImpl implements AisTargetRepository {
|
|||||||
SELECT DISTINCT ON (mmsi) *
|
SELECT DISTINCT ON (mmsi) *
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE message_timestamp BETWEEN ? AND ?
|
WHERE message_timestamp BETWEEN ? AND ?
|
||||||
AND ST_DWithin(
|
AND public.ST_DWithin(
|
||||||
geom::geography,
|
geom::geography,
|
||||||
ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography,
|
public.ST_SetSRID(public.ST_MakePoint(?, ?), 4326)::geography,
|
||||||
?
|
?
|
||||||
)
|
)
|
||||||
ORDER BY mmsi, message_timestamp DESC
|
ORDER BY mmsi, message_timestamp DESC
|
||||||
|
|||||||
@ -4,7 +4,7 @@ spring:
|
|||||||
|
|
||||||
# PostgreSQL Database Configuration
|
# PostgreSQL Database Configuration
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://10.187.58.58:5432/mdadb?currentSchema=snp_data
|
url: jdbc:postgresql://10.187.58.58:5432/mdadb?currentSchema=snp_data,public
|
||||||
username: mda
|
username: mda
|
||||||
password: mda#8932
|
password: mda#8932
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
@ -28,7 +28,7 @@ spring:
|
|||||||
batch:
|
batch:
|
||||||
jdbc:
|
jdbc:
|
||||||
table-prefix: "snp_data.batch_"
|
table-prefix: "snp_data.batch_"
|
||||||
initialize-schema: always # Changed to 'never' as tables already exist
|
initialize-schema: never # Changed to 'never' as tables already exist
|
||||||
job:
|
job:
|
||||||
enabled: false # Prevent auto-run on startup
|
enabled: false # Prevent auto-run on startup
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ spring:
|
|||||||
quartz:
|
quartz:
|
||||||
job-store-type: jdbc # JDBC store for schedule persistence
|
job-store-type: jdbc # JDBC store for schedule persistence
|
||||||
jdbc:
|
jdbc:
|
||||||
initialize-schema: always # Create Quartz tables if not exist
|
initialize-schema: never # Create Quartz tables if not exist
|
||||||
properties:
|
properties:
|
||||||
org.quartz.scheduler.instanceName: SNPBatchScheduler
|
org.quartz.scheduler.instanceName: SNPBatchScheduler
|
||||||
org.quartz.scheduler.instanceId: AUTO
|
org.quartz.scheduler.instanceId: AUTO
|
||||||
@ -55,9 +55,9 @@ spring:
|
|||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
server:
|
server:
|
||||||
port: 8041
|
port: 9000
|
||||||
servlet:
|
servlet:
|
||||||
context-path: /
|
context-path: /snp-api
|
||||||
|
|
||||||
# Actuator Configuration
|
# Actuator Configuration
|
||||||
management:
|
management:
|
||||||
@ -69,18 +69,10 @@ management:
|
|||||||
health:
|
health:
|
||||||
show-details: always
|
show-details: always
|
||||||
|
|
||||||
# Logging Configuration
|
|
||||||
|
# Logging Configuration (logback-spring.xml에서 상세 설정)
|
||||||
logging:
|
logging:
|
||||||
level:
|
config: classpath:logback-spring.xml
|
||||||
root: INFO
|
|
||||||
com.snp.batch: DEBUG
|
|
||||||
org.springframework.batch: DEBUG
|
|
||||||
org.springframework.jdbc: DEBUG
|
|
||||||
pattern:
|
|
||||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
|
||||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
|
||||||
file:
|
|
||||||
name: logs/snp-batch.log
|
|
||||||
|
|
||||||
# Custom Application Properties
|
# Custom Application Properties
|
||||||
app:
|
app:
|
||||||
|
|||||||
@ -91,15 +91,33 @@ app:
|
|||||||
schedule:
|
schedule:
|
||||||
enabled: true
|
enabled: true
|
||||||
cron: "0 0 * * * ?" # Every hour
|
cron: "0 0 * * * ?" # Every hour
|
||||||
|
|
||||||
# AIS Target 배치 설정
|
# AIS Target 배치 설정
|
||||||
ais-target:
|
ais-target:
|
||||||
since-seconds: 60 # API 조회 범위 (초)
|
since-seconds: 60 # API 조회 범위 (초)
|
||||||
chunk-size: 5000 # 배치 청크 크기
|
chunk-size: 5000 # 배치 청크 크기
|
||||||
schedule:
|
schedule:
|
||||||
cron: "15 * * * * ?" # 매 분 15초 실행
|
cron: "15 * * * * ?" # 매 분 15초 실행
|
||||||
partition:
|
|
||||||
months-ahead: 2 # 미리 생성할 파티션 개월 수
|
|
||||||
# AIS Target 캐시 설정
|
# AIS Target 캐시 설정
|
||||||
ais-target-cache:
|
ais-target-cache:
|
||||||
ttl-minutes: 120 # 캐시 TTL (분) - 2시간
|
ttl-minutes: 120 # 캐시 TTL (분) - 2시간
|
||||||
max-size: 300000 # 최대 캐시 크기 - 30만 건
|
max-size: 300000 # 최대 캐시 크기 - 30만 건
|
||||||
|
|
||||||
|
# 파티션 관리 설정
|
||||||
|
partition:
|
||||||
|
# 일별 파티션 테이블 목록 (네이밍: {table}_YYMMDD)
|
||||||
|
daily-tables:
|
||||||
|
- schema: snp_data
|
||||||
|
table-name: ais_target
|
||||||
|
partition-column: message_timestamp
|
||||||
|
periods-ahead: 3 # 미리 생성할 일수
|
||||||
|
# 월별 파티션 테이블 목록 (네이밍: {table}_YYYY_MM)
|
||||||
|
monthly-tables: [] # 현재 없음
|
||||||
|
# 기본 보관기간
|
||||||
|
retention:
|
||||||
|
daily-default-days: 14 # 일별 파티션 기본 보관기간 (14일)
|
||||||
|
monthly-default-months: 1 # 월별 파티션 기본 보관기간 (1개월)
|
||||||
|
# 개별 테이블 보관기간 설정 (옵션)
|
||||||
|
custom:
|
||||||
|
# - table-name: ais_target
|
||||||
|
# retention-days: 30 # ais_target만 30일 보관
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
-- ============================================
|
-- ============================================
|
||||||
-- 용도: 선박 AIS 위치 정보 저장 (항적 분석용)
|
-- 용도: 선박 AIS 위치 정보 저장 (항적 분석용)
|
||||||
-- 수집 주기: 매 분 15초
|
-- 수집 주기: 매 분 15초
|
||||||
-- 예상 데이터량: 약 33,000건/분
|
-- 예상 데이터량: 약 33,000건/분, 일 20GB (인덱스 포함)
|
||||||
-- 파티셔닝: 월별 파티션 (ais_target_YYYY_MM)
|
-- 파티셔닝: 일별 파티션 (ais_target_YYMMDD)
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
-- PostGIS 확장 활성화 (이미 설치되어 있다면 생략)
|
-- PostGIS 확장 활성화 (이미 설치되어 있다면 생략)
|
||||||
@ -77,18 +77,26 @@ CREATE TABLE IF NOT EXISTS snp_data.ais_target (
|
|||||||
) PARTITION BY RANGE (message_timestamp);
|
) PARTITION BY RANGE (message_timestamp);
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 2. 초기 파티션 생성 (현재 월 + 다음 월)
|
-- 2. 초기 파티션 생성 (현재 일 + 다음 3일)
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 예: 2025년 12월과 2026년 1월 파티션
|
-- 파티션 네이밍: ais_target_YYMMDD
|
||||||
-- 실제 운영 시 create_ais_target_partition 함수로 자동 생성
|
-- 실제 운영 시 partitionManagerJob에서 자동 생성
|
||||||
|
|
||||||
-- 2025년 12월 파티션
|
-- 2024년 12월 4일 파티션 (예시)
|
||||||
CREATE TABLE IF NOT EXISTS snp_data.ais_target_2025_12 PARTITION OF snp_data.ais_target
|
CREATE TABLE IF NOT EXISTS snp_data.ais_target_241204 PARTITION OF snp_data.ais_target
|
||||||
FOR VALUES FROM ('2025-12-01 00:00:00+00') TO ('2026-01-01 00:00:00+00');
|
FOR VALUES FROM ('2024-12-04 00:00:00+00') TO ('2024-12-05 00:00:00+00');
|
||||||
|
|
||||||
-- 2026년 1월 파티션
|
-- 2024년 12월 5일 파티션
|
||||||
CREATE TABLE IF NOT EXISTS snp_data.ais_target_2026_01 PARTITION OF snp_data.ais_target
|
CREATE TABLE IF NOT EXISTS snp_data.ais_target_241205 PARTITION OF snp_data.ais_target
|
||||||
FOR VALUES FROM ('2026-01-01 00:00:00+00') TO ('2026-02-01 00:00:00+00');
|
FOR VALUES FROM ('2024-12-05 00:00:00+00') TO ('2024-12-06 00:00:00+00');
|
||||||
|
|
||||||
|
-- 2024년 12월 6일 파티션
|
||||||
|
CREATE TABLE IF NOT EXISTS snp_data.ais_target_241206 PARTITION OF snp_data.ais_target
|
||||||
|
FOR VALUES FROM ('2024-12-06 00:00:00+00') TO ('2024-12-07 00:00:00+00');
|
||||||
|
|
||||||
|
-- 2024년 12월 7일 파티션
|
||||||
|
CREATE TABLE IF NOT EXISTS snp_data.ais_target_241207 PARTITION OF snp_data.ais_target
|
||||||
|
FOR VALUES FROM ('2024-12-07 00:00:00+00') TO ('2024-12-08 00:00:00+00');
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 3. 인덱스 생성 (각 파티션에 자동 상속)
|
-- 3. 인덱스 생성 (각 파티션에 자동 상속)
|
||||||
@ -120,7 +128,7 @@ CREATE INDEX IF NOT EXISTS idx_ais_target_collected_at
|
|||||||
ON snp_data.ais_target (collected_at DESC);
|
ON snp_data.ais_target (collected_at DESC);
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 4. 파티션 자동 생성 함수
|
-- 4. 파티션 자동 생성 함수 (일별)
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
-- 파티션 존재 여부 확인 함수
|
-- 파티션 존재 여부 확인 함수
|
||||||
@ -137,8 +145,8 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- 특정 월의 파티션 생성 함수
|
-- 특정 일의 파티션 생성 함수
|
||||||
CREATE OR REPLACE FUNCTION snp_data.create_ais_target_partition(target_date DATE)
|
CREATE OR REPLACE FUNCTION snp_data.create_ais_target_daily_partition(target_date DATE)
|
||||||
RETURNS TEXT AS $$
|
RETURNS TEXT AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
partition_name TEXT;
|
partition_name TEXT;
|
||||||
@ -146,12 +154,12 @@ DECLARE
|
|||||||
end_date DATE;
|
end_date DATE;
|
||||||
create_sql TEXT;
|
create_sql TEXT;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- 파티션 이름 생성: ais_target_YYYY_MM
|
-- 파티션 이름 생성: ais_target_YYMMDD
|
||||||
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYYY_MM');
|
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYMMDD');
|
||||||
|
|
||||||
-- 시작/종료 날짜 계산
|
-- 시작/종료 날짜 계산
|
||||||
start_date := DATE_TRUNC('month', target_date)::DATE;
|
start_date := target_date;
|
||||||
end_date := (DATE_TRUNC('month', target_date) + INTERVAL '1 month')::DATE;
|
end_date := target_date + INTERVAL '1 day';
|
||||||
|
|
||||||
-- 이미 존재하면 스킵
|
-- 이미 존재하면 스킵
|
||||||
IF snp_data.partition_exists(partition_name) THEN
|
IF snp_data.partition_exists(partition_name) THEN
|
||||||
@ -175,18 +183,18 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- 다음 N개월 파티션 사전 생성 함수
|
-- 다음 N일 파티션 사전 생성 함수
|
||||||
CREATE OR REPLACE FUNCTION snp_data.create_future_ais_target_partitions(months_ahead INTEGER DEFAULT 2)
|
CREATE OR REPLACE FUNCTION snp_data.create_future_ais_target_daily_partitions(days_ahead INTEGER DEFAULT 3)
|
||||||
RETURNS TABLE (partition_name TEXT, status TEXT) AS $$
|
RETURNS TABLE (partition_name TEXT, status TEXT) AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
i INTEGER;
|
i INTEGER;
|
||||||
target_date DATE;
|
target_date DATE;
|
||||||
result TEXT;
|
result TEXT;
|
||||||
BEGIN
|
BEGIN
|
||||||
FOR i IN 0..months_ahead LOOP
|
FOR i IN 0..days_ahead LOOP
|
||||||
target_date := DATE_TRUNC('month', CURRENT_DATE + (i || ' months')::INTERVAL)::DATE;
|
target_date := CURRENT_DATE + i;
|
||||||
result := snp_data.create_ais_target_partition(target_date);
|
result := snp_data.create_ais_target_daily_partition(target_date);
|
||||||
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYYY_MM');
|
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYMMDD');
|
||||||
status := result;
|
status := result;
|
||||||
RETURN NEXT;
|
RETURN NEXT;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@ -194,17 +202,17 @@ END;
|
|||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 5. 오래된 파티션 삭제 함수
|
-- 5. 오래된 파티션 삭제 함수 (일별)
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
-- 특정 월의 파티션 삭제 함수
|
-- 특정 일의 파티션 삭제 함수
|
||||||
CREATE OR REPLACE FUNCTION snp_data.drop_ais_target_partition(target_date DATE)
|
CREATE OR REPLACE FUNCTION snp_data.drop_ais_target_daily_partition(target_date DATE)
|
||||||
RETURNS TEXT AS $$
|
RETURNS TEXT AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
partition_name TEXT;
|
partition_name TEXT;
|
||||||
drop_sql TEXT;
|
drop_sql TEXT;
|
||||||
BEGIN
|
BEGIN
|
||||||
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYYY_MM');
|
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYMMDD');
|
||||||
|
|
||||||
-- 존재하지 않으면 스킵
|
-- 존재하지 않으면 스킵
|
||||||
IF NOT snp_data.partition_exists(partition_name) THEN
|
IF NOT snp_data.partition_exists(partition_name) THEN
|
||||||
@ -221,17 +229,17 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- N개월 이전 파티션 정리 함수
|
-- N일 이전 파티션 정리 함수
|
||||||
CREATE OR REPLACE FUNCTION snp_data.cleanup_old_ais_target_partitions(retention_months INTEGER DEFAULT 3)
|
CREATE OR REPLACE FUNCTION snp_data.cleanup_old_ais_target_daily_partitions(retention_days INTEGER DEFAULT 14)
|
||||||
RETURNS TABLE (partition_name TEXT, status TEXT) AS $$
|
RETURNS TABLE (partition_name TEXT, status TEXT) AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
rec RECORD;
|
rec RECORD;
|
||||||
partition_date DATE;
|
partition_date DATE;
|
||||||
cutoff_date DATE;
|
cutoff_date DATE;
|
||||||
BEGIN
|
BEGIN
|
||||||
cutoff_date := DATE_TRUNC('month', CURRENT_DATE - (retention_months || ' months')::INTERVAL)::DATE;
|
cutoff_date := CURRENT_DATE - retention_days;
|
||||||
|
|
||||||
-- ais_target_YYYY_MM 패턴의 파티션 조회
|
-- ais_target_YYMMDD 패턴의 파티션 조회
|
||||||
FOR rec IN
|
FOR rec IN
|
||||||
SELECT c.relname
|
SELECT c.relname
|
||||||
FROM pg_class c
|
FROM pg_class c
|
||||||
@ -239,12 +247,13 @@ BEGIN
|
|||||||
JOIN pg_inherits i ON i.inhrelid = c.oid
|
JOIN pg_inherits i ON i.inhrelid = c.oid
|
||||||
WHERE n.nspname = 'snp_data'
|
WHERE n.nspname = 'snp_data'
|
||||||
AND c.relname LIKE 'ais_target_%'
|
AND c.relname LIKE 'ais_target_%'
|
||||||
|
AND LENGTH(c.relname) = 17 -- ais_target_YYMMDD = 17자
|
||||||
AND c.relkind = 'r'
|
AND c.relkind = 'r'
|
||||||
ORDER BY c.relname
|
ORDER BY c.relname
|
||||||
LOOP
|
LOOP
|
||||||
-- 파티션 이름에서 날짜 추출 (ais_target_YYYY_MM)
|
-- 파티션 이름에서 날짜 추출 (ais_target_YYMMDD)
|
||||||
BEGIN
|
BEGIN
|
||||||
partition_date := TO_DATE(SUBSTRING(rec.relname FROM 'ais_target_(\d{4}_\d{2})'), 'YYYY_MM');
|
partition_date := TO_DATE(SUBSTRING(rec.relname FROM 'ais_target_(\d{6})'), 'YYMMDD');
|
||||||
|
|
||||||
IF partition_date < cutoff_date THEN
|
IF partition_date < cutoff_date THEN
|
||||||
EXECUTE format('DROP TABLE snp_data.%I', rec.relname);
|
EXECUTE format('DROP TABLE snp_data.%I', rec.relname);
|
||||||
@ -261,8 +270,8 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- 파티션별 통계 조회 함수
|
-- 파티션별 통계 조회 함수 (일별)
|
||||||
CREATE OR REPLACE FUNCTION snp_data.ais_target_partition_stats()
|
CREATE OR REPLACE FUNCTION snp_data.ais_target_daily_partition_stats()
|
||||||
RETURNS TABLE (
|
RETURNS TABLE (
|
||||||
partition_name TEXT,
|
partition_name TEXT,
|
||||||
row_count BIGINT,
|
row_count BIGINT,
|
||||||
@ -273,11 +282,7 @@ BEGIN
|
|||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
c.relname::TEXT as partition_name,
|
c.relname::TEXT as partition_name,
|
||||||
(SELECT COUNT(*)::BIGINT FROM snp_data.ais_target WHERE message_timestamp >=
|
(pg_stat_get_live_tuples(c.oid))::BIGINT as row_count,
|
||||||
TO_DATE(SUBSTRING(c.relname FROM 'ais_target_(\d{4}_\d{2})'), 'YYYY_MM')
|
|
||||||
AND message_timestamp <
|
|
||||||
TO_DATE(SUBSTRING(c.relname FROM 'ais_target_(\d{4}_\d{2})'), 'YYYY_MM') + INTERVAL '1 month'
|
|
||||||
) as row_count,
|
|
||||||
pg_relation_size(c.oid) as size_bytes,
|
pg_relation_size(c.oid) as size_bytes,
|
||||||
pg_size_pretty(pg_relation_size(c.oid)) as size_pretty
|
pg_size_pretty(pg_relation_size(c.oid)) as size_pretty
|
||||||
FROM pg_class c
|
FROM pg_class c
|
||||||
@ -294,7 +299,7 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- 6. 코멘트
|
-- 6. 코멘트
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
COMMENT ON TABLE snp_data.ais_target IS 'AIS 선박 위치 정보 (매 분 15초 수집, 월별 파티션)';
|
COMMENT ON TABLE snp_data.ais_target IS 'AIS 선박 위치 정보 (매 분 15초 수집, 일별 파티션 - ais_target_YYMMDD)';
|
||||||
|
|
||||||
COMMENT ON COLUMN snp_data.ais_target.mmsi IS 'Maritime Mobile Service Identity (복합 PK)';
|
COMMENT ON COLUMN snp_data.ais_target.mmsi IS 'Maritime Mobile Service Identity (복합 PK)';
|
||||||
COMMENT ON COLUMN snp_data.ais_target.message_timestamp IS 'AIS 메시지 발생 시간 (복합 PK, 파티션 키)';
|
COMMENT ON COLUMN snp_data.ais_target.message_timestamp IS 'AIS 메시지 발생 시간 (복합 PK, 파티션 키)';
|
||||||
@ -308,33 +313,6 @@ COMMENT ON COLUMN snp_data.ais_target.draught IS '흘수 (meters)';
|
|||||||
COMMENT ON COLUMN snp_data.ais_target.collected_at IS '배치 수집 시점';
|
COMMENT ON COLUMN snp_data.ais_target.collected_at IS '배치 수집 시점';
|
||||||
COMMENT ON COLUMN snp_data.ais_target.received_date IS 'API 수신 시간';
|
COMMENT ON COLUMN snp_data.ais_target.received_date IS 'API 수신 시간';
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 유지보수용 함수: 오래된 데이터 정리
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- 오래된 데이터 삭제 함수 (기본: 7일 이전)
|
|
||||||
CREATE OR REPLACE FUNCTION snp_data.cleanup_ais_target(retention_days INTEGER DEFAULT 7)
|
|
||||||
RETURNS INTEGER AS $$
|
|
||||||
DECLARE
|
|
||||||
deleted_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
DELETE FROM snp_data.ais_target
|
|
||||||
WHERE message_timestamp < NOW() - (retention_days || ' days')::INTERVAL;
|
|
||||||
|
|
||||||
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Deleted % rows older than % days', deleted_count, retention_days;
|
|
||||||
|
|
||||||
RETURN deleted_count;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
COMMENT ON FUNCTION snp_data.create_ais_target_partition IS '특정 월의 AIS Target 파티션 생성';
|
|
||||||
COMMENT ON FUNCTION snp_data.create_future_ais_target_partitions IS '향후 N개월 파티션 사전 생성';
|
|
||||||
COMMENT ON FUNCTION snp_data.drop_ais_target_partition IS '특정 월의 파티션 삭제';
|
|
||||||
COMMENT ON FUNCTION snp_data.cleanup_old_ais_target_partitions IS 'N개월 이전 파티션 정리';
|
|
||||||
COMMENT ON FUNCTION snp_data.ais_target_partition_stats IS '파티션별 통계 조회';
|
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 7. 유지보수용 함수: 통계 조회
|
-- 7. 유지보수용 함수: 통계 조회
|
||||||
-- ============================================
|
-- ============================================
|
||||||
@ -362,6 +340,11 @@ END;
|
|||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계 조회';
|
COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계 조회';
|
||||||
|
COMMENT ON FUNCTION snp_data.create_ais_target_daily_partition IS '특정 일의 AIS Target 파티션 생성';
|
||||||
|
COMMENT ON FUNCTION snp_data.create_future_ais_target_daily_partitions IS '향후 N일 파티션 사전 생성';
|
||||||
|
COMMENT ON FUNCTION snp_data.drop_ais_target_daily_partition IS '특정 일의 파티션 삭제';
|
||||||
|
COMMENT ON FUNCTION snp_data.cleanup_old_ais_target_daily_partitions IS 'N일 이전 파티션 정리';
|
||||||
|
COMMENT ON FUNCTION snp_data.ais_target_daily_partition_stats IS '파티션별 통계 조회';
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 예시 쿼리
|
-- 예시 쿼리
|
||||||
@ -373,7 +356,7 @@ COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계
|
|||||||
-- 2. 특정 시간 범위의 항적 조회
|
-- 2. 특정 시간 범위의 항적 조회
|
||||||
-- SELECT * FROM snp_data.ais_target
|
-- SELECT * FROM snp_data.ais_target
|
||||||
-- WHERE mmsi = 123456789
|
-- WHERE mmsi = 123456789
|
||||||
-- AND message_timestamp BETWEEN '2025-12-01 00:00:00+00' AND '2025-12-01 01:00:00+00'
|
-- AND message_timestamp BETWEEN '2024-12-04 00:00:00+00' AND '2024-12-04 01:00:00+00'
|
||||||
-- ORDER BY message_timestamp;
|
-- ORDER BY message_timestamp;
|
||||||
|
|
||||||
-- 3. 특정 구역(원형) 내 선박 조회
|
-- 3. 특정 구역(원형) 내 선박 조회
|
||||||
@ -387,26 +370,19 @@ COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계
|
|||||||
-- )
|
-- )
|
||||||
-- ORDER BY mmsi, message_timestamp DESC;
|
-- ORDER BY mmsi, message_timestamp DESC;
|
||||||
|
|
||||||
-- 4. LineString 항적 생성
|
-- 4. 다음 7일 파티션 미리 생성
|
||||||
-- SELECT mmsi, ST_MakeLine(geom ORDER BY message_timestamp) as track
|
-- SELECT * FROM snp_data.create_future_ais_target_daily_partitions(7);
|
||||||
-- FROM snp_data.ais_target
|
|
||||||
-- WHERE mmsi = 123456789
|
|
||||||
-- AND message_timestamp BETWEEN '2025-12-01 00:00:00+00' AND '2025-12-01 01:00:00+00'
|
|
||||||
-- GROUP BY mmsi;
|
|
||||||
|
|
||||||
-- 5. 다음 3개월 파티션 미리 생성
|
-- 5. 특정 일 파티션 생성
|
||||||
-- SELECT * FROM snp_data.create_future_ais_target_partitions(3);
|
-- SELECT snp_data.create_ais_target_daily_partition('2024-12-10');
|
||||||
|
|
||||||
-- 6. 특정 월 파티션 생성
|
-- 6. 14일 이전 파티션 정리
|
||||||
-- SELECT snp_data.create_ais_target_partition('2026-03-01');
|
-- SELECT * FROM snp_data.cleanup_old_ais_target_daily_partitions(14);
|
||||||
|
|
||||||
-- 7. 3개월 이전 파티션 정리
|
-- 7. 파티션별 통계 조회
|
||||||
-- SELECT * FROM snp_data.cleanup_old_ais_target_partitions(3);
|
-- SELECT * FROM snp_data.ais_target_daily_partition_stats();
|
||||||
|
|
||||||
-- 8. 파티션별 통계 조회
|
-- 8. 전체 통계 조회
|
||||||
-- SELECT * FROM snp_data.ais_target_partition_stats();
|
|
||||||
|
|
||||||
-- 9. 전체 통계 조회
|
|
||||||
-- SELECT * FROM snp_data.ais_target_stats();
|
-- SELECT * FROM snp_data.ais_target_stats();
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
@ -431,12 +407,13 @@ VALUES (
|
|||||||
updated_at = NOW();
|
updated_at = NOW();
|
||||||
|
|
||||||
-- 2. partitionManagerJob: 매일 00:10에 실행
|
-- 2. partitionManagerJob: 매일 00:10에 실행
|
||||||
-- Daily 파티션: 매일 생성, Monthly 파티션: 말일에만 생성 (Job 내부에서 분기)
|
-- Daily 파티션: 매일 생성/삭제 (ais_target_YYMMDD)
|
||||||
|
-- Monthly 파티션: 말일 생성, 1일 삭제 (table_YYYY_MM)
|
||||||
INSERT INTO public.job_schedule (job_name, cron_expression, description, active, created_at, updated_at, created_by, updated_by)
|
INSERT INTO public.job_schedule (job_name, cron_expression, description, active, created_at, updated_at, created_by, updated_by)
|
||||||
VALUES (
|
VALUES (
|
||||||
'partitionManagerJob',
|
'partitionManagerJob',
|
||||||
'0 10 0 * * ?',
|
'0 10 0 * * ?',
|
||||||
'파티션 관리 - 매일 00:10 실행 (Daily: 매일, Monthly: 말일만)',
|
'파티션 관리 - 매일 00:10 실행 (Daily: 생성/삭제, Monthly: 말일 생성/1일 삭제)',
|
||||||
true,
|
true,
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW(),
|
NOW(),
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user