Skip to content
Back to skills

Clickhouse Nip33 Addressable Dedup

ASecurity

Fix duplicate Nostr events in ClickHouse when using ReplacingMergeTree for NIP-33 addressable events (Kind 30000+). Use when: (1) Edited videos/events appear as duplicates, (2) Same d_tag shows multiple events with different IDs, (3) FINAL keyword doesn't deduplicate properly for parameterized replaceable events. The issue is that FINAL deduplicates by ORDER BY key (typically `id`), not by (pubkey, kind, d_tag).

  • 265 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 27, 2026
databasessqlgit

Works with

  • claude code
  • cli

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add divinevideo/divine-mobile --skill clickhouse-nip33-addressable-dedup --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Clickhouse Nip33 Addressable Dedup?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Clickhouse Nip33 Addressable Dedup
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/divinevideo-clickhouse-nip33-addressable-dedup/badge)](https://www.skillsdirectory.com/skills/divinevideo-clickhouse-nip33-addressable-dedup)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: clickhouse-nip33-addressable-dedup
description: |
  Fix duplicate Nostr events in ClickHouse when using ReplacingMergeTree for NIP-33
  addressable events (Kind 30000+). Use when: (1) Edited videos/events appear as duplicates,
  (2) Same d_tag shows multiple events with different IDs, (3) FINAL keyword doesn't
  deduplicate properly for parameterized replaceable events. The issue is that FINAL
  deduplicates by ORDER BY key (typically `id`), not by (pubkey, kind, d_tag).
author: Claude Code
version: 1.0.0
date: 2026-01-30
---

# ClickHouse NIP-33 Addressable Event Deduplication

## Problem
When storing Nostr events in ClickHouse using ReplacingMergeTree, edited addressable events
(Kind 30000-39999) appear as duplicates. Users edit their video/event, a new event ID is
created with the same d_tag, but both versions are shown instead of just the latest.

## Context / Trigger Conditions
- Nostr relay storing events in ClickHouse with ReplacingMergeTree
- Users report seeing duplicate videos/events after editing
- Query returns multiple events with same `(pubkey, kind, d_tag)` but different `id` values
- Using `FINAL` keyword but duplicates still appear
- Kind 30000+ events (NIP-33 parameterized replaceable events like Kind 34236 videos)

## Root Cause
The `FINAL` keyword in ClickHouse deduplicates based on the table's `ORDER BY` key. If
your table is defined as:

```sql
ENGINE = ReplacingMergeTree(indexed_at)
ORDER BY (id)
```

Then `FINAL` deduplicates by `id`. Two events with different IDs are NOT considered
duplicates, even if they represent the same addressable "slot" per NIP-33.

For NIP-33 addressable events, the replacement key should be `(pubkey, kind, d_tag)`,
not `id`.

## Solution

### Option 1: Fix at View Level (Recommended)

Change your videos view to use `LIMIT 1 BY` instead of `FINAL`:

```sql
CREATE VIEW videos AS
SELECT
    id,
    pubkey,
    created_at,
    kind,
    content,
    tags,
    d_tag,
    title,
    thumbnail,
    video_url
FROM events_local
WHERE kind IN (34235, 34236)
ORDER BY pubkey, kind, d_tag, created_at DESC
LIMIT 1 BY pubkey, kind, d_tag;
```

The `LIMIT 1 BY` clause keeps only the first row (latest by created_at) for each
unique combination of `(pubkey, kind, d_tag)`.

### Option 2: Fix at Table Level (Breaking Change)

If you can recreate the table, use a composite ORDER BY:

```sql
CREATE TABLE events_addressable (
    ...
) ENGINE = ReplacingMergeTree(created_at)
ORDER BY (pubkey, kind, d_tag);
```

This makes `FINAL` work correctly for NIP-33 events but may not work for all event types.

### Migration Example

```sql
-- Drop dependent views first
DROP VIEW IF EXISTS trending_videos;
DROP VIEW IF EXISTS video_stats;
DROP VIEW IF EXISTS videos;

-- Recreate with proper deduplication
CREATE VIEW videos AS
SELECT *
FROM events_local
WHERE kind IN (34235, 34236)
ORDER BY pubkey, kind, d_tag, created_at DESC
LIMIT 1 BY pubkey, kind, d_tag;

-- Recreate dependent views...
```

## Verification

Query for a specific user's videos and confirm no duplicates:

```sql
SELECT id, d_tag, created_at
FROM videos
WHERE pubkey = 'user_pubkey_here'
ORDER BY created_at DESC;
```

Each `d_tag` should appear only once, with the highest `created_at` value.

## Example

**Before (broken):**
```
| id       | d_tag    | created_at |
|----------|----------|------------|
| abc123   | video1   | 1769697188 | ← Newer edit
| def456   | video1   | 1769697150 | ← Original (should be hidden)
```

**After (fixed):**
```
| id       | d_tag    | created_at |
|----------|----------|------------|
| abc123   | video1   | 1769697188 | ← Only latest shown
```

## Notes
- This applies to all NIP-33 addressable events (Kind 30000-39999), not just videos
- The `LIMIT 1 BY` approach is query-time deduplication, not storage deduplication
- Old event versions remain in storage but won't appear in query results
- Consider periodic cleanup of old event versions if storage is a concern
- Don't forget to recreate dependent views in the correct order

## References
- [NIP-33: Parameterized Replaceable Events](https://github.com/nostr-protocol/nips/blob/master/33.md)
- [ClickHouse LIMIT BY clause](https://clickhouse.com/docs/en/sql-reference/statements/select/limit-by)
- [ClickHouse ReplacingMergeTree](https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replacingmergetree)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…