option('add-months'); $retentionMonths = (int) $this->option('retention-months'); $doDrop = $this->option('drop'); $dryRun = $this->option('dry-run'); $this->info('=== 트리거 감사 로그 파티션 관리 ==='); $this->newLine(); // 현재 파티션 목록 조회 $partitions = $this->getPartitions(); $this->info('현재 파티션: '.count($partitions).'개'); $this->table( ['파티션명', '상한값 (UNIX_TIMESTAMP)', '행 수'], collect($partitions)->map(fn ($p) => [ $p->PARTITION_NAME, $p->PARTITION_DESCRIPTION === 'MAXVALUE' ? 'MAXVALUE' : Carbon::createFromTimestamp($p->PARTITION_DESCRIPTION)->format('Y-m-d'), number_format($p->TABLE_ROWS), ])->toArray() ); $this->newLine(); // 1. 미래 파티션 추가 $added = $this->addFuturePartitions($partitions, $addMonths, $dryRun); // 2. 오래된 파티션 삭제 $dropped = 0; if ($doDrop) { $dropped = $this->dropOldPartitions($partitions, $retentionMonths, $dryRun); } else { $this->warn('파티션 삭제는 --drop 옵션 필요'); } $this->newLine(); $this->info("결과: 추가 {$added}개, 삭제 {$dropped}개".($dryRun ? ' (dry-run)' : '')); return self::SUCCESS; } private function getPartitions(): array { return DB::select(" SELECT PARTITION_NAME, PARTITION_DESCRIPTION, TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs' AND PARTITION_NAME IS NOT NULL ORDER BY PARTITION_ORDINAL_POSITION ", [config('database.connections.mysql.database')]); } private function addFuturePartitions(array $partitions, int $addMonths, bool $dryRun): int { $existingBounds = []; foreach ($partitions as $p) { if ($p->PARTITION_DESCRIPTION !== 'MAXVALUE') { $existingBounds[] = (int) $p->PARTITION_DESCRIPTION; } } $added = 0; $now = Carbon::now(); for ($i = 0; $i <= $addMonths; $i++) { $target = $now->copy()->addMonths($i)->startOfMonth()->addMonth(); $ts = $target->timestamp; $name = 'p'.$target->copy()->subMonth()->format('Ym'); if (in_array($ts, $existingBounds)) { continue; } $sql = "ALTER TABLE trigger_audit_logs REORGANIZE PARTITION p_future INTO ( PARTITION {$name} VALUES LESS THAN ({$ts}), PARTITION p_future VALUES LESS THAN MAXVALUE )"; if ($dryRun) { $this->line(" [DRY-RUN] 추가: {$name} (< {$target->format('Y-m-d')})"); } else { DB::statement($sql); $this->info(" 추가: {$name} (< {$target->format('Y-m-d')})"); } $added++; } if ($added === 0) { $this->info(' 추가할 파티션 없음'); } return $added; } private function dropOldPartitions(array $partitions, int $retentionMonths, bool $dryRun): int { $cutoff = Carbon::now()->subMonths($retentionMonths)->startOfMonth()->timestamp; $dropped = 0; foreach ($partitions as $p) { if ($p->PARTITION_DESCRIPTION === 'MAXVALUE') { continue; } $bound = (int) $p->PARTITION_DESCRIPTION; if ($bound <= $cutoff) { if ($dryRun) { $this->line(" [DRY-RUN] 삭제: {$p->PARTITION_NAME} ({$p->TABLE_ROWS}행)"); } else { DB::statement("ALTER TABLE trigger_audit_logs DROP PARTITION {$p->PARTITION_NAME}"); $this->warn(" 삭제: {$p->PARTITION_NAME} ({$p->TABLE_ROWS}행)"); } $dropped++; } } if ($dropped === 0) { $this->info(' 삭제할 파티션 없음'); } return $dropped; } }