-- DDL: Create Table CREATE TABLE customers ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, name VARCHAR(255), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
-- Test Data INSERT INTO customers (email, name, created_at) VALUES ('alice@example.com', 'Alice Smith', '2023-01-01 10:00:00Z'), ('bob@example.com', 'Bob Johnson', '2023-01-05 11:00:00Z'), ('alice@example.com', 'Alice Smith Duplicate', '2023-01-02 12:00:00Z'), -- Duplicate, created later ('charlie@example.com', 'Charlie Brown', '2023-01-10 09:00:00Z'), ('bob@example.com', 'Bob Johnson Duplicate', '2023-01-06 13:00:00Z'), -- Duplicate, created later ('alice@example.com', 'Alice Smith Another Dup', '2023-01-03 14:00:00Z'), -- Another duplicate, created later ('david@example.com', 'David Lee', '2023-01-15 15:00:00Z');
-- Duplicate Detection Query -- Identifies all duplicate emails and marks which 'id' would be kept (earliest created_at) WITH RankedCustomers AS ( SELECT id, email, name, created_at, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at ASC, id ASC) as rn FROM customers ) SELECT rc.id, rc.email, rc.name, rc.created_at, CASE WHEN rc.rn = 1 THEN 'KEEP' ELSE 'REMOVE' END as action_plan FROM RankedCustomers rc WHERE rc.rn > 1 OR EXISTS (SELECT 1 FROM RankedCustomers rc2 WHERE rc2.email = rc.email AND rc2.rn = 1 AND rc2.id != rc.id) ORDER BY rc.email, rc.created_at;
-- Transactional Cleanup Query BEGIN;
DELETE FROM customers WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at ASC, id ASC) as rn FROM customers ) AS RankedDuplicates WHERE RankedDuplicates.rn > 1 );
-- Optional: SELECT * FROM customers; -- To verify changes before COMMIT -- ROLLBACK; -- Use this to undo changes if something went wrong COMMIT;
-- Expected Execution Plan Notes -- For the DELETE statement, the database typically performs a full table scan or an index scan if a suitable index exists on 'email' and 'created_at'. -- The CTE (RankedDuplicates) will first partition the data by 'email' and order by 'created_at'. This operation can be resource-intensive on large datasets without an optimal index. -- PostgreSQL's query planner will likely use a WindowAgg node for ROW_NUMBER(), which might require sorting the entire table by 'email' and then 'created_at' if no index supports this ordering. -- The subsequent DELETE operation then targets specific 'id' values found by the CTE. If the number of IDs to delete is large, this could lead to significant I/O. -- An efficient index is crucial to speed up the partitioning and ordering phase of the window function.
-- Index Recommendation CREATE INDEX idx_customers_email_created_at ON customers (email, created_at ASC); -- This composite index will allow the database to quickly identify and order records by 'email' and then 'created_at', significantly optimizing the ROW_NUMBER() window function and thus the duplicate detection and deletion process.