Docs

DataSocial docs: TikTok tables, SQL tips and the MCP

Getting started

DataSocial is a free warehouse of public TikTok data: about 4 billion creators, 600 million sounds and 5.5 billion videos, with day-by-day history for the active ones. Ask a question and get a table and a chart; every answer is a SQL query you can read and change.

  1. Open Search and type what you want to know, like “rising sounds” or “US creators”.
  2. Pick a suggested question and it runs, or press Enter and Ask AI writes the SQL for you to run.
  3. Read the table or switch to Chart. Change a number in the SQL and press Run again.

Free: up to 100 rows and 10 seconds per query, fair use (Terms). Sign in for a key to use it from Claude or Cursor: MCP.

Tables

Everything lives in one database, tiktok. Each table answers a different kind of question. Open one to see example questions you can run, and every column.

Latest state

One row per creator, video or sound, as it is now.

Who is big on TikTok, where they are, what they do, and whether they list a business email.
…
Which songs and original sounds exist, who made them, and how many videos use each one.
…
What creators post: captions, hashtags, the sound used, views, likes and ad or shop flags.
…

History

How numbers changed: one row per day, never rewritten.

How a tracked creator's followers, videos and likes change from one day to the next.
…
How a tracked creator's video gains views and likes, day by day for its first 30 days, then week by week to day 90.
…
How many videos used a sound on each day, since February 2026 (with a gap from mid-May to September): which sounds are rising or fading.
…

Follow graph

Who follows whom. Looked up one creator at a time.

Who a creator follows: every account on their following list, with its handle and follower count. One creator per call.
…

creators

Latest state

Who is big on TikTok, where they are, what they do, and whether they list a business email.

Rows now
…
One row is
one creator
Refreshed
Rolling rebuild; every row at most 6 h old.

What it covers: Every TikTok creator we have seen: profile, counts, verification, links, business info for creators we have read in depth, and whether they list a contact email (the address itself is premium and not readable here). tracked = 1 means we read the creator every day. 4.1 billion creators worldwide, discovered through following lists. A creator is tracked while they have 5,000+ followers (1,000+ in the US, Canada, Australia or the UK) and are proven to have posted in the last 30 days (a recent video we know of, or a rising video count). 4.3 million were tracked on 2026-09-28 (after the imported archive proved many more recent posts), and the list grows every day as more creators are proven to post. Tracked creators get a creator_history row every day, and their videos a video_history row every day for 30 days, then weekly to day 90. Business and category fields only for creators we have read in depth (about 1 in 200).

Fastest filters: the table is sorted by country, followers, creator_id, so filters on the first of these are quickest (and cheapest).

Questions it answers

Who are the biggest creators in the US?

Run this
SELECT creator_id, username, nickname, followers, likes, videos
FROM tiktok.creators
WHERE country = 'US'
ORDER BY followers DESC
LIMIT 10

Which countries have the most creators with 1M+ followers?

Run this
SELECT country, count() AS creators
FROM tiktok.creators
WHERE followers >= 1000000 AND country != ''
GROUP BY country
ORDER BY creators DESC
LIMIT 15

Columns

ColumnTypeWhat it is
creator_idUInt64
usernameString
nicknameString
bioStringemails in it replaced by '[email hidden]'
avatar_uriStringprofile picture path on TikTok's CDN; '' if not read yet
countryLowCardinality(String)'US', 'ID', ...: where their recent videos are posted when read in depth, else TikTok's account region
languageLowCardinality(String)account language code: 'en', 'es', ...
followersUInt32
followingUInt32
videosUInt32on a private account 0 means hidden, not none
likesUInt64likes received
sounds_createdUInt32
account_created_atDateTime
handle_changed_atDateTimeonly if within ~30 days; else 1970
is_privateUInt8
is_activeUInt80 = banned / deactivated (hint)
is_verifiedUInt8
verified_labelLowCardinality(String)TikTok's label, lower-cased: 'verified account', 'popular creator', ... (some localized); '' if none
business_verifiedLowCardinality(String)'verified account' | 'institution account' | 'business account' | ''
instagramString
youtube_channel_idString
youtube_channel_nameString
trackedUInt81 = tracked: 5,000+ followers (1,000+ in the US, Canada, Australia or the UK) and posted in the last 30 days. A tracked creator gets a creator_history row every day, and their videos a video_history row every day for 30 days, then weekly to day 90. Rechecked daily.
tracked_sinceDateTimewhen the current tracking began; 1970 if not tracked
is_artistUInt81 = has created sounds (sounds_created > 0) or TikTok labels them an artist
last_seen_atDateTimenewest read of this creator (UTC)
refreshed_atDateTimewhen this row was built

creators read in depth only (else empty)

ColumnTypeWhat it is
categoryLowCardinality(String)TikTok's business category, e.g. 'Entertainment'; set on few accounts, mostly business ones
account_typeLowCardinality(String)'personal' | 'creator' | 'business'; '' = not read in depth
company_nameStringbusiness accounts only

contact

ColumnTypeWhat it is
emailString'[email hidden]' when we have one, '' when not: addresses are never shown; message us to get email data
email_sourceLowCardinality(String)'bio' | 'archive' | '' (none)
has_emailUInt81 = we have an email for them (under 1%)
email_domainLowCardinality(String)the address's domain: 'gmail.com', 'agency.co', ...

sounds

Latest state

Which songs and original sounds exist, who made them, and how many videos use each one.

Rows now
…
One row is
one sound
Refreshed
Rebuilt every 6 h; video_count as of video_count_date.

What it covers: Every sound seen on a video: title, artist, owner, originality, commercial rights, TikTok matching, Spotify/Apple ids, video count. 603 million sound ids seen on videos. About 1 in 5 has not been read yet: only sound_id is set, title is empty and video_count is 0. video_count is read daily for sounds with more than 3 videos, weekly for the rest.

Fastest filters: the table is sorted by sound_id, so filters on the first of these are quickest (and cheapest).

Questions it answers

Which sounds are used in the most videos?

Run this
SELECT title, artist, video_count, video_count_date
FROM tiktok.sounds
WHERE title != ''
ORDER BY video_count DESC
LIMIT 10

Which original sounds (made by a creator, not a label) are used most?

Run this
SELECT title, owner_username, video_count
FROM tiktok.sounds
WHERE is_original = 1
ORDER BY video_count DESC
LIMIT 10

Columns

ColumnTypeWhat it is
sound_idUInt64
titleString
artistString
albumString
created_atDateTimewhen the sound was made; 1970 if not read yet
duration_sUInt16SECONDS (videos are ms); 0 if not read yet
languageLowCardinality(String)'English', 'non_vocal', ...; '' on most sounds
owner_creator_idUInt64mostly original sounds; 0 = none
owner_usernameString
owner_nicknameString
is_originalUInt8made on TikTok by a creator
is_officialUInt8is_pgc: a label/distributor upload
is_commercialUInt8cleared for business use
is_commercial_strictUInt8
has_custom_titleUInt8TikTok's own "the creator gave it a name" signal for originals. The strongest artist-discovery filter measured (median 86 videos vs 1 for other originals, 2026-09-28).
has_vocalsNullable(UInt8)NULL = TikTok did not say
has_strong_beatUInt8
has_lyricsUInt8
theme_tagsArray(LowCardinality(String))
matched_typeLowCardinality(String)How TikTok matched the audio: 'not_found' | 'fingerprint' | 'pgc' | 'speed-pitch'; '' if not read yet. For finding UNDISCOVERED artists, a match is a NEGATIVE signal.
matched_song_titleString
matched_song_artistString
spotify_idStringdsp platform 3
apple_music_idStringdsp platform 1
origin_video_idUInt64the video an original was lifted from
sim_group_idUInt64TikTok's "same audio" cluster; use it to avoid double counting re-uploads
loudness_lufsNullable(Float32)NULL = not measured
cover_uriString
play_urlStringunsigned mp3, does not expire
video_countUInt32videos using it (TikTok's user_count); 0 = not read yet
video_count_dateDatewhen that was read; 1970 if never
is_retiredUInt8never resolves in bulk (3 misses)
first_seenDateTimewhen it entered our store (2026-09-26 at the earliest); not when it was made (created_at)
refreshed_atDateTime

videos

Latest state

What creators post: captions, hashtags, the sound used, views, likes and ad or shop flags.

Rows now
…
One row is
one video, latest stats
Refreshed
Videos posted in the last ~100 days: hourly. Older months: every ~4 h. Stats are as of stats_updated_at: a tracked creator's video is re-read on the video_history schedule, other videos only when we come across them again.

What it covers: Videos with caption, hashtags, sound, ad / branded / shop / AI flags and engagement. 5.5 billion videos posted from 2014 to today. Not every TikTok video: the videos we have read ourselves, plus an archive of earlier reads imported on 2026-09-28 (most rows). Archive-only rows lack a few fields: width, height, downloads and sound_muted are 0, is_ai_generated is NULL, duration_ms is rounded to the second, and generic hashtags such as fyp and foryou are missing. tracked = 1 means the creator is tracked, so from 2026-09-28 the video's first 90 days go into video_history.

Fastest filters: the table is sorted by country, posted_at, video_id, so filters on the first of these are quickest (and cheapest).

Questions it answers

Which US videos are going viral right now?

Run this
WITH recent AS (
    SELECT video_id, creator_id, posted_at, views
    FROM tiktok.videos
    WHERE country = 'US' AND posted_at >= now() - INTERVAL 2 DAY
    ORDER BY views DESC
    LIMIT 200
),
usual AS (
    SELECT creator_id, median(views) AS typical_views
    FROM tiktok.videos
    WHERE country = 'US' AND posted_at BETWEEN now() - INTERVAL 90 DAY AND now() - INTERVAL 2 DAY
      AND creator_id IN (SELECT creator_id FROM recent)
    GROUP BY creator_id
    HAVING count() >= 5
)
SELECT r.video_id, r.posted_at, r.views, round(u.typical_views) AS typical_views,
       round(r.views / u.typical_views, 1) AS times_usual_views
FROM recent AS r
JOIN usual AS u USING (creator_id)
WHERE u.typical_views > 0 AND r.views >= 10 * u.typical_views
ORDER BY times_usual_views DESC
LIMIT 10

What were the most-viewed US videos posted in the last 7 days?

Run this
SELECT video_id, posted_at, views, likes, caption
FROM tiktok.videos
WHERE country = 'US' AND posted_at >= now() - INTERVAL 7 DAY
ORDER BY views DESC
LIMIT 10

Which hashtags are used most in this week's US videos?

Run this
SELECT arrayJoin(hashtags) AS hashtag, count() AS videos
FROM tiktok.videos
WHERE country = 'US' AND posted_at >= now() - INTERVAL 7 DAY
GROUP BY hashtag
ORDER BY videos DESC
LIMIT 15

Where in the world is a popular sound being used (in the videos we have)?

Run this
WITH (
    SELECT sound_id FROM tiktok.videos
    WHERE posted_at >= now() - INTERVAL 30 DAY AND sound_id != 0
    GROUP BY sound_id ORDER BY count() DESC LIMIT 1
) AS top_sound
SELECT country, count() AS videos_30d, round(videos_30d / sum(videos_30d) OVER () * 100, 1) AS share_pct
FROM tiktok.videos
WHERE sound_id = top_sound AND posted_at >= now() - INTERVAL 30 DAY
GROUP BY country
ORDER BY videos_30d DESC
LIMIT 10

Which US creators with 100k+ followers post most often?

Run this
WITH posters AS (
    SELECT creator_id, count() AS videos_30d
    FROM tiktok.videos
    WHERE country = 'US' AND posted_at >= now() - INTERVAL 30 DAY
    GROUP BY creator_id
)
SELECT c.username, c.followers, round(p.videos_30d / 30 * 7, 1) AS posts_per_week, p.videos_30d
FROM posters AS p
JOIN (
    SELECT creator_id, username, followers FROM tiktok.creators
    WHERE country = 'US' AND followers >= 100000 AND creator_id IN (SELECT creator_id FROM posters)
) AS c USING (creator_id)
ORDER BY posts_per_week DESC
LIMIT 10

Which creators post the most AI-generated videos?

Run this
WITH ai AS (
    SELECT creator_id, any(country) AS country, count() AS videos_30d, countIf(is_ai_generated = 1) AS ai_videos
    FROM tiktok.videos
    WHERE posted_at >= now() - INTERVAL 30 DAY
    GROUP BY creator_id
    HAVING videos_30d >= 5 AND ai_videos > 0
    ORDER BY ai_videos / videos_30d DESC, ai_videos DESC
    LIMIT 30
)
SELECT c.username, c.followers, ai.videos_30d, ai.ai_videos, round(ai.ai_videos / ai.videos_30d * 100) AS ai_pct
FROM ai
JOIN (
    SELECT creator_id, username, followers FROM tiktok.creators
    WHERE country IN (SELECT country FROM ai) AND creator_id IN (SELECT creator_id FROM ai)
) AS c USING (creator_id)
ORDER BY ai_pct DESC, ai.ai_videos DESC
LIMIT 10

Columns

ColumnTypeWhat it is
video_idUInt64
creator_idUInt64
posted_atDateTime
countryLowCardinality(String)'US', 'ID', ...: the region TikTok gives the video
languageLowCardinality(String)'en', 'es', ...; 'un' = undetermined (about half)
typeLowCardinality(String)'video' | 'photo'
duration_msUInt320 on photos; archive-only rows rounded to the second
image_countUInt8images in a photo post; 0 on videos and when unknown (most archive-only rows)
widthUInt16pixels; 0 on archive-only rows and photos
heightUInt16pixels; 0 on archive-only rows and photos
captionString
hashtagsArray(String)lower-case; archive-only rows lack generic tags such as fyp and foryou
hashtag_idsArray(UInt64)parallel to hashtags
mentionsArray(UInt64)creator_ids
on_screen_textArray(String)
sound_idUInt64tiktok.sounds.sound_id; 0 = none
is_adUInt8paid promotion (Spark Ads)
branded_contentLowCardinality(String)'' | 'paid_partnership' | 'shop_affiliate'
product_idUInt640 = none seen
is_shop_videoUInt8
seller_idUInt64
is_ai_generatedNullable(UInt8)NULL = TikTok did not say (all archive-only rows)
not_recommendedNullable(UInt8)TikTok's own flag: 1 = kept out of recommendations; NULL = TikTok did not say
sound_mutedUInt8audio removed for copyright; 0 on archive-only rows
viewsUInt64
likesUInt64
commentsUInt64
sharesUInt64
savesUInt64
downloadsUInt640 on archive-only rows
engagement_rateNullable(Float32)(likes + comments + shares + saves) / views; NULL when views = 0.
trackedUInt81 = its creator is tracked (as of this month's rebuild)
stats_updated_atDateTimewhen these stats were read (UTC)
refreshed_atDateTime

creator_history

History

How a tracked creator's followers, videos and likes change from one day to the next.

Rows now
…
One row is
one creator per day
Refreshed
Each day appended once at D+1 02:00 UTC.

What it covers: Daily followers, following, videos and likes: totals on that day (growth = the change between two days, divided by the earlier day). Every tracked creator, every day, from the day tracking starts (see creators.tracked; 96% of them had a row on 2026-09-27); also other large creators we count daily to find out whether they still post (4.2 million rows a day in all). Full days from 2026-09-27 (2026-09-26 holds 19 rows); no earlier backfill exists.

Fastest filters: the table is sorted by creator_id, date, so filters on the first of these are quickest (and cheapest).

Questions it answers

What were tracked creators' follower counts on the latest day?

Run this
SELECT creator_id, date, followers, videos, likes
FROM tiktok.creator_history
WHERE date = (SELECT max(date) FROM tiktok.creator_history)
ORDER BY followers DESC
LIMIT 10

Columns

ColumnTypeWhat it is
creator_idUInt64
dateDate
followersUInt32
followingUInt32
videosUInt32
likesUInt64
observed_atDateTimeWhen the numbers were read. Growth per day = change / (observed_at gap), not change / 1, reads within a "day" can be many hours apart.

video_history

History

How a tracked creator's video gains views and likes, day by day for its first 30 days, then week by week to day 90.

Rows now
…
One row is
one tracked video per day
Refreshed
Each day appended once at D+1 03:00 UTC.

What it covers: Views, likes, comments, shares, saves over a video's first 90 days: totals at that day's read (gained = the change between two days). Videos of tracked creators: a row every day from day 1 to day 29 after posting, then weekly (days 35, 42, ... 84). Each read is at the hour of day the video was posted, so rows are 24 h apart; observed_at is the exact read time. Full days from 2026-09-27 (2026-09-26 holds 190 rows). 2026-09-27 covers only the 0.6 million videos our own crawl had found, and 2026-09-28 is partial (the imported archive arrived that day). From 2026-09-29 every due video gets its row unless it was deleted or made private (about 3.5% of them).

Fastest filters: the table is sorted by video_id, date, so filters on the first of these are quickest (and cheapest).

Questions it answers

How did one video's views grow, day by day?

Run this
WITH (
    SELECT video_id FROM tiktok.video_history
    GROUP BY video_id HAVING count() >= 2
    ORDER BY max(views) DESC LIMIT 1
) AS top_video
SELECT date, views, likes
FROM tiktok.video_history
WHERE video_id = top_video
ORDER BY date

Columns

ColumnTypeWhat it is
video_idUInt64
dateDate
viewsUInt64
likesUInt64
commentsUInt64
sharesUInt64
savesUInt64
observed_atDateTimewhen it was read

sound_history

History

How many videos used a sound on each day, since February 2026 (with a gap from mid-May to September): which sounds are rising or fading.

Rows now
…
One row is
one sound per day
Refreshed
Each day appended once at D+1 12:00 UTC.

What it covers: Each sound's total video count on each day (a running total: new videos = the change between two days, never a sum). Daily for sounds with > 3 videos, weekly for the rest, from our own reads since 2026-09-27: every sound we know (about 480 million) was read on 2026-09-27; from 2026-10-04 the weekly reads are spread over the week, so a day holds about 100 million sounds (about 40 million before that). Before that, history imported from an earlier system that followed 0.8 to 1.6 million sounds a day: one day on 2026-01-16, then 2026-02-07 to 2026-05-14 (a few days missing, only 4 in May), then 2026-09-02 to 2026-09-26. No data from 2026-05-15 to 2026-09-01.

Fastest filters: the table is sorted by date, sound_id, so filters on the first of these are quickest (and cheapest).

Questions it answers

Which sounds added the most videos this week, with their 30-day history?

Run this
WITH (SELECT max(date) FROM tiktok.sound_history) AS d0,
(
    SELECT groupArray(sound_id) FROM (
        SELECT sound_id, toInt64(n.video_count) - w.video_count AS added_7d
        FROM (SELECT sound_id, video_count FROM tiktok.sound_history WHERE date = d0 - 7) AS w
        JOIN (SELECT sound_id, video_count FROM tiktok.sound_history WHERE date = d0 AND video_count >= 100000) AS n
            USING (sound_id)
        ORDER BY added_7d DESC
        LIMIT 30
    )
) AS rising
SELECT s.title, s.artist, h.videos, h.added_7d, h.history
FROM (
    SELECT sound_id,
           anyIf(video_count, date = d0) AS videos,
           toInt64(videos) - anyIf(video_count, date = d0 - 7) AS added_7d,
           arraySort(groupArray((date, video_count))) AS history
    FROM tiktok.sound_history
    WHERE sound_id IN (SELECT arrayJoin(rising)) AND date >= d0 - 30
    GROUP BY sound_id
) AS h
JOIN (SELECT sound_id, title, artist FROM tiktok.sounds WHERE sound_id IN (SELECT arrayJoin(rising)) AND title != '') AS s USING (sound_id)
ORDER BY h.added_7d DESC
LIMIT 10

Which sounds are rising fastest this week?

Run this
WITH (SELECT max(date) FROM tiktok.sound_history) AS d0,
(
    SELECT groupArray(sound_id) FROM (
        SELECT sound_id, (toInt64(n.video_count) - w.video_count) / w.video_count AS growth
        FROM (SELECT sound_id, video_count FROM tiktok.sound_history WHERE date = d0 AND video_count >= 10000) AS n
        JOIN (SELECT sound_id, video_count FROM tiktok.sound_history WHERE date = d0 - 7 AND video_count >= 10000) AS w
            USING (sound_id)
        ORDER BY growth DESC
        LIMIT 30
    )
) AS rising
SELECT s.title, s.artist, h.videos_now, h.added_7d, round(h.added_7d / h.videos_week_ago * 100, 1) AS growth_pct_7d
FROM (
    SELECT sound_id,
           anyIf(video_count, date = d0) AS videos_now,
           anyIf(video_count, date = d0 - 7) AS videos_week_ago,
           toInt64(videos_now) - videos_week_ago AS added_7d
    FROM tiktok.sound_history
    WHERE sound_id IN (SELECT arrayJoin(rising)) AND date IN (d0, d0 - 7)
    GROUP BY sound_id
) AS h
JOIN (SELECT sound_id, title, artist FROM tiktok.sounds WHERE sound_id IN (SELECT arrayJoin(rising)) AND title != '') AS s USING (sound_id)
ORDER BY growth_pct_7d DESC
LIMIT 10

How many videos did today's most-used sound have, day by day?

Run this
WITH (
    SELECT sound_id FROM tiktok.sound_history
    WHERE date = (SELECT max(date) FROM tiktok.sound_history)
    ORDER BY video_count DESC LIMIT 1
) AS top_sound
SELECT date, video_count
FROM tiktok.sound_history
WHERE sound_id = top_sound
ORDER BY date

Which mid-size sounds (10k to 100k videos) are speeding up: more new videos this week than the week before?

Run this
WITH (SELECT max(date) FROM tiktok.sound_history) AS d0,
(
    SELECT groupArray(sound_id) FROM (
        SELECT sound_id,
               toInt64(any(n.video_count)) - anyIf(o.video_count, o.date = d0 - 7) AS added_this_week,
               toInt64(anyIf(o.video_count, o.date = d0 - 7)) - anyIf(o.video_count, o.date = d0 - 14) AS added_week_before
        FROM (SELECT sound_id, date, video_count FROM tiktok.sound_history WHERE date IN (d0 - 7, d0 - 14)) AS o
        JOIN (SELECT sound_id, video_count FROM tiktok.sound_history WHERE date = d0 AND video_count BETWEEN 10000 AND 100000) AS n
            USING (sound_id)
        GROUP BY sound_id
        HAVING countIf(o.date = d0 - 7) > 0 AND countIf(o.date = d0 - 14) > 0 AND added_week_before > 0
        ORDER BY added_this_week - added_week_before DESC
        LIMIT 30
    )
) AS picked
SELECT s.title, s.artist, h.videos_now, h.added_this_week, h.added_week_before
FROM (
    SELECT sound_id,
           anyIf(video_count, date = d0) AS videos_now,
           toInt64(videos_now) - anyIf(video_count, date = d0 - 7) AS added_this_week,
           toInt64(anyIf(video_count, date = d0 - 7)) - anyIf(video_count, date = d0 - 14) AS added_week_before
    FROM tiktok.sound_history
    WHERE sound_id IN (SELECT arrayJoin(picked)) AND date IN (d0, d0 - 7, d0 - 14)
    GROUP BY sound_id
) AS h
JOIN (SELECT sound_id, title, artist FROM tiktok.sounds WHERE sound_id IN (SELECT arrayJoin(picked)) AND title != '') AS s USING (sound_id)
ORDER BY h.added_this_week - h.added_week_before DESC
LIMIT 10

Columns

ColumnTypeWhat it is
sound_idUInt64
dateDate
video_countUInt32
observed_atDateTime

follows

Follow graph

Who a creator follows: every account on their following list, with its handle and follower count. One creator per call.

Rows
One following list per call
One row is
one creator a creator follows
Refreshed
Not refreshed since the crawl: following lists are not re-read yet. A lookup reads the source directly, so it is never staler than the crawl.

What it covers: Who one creator follows: every account on their following list, with its handle and follower count. Call it with a creator id: SELECT * FROM tiktok.follows(creator_id = 6596805238354558982). Find the id with SELECT creator_id FROM tiktok.creators WHERE username = 'kimberly.loaiza'. 113.7 billion follow edges from the following lists of 593 million creators, read 2026-09-06 to 2026-09-23. Many of the oldest, biggest accounts (short ids, such as charlidamelio) were not crawled and return no rows. Who follows a creator is not offered: it would read the whole graph.

How to call it: SELECT * FROM tiktok.follows(creator_id = …), with a creator id. It is a lookup, not a table: there is no way to read it without one. See the map of shared audiences.

Questions it answers

Who does Kimberly Loaiza follow, biggest accounts first?

Run this
SELECT follows_username, follows_nickname, follows_followers, position
FROM tiktok.follows(creator_id = 6596805238354558982)
ORDER BY follows_followers DESC
LIMIT 20

Which accounts did Rosalía follow most recently?

Run this
SELECT position, follows_username, follows_nickname, follows_followers
FROM tiktok.follows(creator_id = 6531081683746165760)
ORDER BY position
LIMIT 20

Columns

ColumnTypeWhat it is
creator_idUInt64the creator whose following list this is (the parameter)
follows_idUInt64a creator they follow (tiktok.creators.creator_id)
follows_usernameStringthat creator's handle when their profile was last read, empty if never read
follows_nicknameStringthat creator's display name, emails replaced (decision 109; pattern: build/creators.sql)
follows_followersUInt32that creator's follower count when their profile was last read
positionUInt16place in the following list as TikTok returned it, 0 = the most recent follow
seen_atDateTimewhen this follow was last read (UTC)

SQL tips

The few things worth knowing to change a query: names, fast filters, dates, ids and the metrics.

Queries are ClickHouse SQL. If you have written any SQL, you already know most of it.

The basics

  • Every table is in the tiktok database: write tiktok.creators, tiktok.sounds and so on.
  • End with a LIMIT. Results stop at 100 rows per query, for everyone: aggregate or filter to get the rows you want.
  • Tables are already clean: one row per thing, no duplicates. You never need FINAL or DISTINCT to fix them.
SELECT username, followers
FROM tiktok.creators
WHERE country = 'GB'
ORDER BY followers DESC
LIMIT 20

Fast, cheap filters

Each table is stored sorted by a few columns. A filter on the first of them skips most of the table, so it is faster and cheaper. Each table's page lists them under “Fastest filters”. The big ones:

TableSorted bySo filter on
creatorscountry, followerscountry, then followers
videoscountry, posted_atcountry, then a date range
soundssound_ida list of sound ids
sound_historydate, sound_idone day (or a few), then sound ids

For example, the biggest US creators read about 60 MB, while the same question with no country reads every creator's followers.

Dates, ids and units

  • Ids (creators, videos, sounds) are 64-bit numbers, too big for JavaScript. The API sends them as strings; keep them as text in spreadsheets.
  • Recent rows: WHERE posted_at >= now() - INTERVAL 7 DAY. A day: toDate(posted_at).
  • Video length is in milliseconds (videos.duration_ms); sound length is in seconds (sounds.duration_s).
  • Flags are 0 or 1, e.g. is_verified = 1.

Handy functions

You wantWrite
How manycount()
How many that matchcountIf(followers >= 1000000)
One row per hashtag of a videoarrayJoin(hashtags)
The value on the latest dateargMax(video_count, date)
The newest date in a table(SELECT max(date) FROM tiktok.sound_history)

History in one cell: groupArray

groupArray turns many rows into one array, so each sound (or creator) gets one row with its whole history. The results table draws an array of numbers as a mini chart: green if the last value is above the first, red if below. Add the date as a pair, groupArray((date, video_count)), and hovering the chart shows the dates too.

SELECT sound_id, groupArray(video_count) AS history
FROM (
    SELECT sound_id, date, video_count
    FROM tiktok.sound_history
    WHERE sound_id IN (7171140178143266818, 7673034501618567944)
      AND date >= today() - 30
    ORDER BY date
)
GROUP BY sound_id

Sort inside the subquery (ORDER BY date) so the array runs oldest to newest. For daily change instead of totals, wrap it: arrayDifference(groupArray(video_count)).

How metrics are defined

  • Growth % = change ÷ the previous value, never the current one. Put a floor on the previous value (e.g. 10,000 videos) so tiny sounds don't top the list.
  • Compare two days by reading both: WHERE date IN (d0, d0 - 7), then anyIf(video_count, date = d0 - 7) for the old value, and keep only rows that have both days (HAVING countIf(date = d0 - 7) > 0). The rising-sounds questions on the sound_history page show the whole query.
  • A missing day is NULL, never zero, and never filled in.
  • Engagement rate = (likes + comments + shares + saves) ÷ views.
  • Country spread of a sound, counted from videos, is an estimate from the videos we hold, which lean towards the US.

What isn't allowed

  • Only reading: no INSERT, CREATE or anything that changes data.
  • Only the tiktok database, and no SET, SETTINGS or FORMAT: results always come back as a table (JSON over the API).
  • Email addresses are hidden: creators.email reads [email hidden] when we have one, and addresses in bios and captions are replaced the same way. has_email and email_domain say who has one. The 31M creator emails are sold privately: message me.

MCP

Query DataSocial from Claude, Cursor or your own code: an MCP server, or one HTTP call, with your key.

Everything Search does, from your own tools. 100 rows per request. Base URL: https://api.datasocial.ai

Get a key

Sign in and open MCP: your key is already there, inside commands ready to copy. It looks like ds_live_…; one per account, and Reset key there swaps it for a new one. Send it as a bearer token. Every query answers up to 100 rows; there is no daily cap.

Without a key the SQL endpoint still answers, with the same limits per address.

Use it from Claude or Cursor (MCP)

The MCP server lets an AI assistant query the warehouse itself. It has three tools: list_tables, describe_table and run_sql (100 rows per call). Claude Code:

claude mcp add --transport http datasocial https://api.datasocial.ai/mcp \
  --header "Authorization: Bearer $DATASOCIAL_KEY"

Then ask, for example, “Which sounds grew fastest in the US this week?”. The MCP tab has this with your key filled in, and the config for Claude Desktop and Cursor too. The server speaks Streamable HTTP at POST /mcp, without sessions.

Run a query over HTTP

curl https://api.datasocial.ai/v1/data/sql \
  -H "Authorization: Bearer $DATASOCIAL_KEY" \
  -H 'content-type: application/json' \
  -d '{"sql": "SELECT username, followers FROM tiktok.creators WHERE country = '\''US'\'' ORDER BY followers DESC LIMIT 3"}'

The response

{
  "columns": [{ "name": "username", "type": "String" }, { "name": "followers", "type": "UInt32" }],
  "rows": [["khaby.lame", 162989705], ["charlidamelio", 160221160], ["tiktok", 95949280]],
  "row_cap": 100,
  "capped": false,
  "rows_read": 5029786,
  "bytes_read": 65283020,
  "elapsed_ms": 187
}
  • rows are arrays in the order of columns. 64-bit integers (ids) arrive as strings.
  • capped is true when the result hit the 100-row cap (row_cap).

Errors

A failed query answers with a status and {"error": "…"} in plain words. Over the MCP, the same message comes back as the tool's error.

StatusMeans
400The SQL has a mistake; the message says where.
401The key or session is missing, wrong, revoked or expired.
403Not allowed: another database, a setting or a write.
429Too many queries at once or per minute (wait a moment), or today's rows are used up (code: "daily_rows", back at 00:00 UTC).

Endpoints

EndpointWhat it doesNeeds
POST /mcpThe MCP server (Streamable HTTP): list_tables, describe_table, run_sql.A key
POST /v1/data/sqlRuns one read-only query on tiktok; body {"sql": "…"}.Nothing (a key: the account's limits)
GET /v1/tablesEvery table and column, with live row counts.Nothing
POST /v1/askA question in plain English → {"sql", "table", "chart", "note"}, checked but not run; body {"question": "…"}.Nothing