Three Thousand Small CSV Files and a Duplicate Feed
An upstream application drops 22 CSV files per hour into HDFS under /data/events/dt=YYYY-MM-DD/hour=HH - about 3,100 files per day, averaging 4 MB each. Analysts query an external Hive table over that directory using OpenCSVSerde and see a simple GROUP BY event_type degrade from 2 minutes to 18 minutes over three months; the NameNode RPC queue length frequently exceeds 200. The feed retries failed batches, so roughly 2.5% of rows are duplicates on event_id that arrive with a later ingestion_ts. Revenue dashboards must count each event exactly once. The application team will not change its output format, there is no budget for a streaming rewrite this quarter, and the table is external and non-ACID. The analyst proposes setting hive.merge.mapfiles and hive.merge.smallfiles.avgsize and re-running the dashboard query.
1. Can the hive.merge settings fix the 3,100-files-per-day problem? Explain what those settings actually do and what the real fix is.
Read the worked answer
No. hive.merge.mapfiles, hive.merge.smallfiles.avgsize, and hive.merge.mapredfiles are write-time options for Hive jobs: they merge the files that a Hive query or INSERT produces. The application writes directly to HDFS, so Hive never sees those files as output and will never rewrite them. Re-running the dashboard query changes nothing on disk.
Even a compaction job that copied the files would leave the bigger problem: OpenCSVSerde means text parsing of every column, no column pruning, and no predicate pushdown on non-partition columns. The 18-minute query is paying for reading all columns of all ~12 GB per day of text.
The real fix is a one-time rewrite into a columnar format, plus control of file size:
- Create a new external Parquet table with the same partition spec (
dt, andhourif hour-level pruning is actually used). - Rewrite with a deduplicating INSERT that controls output file count -
DISTRIBUTE BY dt, hour(or a repartition to roughly 128–256 MB per file) so each reducer writes one or a few files instead of thousands. - Point a view at the new table, validate, and keep the old table for a rollback window.
With Parquet and ~200 MB files, a day is a few dozen files instead of 3,100, and the GROUP BY reads only event_type plus the partition column.
2. Write the deduplication and rewrite logic. Why does the window function need DISTRIBUTE BY, and how do you make reruns safe?
Read the worked answer
INSERT OVERWRITE TABLE events_clean PARTITION (dt = '${dt}')
SELECT event_id, user_id, event_type, event_ts, ingestion_ts, ...
FROM (
SELECT e.*,
ROW_NUMBER() OVER (
PARTITION BY event_id
ORDER BY ingestion_ts DESC, hour DESC
) AS rn
FROM events_staging e
WHERE e.dt = '${dt}'
DISTRIBUTE BY event_id
) t
WHERE rn = 1;
Why DISTRIBUTE BY: a window function requires all rows of a partition to be in one reducer. DISTRIBUTE BY event_id guarantees that; without it Hive may spread the same event_id across reducers and produce two surviving rows. The ORDER BY ingestion_ts DESC keeps the latest retry; add a deterministic tiebreaker (hour) so results are reproducible when timestamps tie.
Why reruns are safe: INSERT OVERWRITE ... PARTITION replaces the partition's contents, so re-running the same ${dt} yields the same result rather than appending duplicates. That idempotency is what makes a backfill safe.
Edge case to flag: a retry can land under a different dt (for example, a batch from 23:50 delivered at 00:10). Deduplicating one day in isolation will miss those. Either widen the window to dt IN (${dt}, date_sub(${dt},1)) and write only matching rows, or run a periodic anti-join against the target table. Validate with COUNT(*) versus COUNT(DISTINCT event_id) on a sample day; the gap should be about 2.5%.
3. How do you roll this out without breaking the dashboards, and what do you monitor afterwards?
Read the worked answer
Roll out behind a stable name.
- Build
events_cleanalongside the existing table and backfill 90 days. - Reconcile before switching: per-day
COUNT(*),COUNT(DISTINCT event_id),SUM(revenue), min/maxevent_ts, and a row-level checksum on a sample day. A 2.5% row reduction from dedup is expected; anything else is a bug. - Create a view named
eventspointing atevents_clean, so dashboards keep their existing SQL. If the dashboard cannot be repointed, swap the partition locations under a maintenance window and keep the old directory for rollback. - Run old and new in parallel for 3–5 days and diff the daily aggregates.
Ongoing monitoring and maintenance:
- File health per partition: file count and average file size; alert if a day exceeds about 30 files or average size falls below 64 MB.
- Partition registration: run
MSCK REPAIR TABLEor anALTER TABLE ... ADD PARTITIONstep after every load, or new data will silently be invisible. - Duplicate ratio: a daily check that dedup removes 2.0–3.0% of rows; a sudden change means the upstream retry behavior changed.
- Query p95 for the top dashboard queries, and NameNode RPC queue length, as the proof the fix worked.
- Layout drift: if the app later adds columns, the SerDe/Parquet schema must be updated or the new fields vanish silently.