sql 254 lines · 2 tabs

Data archival and retention strategies

Maria Garcia Feb 2026
2 tabs
-- Partition-based archival strategy

-- Create partitioned table
CREATE TABLE logs (
  id BIGSERIAL,
  level VARCHAR(20),
  message TEXT,
  created_at TIMESTAMP NOT NULL,
  PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Create monthly partitions
CREATE TABLE logs_2024_01 PARTITION OF logs
  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE logs_2024_02 PARTITION OF logs
  FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Archive old partition
-- 1. Detach partition (quick operation)
ALTER TABLE logs DETACH PARTITION logs_2024_01;

-- 2. Export to file
COPY logs_2024_01 TO '/archive/logs_2024_01.csv' WITH (FORMAT csv, HEADER);

-- Or compress while exporting
COPY logs_2024_01 TO PROGRAM 'gzip > /archive/logs_2024_01.csv.gz'
WITH (FORMAT csv);

-- 3. Drop partition
DROP TABLE logs_2024_01;

-- Automated archival function
CREATE OR REPLACE FUNCTION archive_old_partitions(
  table_name TEXT,
  retention_months INT,
  archive_path TEXT
)
RETURNS VOID AS $$
DECLARE
  partition_record RECORD;
  cutoff_date DATE;
BEGIN
  cutoff_date := CURRENT_DATE - (retention_months || ' months')::INTERVAL;

  FOR partition_record IN
    SELECT
      c.relname AS partition_name,
      pg_get_expr(c.relpartbound, c.oid) AS partition_bound
    FROM pg_class c
    JOIN pg_inherits i ON c.oid = i.inhrelid
    JOIN pg_class p ON i.inhparent = p.oid
    WHERE p.relname = table_name
      AND c.relname ~ '\d{4}_\d{2}$'
      AND c.relname < table_name || '_' || TO_CHAR(cutoff_date, 'YYYY_MM')
  LOOP
    RAISE NOTICE 'Archiving partition: %', partition_record.partition_name;

    -- Detach
    EXECUTE format('ALTER TABLE %I DETACH PARTITION %I',
                  table_name, partition_record.partition_name);

    -- Export
    EXECUTE format(
      'COPY %I TO PROGRAM ''gzip > %s/%I.csv.gz'' WITH (FORMAT csv)',
      partition_record.partition_name,
      archive_path,
      partition_record.partition_name
    );

    -- Drop
    EXECUTE format('DROP TABLE %I', partition_record.partition_name);
  END LOOP;
END;
$$ LANGUAGE plpgsql;

-- Run archival
SELECT archive_old_partitions('logs', 12, '/archive');

-- Soft delete pattern
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP;
CREATE INDEX idx_users_deleted_at ON users(deleted_at);

-- Soft delete
UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = 123;

-- Query active records only
SELECT * FROM users WHERE deleted_at IS NULL;

-- Archive soft-deleted records
CREATE TABLE users_archive (LIKE users INCLUDING ALL);

INSERT INTO users_archive
SELECT * FROM users
WHERE deleted_at < CURRENT_DATE - INTERVAL '90 days';

DELETE FROM users
WHERE deleted_at < CURRENT_DATE - INTERVAL '90 days';

-- Audit log with automatic archival
CREATE TABLE audit_log (
  id BIGSERIAL PRIMARY KEY,
  table_name VARCHAR(50),
  record_id INT,
  action VARCHAR(20),
  old_data JSONB,
  new_data JSONB,
  user_id INT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Partition by month
CREATE TABLE audit_log_2024_01 PARTITION OF audit_log
  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- Retention policy: Keep 2 years
SELECT add_retention_policy('audit_log', INTERVAL '2 years');

-- Compressed archive table
CREATE TABLE orders_archive (
  id BIGINT PRIMARY KEY,
  user_id INT,
  total DECIMAL(10,2),
  status VARCHAR(20),
  created_at TIMESTAMP,
  archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Enable compression (TimescaleDB)
ALTER TABLE orders_archive SET (
  timescaledb.compress,
  timescaledb.compress_segmentby = 'user_id',
  timescaledb.compress_orderby = 'created_at DESC'
);

-- Archive completed orders older than 1 year
WITH archived AS (
  INSERT INTO orders_archive (id, user_id, total, status, created_at)
  SELECT id, user_id, total, status, created_at
  FROM orders
  WHERE status = 'completed'
    AND created_at < CURRENT_DATE - INTERVAL '1 year'
  RETURNING id
)
DELETE FROM orders
WHERE id IN (SELECT id FROM archived);
2 files · sql Explain with highlit

Data archival moves old data to cheaper storage. I use partitioning for time-based archival. Detach old partitions, export to S3, drop table. Archive tables store historical data with reduced indexes. Understanding retention policies prevents runaway growth. Cold storage uses compression for space savings. Soft deletes mark records for later archival. Audit requirements dictate retention periods. Restoring archived data requires documented procedures. Proper archival balances compliance, cost, performance. Essential for log data, transactions, historical records. PostgreSQL table inheritance simplifies archival workflows.


Related snips

sql
-- Simple function
CREATE OR REPLACE FUNCTION get_full_name(
  first_name VARCHAR,
  last_name VARCHAR
)
RETURNS VARCHAR AS $$

Stored procedures and functions in PostgreSQL

postgresql stored-procedures functions
by Maria Garcia 2 tabs
ruby
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]

sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first

SQL injection prevention with unsafe and safe query patterns

sql-injection owasp database
by Kai Nakamura 3 tabs
sql
-- EXPLAIN ANALYZE (actual execution statistics)
EXPLAIN ANALYZE
SELECT u.username, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2024-01-01'

Advanced query optimization techniques

database optimization query-performance
by Maria Garcia 2 tabs
python
from django.db.models import Count, Avg, Sum, Q, F
from django.views.generic import TemplateView
from products.models import Product, Order, OrderItem


class DashboardView(TemplateView):

Django aggregation with annotate for statistics

django python database
by Priya Sharma 1 tab
ruby
class ReportQuery
  SQL = <<~SQL.freeze
    SELECT date_trunc('day', events.created_at) AS day,
           count(*) AS total,
           count(*) FILTER (WHERE events.kind = 'purchase') AS purchases
    FROM events

Safe Raw SQL with exec_query + Binds

rails activerecord sql
by codesnips 2 tabs
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Laravel database migrations for schema management

laravel migrations database
by Carlos Mendez 3 tabs

Share this code

Here's the card — post it anywhere.

Data archival and retention strategies — share card
Link copied