.agents/skills/tinybird/rules/deduplication-patterns.md
Strategies for handling duplicates and combining batch with real-time processing.
| Strategy | When to use |
|---|---|
Query-time (argMax, LIMIT BY, subquery) | Prototyping or small datasets |
| ReplacingMergeTree | Large datasets, need latest row per key |
| Periodic snapshots (Copy Pipes) | Freshness not critical, need rollups or different sorting keys |
| Lambda architecture | Need freshness + complex transformations that MVs can't handle |
For dimensional/small tables, periodic full replace is usually best.
-- argMax: get latest value per key
SELECT post_id, argMax(views, updated_at) as views
FROM posts GROUP BY post_id
-- LIMIT BY
SELECT * FROM posts ORDER BY updated_at DESC LIMIT 1 BY post_id
-- Subquery
SELECT * FROM posts WHERE (post_id, updated_at) IN (
SELECT post_id, max(updated_at) FROM posts GROUP BY post_id
)
ENGINE "ReplacingMergeTree"
ENGINE_SORTING_KEY "unique_id"
ENGINE_VER "updated_at"
ENGINE_IS_DELETED "is_deleted" -- optional, UInt8: 1=deleted, 0=active
FINAL or use alternative deduplication methodSELECT * FROM posts FINAL WHERE post_id = {{Int64(post_id)}}
Use Copy Pipes when:
copy_mode is append.COPY_MODE replace for full refreshes when the table is not massive and you don't control when duplicates can occur.COPY_MODE append (default) when you do control duplicate generation and can process incrementally.NODE generate_snapshot
SQL >
SELECT post_id, argMax(views, updated_at) as views, max(updated_at) as updated_at
FROM posts_raw
GROUP BY post_id
TYPE COPY
TARGET_DATASOURCE posts_snapshot
COPY_SCHEDULE 0 * * * *
COPY_MODE replace
Combine batch snapshots with real-time queries when:
uniqState performance is problematicSELECT * FROM posts_snapshot
UNION ALL
SELECT post_id, argMax(views, updated_at) as views, max(updated_at) as updated_at
FROM posts_raw
WHERE updated_at > (SELECT max(updated_at) FROM posts_snapshot)
GROUP BY post_id
Warning: argMaxMerge prefers non-null values over null, even with lower timestamps.
Workaround—convert nulls to epoch before aggregation:
SELECT post_id,
argMaxState(CASE WHEN flagged_at IS NULL THEN toDateTime('1970-01-01 00:00:00') ELSE flagged_at END, updated_at) as flagged_at
FROM posts
GROUP BY post_id
Handle the sentinel value in downstream queries.