一键导入
exasol-table-design
Exasol table design for performance: DISTRIBUTE BY, PARTITION BY, zone maps, data types, replication, surrogate keys, and CREATE TABLE syntax.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Exasol table design for performance: DISTRIBUTE BY, PARTITION BY, zone maps, data types, replication, surrogate keys, and CREATE TABLE syntax.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Exasol IMPORT and EXPORT SQL statements: syntax, file formats (CSV, FBV, Parquet), cloud storage (S3, Azure, GCS), connection objects, error handling, and ETL staging patterns.
Exasol MCP Server usage guide: common workflows for database exploration, query debugging, BucketFS management, and working with UDFs and preprocessors.
Exasol SQL dialect specifics: syntax, data types, functions, and common pitfalls for generating correct Exasol SQL.
Exasol system and statistics tables: what they contain, visibility prefixes (EXA_ALL_*, EXA_DBA_*, EXA_USER_*), and when to query them directly vs using MCP tools.
Exasol User-Defined Functions (UDFs) and Scripts: CREATE SCRIPT syntax, language options, SQL-to-language data type mappings, ExaIterator API, BucketFS access, and Script Language Containers.
Exasol Virtual Schemas: creating and managing read-only federated access to external data sources via adapter scripts, JDBC connections, and pushdown query optimization.
| name | exasol-table-design |
| description | Exasol table design for performance: DISTRIBUTE BY, PARTITION BY, zone maps, data types, replication, surrogate keys, and CREATE TABLE syntax. |
| tags | ["exasol","table-design","performance","distribution","partitioning","zonemaps"] |
CREATE [OR REPLACE] TABLE [IF NOT EXISTS] schema.table_name (
column_name data_type [DEFAULT value] [IDENTITY [start [INCREMENT BY step]]]
[NOT NULL] [PRIMARY KEY] [REFERENCES ref_table (ref_col)],
...,
[PRIMARY KEY (col1, col2, ...)],
[FOREIGN KEY (col) REFERENCES ref_table (ref_col)],
[LIKE other_table [INCLUDING DEFAULTS] [INCLUDING IDENTITY] [INCLUDING COMMENTS]],
[DISTRIBUTE BY col1 [, col2, ...]],
[PARTITION BY col]
);
Key options:
| Clause | Effect |
|---|---|
OR REPLACE | Silently overwrites an existing table |
IF NOT EXISTS | No-op if the table already exists |
LIKE t | Copies column definitions (not constraints) from t; add INCLUDING DEFAULTS / INCLUDING IDENTITY / INCLUDING COMMENTS to copy those properties |
DISTRIBUTE BY | Controls how rows are spread across cluster nodes |
PARTITION BY | Controls physical data layout within each node |
-- Create and populate from a query
CREATE TABLE new_table AS SELECT * FROM source_table;
-- Create schema only (no data)
CREATE TABLE new_table AS SELECT * FROM source_table WITH NO DATA;
Choose the smallest type that fits the data — oversized declarations waste memory and slow comparisons.
| Type | Recommendation |
|---|---|
DECIMAL(p, s) | Preferred exact numeric; use instead of DOUBLE when precision matters |
DOUBLE / FLOAT | 64-bit floating point; only when approximate arithmetic is acceptable |
VARCHAR(n) | Variable-length string; avoid declaring much larger than needed (e.g. VARCHAR(2000000) when 100 suffices) |
CHAR(n) | Fixed-length; prefer over VARCHAR when all values have the same length (e.g. ISO country codes) |
DATE | Calendar date only; use instead of TIMESTAMP when time-of-day is irrelevant |
TIMESTAMP(p) | Include time; default precision is 3 (milliseconds) |
BOOLEAN | For true/false flags |
HASHTYPE(n BYTE) | Fixed-length binary hashes (UUIDs, MD5 digests, etc.) |
Rules:
DECIMAL join and group-by keys; joins on VARCHAR and DATE/TIMESTAMP are significantly more expensive.Controls how rows are spread across cluster nodes. Correct distribution turns global joins (network shuffle) into local joins (node-local, no network cost).
-- At creation
CREATE TABLE orders (
order_id INT,
customer_id INT,
order_date DATE,
amount DECIMAL(10, 2),
DISTRIBUTE BY customer_id
);
-- Alter existing table
ALTER TABLE orders DISTRIBUTE BY customer_id;
-- Remove distribution key (round-robin)
ALTER TABLE orders DROP DISTRIBUTION KEYS;
A join between T1 and T2 is local when both tables are distributed by columns that are a subset of the join condition:
-- T1 DISTRIBUTE BY customer_id
-- T2 DISTRIBUTE BY customer_id
-- → local join: no network transfer
SELECT * FROM orders T1 JOIN customers T2 ON T1.customer_id = T2.customer_id;
-- T1 DISTRIBUTE BY (customer_id, region_id)
-- T2 DISTRIBUTE BY (customer_id, region_id)
-- → local join: both columns present in join condition
SELECT * FROM T1 JOIN T2 ON T1.customer_id = T2.customer_id AND T1.region_id = T2.region_id;
-- T1 DISTRIBUTE BY (customer_id, region_id)
-- Joining only on customer_id → NOT local (multi-column key, only partial match)
Envelope matching rule: distributing by (x, y) enables local joins when the join condition contains all of (x, y). It does NOT enable local joins on just x or just y alone.
-- Check current distribution (row count per node)
SELECT iproc() AS node, COUNT(*) FROM my_table GROUP BY 1 ORDER BY 1;
-- Preview distribution with a candidate column (before changing)
SELECT value2proc(customer_id) AS future_node,
ROUND(COUNT(*) / SUM(COUNT(*)) OVER () * 100, 2) AS pct
FROM orders
GROUP BY 1
ORDER BY 1;
-- Inspect the current distribution key
SELECT COLUMN_NAME, COLUMN_IS_DISTRIBUTION_KEY
FROM EXA_ALL_COLUMNS
WHERE COLUMN_TABLE = 'ORDERS' AND COLUMN_SCHEMA = 'MY_SCHEMA'
ORDER BY COLUMN_ORDINAL_POSITION;
| Anti-pattern | Why it hurts |
|---|---|
Distributing by a status or country column | Low cardinality → severe data skew |
Distributing by a column only used in WHERE | Disables MPP for the common filter case |
| Multi-column distribution when joins use only a subset | The key is never matched; no local joins |
| Different data types on distribution columns of joined tables | Prevents local join optimization |
Controls physical data layout within each node. Enables range pruning — the engine skips partitions that don't match the WHERE predicate, reducing I/O and memory.
-- Combined with DISTRIBUTE BY (typical)
CREATE TABLE orders (
order_id INT,
customer_id INT,
order_date DATE,
amount DECIMAL(10, 2),
DISTRIBUTE BY customer_id,
PARTITION BY order_date
);
-- Alter existing table
ALTER TABLE orders PARTITION BY order_date;
-- Remove partitioning
ALTER TABLE orders DROP PARTITION KEYS;
DECIMAL, DATE, TIMESTAMP, DOUBLE, BOOLEAN, INTERVAL YEAR TO MONTH, INTERVAL DAY TO SECOND, HASHTYPE
VARCHAR and CHAR are not supported as partition columns.
WHERE conditions with BETWEEN, <, >, date ranges).The canonical pattern for large fact tables:
CREATE TABLE fact_sales (
sale_id INT,
customer_id INT, -- join column → distribute
sale_date DATE, -- range filter column → partition
amount DECIMAL(15, 2),
DISTRIBUTE BY customer_id,
PARTITION BY sale_date
);
This enables:
customer_id (distribution)sale_date range filtersZone maps are a metadata layer on data segments that store the minimum and maximum values for a column within each segment. When a query has a predicate on a zone-mapped column, Exasol checks the zone records first and skips any segment whose min/max range cannot satisfy the predicate — reducing I/O without scanning data.
Zone maps are automatically enabled on partition columns. For non-partitioned columns, use:
-- Enable zone map on a column
ENFORCE ZONEMAP ON my_table (column_name);
-- Remove zone map from a column
DROP ZONEMAP ON my_table (column_name);
Zone maps are most effective when the column has data locality — values that are naturally clustered or sorted within segments:
IDENTITY columns)PARTITION BY keysZone maps are applied for: =, <, >, <=, >=, BETWEEN, IN, IS NULL, IS NOT NULL, and AND combinations.
| Support level | Types |
|---|---|
| Full | BOOLEAN, DATE, DECIMAL, DOUBLE, INTERVAL YEAR TO MONTH, TIMESTAMP |
| Limited (equality only) | INTERVAL DAY TO SECOND |
| Not supported | CHAR, VARCHAR, GEOMETRY, HASHTYPE |
-- Check which columns have zone maps
SELECT COLUMN_NAME, COLUMN_IS_ZONEMAPPED
FROM EXA_ALL_COLUMNS
WHERE COLUMN_TABLE = 'MY_TABLE' AND COLUMN_SCHEMA = 'MY_SCHEMA';
-- DESCRIBE also shows zone-mapped columns
DESCRIBE my_table;
Profiling output shows WITH ZONEMAP next to a scan step when zone records pruned segments during execution.
| Anti-pattern | Why it hurts |
|---|---|
Zone map on a VARCHAR or CHAR column | Not supported — zone map has no effect |
| Zone map on a randomly inserted column (no locality) | Min/max spans the full range in every segment — no segments are skipped |
Zone map instead of PARTITION BY for large tables | Partitioning prunes whole partitions; zone maps only skip segments within a partition — use both together on large tables |
Small tables (dimension tables in star schemas) can be automatically replicated to every node, turning global joins into local joins without explicit distribution.
ALTER SYSTEM SET REPLICATION_BORDER = 1000000;
The query optimizer replicates tables below this threshold into local DB RAM on each node, making any join against them local automatically. This complements (not replaces) proper distribution key design on large fact tables.
Use surrogate keys (synthetic DECIMAL IDs) in place of:
VARCHAR or DATE/TIMESTAMP keys are significantly slower than joins on DECIMAL.-- Preferred: surrogate key as DECIMAL
CREATE TABLE customers (
customer_id DECIMAL(18, 0) IDENTITY PRIMARY KEY,
email VARCHAR(255) NOT NULL,
...
);
-- Avoid: joining on VARCHAR natural key
SELECT * FROM orders o JOIN customers c ON o.email = c.email;
-- Prefer: joining on DECIMAL surrogate key
SELECT * FROM orders o JOIN customers c ON o.customer_id = c.customer_id;
ORDER BY in views — views are inlined into queries; an ORDER BY inside forces a sort that the outer query may discard.UNION ALL over UNION — UNION performs duplicate elimination (an expensive sort/hash); use UNION ALL unless deduplication is required.ON conditions instead of USING in multi-join queries — USING in complex multi-join queries can cause exponential heap memory consumption.CROSS JOIN unintentionally — a missing ON condition produces a cartesian product.-- Inherit schema from production table; no data, no constraints
CREATE TABLE staging.orders_stg (LIKE production.orders INCLUDING DEFAULTS);
-- After loading into staging, merge into production
MERGE INTO production.orders t
USING staging.orders_stg s ON t.order_id = s.order_id
WHEN MATCHED THEN UPDATE SET t.status = s.status, t.amount = s.amount
WHEN NOT MATCHED THEN INSERT VALUES (s.order_id, s.customer_id, s.status, s.amount, s.order_date);
TRUNCATE TABLE staging.orders_stg;
COMMIT;
CREATE TABLE fact_events (
event_id DECIMAL(18, 0) IDENTITY PRIMARY KEY,
user_id DECIMAL(18, 0) NOT NULL, -- distribute: frequent join column
event_ts TIMESTAMP NOT NULL, -- partition: range filter column
event_type VARCHAR(64) NOT NULL,
payload VARCHAR(4000),
DISTRIBUTE BY user_id,
PARTITION BY event_ts
);