三十年来,从 10 用户的 SAP 实施到 10,000 用户的 Oracle 部署,我们一直在优化 ERP 系统。从大型机性能调优到云优化,本指南提供了经过验证的方法论,以实现最大的系统性能。
目录
大型数据集的数据库优化
在优化了支持跨多个 ERP 平台数十亿笔交易的数据库之后,我认识到数据库性能是 ERP 成功的基础。
数据库架构与设计
BigLedger 的最优数据库配置
Database Platform Optimization:
PostgreSQL Configuration:
Memory Settings:
shared_buffers: 25% of total RAM
effective_cache_size: 75% of total RAM
work_mem: RAM/max_connections
maintenance_work_mem: 1-2GB
Connection Management:
max_connections: Based on concurrent users
connection_pooling: PgBouncer implementation
idle_in_transaction_timeout: 30 minutes
statement_timeout: 60 seconds
MySQL Configuration:
InnoDB Settings:
innodb_buffer_pool_size: 70-80% of RAM
innodb_log_file_size: 25% of buffer pool
innodb_flush_log_at_trx_commit: 2
innodb_file_per_table: ON
Query Cache:
query_cache_type: ON
query_cache_size: 256MB-1GB
query_cache_limit: 16MB
SQL Server Configuration:
Memory Management:
max_server_memory: Leave 2-4GB for OS
buffer_pool_extension: SSD storage
columnstore_archive_data_compression: ON
Parallelism:
max_degree_of_parallelism: CPU cores
cost_threshold_for_parallelism: 50
Indexing Strategy:
fill_factor: 85-90%
auto_create_statistics: ON
auto_update_statistics: ON表设计与索引策略
Optimal Table Design:
Partitioning Strategy:
Transaction Tables:
Date-Based Partitioning:
- Monthly partitions for current year
- Quarterly partitions for historical data
- Automatic partition creation
- Partition pruning optimization
Range Partitioning Example:
-- Sales Transaction Table
CREATE TABLE sales_transactions (
transaction_id BIGSERIAL,
transaction_date DATE NOT NULL,
customer_id INTEGER,
amount DECIMAL(18,2),
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY RANGE (transaction_date);
-- Monthly Partitions
CREATE TABLE sales_2024_01 PARTITION OF sales_transactions
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE sales_2024_02 PARTITION OF sales_transactions
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
List Partitioning Example:
-- Customer Table by Region
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
customer_name VARCHAR(255),
region VARCHAR(50),
created_date DATE
) PARTITION BY LIST (region);
CREATE TABLE customers_malaysia PARTITION OF customers
FOR VALUES IN ('MALAYSIA', 'MY');
CREATE TABLE customers_singapore PARTITION OF customers
FOR VALUES IN ('SINGAPORE', 'SG');
Index Optimization:
Primary Performance Indexes:
-- Customer lookup optimization
CREATE INDEX idx_customer_name_active
ON customers (customer_name)
WHERE active = true;
-- Transaction date range queries
CREATE INDEX idx_sales_date_customer
ON sales_transactions (transaction_date, customer_id);
-- Inventory movement tracking
CREATE INDEX idx_inventory_product_date
ON inventory_movements (product_id, movement_date)
INCLUDE (quantity, movement_type);
Composite Index Strategy:
-- Order processing optimization
CREATE INDEX idx_orders_status_date_customer
ON sales_orders (order_status, order_date, customer_id)
WHERE order_status IN ('PENDING', 'PROCESSING');
-- Financial reporting indexes
CREATE INDEX idx_gl_account_period
ON general_ledger (account_code, fiscal_period, posting_date)
INCLUDE (debit_amount, credit_amount);查询优化技术
高级查询性能调优
Query Optimization Framework:
Execution Plan Analysis:
Key Metrics to Monitor:
- Query execution time
- Logical reads per query
- Physical reads ratio
- CPU time consumption
- Memory usage patterns
Common Performance Issues:
Table Scans:
Problem: Full table scan on large tables
Solution: Proper indexing strategy
Query Example:
-- Inefficient query (table scan)
SELECT * FROM sales_transactions
WHERE customer_id = 12345;
-- Optimized query (index seek)
CREATE INDEX idx_sales_customer ON sales_transactions (customer_id);
SELECT transaction_id, transaction_date, amount
FROM sales_transactions
WHERE customer_id = 12345;
Nested Loops:
Problem: Inefficient join operations
Solution: Join optimization and statistics
-- Inefficient nested loop
SELECT c.customer_name, s.total_sales
FROM customers c,
(SELECT customer_id, SUM(amount) as total_sales
FROM sales_transactions
GROUP BY customer_id) s
WHERE c.customer_id = s.customer_id;
-- Optimized join
SELECT c.customer_name, COALESCE(s.total_sales, 0)
FROM customers c
LEFT JOIN (
SELECT customer_id, SUM(amount) as total_sales
FROM sales_transactions
WHERE transaction_date >= '2024-01-01'
GROUP BY customer_id
) s ON c.customer_id = s.customer_id
WHERE c.active = true;
Query Rewriting Techniques:
Subquery to Join Conversion:
-- Inefficient subquery
SELECT product_id, product_name
FROM products
WHERE product_id IN (
SELECT DISTINCT product_id
FROM sales_transactions
WHERE transaction_date >= '2024-01-01'
);
-- Optimized join
SELECT DISTINCT p.product_id, p.product_name
FROM products p
INNER JOIN sales_transactions s ON p.product_id = s.product_id
WHERE s.transaction_date >= '2024-01-01';
Conditional Logic Optimization:
-- Inefficient OR conditions
SELECT * FROM customers
WHERE region = 'MALAYSIA' OR region = 'SINGAPORE';
-- Optimized IN clause
SELECT * FROM customers
WHERE region IN ('MALAYSIA', 'SINGAPORE');
Window Function Optimization:
-- Efficient ranking queries
SELECT
customer_id,
customer_name,
total_sales,
ROW_NUMBER() OVER (ORDER BY total_sales DESC) as rank
FROM (
SELECT
c.customer_id,
c.customer_name,
SUM(s.amount) as total_sales
FROM customers c
JOIN sales_transactions s ON c.customer_id = s.customer_id
WHERE s.transaction_date >= '2024-01-01'
GROUP BY c.customer_id, c.customer_name
) ranked_customers
WHERE total_sales > 100000;数据库维护与监控
主动式数据库管理
Maintenance Procedures:
Statistics Management:
Automated Statistics Updates:
-- PostgreSQL auto-vacuum configuration
ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.1;
ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0.05;
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 2000;
-- SQL Server statistics update job
UPDATE STATISTICS sales_transactions WITH FULLSCAN;
UPDATE STATISTICS customers WITH SAMPLE 25 PERCENT;
Index Maintenance:
-- PostgreSQL index maintenance
REINDEX TABLE sales_transactions;
VACUUM ANALYZE sales_transactions;
-- SQL Server index reorganization
ALTER INDEX idx_sales_date_customer
ON sales_transactions
REORGANIZE;
-- MySQL index optimization
OPTIMIZE TABLE sales_transactions;
Performance Monitoring:
Key Performance Indicators:
Database Response Time:
- Average query response: <100ms
- 95th percentile: <500ms
- Maximum acceptable: <2 seconds
- Peak hour performance: <200ms
Resource Utilization:
- CPU utilization: <70% average
- Memory usage: <80% of allocated
- Disk I/O: <80% capacity
- Network bandwidth: <60% capacity
Concurrency Metrics:
- Active connections: Monitor trends
- Lock wait time: <100ms average
- Deadlock frequency: <1 per hour
- Blocking processes: <5% of queries
Monitoring Queries:
-- PostgreSQL performance monitoring
SELECT
query,
calls,
total_time,
mean_time,
rows
FROM pg_stat_statements
WHERE mean_time > 1000
ORDER BY mean_time DESC
LIMIT 20;
-- SQL Server wait statistics
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms
FROM sys.dm_os_wait_stats
WHERE wait_time_ms > 1000
ORDER BY wait_time_ms DESC;
-- MySQL slow query analysis
SELECT
DIGEST_TEXT,
COUNT_STAR,
AVG_TIMER_WAIT/1000000 as avg_ms,
MAX_TIMER_WAIT/1000000 as max_ms
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 20;
Automated Alerts:
Performance Alert Thresholds:
-- Database response time alerts
WHEN average_response_time > 500ms FOR 5 minutes
THEN send_alert('Database Performance Degradation')
-- Resource utilization alerts
WHEN cpu_usage > 80% FOR 10 minutes
THEN send_alert('High CPU Utilization')
-- Connection pool alerts
WHEN active_connections > 80% of max_connections
THEN send_alert('Connection Pool Nearly Exhausted')
-- Deadlock alerts
WHEN deadlock_count > 5 IN 1 hour
THEN send_alert('High Deadlock Frequency')报告性能调优
报告性能直接影响用户生产力和系统采用率。缓慢的报告是 ERP 实施中的首要抱怨。
报告架构优化
高效的报告框架
Reporting Architecture:
Data Warehouse Approach:
Dimensional Modeling:
Fact Tables:
- sales_fact: Transaction-level sales data
- inventory_fact: Stock movement details
- financial_fact: GL transaction details
- production_fact: Manufacturing metrics
Dimension Tables:
- date_dimension: Complete date hierarchy
- customer_dimension: Customer attributes
- product_dimension: Product hierarchies
- geography_dimension: Location data
Star Schema Example:
-- Sales fact table
CREATE TABLE sales_fact (
fact_id BIGSERIAL PRIMARY KEY,
date_key INTEGER REFERENCES date_dimension(date_key),
customer_key INTEGER REFERENCES customer_dimension(customer_key),
product_key INTEGER REFERENCES product_dimension(product_key),
geography_key INTEGER REFERENCES geography_dimension(geography_key),
sales_amount DECIMAL(18,2),
quantity INTEGER,
cost_amount DECIMAL(18,2),
profit_amount DECIMAL(18,2)
);
-- Optimized indexes for reporting
CREATE INDEX idx_sales_fact_date ON sales_fact (date_key);
CREATE INDEX idx_sales_fact_customer ON sales_fact (customer_key);
CREATE INDEX idx_sales_fact_product ON sales_fact (product_key);
CREATE INDEX idx_sales_fact_combo ON sales_fact (date_key, customer_key, product_key);
Materialized Views:
-- Monthly sales summary
CREATE MATERIALIZED VIEW monthly_sales_summary AS
SELECT
d.year,
d.month,
c.customer_segment,
p.product_category,
SUM(f.sales_amount) as total_sales,
SUM(f.quantity) as total_quantity,
SUM(f.profit_amount) as total_profit,
COUNT(*) as transaction_count
FROM sales_fact f
JOIN date_dimension d ON f.date_key = d.date_key
JOIN customer_dimension c ON f.customer_key = c.customer_key
JOIN product_dimension p ON f.product_key = p.product_key
GROUP BY d.year, d.month, c.customer_segment, p.product_category;
-- Refresh strategy
CREATE OR REPLACE FUNCTION refresh_monthly_summary()
RETURNS void AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales_summary;
END;
$$ LANGUAGE plpgsql;
-- Scheduled refresh (daily at 2 AM)
SELECT cron.schedule('refresh-summary', '0 2 * * *', 'SELECT refresh_monthly_summary();');
Aggregate Tables:
-- Pre-calculated daily summaries
CREATE TABLE daily_sales_summary (
summary_date DATE PRIMARY KEY,
total_sales DECIMAL(18,2),
total_orders INTEGER,
average_order_value DECIMAL(18,2),
top_customer_id INTEGER,
top_product_id INTEGER,
created_at TIMESTAMP DEFAULT NOW()
);
-- Automated population
CREATE OR REPLACE FUNCTION populate_daily_summary(p_date DATE)
RETURNS void AS $$
BEGIN
INSERT INTO daily_sales_summary (
summary_date, total_sales, total_orders,
average_order_value, top_customer_id, top_product_id
)
SELECT
p_date,
SUM(amount),
COUNT(*),
AVG(amount),
(SELECT customer_id FROM sales_transactions
WHERE DATE(transaction_date) = p_date
GROUP BY customer_id
ORDER BY SUM(amount) DESC LIMIT 1),
(SELECT product_id FROM sales_transaction_lines stl
JOIN sales_transactions st ON stl.transaction_id = st.transaction_id
WHERE DATE(st.transaction_date) = p_date
GROUP BY product_id
ORDER BY SUM(quantity) DESC LIMIT 1)
FROM sales_transactions
WHERE DATE(transaction_date) = p_date
ON CONFLICT (summary_date) DO UPDATE SET
total_sales = EXCLUDED.total_sales,
total_orders = EXCLUDED.total_orders,
average_order_value = EXCLUDED.average_order_value,
top_customer_id = EXCLUDED.top_customer_id,
top_product_id = EXCLUDED.top_product_id;
END;
$$ LANGUAGE plpgsql;报告查询优化
高级报告查询技术
Query Optimization for Reports:
Efficient Aggregation:
-- Optimized sales report query
WITH monthly_metrics AS (
SELECT
DATE_TRUNC('month', transaction_date) as month,
customer_id,
SUM(amount) as monthly_sales,
COUNT(*) as transaction_count,
AVG(amount) as avg_transaction
FROM sales_transactions
WHERE transaction_date >= '2024-01-01'
GROUP BY DATE_TRUNC('month', transaction_date), customer_id
),
customer_rankings AS (
SELECT
month,
customer_id,
monthly_sales,
RANK() OVER (PARTITION BY month ORDER BY monthly_sales DESC) as sales_rank
FROM monthly_metrics
)
SELECT
c.customer_name,
cr.month,
cr.monthly_sales,
cr.sales_rank,
mm.transaction_count,
mm.avg_transaction
FROM customer_rankings cr
JOIN customers c ON cr.customer_id = c.customer_id
JOIN monthly_metrics mm ON cr.customer_id = mm.customer_id
AND cr.month = mm.month
WHERE cr.sales_rank <= 10
ORDER BY cr.month, cr.sales_rank;
Parallel Processing:
-- Enable parallel query execution
SET max_parallel_workers_per_gather = 4;
SET parallel_tuple_cost = 0.1;
SET parallel_setup_cost = 1000;
-- Large dataset aggregation with parallel processing
EXPLAIN (ANALYZE, BUFFERS)
SELECT
product_category,
COUNT(*) as product_count,
SUM(sales_amount) as total_sales,
AVG(sales_amount) as avg_sales
FROM large_sales_fact
WHERE transaction_date >= '2023-01-01'
GROUP BY product_category
HAVING SUM(sales_amount) > 1000000;
Column Store Optimization:
-- PostgreSQL columnar extension for analytics
CREATE EXTENSION IF NOT EXISTS columnar;
-- Convert large fact table to columnar format
SELECT alter_table_set_access_method('sales_fact', 'columnar');
-- Compress columnar data
SELECT compress_chunk(chunk_name)
FROM chunk_info
WHERE table_name = 'sales_fact';
Incremental Reporting:
-- Delta processing for large reports
CREATE TABLE report_processing_log (
report_name VARCHAR(100),
last_processed_date TIMESTAMP,
last_processed_id BIGINT,
processing_status VARCHAR(20)
);
-- Incremental data processing function
CREATE OR REPLACE FUNCTION process_incremental_sales_report()
RETURNS void AS $$
DECLARE
last_id BIGINT;
last_date TIMESTAMP;
BEGIN
-- Get last processed position
SELECT last_processed_id, last_processed_date
INTO last_id, last_date
FROM report_processing_log
WHERE report_name = 'daily_sales_summary';
-- Process new records only
INSERT INTO daily_sales_aggregate (
process_date, customer_segment, product_category,
sales_amount, order_count, profit_margin
)
SELECT
DATE(s.transaction_date),
c.customer_segment,
p.product_category,
SUM(s.amount),
COUNT(*),
(SUM(s.amount) - SUM(s.cost_amount)) / SUM(s.amount) * 100
FROM sales_transactions s
JOIN customers c ON s.customer_id = c.customer_id
JOIN products p ON s.product_id = p.product_id
WHERE s.transaction_id > last_id
AND s.transaction_date > last_date
GROUP BY DATE(s.transaction_date), c.customer_segment, p.product_category;
-- Update processing log
UPDATE report_processing_log
SET last_processed_id = (SELECT MAX(transaction_id) FROM sales_transactions),
last_processed_date = NOW(),
processing_status = 'COMPLETED'
WHERE report_name = 'daily_sales_summary';
END;
$$ LANGUAGE plpgsql;报告缓存与交付
智能报告缓存
Caching Strategy:
Multi-Level Caching:
Database Level Caching:
- Query result caching
- Materialized view caching
- Prepared statement caching
- Connection pooling
Application Level Caching:
- Report object caching
- Parameter-based caching
- User session caching
- Memory-based storage
CDN Level Caching:
- Static report delivery
- Geographic distribution
- Edge server caching
- Bandwidth optimization
Cache Implementation:
-- Redis caching for report results
const reportCache = {
// Generate cache key based on report parameters
generateKey: (reportType, parameters) => {
const params = JSON.stringify(parameters);
return `report:${reportType}:${crypto.createHash('md5').update(params).digest('hex')}`;
},
// Cache report with TTL
set: async (key, data, ttlSeconds = 3600) => {
await redis.setex(key, ttlSeconds, JSON.stringify(data));
},
// Retrieve cached report
get: async (key) => {
const cached = await redis.get(key);
return cached ? JSON.parse(cached) : null;
},
// Invalidate cache pattern
invalidatePattern: async (pattern) => {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
};
// Report generation with caching
async function generateSalesReport(parameters) {
const cacheKey = reportCache.generateKey('sales', parameters);
// Check cache first
let reportData = await reportCache.get(cacheKey);
if (reportData) {
return { data: reportData, cached: true };
}
// Generate report if not cached
reportData = await executeReportQuery(parameters);
// Cache for 1 hour
await reportCache.set(cacheKey, reportData, 3600);
return { data: reportData, cached: false };
}
Scheduled Report Generation:
-- Pre-generate popular reports
CREATE TABLE scheduled_reports (
report_id SERIAL PRIMARY KEY,
report_name VARCHAR(100),
report_parameters JSONB,
schedule_expression VARCHAR(50),
last_generated TIMESTAMP,
next_generation TIMESTAMP,
output_format VARCHAR(20),
delivery_method VARCHAR(20),
recipients TEXT[]
);
-- Daily executive dashboard
INSERT INTO scheduled_reports (
report_name, report_parameters, schedule_expression,
output_format, delivery_method, recipients
) VALUES (
'Executive Dashboard',
'{"period": "yesterday", "format": "summary"}',
'0 7 * * *', -- Daily at 7 AM
'PDF',
'EMAIL',
ARRAY['ceo@company.com', 'cfo@company.com']
);
-- Monthly financial package
INSERT INTO scheduled_reports (
report_name, report_parameters, schedule_expression,
output_format, delivery_method, recipients
) VALUES (
'Monthly Financial Package',
'{"period": "last_month", "detail_level": "full"}',
'0 8 1 * *', -- First day of month at 8 AM
'PDF',
'EMAIL',
ARRAY['finance-team@company.com', 'board@company.com']
);
Report Delivery Optimization:
-- Asynchronous report generation
const reportQueue = new Queue('report generation', {
redis: redisConfig,
defaultJobOptions: {
removeOnComplete: 100,
removeOnFail: 50,
attempts: 3,
backoff: 'exponential'
}
});
// Add report to processing queue
reportQueue.add('generate-report', {
reportType: 'financial-summary',
parameters: { period: '2024-01', detail: 'full' },
requesterId: 'user123',
priority: 'high'
}, {
priority: 10,
delay: 0
});
// Process report generation
reportQueue.process('generate-report', 5, async (job) => {
const { reportType, parameters, requesterId } = job.data;
// Update progress
job.progress(10);
// Generate report data
const reportData = await generateReportData(reportType, parameters);
job.progress(60);
// Format report
const formattedReport = await formatReport(reportData, parameters.format);
job.progress(90);
// Deliver report
await deliverReport(formattedReport, requesterId);
job.progress(100);
return { status: 'completed', reportId: formattedReport.id };
});批处理最佳实践
高效的批处理对于月末结账、数据同步和大规模计算等数据密集型操作至关重要。
批处理作业架构
企业级批处理框架
Batch Processing Architecture:
Job Scheduling Framework:
Cron-based Scheduling:
# Daily data synchronization (2 AM)
0 2 * * * /opt/bigledger/scripts/sync_daily_data.sh
# Weekly report generation (Sunday 3 AM)
0 3 * * 0 /opt/bigledger/scripts/generate_weekly_reports.sh
# Monthly closing procedures (First day 4 AM)
0 4 1 * * /opt/bigledger/scripts/monthly_closing.sh
# Quarterly compliance reports (First day of quarter 5 AM)
0 5 1 1,4,7,10 * /opt/bigledger/scripts/quarterly_compliance.sh
Advanced Job Scheduling:
-- Database-driven job scheduler
CREATE TABLE batch_jobs (
job_id SERIAL PRIMARY KEY,
job_name VARCHAR(100) UNIQUE,
job_type VARCHAR(50),
schedule_expression VARCHAR(100),
job_parameters JSONB,
enabled BOOLEAN DEFAULT true,
last_run TIMESTAMP,
next_run TIMESTAMP,
max_runtime_minutes INTEGER DEFAULT 60,
retry_count INTEGER DEFAULT 3,
notification_emails TEXT[],
created_at TIMESTAMP DEFAULT NOW()
);
-- Job execution log
CREATE TABLE batch_job_executions (
execution_id BIGSERIAL PRIMARY KEY,
job_id INTEGER REFERENCES batch_jobs(job_id),
start_time TIMESTAMP DEFAULT NOW(),
end_time TIMESTAMP,
status VARCHAR(20), -- RUNNING, COMPLETED, FAILED, TIMEOUT
records_processed INTEGER,
error_message TEXT,
execution_details JSONB
);
-- Example job definitions
INSERT INTO batch_jobs (
job_name, job_type, schedule_expression, job_parameters,
max_runtime_minutes, notification_emails
) VALUES (
'Daily Customer Synchronization',
'DATA_SYNC',
'0 2 * * *',
'{"source": "CRM", "target": "BigLedger", "chunk_size": 1000}',
30,
ARRAY['it-ops@company.com']
), (
'Monthly Depreciation Calculation',
'FINANCIAL_CALC',
'0 1 1 * *',
'{"calculation_type": "depreciation", "period": "monthly"}',
120,
ARRAY['finance@company.com', 'it-ops@company.com']
);
Parallel Processing Framework:
-- Chunk-based processing for large datasets
CREATE OR REPLACE FUNCTION process_large_dataset(
p_table_name TEXT,
p_processing_function TEXT,
p_chunk_size INTEGER DEFAULT 10000,
p_max_parallel INTEGER DEFAULT 4
)
RETURNS TABLE(chunk_id INTEGER, records_processed INTEGER, processing_time INTERVAL) AS $$
DECLARE
total_records INTEGER;
chunk_count INTEGER;
current_chunk INTEGER;
BEGIN
-- Get total record count
EXECUTE format('SELECT COUNT(*) FROM %I WHERE processing_status = ''PENDING''', p_table_name)
INTO total_records;
chunk_count := CEIL(total_records::FLOAT / p_chunk_size);
-- Process chunks in parallel
FOR current_chunk IN 1..chunk_count LOOP
-- Return chunk processing info
RETURN QUERY
SELECT current_chunk, p_chunk_size, INTERVAL '0 seconds';
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- Example: Parallel customer data processing
SELECT * FROM process_large_dataset(
'customer_updates',
'update_customer_credit_limits',
5000, -- 5000 records per chunk
6 -- 6 parallel processes
);
Error Handling and Recovery:
-- Robust error handling framework
CREATE TABLE batch_job_errors (
error_id BIGSERIAL PRIMARY KEY,
execution_id BIGINT REFERENCES batch_job_executions(execution_id),
error_time TIMESTAMP DEFAULT NOW(),
error_type VARCHAR(50),
error_message TEXT,
stack_trace TEXT,
recovery_action TEXT,
resolved BOOLEAN DEFAULT false
);
-- Checkpoint and restart mechanism
CREATE TABLE batch_job_checkpoints (
checkpoint_id BIGSERIAL PRIMARY KEY,
execution_id BIGINT REFERENCES batch_job_executions(execution_id),
checkpoint_name VARCHAR(100),
checkpoint_data JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Example checkpoint function
CREATE OR REPLACE FUNCTION save_checkpoint(
p_execution_id BIGINT,
p_checkpoint_name TEXT,
p_data JSONB
)
RETURNS void AS $$
BEGIN
INSERT INTO batch_job_checkpoints (
execution_id, checkpoint_name, checkpoint_data
) VALUES (
p_execution_id, p_checkpoint_name, p_data
);
END;
$$ LANGUAGE plpgsql;
-- Recovery function
CREATE OR REPLACE FUNCTION recover_from_checkpoint(
p_execution_id BIGINT,
p_checkpoint_name TEXT
)
RETURNS JSONB AS $$
DECLARE
checkpoint_data JSONB;
BEGIN
SELECT checkpoint_data INTO checkpoint_data
FROM batch_job_checkpoints
WHERE execution_id = p_execution_id
AND checkpoint_name = p_checkpoint_name
ORDER BY created_at DESC
LIMIT 1;
RETURN checkpoint_data;
END;
$$ LANGUAGE plpgsql;批处理作业的性能优化
高性能批处理
Memory Management:
Streaming Data Processing:
-- Process large datasets without loading all into memory
CREATE OR REPLACE FUNCTION stream_process_transactions()
RETURNS void AS $$
DECLARE
transaction_cursor CURSOR FOR
SELECT transaction_id, customer_id, amount, transaction_date
FROM sales_transactions
WHERE processing_status = 'PENDING'
ORDER BY transaction_id;
current_record RECORD;
batch_count INTEGER := 0;
commit_interval INTEGER := 1000;
BEGIN
FOR current_record IN transaction_cursor LOOP
-- Process individual record
PERFORM process_transaction_record(current_record);
batch_count := batch_count + 1;
-- Commit periodically to free locks
IF batch_count % commit_interval = 0 THEN
COMMIT;
RAISE NOTICE 'Processed % records', batch_count;
END IF;
END LOOP;
RAISE NOTICE 'Total records processed: %', batch_count;
END;
$$ LANGUAGE plpgsql;
Bulk Operations:
-- Efficient bulk insert operations
CREATE OR REPLACE FUNCTION bulk_insert_customer_updates(
p_data JSONB[]
)
RETURNS INTEGER AS $$
DECLARE
inserted_count INTEGER;
BEGIN
-- Use COPY for maximum performance
CREATE TEMP TABLE temp_customer_updates (
customer_id INTEGER,
customer_name VARCHAR(255),
credit_limit DECIMAL(18,2),
last_updated TIMESTAMP DEFAULT NOW()
) ON COMMIT DROP;
-- Bulk insert using COPY
COPY temp_customer_updates (customer_id, customer_name, credit_limit)
FROM PROGRAM 'echo ''' || array_to_string(p_data, E'\n') || ''''
WITH (FORMAT csv, DELIMITER ',');
-- Merge with main table
INSERT INTO customers (customer_id, customer_name, credit_limit, last_updated)
SELECT customer_id, customer_name, credit_limit, last_updated
FROM temp_customer_updates
ON CONFLICT (customer_id) DO UPDATE SET
customer_name = EXCLUDED.customer_name,
credit_limit = EXCLUDED.credit_limit,
last_updated = EXCLUDED.last_updated;
GET DIAGNOSTICS inserted_count = ROW_COUNT;
RETURN inserted_count;
END;
$$ LANGUAGE plpgsql;
Database Connection Management:
-- Connection pooling for batch jobs
const batchJobPool = new Pool({
host: 'database-server',
port: 5432,
database: 'bigledger',
user: 'batch_user',
password: process.env.BATCH_DB_PASSWORD,
max: 10, // Maximum pool size
min: 2, // Minimum pool size
acquireTimeoutMillis: 30000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 2000
});
// Batch processing with connection management
async function processBatchJob(jobDefinition) {
const client = await batchJobPool.connect();
try {
await client.query('BEGIN');
const batchSize = jobDefinition.parameters.batchSize || 1000;
let offset = 0;
let processedCount = 0;
while (true) {
const result = await client.query(`
SELECT * FROM ${jobDefinition.sourceTable}
WHERE processing_status = 'PENDING'
ORDER BY id
LIMIT $1 OFFSET $2
`, [batchSize, offset]);
if (result.rows.length === 0) break;
// Process batch
for (const row of result.rows) {
await processRecord(client, row);
processedCount++;
}
// Commit batch
await client.query('COMMIT');
await client.query('BEGIN');
offset += batchSize;
// Progress reporting
console.log(`Processed ${processedCount} records`);
}
await client.query('COMMIT');
return { status: 'completed', recordsProcessed: processedCount };
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
Resource Monitoring:
-- Monitor batch job resource usage
CREATE TABLE batch_job_metrics (
metric_id BIGSERIAL PRIMARY KEY,
execution_id BIGINT REFERENCES batch_job_executions(execution_id),
metric_time TIMESTAMP DEFAULT NOW(),
cpu_usage_percent DECIMAL(5,2),
memory_usage_mb INTEGER,
disk_io_mb INTEGER,
network_io_mb INTEGER,
active_connections INTEGER
);
-- Automated metric collection
CREATE OR REPLACE FUNCTION collect_batch_metrics(p_execution_id BIGINT)
RETURNS void AS $$
BEGIN
INSERT INTO batch_job_metrics (
execution_id, cpu_usage_percent, memory_usage_mb,
disk_io_mb, active_connections
)
SELECT
p_execution_id,
-- CPU usage from system stats
(SELECT cpu_user + cpu_system FROM pg_stat_bgwriter) as cpu_usage,
-- Memory usage estimation
(SELECT setting::INTEGER / 1024 FROM pg_settings WHERE name = 'shared_buffers') as memory_mb,
-- Disk I/O from pg_stat_database
(SELECT blks_read + blks_hit FROM pg_stat_database WHERE datname = current_database()) / 1024 as disk_io,
-- Active connections
(SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as connections;
END;
$$ LANGUAGE plpgsql;历史数据归档策略
有效的数据归档在保持系统性能的同时,为合规和分析保留历史信息。
数据生命周期管理
全面的归档框架
Data Retention Policies:
Regulatory Requirements:
Financial Data (7 years - Malaysian Companies Act):
- General ledger transactions
- Customer and vendor invoices
- Bank statements and reconciliations
- Tax returns and supporting documents
- Audit trails and approvals
Employment Records (12 years - Employment Act):
- Employee personal records
- Payroll and benefits data
- Time and attendance records
- Performance evaluations
- Disciplinary records
Operational Data (3-5 years - Business needs):
- Sales transactions and orders
- Purchase orders and receipts
- Inventory movements
- Production records
- Customer communications
System Data (1-2 years - Performance):
- Application logs
- System metrics
- User activity logs
- Error logs and traces
- Performance monitoring data
Archiving Strategy by Data Type:
Hot Data (Active - 0-12 months):
- Full system performance
- Real-time access required
- Complete indexing
- Backup and replication
- Regular maintenance
Warm Data (Reference - 1-3 years):
- Reduced performance acceptable
- Occasional access required
- Selective indexing
- Compressed storage
- Periodic maintenance
Cold Data (Archive - 3+ years):
- Minimal performance requirements
- Rare access required
- Basic indexing only
- High compression
- Minimal maintenance
Frozen Data (Compliance - 7+ years):
- Compliance access only
- Immutable storage
- Legal hold capability
- Offline storage acceptable
- Audit trail required归档实施策略
技术归档框架
Table Partitioning for Archiving:
Date-Based Partitioning:
-- Sales transactions with automatic archiving
CREATE TABLE sales_transactions (
transaction_id BIGSERIAL,
transaction_date DATE NOT NULL,
customer_id INTEGER,
amount DECIMAL(18,2),
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY RANGE (transaction_date);
-- Current year partitions (Hot data)
CREATE TABLE sales_2024_q1 PARTITION OF sales_transactions
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE sales_2024_q2 PARTITION OF sales_transactions
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- Previous year partitions (Warm data)
CREATE TABLE sales_2023_q1 PARTITION OF sales_transactions
FOR VALUES FROM ('2023-01-01') TO ('2023-04-01')
WITH (
toast_tuple_target = 8160,
fillfactor = 85,
parallel_workers = 2
);
-- Archive table structure (Cold data)
CREATE TABLE sales_transactions_archive (
transaction_id BIGINT,
transaction_date DATE,
customer_id INTEGER,
amount DECIMAL(18,2),
archive_date TIMESTAMP DEFAULT NOW(),
original_table VARCHAR(50)
);
-- Automated archiving procedure
CREATE OR REPLACE FUNCTION archive_old_partitions()
RETURNS void AS $$
DECLARE
partition_name TEXT;
archive_date DATE := CURRENT_DATE - INTERVAL '3 years';
BEGIN
-- Find partitions older than 3 years
FOR partition_name IN
SELECT schemaname||'.'||tablename
FROM pg_tables
WHERE tablename LIKE 'sales_%'
AND tablename < 'sales_' || EXTRACT(YEAR FROM archive_date)
LOOP
-- Move data to archive table
EXECUTE format('
INSERT INTO sales_transactions_archive
SELECT *, NOW(), %L FROM %s',
partition_name, partition_name
);
-- Drop old partition
EXECUTE format('DROP TABLE %s', partition_name);
RAISE NOTICE 'Archived and dropped partition: %', partition_name;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- Schedule monthly archiving
SELECT cron.schedule(
'monthly-archiving',
'0 2 1 * *', -- First day of month at 2 AM
'SELECT archive_old_partitions();'
);
Data Compression Strategies:
-- Enable compression on archive tables
ALTER TABLE sales_transactions_archive
SET (toast_compression = 'lz4');
-- Column-specific compression
CREATE TABLE financial_transactions_archive (
transaction_id BIGINT,
account_code VARCHAR(20),
description TEXT,
amount DECIMAL(18,2),
document_data BYTEA, -- Compressed documents
archive_metadata JSONB
) WITH (
compression = 'lz4',
compress_level = 6
);
-- Automated compression function
CREATE OR REPLACE FUNCTION compress_archive_data()
RETURNS void AS $$
DECLARE
table_name TEXT;
compression_ratio DECIMAL;
BEGIN
FOR table_name IN
SELECT tablename
FROM pg_tables
WHERE tablename LIKE '%_archive'
LOOP
-- Apply compression
EXECUTE format('VACUUM FULL %I', table_name);
-- Report compression ratio
SELECT
pg_total_relation_size(table_name::regclass)::DECIMAL /
pg_relation_size(table_name::regclass)
INTO compression_ratio;
RAISE NOTICE 'Compressed %: ratio %.2f:1', table_name, compression_ratio;
END LOOP;
END;
$$ LANGUAGE plpgsql;
External Storage Integration:
-- Archive to external storage (S3, Azure Blob, etc.)
CREATE EXTENSION IF NOT EXISTS aws_s3 CASCADE;
-- Function to export archive data to S3
CREATE OR REPLACE FUNCTION export_to_s3(
p_table_name TEXT,
p_date_filter DATE,
p_s3_bucket TEXT,
p_s3_key TEXT
)
RETURNS BOOLEAN AS $$
DECLARE
export_query TEXT;
export_result BOOLEAN;
BEGIN
export_query := format('
SELECT aws_s3.query_export_to_s3(
''SELECT * FROM %I WHERE archive_date >= %L'',
aws_commons.create_s3_uri(%L, %L, ''us-east-1''),
options := ''format csv, header true''
)', p_table_name, p_date_filter, p_s3_bucket, p_s3_key);
EXECUTE export_query INTO export_result;
RETURN export_result;
END;
$$ LANGUAGE plpgsql;
-- Schedule S3 export
SELECT cron.schedule(
'quarterly-s3-export',
'0 3 1 1,4,7,10 *', -- Quarterly at 3 AM
'SELECT export_to_s3(''financial_transactions_archive'',
CURRENT_DATE - INTERVAL ''1 year'',
''company-archive-bucket'',
''financial/'' || EXTRACT(YEAR FROM CURRENT_DATE) || ''/q'' || EXTRACT(QUARTER FROM CURRENT_DATE) || ''.csv'');'
);归档访问与检索
高效的归档查询框架
Archive Query Optimization:
Federated Query System:
-- Create foreign data wrapper for archive
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- Connect to archive database
CREATE SERVER archive_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'archive-db-server', port '5432', dbname 'bigledger_archive');
-- User mapping for archive access
CREATE USER MAPPING FOR bigledger_user
SERVER archive_server
OPTIONS (user 'archive_reader', password 'secure_password');
-- Foreign table for archived data
CREATE FOREIGN TABLE sales_transactions_archive_remote (
transaction_id BIGINT,
transaction_date DATE,
customer_id INTEGER,
amount DECIMAL(18,2),
archive_date TIMESTAMP
)
SERVER archive_server
OPTIONS (schema_name 'public', table_name 'sales_transactions_archive');
-- Unified view combining current and archived data
CREATE VIEW sales_transactions_complete AS
SELECT
transaction_id, transaction_date, customer_id, amount,
'CURRENT' as data_source
FROM sales_transactions
WHERE transaction_date >= CURRENT_DATE - INTERVAL '3 years'
UNION ALL
SELECT
transaction_id, transaction_date, customer_id, amount,
'ARCHIVE' as data_source
FROM sales_transactions_archive_remote
WHERE transaction_date < CURRENT_DATE - INTERVAL '3 years';
Archive Search Optimization:
-- Create search indexes on archive tables
CREATE INDEX idx_archive_customer_date
ON sales_transactions_archive (customer_id, transaction_date);
CREATE INDEX idx_archive_amount_range
ON sales_transactions_archive (amount)
WHERE amount > 10000;
-- Full-text search for archived documents
CREATE INDEX idx_archive_description_fts
ON financial_transactions_archive
USING gin(to_tsvector('english', description));
-- Optimized archive search function
CREATE OR REPLACE FUNCTION search_archived_transactions(
p_customer_id INTEGER DEFAULT NULL,
p_date_from DATE DEFAULT NULL,
p_date_to DATE DEFAULT NULL,
p_amount_min DECIMAL DEFAULT NULL,
p_amount_max DECIMAL DEFAULT NULL,
p_description TEXT DEFAULT NULL
)
RETURNS TABLE(
transaction_id BIGINT,
transaction_date DATE,
customer_id INTEGER,
amount DECIMAL(18,2),
description TEXT,
data_source TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
t.transaction_id,
t.transaction_date,
t.customer_id,
t.amount,
COALESCE(t.description, '') as description,
'ARCHIVE' as data_source
FROM sales_transactions_archive t
WHERE (p_customer_id IS NULL OR t.customer_id = p_customer_id)
AND (p_date_from IS NULL OR t.transaction_date >= p_date_from)
AND (p_date_to IS NULL OR t.transaction_date <= p_date_to)
AND (p_amount_min IS NULL OR t.amount >= p_amount_min)
AND (p_amount_max IS NULL OR t.amount <= p_amount_max)
AND (p_description IS NULL OR t.description ILIKE '%' || p_description || '%')
ORDER BY t.transaction_date DESC
LIMIT 1000;
END;
$$ LANGUAGE plpgsql;
Data Recovery Procedures:
-- Emergency data recovery from archive
CREATE OR REPLACE FUNCTION restore_from_archive(
p_transaction_ids BIGINT[],
p_target_table TEXT DEFAULT 'sales_transactions'
)
RETURNS INTEGER AS $$
DECLARE
restored_count INTEGER;
BEGIN
-- Restore specific transactions from archive
EXECUTE format('
INSERT INTO %I (transaction_id, transaction_date, customer_id, amount)
SELECT transaction_id, transaction_date, customer_id, amount
FROM sales_transactions_archive
WHERE transaction_id = ANY($1)
ON CONFLICT (transaction_id) DO NOTHING',
p_target_table
) USING p_transaction_ids;
GET DIAGNOSTICS restored_count = ROW_COUNT;
-- Log restoration activity
INSERT INTO archive_access_log (
access_type, target_table, record_count,
access_user, access_time
) VALUES (
'RESTORE', p_target_table, restored_count,
current_user, NOW()
);
RETURN restored_count;
END;
$$ LANGUAGE plpgsql;
-- Bulk data restoration for specific date range
CREATE OR REPLACE FUNCTION restore_date_range(
p_date_from DATE,
p_date_to DATE,
p_verification_required BOOLEAN DEFAULT true
)
RETURNS TABLE(
status TEXT,
records_restored INTEGER,
verification_passed BOOLEAN
) AS $$
DECLARE
restore_count INTEGER;
verification_result BOOLEAN := true;
BEGIN
-- Restore data from archive
INSERT INTO sales_transactions_temp (
SELECT * FROM sales_transactions_archive
WHERE transaction_date BETWEEN p_date_from AND p_date_to
);
GET DIAGNOSTICS restore_count = ROW_COUNT;
-- Verification if requested
IF p_verification_required THEN
SELECT COUNT(*) = restore_count INTO verification_result
FROM sales_transactions_temp
WHERE transaction_date BETWEEN p_date_from AND p_date_to;
END IF;
RETURN QUERY SELECT
'COMPLETED'::TEXT,
restore_count,
verification_result;
END;
$$ LANGUAGE plpgsql;本综合性能优化指南为在企业规模上维护高性能 BigLedger 实施奠定了基础。成功的关键在于主动监控、智能缓存、高效批处理和战略性数据归档。
每个部分都包含基于三十年跨多种环境优化 ERP 系统经验的实用实施指导。重点是创造可随业务增长而扩展的可持续性能,同时保持数据完整性和合规要求。