
Join StarRocks Community on Slack
Connect on SlackIn customer-facing analytics, delivering fast, interactive insights isn’t just a bonus—it’s a baseline expectation. Users expect sub-second dashboards, responsive drill-downs, and always-fresh metrics. But under the hood, many systems silently struggle with an age-old problem: data skew.
This blog explores what data skew really is, why it’s especially dangerous in multi-tenant customer-facing applications, and how to solve it with a practical, production-ready approach: multi-level partitioning.
What Is Data Skew?
At its core, data skew means an uneven distribution of data or workload—where a small number of keys, tenants, or time ranges account for a disproportionately large share of processing, storage, or access. It shows up in three main ways:
-
Input skew: Some partitions are much larger or more frequently accessed than others.
-
Computation skew: A few keys dominate joins or aggregations, causing single-node bottlenecks.
-
Access skew: One tenant or segment of data generates the majority of query traffic.
The result? Even if 90% of queries perform well, the slowest 1%—your P99—can bring down the entire user experience.
Why Is It Worse in Customer-Facing Analytics?
Data skew exists everywhere, but customer-facing systems amplify it:
-
Multi-tenancy is inherently uneven. Some tenants are small and quiet; others are massive and noisy.
-
User behavior is unpredictable. A viral campaign or product launch can flood a single tenant or dashboard with thousands of queries per second.
-
Strong SLAs and high concurrency collide. Long-tail delays caused by skew can break SLAs even if the average looks good.
-
Hot tenants pollute shared resources. They can evict others from caches or monopolize compute nodes.
Without proper safeguards, a single tenant can drag down the performance of everyone else.
What’s at Stake?
If left unaddressed, data skew causes real operational and business pain:
-
Long-tail latency: One slow partition slows down the whole query. Users hit “refresh” again and again.
-
Cost blowouts: You end up overprovisioning just to handle rare spikes.
-
Data duplication: Teams replicate hot data into specialized tables or precompute everything—only to fight constant maintenance.
-
Query instability: You never know which user or which day will trigger the next incident.
Why Common Fixes Don’t Scale
Many teams attempt workarounds:
-
Over-aggressive precomputation: Works for static dashboards, but breaks down with ad-hoc queries or dynamic slicing.
-
Manual sharding or tenant isolation: Adds huge complexity and introduces data governance headaches.
-
Rate limiting: Protects the system, but frustrates the customer.
None of these fix the root cause: imbalanced data layout and poor parallelism.
Multi-level Partitioning and Bucketing for B2B SaaS With StarRocks
Beating skew is mostly a layout problem. You want the planner to read exactly the data a query needs, while still fanning heavy tenants across enough compute to avoid hotspots. In StarRocks, the most reliable way to do this is multi-level partitioning: combine time-based partitioning for pruning with key-aware distribution for parallelism. There are two production patterns; pick one per table (and mix across tables if needed).
What is StarRocks
StarRocks is a high-performance SQL engine built for customer-facing analytics. It’s designed to serve sub-second queries at high concurrency—ideal for B2B SaaS products, interactive dashboards, and real-time APIs. With built-in support for partitioning, bucketing, and indexing, StarRocks helps you keep your P99 fast even under skewed, multi-tenant workloads.
Pattern A — two-level partitioning for hard tenant isolation
If most queries filter by time + tenant and your daily active tenant count is manageable (≈ ≤ 500–1000/day), make tenant a first-class partition dimension. You’ll get precise pruning and predictable performance per tenant.
How it works
Partition by day (or hour) and tenant, then distribute within each partition by a high-cardinality key (e.g., user_id) to keep intra-tenant work parallel.
CREATE TABLE fact_events (
dt DATE NOT NULL,
tenant_id BIGINT NOT NULL,
ts DATETIME,
user_id BIGINT,
event_name VARCHAR(64),
value DOUBLE
)
ENGINE=OLAP
DUPLICATE KEY (dt, tenant_id)
PARTITION BY date_trunc('day', dt), tenant_id
DISTRIBUTED BY HASH(user_id) BUCKETS 64
PROPERTIES (
"replication_num" = "1");
Why it works
-
Only scans what’s needed: Queries like WHERE dt = x AND tenant_id = x hit just that tenant’s partition. No other tenant’s data is read.
-
Noisy tenants don’t affect others: Compaction, cache, and background tasks stay inside each tenant’s partition. One tenant can’t slow down the rest.
-
Parallel within each tenant: Data is spread across tablets by user_id, so heavy tenants still scale. Joins or filters on user_id trigger bucket pruning for faster scans.
What to watch for
-
Partition count grows quickly: You’ll get one partition per day per tenant. With just 1,000 active tenants per day, that’s 30,000 partitions in a month. Set retention (partition_live_number or a retention condition), and limit auto-created partitions.
Pattern B — Time partitioning + hash bucketing + aligned sorting
If you have thousands of active tenants per day—or traffic spikes that hit specific tenants—this layout keeps things balanced and efficient. You partition by time, spread tenants across tablets with a composite hash, and sort rows by the same key to speed up filtering.
How it works
-
Partition by day (or hour): keeps partition number under control.
-
Distribute by a composite hash like (tenant_id, user_id): spreads each tenant across tablets.
-
Sort with the same prefix: ORDER BY(tenant_id, user_id,ts). This clusters each tenant's data, making filters and joins faster.
CREATE TABLE fact_events (
tenant_id BIGINT NOT NULL,
user_id BIGINT,
ts DATETIME NOT NULL,
dt DATE NOT NULL,
event_name VARCHAR(64),
value DOUBLE
)
ENGINE=OLAP
DUPLICATE KEY (tenant_id, user_id, ts)
PARTITION BY date_trunc('day', dt)
DISTRIBUTED BY HASH(tenant_id, user_id,ts) BUCKETS 96
ORDER BY (tenant_id, user_id, ts)
PROPERTIES (
"replication_num" = "1"
);
Why this works
-
Full-key filters = maximum pruning: If your query filters both tenant_id and user_id, StarRocks prunes directly to the right tablet(s).
-
Partial filters still fast: Even if only tenant_id is filtered, the sort order helps skip blocks inside each tablet using metadata (short key index + zone maps).
Choosing quickly
-
If most queries filter by time and tenant, and daily time×tenant partitions stay under ~1000, use Pattern A (two-level partitioning) with a high-cardinality HASH key inside each partition.
-
Tenant count large/volatile, or you need fine-grained time partitions (day/hour)→ Use Pattern B (time partitions + composite hashing), accept all-bucket scans for tenant-only filters, and cover hot paths with partitioned MVs.
Fix Your Data Skew Today
You don’t buy your way out of skew—you design your way out. StarRocks helps you isolate hotspots, scale with confidence, and keep your customer-facing P99 fast and predictable.
Need help tuning your schema?
Join the StarRocks community on Slack → starrocks.io/slack.