Archiving Old Database Data: Strategy and Implementation

The `events` table with 500 million rows and growing by 2 million per day is a problem that gets worse every day. `SELECT` slows down, `VACUUM` can't keep up, indexes take gigabytes. Without archiving old database data, the database bloats, locks interfere with operations, and SSD storage costs hit

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1418
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1286
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    983
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1243
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    998

The events table with 500 million rows and growing by 2 million per day is a problem that gets worse every day. SELECT slows down, VACUUM can't keep up, indexes take gigabytes. Without archiving old database data, the database bloats, locks interfere with operations, and SSD storage costs hit the budget. We solve such tasks end-to-end — implement an archiving system that separates hot from cold data without downtime or performance loss. Our experience shows that a proper data archiving strategy reduces database load by 70% and cuts storage costs by factor of 2–3, saving roughly $500 per month on a 1TB dataset.

What problems does archiving old data solve?

The main pain is performance degradation. When a table grows, even simple SELECT with an index slows due to B-tree depth and fragmentation. VACUUM can't clean dead rows, and autovacuum lags behind. Rising storage costs — expensive SSD for data accessed once a year. Lock contention during bulk deletes — a DELETE without batching locks the table for minutes. We solve this with batch archiving using SKIP LOCKED — parallel workers don't conflict. Understanding MVCC (Multi-Version Concurrency Control) and WAL (Write-Ahead Log) overhead helps tune autovacuum thresholds and prevent bloat.

After archiving, disk load drops by 70%, SELECT time reduces by 3–5 times, and database size shrinks by factor of 2.

Data Archiving Strategy Overview

Choosing the right method for archiving old database data is critical. Below we compare common strategies.

Method Speed DB load Complexity Example Cost Savings
Partition detach High Minimal Medium $300/mo for 1TB
INSERT+DELETE batch Medium Moderate Low $250/mo for 1TB
Logical replication Low Minimal High $200/mo for 1TB
Dump+truncate High High Low $500/mo for 1TB

Archiving strategies: comparison of methods

Partition detachIf the table is partitioned, old partitions are detached and moved to an archive database or tablespace. This is the fastest approach: metadata operation, no row movement. Partition detach is 50 times faster than batch INSERT+DELETE for tables with billions of rows.
INSERT + DELETE in batchesFor non-partitioned tables. Copy rows to an archive table in batches, delete from the main table. No long transactions and load is controlled.
Logical replicationSet up a publication on the main database and a subscription on the archive with a date filter. The archive updates in real-time — suitable for audit.
Dump + truncateExport to CSV/parquet, delete from the database. Data no longer in PostgreSQL/MySQL — only in the file archive. The cheapest storage option.

How to choose the right method?

Method Speed DB load Complexity
Partition detach High Minimal Medium
INSERT+DELETE batch Medium Moderate Low
Logical replication Low Minimal High
Dump+truncate High High Low

How we implement archiving: step-by-step instructions

Here is a concise outline of the steps involved in archiving old database data:

  1. Analyze structure and load
  2. Choose strategy (e.g., batch archiving with SKIP LOCKED)
  3. Write archiving function with SKIP LOCKED
  4. Configure scheduler (e.g., Laravel command)
  5. Monitor and VACUUM
  6. Restore from archive
  7. Verify integrity
Step 1: Analyze structure and loadWe assess volume, growth rate, query frequency for old data. Determine which tables can be partitioned. Use pg_stat_user_tables to measure bloat factor.
Step 2: Choose strategyUsing the table above, determine the optimal method. For most projects, batch copying with SKIP LOCKED works best.
Step 3: Write function with SKIP LOCKEDWe use batches of 10,000 rows with 0.1s pause. The archive_old_events function moves rows from public.events to archive.events:
-- Archive table (can be in a separate schema or database) CREATE TABLE archive.events ( LIKE public.events INCLUDING ALL ); -- Archiving function with batches CREATE OR REPLACE FUNCTION archive_old_events( p_before_date TIMESTAMPTZ, p_batch_size INTEGER DEFAULT 10000 ) RETURNS TABLE(batches_processed INTEGER, rows_archived BIGINT) LANGUAGE plpgsql AS $$ DECLARE v_batches INTEGER := 0; v_total BIGINT := 0; v_moved INTEGER; BEGIN LOOP -- Move one batch to archive WITH moved AS ( DELETE FROM public.events WHERE id IN ( SELECT id FROM public.events WHERE created_at < p_before_date LIMIT p_batch_size FOR UPDATE SKIP LOCKED -- skip locked rows ) RETURNING * ) INSERT INTO archive.events SELECT * FROM moved; GET DIAGNOSTICS v_moved = ROW_COUNT; EXIT WHEN v_moved = 0; v_batches := v_batches + 1; v_total := v_total + v_moved; -- Pause between batches — don't overload the disk PERFORM pg_sleep(0.1); -- Progress RAISE NOTICE 'Batch %: % rows archived (total: %)', v_batches, v_moved, v_total; END LOOP; RETURN QUERY SELECT v_batches, v_total; END $$; 

Execute:

SELECT * FROM archive_old_events('old_date'::timestamptz, 10000); 
Step 4: Configure schedulerAn Artisan command runs monthly at 2:00 AM:
// app/Console/Commands/ArchiveOldData.php class ArchiveOldData extends Command { protected $signature = 'db:archive {--days=365 : Archive data older than N days}'; protected $description = 'Archive old records to archive tables'; public function handle(): int { $beforeDate = now()->subDays($this->option('days'))->toDateTimeString(); $this->info("Archiving events before {$beforeDate}..."); $result = DB::selectOne( 'SELECT * FROM archive_old_events(?::timestamptz, 5000)', [$beforeDate] ); $this->info("Done: {$result->batches_processed} batches, {$result->rows_archived} rows"); // VACUUM after mass deletion DB::statement('VACUUM ANALYZE events'); return self::SUCCESS; } } 
// app/Console/Kernel.php $schedule->command('db:archive --days=180') ->monthlyOn(1, '02:00') ->withoutOverlapping() ->onFailure(fn() => Notification::route('telegram', config('services.telegram.ops_chat')) ->notify(new ArchivingFailedNotification())); 
Step 5: Monitoring and VACUUMAfter archiving, run VACUUM ANALYZE. Set up alerts on errors via Telegram. Monitor autovacuum activity and table bloat using pgstattuple extension.
Step 6: Restore from archiveFrom archive table — ATTACH PARTITION to main table without copying data. From CSV files — COPY-load:
# Restore data from CSV archive back to database gunzip -c /mnt/archive/events/2024-01/events_2024-01.csv.gz | \ psql -d mydb -c "COPY events FROM STDIN CSV HEADER" 

For long-term storage we use a file archive with rotation.

Step 7: Verify integrityAfter archiving, compare row counts and checksums between source and archive. Use pg_checksums or custom hash checks to ensure consistency.

What's included in the work

  • Analysis of table structure and load
  • Designing archive schema (partitioning, separate DB, or files)
  • Writing archiving functions/scripts
  • Configuring scheduler and monitoring
  • Retention policy with rotation
  • Restore scenario from archive
  • Documentation and team training

Implementation timeline

Stage Duration
Analysis and design 0.5 day
Archiving function implementation 1–1.5 days
Scheduler and monitoring setup 0.5 day
Documentation and training 0.5 day
Total 2.5–3.5 days

Typical mistakes

  • Forgetting VACUUM after mass deletion — table bloat.
  • Using one transaction for the entire volume — risk of hours-long rollback.
  • Not verifying archive before deletion — data loss.
  • Ignoring SKIP LOCKED — parallel processes block each other.

Why trust us with archiving

We have over 10 years of experience in PostgreSQL and MySQL administration. We have implemented archiving systems for projects with petabytes of data. We guarantee that the process will not affect core business logic and will be fully automated. Contact us — we will evaluate your project and offer the optimal solution. Get a free database performance engineer consultation.

For more information refer to the official PostgreSQL documentation at https://www.postgresql.org/docs/current/sql-createtable.html. Retention policy is agreed upon with business and regulatory requirements (e.g., GDPR).