Max Kemel
07/05/2026, 8:59 AMRocky
07/05/2026, 8:59 AMPIPE that monitors an S3 path. When you upload new Parquet files, StarRocks automatically detects and ingests them.
_ Example: CREATE PIPE my_pipe PROPERTIES ("AUTO_INGEST" = "TRUE") AS INSERT INTO my_table SELECT _ FROM FILES(...)
* INSERT INTO ... SELECT FROM FILES(): This allows you to run a SQL command that reads files from S3 and inserts them into a table. It is great for ad-hoc loads or manual triggers.
* Broker Load: An asynchronous background process suitable for very large batch imports from S3.
2. Capabilities: Inserts, Updates, and Partial Updates
StarRocks provides flexible data modification capabilities when ingesting from S3:
* Simple INSERTS: Supported on all table types (Duplicate, Aggregate, Unique, Primary Key).
* UPSERT (Update or Insert): If your target is a Primary Key (PK) table, any ingestion from S3 will default to "Upsert" logic—it will update the row if the primary key exists and insert it if it does not.
* PARTIAL UPDATES:
* Supported on Primary Key tables.
* Starting from v3.3.1 (and certainly in v4.1.1), if you use INSERT INTO (via FILES() or PIPE) and specify only a subset of columns, StarRocks performs a Partial Update. Only the specified columns are changed; others remain untouched.
* Conditional Updates: You can specify a column (like a version or updated_at timestamp) as a merge_condition to ensure that older files in S3 do not overwrite newer data already in the table.
3. Shared-data vs. Non-shared data
Both architectures are supported.
* Shared-nothing (Non-shared): Data is stored on the local disks of the BE (Backend) nodes. S3 ingestion works via any of the methods above.
* Shared-data (Compute-storage separation): Data is stored in S3/Object Storage. Ingestion from your Parquet S3 bucket into the StarRocks "native" tables (also in S3) is fully supported. Primary Key tables and the Pipe feature are optimized for this architecture.
Summary for Version 4.1.1
In your version, the Pipe feature is the most robust way to handle your workflow. It handles file discovery, ensures "exactly-once" semantics (by tracking which files have been loaded), and supports the partial update logic you need for Primary Key tables.
Useful Reference:
* Load data from AWS S3 using Pipe
* Partial updates in Primary Key tables
References
• loading/Load_to_Primary_Key_tables.md
• table_design/table_types/primary_key_table.mdMax Kemel
07/05/2026, 9:04 AMRocky
07/05/2026, 9:05 AMFILES() table function, which is the standard way to consume Parquet from S3.
Scenario 1: Standard UPSERT into a PK Table
In a Primary Key table, any INSERT operation automatically acts as an UPSERT. If the Primary Key already exists, the row is updated; if it doesn't, it is inserted.
SQL Example:
sql
-- 1. Create the PK table
CREATE TABLE users (
user_id BIGINT,
username STRING,
city STRING,
update_time DATETIME
) PRIMARY KEY (user_id)
DISTRIBUTED BY HASH(user_id);
-- 2. Consume from S3 and UPSERT
INSERT INTO users
SELECT user_id, username, city, update_time
FROM FILES(
"path" = "<s3://my-bucket/data/*.parquet>",
"format" = "parquet",
"aws.s3.access_key" = "...",
"aws.s3.secret_key" = "..."
);
──────────
Scenario 2: PARTIAL UPSERT with JSON and Conditional Logic
For this scenario, there are two important concepts in StarRocks:
1. Partial Update: You only specify the columns you want to change.
2. Conditional Update (merge_condition): You can ensure an update only happens if the incoming data is "newer" (based on a timestamp) than what is already in the table.
Note on JSON: StarRocks replaces the entire value of a column during an update. It does not natively "merge" keys inside a single JSON object during a partial update. To update specific fields within a JSON column based on a timestamp inside that JSON, you should:
* Extract the timestamp from the JSON during the load.
* Use that timestamp as the merge_condition.
SQL Example:
Assume your Parquet file has a JSON column extra_info like {"city": "NYC", "ts": 1719830400} and you only want to update this column if the internal ts is newer.
sql
-- 1. Create table with a dedicated column for the JSON timestamp
CREATE TABLE users_partial (
user_id BIGINT,
username STRING,
extra_info JSON,
json_version BIGINT -- Used for conditional logic
) PRIMARY KEY (user_id)
DISTRIBUTED BY HASH(user_id);
-- 2. Perform Partial Update with a Condition
-- We use the 'partial_update' and 'merge_condition' session variables
SET partial_update_mode = 'column';
INSERT INTO users_partial (user_id, extra_info, json_version)
SELECT
user_id,
extra_info,
-- Extract the timestamp from the nested JSON to use as a version
get_json_int(extra_info, '$.ts') as json_version
FROM FILES(
"path" = "<s3://my-bucket/updates/*.parquet>",
"format" = "parquet",
...
)
PROPERTIES (
"partial_update" = "true",
"merge_condition" = "json_version"
);
Key Technical Details for v4.1.1:
* partial_update: Setting this to true ensures that if your SELECT list only contains 3 out of 10 columns, the other 7 columns in the table remain unchanged.
* merge_condition: This is critical for out-of-order data. StarRocks will compare the json_version in your S3 file with the json_version currently in the table. The update only happens if source.json_version >= target.json_version.
* Shared Data vs Non-Shared: This logic works identically on both architectures. On Shared-data (StarRocks 3.x/4.x cloud-native), this is highly optimized as it avoids reading the original data for PK tables (using the "Persistent Index").
* Automation: You can wrap the INSERT INTO ... SELECT FROM FILES() logic inside a PIPE to make this a continuous, automated background process that triggers every time a new file hits your S3 bucket.
References
• loading/Flink-connector-starrocks.md
• faq/Others.mdMax Kemel
07/05/2026, 9:27 AMRocky
07/05/2026, 9:27 AMsql
CREATE EXTERNAL CATALOG iceberg_s3
PROPERTIES (
"type" = "iceberg",
"iceberg.catalog.type" = "glue", -- or "hive"
"aws.s3.region" = "us-east-1",
"aws.s3.access_key" = "YOUR_ACCESS_KEY",
"aws.s3.secret_key" = "YOUR_SECRET_KEY"
);
2. Create the target Iceberg table
You can create the table directly through StarRocks in that catalog.
sql
CREATE TABLE iceberg_s3.target_db.archived_users (
user_id BIGINT,
username STRING,
event_date DATE
)
ENGINE=ICEBERG
PARTITION BY (event_date);
3. Offload the data
To move or copy data from your native StarRocks table to S3 Iceberg, use an INSERT INTO statement.
sql
INSERT INTO iceberg_s3.target_db.archived_users
SELECT user_id, username, event_date
FROM default_catalog.native_db.users
WHERE event_date < '2024-01-01';
Key Capabilities for your version (4.1.1):
* Insert/Overwrite: You can use INSERT INTO to append data or INSERT OVERWRITE to replace partitions.
* Automatic Metadata Management: StarRocks handles the Iceberg manifest files and snapshots automatically.
* Open Format: Once the data is in the Iceberg catalog, it is no longer proprietary to StarRocks; it exists on S3 as Parquet files with Iceberg metadata.
* Architecture Agnostic: This feature is a function of the Compute Layer. Whether your "hot" data is on local BE disks (shared-nothing) or in StarRocks' own shared-storage format doesn't matter—the compute nodes can write out to the Iceberg format regardless.
Summary: For your goal of "offloading," you would typically schedule a job (via an external orchestrator or a StarRocks INSERT task) to periodically move older data from your local PK tables into the Iceberg S3 tables.Max Kemel
07/05/2026, 9:32 AMRocky
07/05/2026, 9:33 AMINSERT OVERWRITE replaces entire partitions. If you have late data for "Day 1" and you run an overwrite, any existing data for "Day 1" will be deleted.
To solve this, use Dynamic Overwrite. When enabled, StarRocks only overwrites the partitions that are actually present in your SELECT result. Other partitions in the Iceberg table remain untouched.
However, if you want to add late data to an already existing partition without deleting the data already in it, you should use INSERT INTO (Append).
2. Ensuring No Duplicates (Idempotency)
While StarRocks ensures that an INSERT is atomic (it won't leave "half-finished" data if it fails), it does not automatically know if you are accidentally sending the same data twice.
To prevent duplicates when moving data from a native StarRocks table to Iceberg, you can use a Left Anti-Join in your task. This ensures that only rows that don't already exist in the target Iceberg table are moved.
3. Implementation Example using a StarRocks Task
You can create a scheduled TASK that runs every hour to offload "new" data.
sql
-- 1. Enable Dynamic Overwrite for the session
SET dynamic_overwrite = true;
-- 2. Create a scheduled task
CREATE TASK offload_to_iceberg
SCHEDULE PERIOD 1 HOUR
AS
INSERT INTO iceberg_s3.target_db.archived_users
SELECT s.*
FROM default_catalog.native_db.hot_table s
LEFT ANTI JOIN iceberg_s3.target_db.archived_users t
ON s.user_id = t.user_id -- Check against unique ID
WHERE s.update_time >= now() - INTERVAL 1 DAY;
Key Technical Behaviors:
* Atomicity: If the task fails midway (e.g., S3 connection drop), StarRocks will not commit the metadata to Iceberg. No partial data will appear in your Iceberg table.
* Retries: If you retry a failed task, it starts fresh. Because it was atomic, the second attempt won't see any "leftover" data from the first attempt.
* Overlapping Late Data: By using the LEFT ANTI JOIN (or a WHERE NOT EXISTS clause), even if late data for a previous day arrives, the task will check if that specific record already exists in the S3 Iceberg table before appending it.
* Small Files: StarRocks v4.1.1 includes a "Global Shuffle" for Iceberg sinks, which automatically groups data by partition across all BE nodes before writing, significantly reducing the number of small files created in your S3 bucket.
Summary of Options
Goal | Method | Recommendation
Simple Append | INSERT INTO | Use for high-frequency small batches.
Avoid Duplicates | LEFT ANTI JOIN | Filter source data against the target Iceberg table.
Replace Specific Days | Dynamic Overwrite | SET dynamic_overwrite = true before INSERT OVERWRITE.
Automation | CREATE TASK | Use for periodic offloading with built-in status tracking (information_schema.task_runs).
References
• data_source/catalog/iceberg/DML.md
• loading/loading_introduction/loading_concepts.md
• loading/InsertInto.md
• release_notes/release-4.0.md
References
• data_source/catalog/iceberg/DML.md
• data_source/catalog/iceberg/iceberg_catalog.md
• quick_start/iceberg.mdMax Kemel
07/05/2026, 9:42 AMRocky
07/05/2026, 9:43 AMSUBMIT TASK statement to create a recurring job. To move data older than 7 days, you use a WHERE clause with date arithmetic.
Implementation Example:
sql
CREATE TASK offload_7d_to_iceberg
SCHEDULE EVERY (INTERVAL 2 HOUR)
AS
INSERT INTO iceberg_s3.archive_db.users_iceberg
SELECT * FROM native_db.users
WHERE event_date < CURRENT_DATE() - INTERVAL 7 DAY;
Important Note on Deletion: In StarRocks, a single TASK can only execute one ETL statement (like INSERT). It does not support a sequence of statements (e.g., INSERT; DELETE;) or transactional blocks (BEGIN...COMMIT) in shared-nothing mode. To remove data from the native table after a successful move, you have two options:
* External Orchestrator (Recommended): Use Airflow or a script to run the INSERT, check for success, then run the DELETE.
* StarRocks Native TTL: Instead of a manual delete, set a Partition TTL on your native StarRocks table. If you partition your table by day, you can set it to automatically drop partitions older than 8 days. This is much more efficient than a DELETE statement.
2. Monitoring and Alerting
StarRocks provides built-in metadata views and Prometheus metrics for task monitoring.
* Metadata Tracking: Query the information_schema.task_runs table.
sql
SELECT * FROM information_schema.task_runs
WHERE task_name = 'offload_7d_to_iceberg'
ORDER BY create_time DESC;
* Atomicity: If the INSERT fails, no data is moved and the task state becomes FAILED. Because the delete logic must be a separate step (or handled by TTL), a failure in the move task naturally prevents data loss because you haven't deleted the source yet.
* Metrics for Alerting: You can monitor the following Prometheus metrics:
* `starrocks_fe_query_err`: Total failed queries (includes failed tasks).
* `starrocks_fe_txn_failed`: Failed transactions.
* You can also create a custom alert by querying task_runs via a script that checks for STATE = 'FAILED'.
3. Retry Mechanism
* Scheduled Retries: A scheduled task does not have a "retry immediate" property. If it fails, it will simply attempt to run again at the next scheduled interval (2 hours later).
* Manual Retry: You can force an immediate run of a failed task using:
sql
SUBMIT TASK AS INSERT INTO ... -- or submit the same task again
4. Failure Information and Partition Data
If a task fails, the task_runs view provides specific debugging info:
* `ERROR_MESSAGE`: Contains the technical reason (e.g., S3 Connection Timeout, Permission Denied, or Disk Full).
* Partition Context: For a standard INSERT, the error won't explicitly list "Tenant X failed" in a structured column. However, the DEFINITION column stores the SQL executed, and the ERROR_MESSAGE usually includes context if the failure happened during a specific write.
* Recommendation: If you have multiple tenants and want granular visibility, consider creating one task per tenant or include the tenant ID in the task name dynamically if using an external script.
5. Summary & Best Practices
* Use Partition TTL: For "removing data" in a high-scale environment, don't use DELETE FROM table WHERE date < .... Instead, define your native table with PARTITION BY and set the property "partition_ttl_number" = "8". This is a background metadata operation that is nearly "free" in terms of performance.
* Task Cleanup: StarRocks cleans up task_runs history based on FE configurations (task_runs_ttl_second, default 24 hours). Ensure your monitoring tool scrapes this data frequently enough.Rocky
07/05/2026, 9:43 AMDELETE in a transaction block is a shared-data (Cloud Native) feature. In your shared-nothing setup, stick to the Move (Task) + Drop (TTL) pattern for the most robust production workflow.
References
• sql-reference/sql-statements/loading_unloading/ETL/SUBMIT_TASK.md
• sql-reference/information_schema/task_runs.md
• administration/management/monitoring/Monitor_and_Alert.md
• sql-reference/sql-statements/table_bucket_part_index/DELETE.md
• table_design/table_types/primary_key_table.md
• loading/SQL_transaction.md
References
• table_design/table_types/primary_key_table.mdMax Kemel
07/05/2026, 9:48 AMDELETE FROM table WHERE date < .... Instead, define your native table with PARTITION BY and set the property "partition_ttl_number" = "8". This is a background metadata operation that is nearly "free" in terms of performance.
In case the offload fails, and then the TTL triggered - it means we lose the data, its not the best approach here right?Rocky
07/05/2026, 9:49 AMpartition_live_number) is risky for offloading because the TTL is a "blind" background process. If your S3 connection fails for 2 days and the TTL keeps running, StarRocks will drop the local data before it is safely backed up to Iceberg.
For a production-grade "Move then Delete" workflow in a shared-nothing architecture, you should avoid automatic TTL and use a Safe Orchestration Pattern.
1. The Safe Production Workflow (Orchestrated)
Since StarRocks tasks currently support only one statement at a time, you should use an external tool (Airflow, a Python script, or even a simple Cron job) to ensure the "Delete" only happens after a verified "Move."
Logic Flow:
1. Submit the Offload Task: INSERT INTO iceberg_catalog.db.table SELECT ... WHERE date = 'YYYY-MM-DD' AND tenant = 'X';
2. Poll for Success: Check information_schema.task_runs for that specific query_id or task_name.
3. Verify & Delete: Only if the state is 'SUCCESS', execute the command to remove data.
Pro-Tip: Instead of DELETE, use ALTER TABLE native_table DROP PARTITION p20240101;. Dropping a partition is a metadata-only operation that is instantaneous and doesn't stress the CPU/IO like a DELETE statement does.
2. Monitoring & Failure Details
If a task fails, you can get detailed information from the information_schema.task_runs view.
* To see the reason:
sql
SELECT task_name, state, error_message, create_time, finish_time
FROM information_schema.task_runs
WHERE state = 'FAILED'
ORDER BY create_time DESC LIMIT 5;
* Granular Data (Tenant/Day):
StarRocks won't automatically parse "which tenant failed" into a separate column. However, the definition column in task_runs contains the exact SQL that was running. If your script creates a task per day or per tenant (e.g., task_offload_tenantA_20240501), the task_name itself becomes your monitoring key.
3. Handling Failure & Retries
* Retry Mechanism: By default, a scheduled task just tries again at the next interval. It does not have an exponential backoff retry.
* Data Integrity: Because the INSERT into Iceberg is atomic, if it fails, no partial data is committed to the Iceberg manifest. Your source data remains untouched in StarRocks, so you can simply re-run the task.
4. Better Approach: The "Wait and Drop" Script
Here is how you would structure the logic in a production script:
python
# 1. Start the offload
cursor.execute("INSERT INTO iceberg_catalog.archive SELECT * FROM native WHERE date = '2024-07-01'")
# 2. Wait and check status
status = "RUNNING"
while status == "RUNNING":
cursor.execute("SELECT state, error_message FROM information_schema.task_runs WHERE ...")
row = cursor.fetchone()
status = row[0]
error = row[1]
time.sleep(30)
# 3. Only drop if successful
if status == "SUCCESS":
cursor.execute("ALTER TABLE native_table DROP PARTITION p20240701")
else:
send_alert(f"Offload failed for 2024-07-01. Reason: {error}")
Summary of Recommendations
* Disable automatic TTL for tables you are offloading. It is too dangerous.
* Use DROP PARTITION instead of DELETE. It is much safer and faster for cleaning up old data.
* Partition your native table by both day and tenant (List-Range partitioning) if you need to drop data for a specific tenant independently.
* Monitor via Prometheus: StarRocks exports starrocks_fe_query_err. You can set an alert in Grafana if this count increases, then check task_runs for the specific error_message.
References
• using_starrocks/Cost_based_optimizer.md
• knowledge/admin_and_operation/shared_data_compaction.mdMax Kemel
07/05/2026, 9:54 AMDROP PARTITION instead of DELETE. It is much safer and faster for cleaning up old data.
1. In case we drop the partition when the task is successful and a new late data arrives to the same partition - meaning we can delete non-offloaded data
2. Instead of a python script, wouldn't it be better to have a task per tenant that deletes data older then 7 days, only if, it exists in S3 iceberg? would it cause performance issues?Rocky
07/05/2026, 9:55 AMDROP PARTITION is Dangerous
You are 100% correct. If you use DROP PARTITION, you delete the entire "bucket" of data. If 99% of your data was offloaded to S3, but 1% arrived as "late data" five minutes ago, DROP PARTITION will destroy that 1% forever.
The Solution: Use DELETE ... USING (Multi-table Join Delete).
Instead of dropping the whole partition, you tell StarRocks: "Only delete rows from the native table if they already exist in the Iceberg table."
sql
-- Example: Delete only rows confirmed to be in Iceberg
DELETE FROM native_db.users_table
USING iceberg_s3.archive_db.users_iceberg AS archive
WHERE native_db.users_table.event_id = archive.event_id
AND native_db.users_table.event_date < CURRENT_DATE() - INTERVAL 7 DAY;
Why this is safe: Any "late data" that arrived after the last offload task will not have a matching event_id in the Iceberg table. Therefore, the DELETE statement will ignore those rows, leaving them in the native table to be picked up by the next offload run.
2. Task Per Tenant vs. Script
You can definitely use StarRocks TASK instead of a Python script. However, creating 1,000+ separate tasks (one per tenant) can become a management burden.
* Concurrency: StarRocks handles high concurrency well. The FE configuration max_task_runs_threads_num (default 512) controls how many tasks can run simultaneously. If you have 1,000 tenants, they will simply queue and run in batches of 512.
* Recommendation: Instead of one task per tenant, use one single Task that handles all tenants at once using a join. This is much more efficient for the StarRocks scheduler.
3. The "Two-Step" Task Pattern (The Safest Production Design)
Since a single Task can only run one statement, and you need an INSERT followed by a DELETE, you should create two separate tasks.
Task 1: The Offloader (Runs every 2 hours)
sql
CREATE TASK offload_task
SCHEDULE EVERY (INTERVAL 2 HOUR)
AS
INSERT INTO iceberg_s3.archive_db.users_iceberg
SELECT s.* FROM native_db.users_table s
LEFT ANTI JOIN iceberg_s3.archive_db.users_iceberg t ON s.event_id = t.event_id
WHERE s.event_date < CURRENT_DATE() - INTERVAL 7 DAY;
Task 2: The Cleaner (Runs every 2 hours, offset by 30 mins)
sql
CREATE TASK cleanup_task
SCHEDULE EVERY (INTERVAL 2 HOUR)
AS
DELETE FROM native_db.users_table
USING iceberg_s3.archive_db.users_iceberg AS archive
WHERE native_db.users_table.event_id = archive.event_id
AND native_db.users_table.event_date < CURRENT_DATE() - INTERVAL 7 DAY;
4. Performance Considerations
* Delete Overhead: Row-level DELETE is a "heavy" operation compared to DROP PARTITION. In StarRocks Primary Key tables, this is very fast. In Duplicate/Unique tables, it creates "tombstones" which requires background compaction.
* Monitoring Failures: If Task 1 (Offload) fails, Task 2 (Cleanup) will still run but it won't delete anything because it won't find any new matching IDs in the Iceberg table. This is a "self-healing" design.
* Error Logs: You can monitor the failure reason and which query was running by checking:
sql
SELECT task_name, state, error_message, create_time
FROM information_schema.task_runs
WHERE state = 'FAILED';
Summary
1. Stop using DROP PARTITION if you have late-arriving data.
2. Use DELETE ... USING [iceberg_table] to ensure only offloaded data is removed.
3. Use Primary Key tables for your native data if you plan to do frequent row-level deletes; they are optimized for this exact "move-and-delete" workflow.Rocky
07/05/2026, 9:55 AMMax Kemel
07/05/2026, 10:00 AMRocky
07/05/2026, 10:01 AMparallel_fragment_exec_instance_num: Increases the number of parallel threads per node to speed up the scan and write.
* query_timeout: For very large offloads (millions of rows), the default 5-minute timeout might be too short. You should increase this for your offload task:
sql
SET TABLE task_offload_name ( "query_timeout" = "3600" ); -- 1 hour
Summary
* Is it one transaction? Yes, from an Iceberg perspective, it is one atomic snapshot commit.
* Is it efficient? Yes, it uses all available nodes to write in parallel and streams the data to keep memory usage low.
* Does it fail safely? Yes, if the task fails, the Iceberg table is not updated, and no data is removed from your source table (if you follow the "Delete after Success" pattern).
References
• knowledge/usage/load/shared_data_batch_ingestion.md
• unloading/unload_using_insert_into_files.md
• data_source/catalog/iceberg/iceberg_catalog.md
• sql-reference/sql-statements/loading_unloading/ETL/SUBMIT_TASK.md
• loading/SQL_transaction.md
• administration/management/FE_configuration.md
ReferencesRocky
07/05/2026, 10:01 AMMax Kemel
07/05/2026, 10:05 AMRocky
07/05/2026, 10:05 AMMax Kemel
07/05/2026, 10:25 AMRocky
07/05/2026, 10:26 AMINSERT INTO ... SELECT statement running on a loop, you can use a Left Anti-Join inside the Pipe's SQL definition. This tells StarRocks: "Only insert rows from S3 if the ID does not already exist in the native table."
sql
CREATE PIPE my_s3_pipe
AS
INSERT INTO native_alerts
SELECT s.*
FROM FILES(
"path" = "<s3://my-bucket/alerts/*.parquet>",
"format" = "parquet",
...
) s
LEFT JOIN native_alerts t ON s.alert_id = t.alert_id
WHERE t.alert_id IS NULL; -- This filters out any record already in your table
Why this works: Even if you re-run the task or the S3 file contains an old OPEN state, the WHERE t.alert_id IS NULL clause will discard those rows before they ever reach the "Upsert" phase.
──────────
2. Conditional Updates (merge_condition)
If you are using a Primary Key table, you can use the merge_condition property. This is the most robust way to handle "out-of-order" data or "stale" updates. It ensures an update only happens if the incoming record is "newer" than the existing one.
Step A: Use a version or timestamp column
Ideally, your S3 data has a version or updated_at column. When a user manually updates the state in StarRocks, you should also increment that version.
Step B: Define the load with a condition
sql
-- Inside your Pipe or manual INSERT
INSERT INTO native_alerts
PROPERTIES("merge_condition" = "version_col")
SELECT * FROM FILES(...);
How it behaves:
* If S3 (version 1, 'OPEN') arrives, and StarRocks has (version 2, 'IN-PROGRESS'), StarRocks will ignore the S3 row because 1 < 2.
* This is the standard industry practice for preventing "stale" data from S3 overwriting fresh data in the database.
──────────
3. Native Pipe File Tracking (Automatic)
It is worth noting that StarRocks Pipes have built-in idempotency.
* Pipe maintains a record of every file it has successfully loaded in information_schema.pipe_files.
* If you "re-run" the same Pipe, it will check the File Name and ETag (digest).
* If the file hasn't changed, Pipe will skip it automatically.
When this fails: If you delete the Pipe and recreate it, or if you upload the same data to S3 under a new filename, the Pipe will treat it as new data. In those cases, use Option 1 (Anti-Join) or Option 2 (Merge Condition).
Which should you choose?
* Use Option 1 (Anti-Join) if you never want S3 to update an existing row (Strict "Insert Only").
* Use Option 2 (Merge Condition) if you want S3 to update rows only if the S3 data is actually newer than what's in the table. This is the most flexible and professional approach.
Summary of properties for Primary Key tables:
Goal | Method
Avoid any update | LEFT JOIN ... WHERE t.id IS NULL
Avoid older updates | PROPERTIES("merge_condition" = "timestamp_col")
Update only specific columns | PARTIAL UPDATE (v3.3+)
References
• loading/s3.md
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• loading/Load_to_Primary_Key_tables.md
• table_design/table_types/primary_key_table.md
References
• loading/Load_to_Primary_Key_tables.md
• loading/Spark-connector-starrocks.mdRocky
07/05/2026, 10:26 AMMax Kemel
07/05/2026, 10:31 AMsql
CREATE PIPE my_s3_pipe
AS
INSERT INTO native_alerts
SELECT s.*
FROM FILES(
"path" = "<s3://my-bucket/alerts/*.parquet>",
"format" = "parquet",
...
) s
LEFT JOIN native_alerts t ON s.alert_id = t.alert_id
WHERE t.alert_id IS NULL; -- This filters out any record already in your table
is the WHERE t.alert_id IS NULL is equivallent to ANTI JOIN?Rocky
07/05/2026, 10:31 AMLEFT JOIN combined with a WHERE ... IS NULL check is the traditional way to perform what is formally known as an Anti Join.
1. The Comparison
Both of the following queries will yield the exact same result:
Option A: The "IS NULL" Pattern (Your example)
sql
SELECT s.*
FROM FILES(...) s
LEFT JOIN native_alerts t ON s.alert_id = t.alert_id
WHERE t.alert_id IS NULL;
Option B: Explicit LEFT ANTI JOIN Syntax
StarRocks supports the explicit LEFT ANTI JOIN syntax, which is cleaner and more readable:
sql
SELECT s.*
FROM FILES(...) s
LEFT ANTI JOIN native_alerts t ON s.alert_id = t.alert_id;
2. How it works
* The Join: It looks for a match in the native_alerts table for every row coming from S3.
* The Filter:
* If a match is found, t.alert_id will have a value, and the row is discarded.
* If a match is NOT found, t.alert_id will be NULL, and the row is kept.
3. Performance & Optimizer
In StarRocks, the query optimizer is smart enough to recognize both patterns. If you write the LEFT JOIN ... WHERE IS NULL version, the optimizer will typically transform it into a physical Null Aware Anti Join operator anyway.
Recommendation:
While both work, use the LEFT ANTI JOIN syntax if you want your code to be more intentional. It explicitly tells anyone reading the code: "I am only looking for rows that do NOT exist in the target table."
One important Note for Primary Key Tables:
If your native_alerts table is a Primary Key table, using LEFT ANTI JOIN in a Pipe is a great way to prevent "re-inserting" rows. However, keep in mind that this check happens at the moment the Pipe runs. If two identical IDs are in the same S3 file batch, the Join won't see them as "existing" yet, and both might be sent to the storage engine.
For Primary Key tables, the storage engine will then handle the de-duplication automatically, ensuring you still only have one final version of that ID 1 in the end.Max Kemel
07/05/2026, 10:54 AMRocky
07/05/2026, 10:54 AMPIPE or FILES() function that automatically converts all top-level column names and all nested JSON keys to lowercase.
However, you can achieve this using SQL transformation within your Pipe's SELECT statement.
1. Top-Level Field (Column) Names
StarRocks is case-insensitive for SQL identifiers (column names) by default. If your Parquet file has a column named STATE and your StarRocks table has a column named state, StarRocks will automatically map them correctly during an INSERT INTO ... SELECT *.
If you want to be explicit or if you are creating a new table via CTAS, you can alias them:
sql
CREATE PIPE my_pipe AS
INSERT INTO my_table
SELECT
STATE AS state, -- Explicitly mapping/lowercasing
USER_ID AS user_id
FROM FILES(...)
──────────
2. JSON Keys (Top-Level Only)
If you have a JSON column and you want to ensure all top-level keys are lowercase, you can use a combination of MAP and ARRAY functions to reconstruct the JSON object.
The logic: Convert the JSON to a Map, lowercase the keys in the array, and convert it back to JSON.
sql
SELECT
-- 1. Get keys and lowercase them
-- 2. Map those new keys to the original values
-- 3. Cast back to JSON
CAST(
map_from_arrays(
array_map(k -> lower(k), map_keys(CAST(json_col AS MAP<STRING, JSON>))),
map_values(CAST(json_col AS MAP<STRING, JSON>))
)
AS JSON) as lower_json_col
FROM FILES(...)
──────────
3. Nested JSON Keys
StarRocks does not currently have a recursive "lowercase all keys" function for deeply nested JSON objects. To lowercase nested keys, you must explicitly extract and reconstruct them using `json_object()`:
sql
CREATE PIPE my_pipe AS
INSERT INTO my_table
SELECT
json_object(
'metadata', json_object(
-- Manually lowercasing nested keys
'timestamp', json_col->"$.METADATA.TIMESTAMP",
'source', json_col->"$.METADATA.SOURCE"
),
'payload', json_col->"$.PAYLOAD"
) as processed_json
FROM FILES(...)
Summary of Solutions
Target | Method
Column Names | Automatically handled (case-insensitive) or use AS alias.
Top JSON Keys | Use map_from_arrays with array_map(k -> lower(k), ...).
Nested JSON Keys | Must be manually reconstructed using json_object.
Important Note for Pipes:
Since transformations (like the Map reconstruction above) can be computationally expensive for very large JSON blobs, it is usually recommended to handle this either at the source (before uploading to S3) or during the query phase using a Generated Column to create a clean, lowercased version of the data for easier filtering.
References
• sql-reference/sql-functions/array-functions/array_map.md
• sql-reference/sql-functions/map-functions/map_from_arrays.md
• sql-reference/sql-functions/json-functions/json-constructor-functions/json_object.md
• sql-reference/data-types/semi_structured/JSON.md
References
• sql-reference/sql-functions/array-functions/array_map.md
• sql-reference/sql-functions/map-functions/map_from_arrays.md
• sql-reference/sql-functions/map-functions/map_values.mdRocky
07/05/2026, 10:54 AMMax Kemel
07/05/2026, 10:57 AMRocky
07/05/2026, 10:58 AMSTRUCT or JSON type.
Here is the breakdown of how StarRocks handles these scenarios during a PIPE load from S3:
1. Top-Level Columns (Schema Mismatch)
If your S3 file (e.g., Parquet or JSON) contains columns that do not exist in your StarRocks table:
* Behavior: StarRocks ignores the extra columns.
* Result: Only the columns that match the names in your StarRocks table (or are explicitly mapped in your SELECT statement) will be loaded. The data for the "extra" fields in the S3 file is simply discarded during the scan.
2. Nested Fields in a STRUCT Column
If you have a column defined as a STRUCT<name STRING, age INT> and the Parquet file contains a struct with more fields (e.g., name, age, and gender):
* Behavior: StarRocks typically performs mapping by name (from v3.x onwards).
* Result: It will extract the name and age fields and ignore the gender field.
* Important Note on Order: Historically, StarRocks was sensitive to the order of fields in a Struct. In recent versions (v3.1+), it has moved toward name-based mapping for Parquet, but it is a best practice to ensure the names match exactly. If a field exists in the table's Struct but is missing in the file, it will be loaded as NULL.
3. Nested Fields in a JSON Column
If you are loading into a column with the JSON data type:
* Behavior: The JSON type is "schema-less" in terms of its internal content.
* Result: StarRocks will ingest the entire JSON object exactly as it exists in S3.
* Extra Fields: If the S3 JSON has 10 fields and your application only uses 5, StarRocks still stores all 10. This is the main advantage of the JSON type—it is resilient to "upstream" schema changes where new fields are added without warning.
4. Summary Table
Scenario | Target Type | Behavior
Field in S3, NOT in Table | Top-level | Ignored (Skipped).
Field in S3, NOT in Table | STRUCT | Ignored (Matched by name; extra sub-fields are dropped).
Field in S3, NOT in Table | JSON | Preserved (The whole JSON blob is stored).
Field in Table, NOT in S3 | Any | Loaded as NULL (or default value).
Pro-Tip: Schema Evolution
If you want StarRocks to automatically add those new top-level columns to your table when they appear in S3, you can look into the Schema Evolution features (available in newer versions for specific load methods) or use the VARIANT type (introduced in v3.2/3.3) which acts like a "super-JSON" and can automatically optimize storage for new fields as they arrive.
Recommendation for your case:
* If you want to strictly ignore new fields and keep your table "clean," use the Native Table Columns or STRUCT type.
* If you want to capture everything for future-proofing (even if the fields change), use the JSON type.
References
• loading/Etl_in_loading.md
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/data-types/semi_structured/JSON.md
• using_starrocks/Flat_json.md
References
• using_starrocks/Flat_json.md
• loading/Json_loading.md
• sql-reference/data-types/semi_structured/JSON.mdMax Kemel
07/05/2026, 11:01 AMRocky
07/05/2026, 11:02 AMPIPE to load Parquet data into a STRUCT column in StarRocks, the behavior depends on whether the "nested JSON" in your Parquet file is a Native Parquet Group (Struct) or a String column containing JSON text.
1. Native Parquet Struct vs. JSON String
* If the Parquet column is a Native Struct: StarRocks will attempt to map the fields of the Parquet struct to the fields of your StarRocks STRUCT.
* If the Parquet column is a String (containing JSON): StarRocks cannot automatically map this to a STRUCT column. You would need to load it into a JSON type column first or use parse_json() in your SELECT statement to convert it.
──────────
2. Is Mapping Case-Sensitive?
The answer depends on whether StarRocks is mapping by Name or by Position (Order), which changed in recent versions:
A. Mapping by Name (v3.4+ and Data Lake Catalogs)
In StarRocks v3.4 and later, the FILES() function has improved support for name-based mapping.
* Top-Level Columns: Case-insensitive. StarRocks will match MY_COLUMN in Parquet to my_column in your table.
* Nested Fields (inside the STRUCT): These are generally case-sensitive. Parquet itself is a case-sensitive format. If your StarRocks table defines STRUCT<city STRING> but the Parquet file has CITY, the mapping might fail or return NULL because the internal field names are treated as identifiers that must match the source file's schema exactly.
B. Mapping by Position (Earlier Versions / Standard Loads)
Historically, and in many current loading paths (like BROKER LOAD or PIPE on older versions), StarRocks maps STRUCT fields strictly by position (order).
* Behavior: It doesn't matter what the field is named in Parquet. StarRocks takes the 1st field in the Parquet struct and puts it into the 1st field of your StarRocks STRUCT.
* Requirement: You must ensure the fields in your CREATE TABLE statement are in the exact same order as they appear in the Parquet file.
──────────
3. Summary of Behavior for "Missing" or "Extra" Fields
If your StarRocks table and Parquet file have different nested structures:
Scenario | Behavior
Field in Parquet, NOT in Table STRUCT | Ignored. The extra data is dropped.
Field in Table STRUCT, NOT in Parquet | Loaded as NULL.
Names match but Cases differ | May return NULL if using name-based mapping (v3.4+), or work fine if using position-based mapping.
Recommendation
If you are dealing with inconsistent schemas (changing field names, different cases, or evolving JSON), it is highly recommended to use the JSON data type in StarRocks instead of STRUCT.
* The JSON type is case-sensitive for keys (matching the standard JSON spec).
* It handles missing/extra fields gracefully without requiring you to redefine the table.
* You can then use Generated Columns to extract specific keys into high-performance columns while ignoring case issues like case-sensitivity are handled at the query level.
References
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/data-types/semi_structured/JSON.md
• loading/Etl_in_loading.md
References
• sql-reference/sql-functions/table-functions/files.mdMax Kemel
07/05/2026, 11:04 AMparse_json() in our FILE, how will the mapping to the nested struct column?Rocky
07/05/2026, 11:05 AMparse_json() on a string column from Parquet and then attempt to load it into a STRUCT column (or cast it to a STRUCT in your SELECT statement), StarRocks follows specific mapping rules.
1. Mapping Logic: Name-Based
When converting from a JSON object to a STRUCT, StarRocks maps the JSON keys to the STRUCT field names.
* Behavior: If the JSON key matches the STRUCT field name, the value is populated.
* Missing Fields: If a field defined in your STRUCT is missing from the JSON object, it will be loaded as NULL.
* Extra Fields: If the JSON object contains keys that are not defined in your STRUCT, those extra keys are silently ignored.
2. Case Sensitivity: Strictly Sensitive
Unlike top-level table columns (which are case-insensitive), mapping within a JSON-to-STRUCT conversion is case-sensitive.
* If your table defines a column as STRUCT<user_id INT>, but the JSON string from Parquet contains {"USER_ID": 123}, the result will be NULL for that field because USER_ID does not match user_id.
* Since your goal is to handle potentially inconsistent casing, you must ensure the cases match exactly.
3. How to implement this in a PIPE
To load a Parquet string column into a Struct while ensuring it maps correctly (or handling the lowercase requirement you mentioned earlier), you would use a CAST in your Pipe's SELECT statement:
sql
CREATE PIPE my_pipe AS
INSERT INTO my_table
SELECT
-- 1. Parse string to JSON
-- 2. Cast JSON to the target STRUCT type
CAST(parse_json(parquet_string_col) AS STRUCT<id INT, name STRING, city STRING>)
FROM FILES(...)
4. Handling the "Lowercase" Requirement
If your incoming JSON string has uppercase keys (e.g., {"NAME": "Rocky"}) and your table uses lowercase struct fields (STRUCT<name STRING>), the simple CAST above will return NULL.
To solve this, you can lowercase the JSON keys before casting by using the Map transformation trick:
sql
SELECT
CAST(
map_from_arrays(
array_map(k -> lower(k), map_keys(CAST(parse_json(parquet_string_col) AS MAP<STRING, JSON>))),
map_values(CAST(parse_json(parquet_string_col) AS MAP<STRING, JSON>))
)
AS STRUCT<id INT, name STRING, city STRING>)
FROM FILES(...)
Summary of Behavior
Feature | Behavior for JSON $\rightarrow$ STRUCT
Mapping Type | Name-based (Keys must match Field Names).
Case Sensitivity | Case-sensitive ("A" $\neq$ "a").
Extra JSON Keys | Ignored (Dropped during conversion).
Missing JSON Keys | NULL (Field becomes null in the table).
Recommendation: If you cannot guarantee the casing of the keys in the S3 files, it is safer to load the data into a JSON type column first. You can then use StarRocks' Generated Columns to extract the values you need using case-insensitive logic or simple path lookups.
References
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/sql-functions/json-functions/json-constructor-functions/parse_json.md
• sql-reference/data-types/semi_structured/JSON.md
• loading/Etl_in_loading.md
• sql-reference/sql-functions/map-functions/map_from_arrays.md
• sql-reference/sql-functions/array-functions/array_map.md
References
• using_starrocks/Flat_json.mdRocky
07/05/2026, 11:05 AMMax Kemel
07/05/2026, 11:09 AMCAST(
map_from_arrays(
array_map(k -> lower(k), map_keys(CAST(parse_json(parquet_string_col) AS MAP<STRING, JSON>))),
map_values(CAST(parse_json(parquet_string_col) AS MAP<STRING, JSON>))
)
2. CAST(parse_json(parquet_string_col) AS STRUCT<id INT, name STRING, city STRING>) - do we have to say 'AS STRUCT', we want to keep the SQL definition simple, in case a struct is being changed, we won't need to change the query - how can we achieve it?Rocky
07/05/2026, 11:09 AMsql
CAST(
map_from_arrays(
array_map(k -> lower(k), map_keys(CAST(parse_json(col) AS MAP<STRING, JSON>))),
map_values(CAST(parse_json(col) AS MAP<STRING, JSON>))
)
AS ...)
* How it works: It converts the JSON into a Map of String -> JSON. It lowercases the keys (Strings) but leaves the values (JSON objects) untouched.
* Recursive behavior: StarRocks does not currently have a built-in function to recursively lowercase all keys in a nested JSON structure. To achieve this, you would either need a very complex nested expression (if the depth is known and shallow) or, more practically, handle the case normalization before the data reaches S3 (e.g., in your ETL/Spark job).
──────────
2. Avoiding explicit AS STRUCT<id INT, ...> in the query
In StarRocks, when converting a JSON type (from parse_json) to a STRUCT type, you must explicitly define the fields in the CAST statement. This is because the engine needs a "template" to know which JSON keys to extract and what their data types are.
However, there are two ways to achieve your goal of keeping the SQL simple and avoiding manual updates:
Option A: Use the JSON Data Type with "Flat JSON" (Recommended)
Instead of defining the column as a STRUCT, define it as a JSON column in your table and enable the Flat JSON feature (available in v3.3+).
* Why this solves your problem:
* No Maintenance: You don't need to change your PIPE or table schema when new fields are added to the JSON. StarRocks will simply store the new fields.
* Performance: Flat JSON automatically detects frequently used keys and stores them in a columnar format behind the scenes, giving you STRUCT-like performance for queries.
* Usage: Your pipe becomes extremely simple:
sql
INSERT INTO my_table (json_col)
SELECT parse_json(parquet_string_col) FROM FILES(...)
* Case Sensitivity: You can handle casing in your queries (e.g., SELECT json_col->'lower_key' ...) without being locked into a rigid schema.
Option B: Map by Name (if Parquet is already a Struct)
If you can change your S3 files to store the nested data as a Native Parquet Struct (instead of a String), StarRocks (v3.1+) can map the fields automatically by name during a load, provided the column names match.
* In this case, you don't need parse_json or a complex CAST. You simply SELECT nested_col FROM FILES(...) and StarRocks will align the Parquet struct fields with your table's struct fields.
Summary
* Recursive Casing: Not supported natively in SQL; must be done at the top level or upstream.
* Maintenance-Free SQL: Use the JSON data type combined with Flat JSON. It provides the flexibility of JSON (handles extra/missing fields and changing schemas automatically) with the speed of a STRUCT.
References
• sql-reference/data-types/semi_structured/JSON.md
• using_starrocks/Flat_json.md
• sql-reference/data-types/semi_structured/STRUCT.md
• loading/Etl_in_loading.md
References
• using_starrocks/Flat_json.md
• sql-reference/data-types/semi_structured/JSON.md
• sql-reference/data-types/semi_structured/STRUCT.mdRocky
07/05/2026, 11:09 AMMax Kemel
07/05/2026, 11:11 AMRocky
07/05/2026, 11:12 AMuser_id (varchar 256) and USER_ID (varchar 512)—the behavior in StarRocks depends on the version and the specific loading method used.
1. Mapping is Case-Sensitive
In modern StarRocks versions (v3.1+ for Data Lake and v3.4+ for FILES()), the mapping between StarRocks and Parquet/ORC is case-sensitive.
* How it resolves: If your StarRocks table (or FILES() schema) defines the field as user_id, the reader will search the Parquet file for the exact string user_id.
* The "Other" Field: The field USER_ID will be treated as an entirely different column. Because it does not match the requested case, it will be silently ignored (skipped) by the reader.
* Result: You will only get the data from the field that matches your schema's case exactly.
2. Type Mismatches
If you successfully map to a field (e.g., StarRocks user_id matches Parquet user_id), but the types are different:
* Automatic Coercion: StarRocks will attempt to cast the Parquet type to the StarRocks type. For example, if the file has VARCHAR(256) and your table has VARCHAR(512), it will load successfully because 256 fits into 512.
* Incompatible Types: If the types are fundamentally incompatible (e.g., trying to load a MAP into a BIGINT), the value will be loaded as NULL (if strict_mode is false) or the load will fail (if strict_mode is true).
3. Ambiguity in StarRocks Tables
While the Parquet reader is case-sensitive, StarRocks table columns are generally case-insensitive. This creates a specific edge case:
* If you define a StarRocks table with user_id, StarRocks internally treats the column name as lowercase.
* When the Parquet reader looks at the file, it uses that lowercase user_id to search.
_ Therefore, even if the data you _actually* wanted was in the uppercase USER_ID field, StarRocks will miss it and pick the lowercase one (or return NULL if only the uppercase one exists).
Summary Table
File contains... | StarRocks Table defines... | Result
user_id and USER_ID | user_id | Loads **`user_id`**; ignores USER_ID.
USER_ID only | user_id | Result is NULL (Case does not match).
user_id (INT) | user_id (VARCHAR) | Loads and casts INT to VARCHAR.
Recommendation: If your source files have inconsistent casing or duplicate names with different cases, it is safest to use the JSON or VARIANT (v3.2+) data types. These types allow you to ingest the entire blob and then use path expressions like column->"user_id" or column->"USER_ID" in your queries to explicitly target the specific data you need.
References
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/data-types/semi_structured/VARIANT.md
• loading/Etl_in_loading.md
References
• sql-reference/data-types/semi_structured/VARIANT.md
• sql-reference/data-types/semi_structured/STRUCT.md
• knowledge/usage/best-practice.md
• developers/code-style-guides/thrift-guides.md
• administration/management/BE_configuration.md
• unloading/Spark_connector.mdRocky
07/05/2026, 11:12 AMMax Kemel
07/05/2026, 11:15 AMRocky
07/05/2026, 11:15 AMstrict_mode setting.
1. VARCHAR 512 (Source) vs. VARCHAR 256 (Table)
StarRocks does not perform "silent truncation" (it won't just chop off the end of the string). Instead, it treats values longer than the destination's length as unqualified rows.
* If strict_mode is false (default for most loads):
* The long string will be converted to NULL.
* Data Loss: Yes, the original value is gone and replaced by NULL.
* If strict_mode is true (default for INSERT in v3.4+):
* The entire row is filtered out (it won't be loaded at all).
* The load might fail entirely if the number of filtered rows exceeds your max_filter_ratio.
2. BIGINT (Source) vs. TINYINT (Table)
Similar logic applies to numeric overflows. TINYINT can only hold values from -128 to 127. If your Parquet file contains a value like `2000`:
* If strict_mode is `false`:
* The value 2000 is converted to NULL.
* If strict_mode is `true`:
* The row is filtered out.
* Data Loss: Yes, because a value like 2000 cannot be safely represented in 1 byte, StarRocks rejects the conversion rather than wrapping the number around (no modulo arithmetic).
Summary of Behavior
Scenario | Strict Mode = FALSE (Default) | Strict Mode = TRUE
String too long | Value becomes NULL | Row is filtered out
Number too large | Value becomes NULL | Row is filtered out
Incompatible Type | Value becomes NULL | Row is filtered out
Recommendations
1. Safety: If you aren't sure about the lengths, use a larger VARCHAR in StarRocks (up to 1,048,576 bytes). There is no significant storage penalty because VARCHAR only stores the actual bytes present.
2. Validation: Always check the tracking_url or the REASON field in your load results. If data is being converted to NULL or filtered, StarRocks will report "column length overflow" or "value out of range".
3. Strict Mode Control:
* For `INSERT`: Set SET enable_insert_strict = true; (to fail/filter) or false (to get NULLs).
* For BROKER LOAD / `ROUTINE LOAD`: Set "strict_mode" = "true" in the PROPERTIES block.
References
• loading/load_concept/strict_mode.md
• sql-reference/data-types/numeric/TINYINT.md
• sql-reference/data-types/string-type/VARCHAR.md
• faq/Others.md
• knowledge/usage/best-practice.md
References
• faq/Others.md
• knowledge/usage/best-practice.md
• sql-reference/data-types/string-type/VARCHAR.md
• sql-reference/System_limit.md
• faq/table_design_faq.md
• faq/Sql_faq.mdEliezer Talker
07/05/2026, 11:20 AMRocky
07/05/2026, 11:20 AMFILES() in an INSERT INTO ... SELECT query) is due to where the "intelligence" happens in the loading process.
1. Loader Logic (Stream Load / Broker Load)
When you use a dedicated loader like Stream Load, the engine knows the Target Table Schema before it even starts reading the file.
* How it works: If you have a table with a column user STRUCT<id INT, name STRING> and you send a JSON like {"user": {"id": 1, "name": "Rocky"}}, the loader sees the target is a STRUCT.
* Automatic Mapping: It automatically attempts to "fit" the JSON object into that STRUCT based on the field names. It does the "casting" for you behind the scenes because it has the context of where the data is going.
2. Functional Logic (FILES() / Pipe)
When you use INSERT INTO ... SELECT FROM FILES(), you are using the SQL Execution Engine.
* The Conflict: In your specific snippet, you are reading a Parquet file where the data is stored as a String (parquet_string_col).
* Lack of Context: When you call parse_json(parquet_string_col), the result is a generic JSON data type. At this point in the SELECT list, the SQL engine doesn't "know" yet that this value is eventually going into a STRUCT column in a table.
* Why the CAST is required: In SQL, functions must return a specific type. A JSON type is a flexible blob; a STRUCT is a rigid, schema-defined type. To move from the "flexible" world to the "rigid" world within a SQL query, you must explicitly tell the engine the structure using CAST(... AS STRUCT<...>).
How to get the "Stream Load" experience in a Pipe
If you want to avoid the complex SQL definitions and CAST statements, you have two options that align with how "Loader Logic" works:
Option A: Use the VARIANT Type (v3.2+)
The VARIANT type is designed exactly for this "I don't want to define the schema" problem.
1. Change your table column from STRUCT to VARIANT.
2. Your query becomes: SELECT parse_json(parquet_string_col) FROM FILES(...).
3. Result: No CAST needed. StarRocks will handle the internal mapping and indexing automatically.
Option B: Store as Native Parquet Structs
If you change your upstream process to save the data as a Native Parquet Struct (instead of a String containing JSON), the FILES() function will recognize it as a STRUCT immediately.
1. The FILES() function will infer the schema.
2. In version v3.4+, you can simply SELECT * FROM FILES(...) and it will map the Parquet struct fields to your table struct fields by name automatically, just like Stream Load does.
Summary
The FILES() pipe requires a CAST because you are performing a functional transformation (String -> JSON -> Struct) within a SQL query. Stream Load avoids this because it is a direct mapping from a file to a table. To simplify, use the VARIANT type or Flat JSON features which remove the need for strict STRUCT definitions.
References
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/data-types/semi_structured/VARIANT.md
• sql-reference/data-types/semi_structured/STRUCT.md
• loading/Etl_in_loading.md
• using_starrocks/Flat_json.md
References
• sql-reference/data-types/semi_structured/VARIANT.md
• sql-reference/data-types/semi_structured/STRUCT.mdRocky
07/05/2026, 11:20 AMEliezer Talker
07/05/2026, 11:24 AMRocky
07/05/2026, 11:24 AMSTRUCT definitions in SQL statements is exactly what we want to avoid in a modern data pipeline.
Currently, StarRocks does not support a syntax like CAST(value AS TYPE OF column_x). In SQL, the SELECT part of the query must produce a concrete type before the data is "handed over" to the table for insertion.
However, there are three ways to achieve exactly what you want (adding fields without touching your SQL code):
1. The Best Solution: Use the VARIANT Type (v3.2+)
The VARIANT type was created specifically to solve this maintenance headache. It is a "schema-less" type that behaves like a STRUCT for performance but doesn't require you to define the fields in your table or your SQL.
* Table Definition: Change your column type from STRUCT<...> to VARIANT.
* SQL Query:
sql
INSERT INTO my_table
SELECT parse_json(parquet_string_col) FROM FILES(...);
* Why this works: When you add a field to the JSON in your Parquet file, StarRocks automatically detects it, indexes it, and makes it queryable. You never have to update your CREATE TABLE or your INSERT statement again.
2. The "Modern Ingestion" Way: Use JSON Columns with "Flat JSON"
If you prefer the JSON data type over VARIANT, you can enable Flat JSON (available in v3.3+). This gives you the performance of a STRUCT but the flexibility of JSON.
* Table Definition: Set the column to JSON.
* SQL Query: No CAST needed. parse_json() output is natively accepted by a JSON column.
* Performance: By setting enable_generated_column = true or using the global flat JSON optimization, StarRocks internally turns that JSON into a high-performance columnar format, similar to a STRUCT.
3. If you MUST use `STRUCT`: Use a View to Hide the Cast
If your organization requires the column to be a strict STRUCT, you can't avoid the CAST, but you can hide it so you only have to update it in one place.
1. Create a View that performs the FILES() read and the CAST.
2. Your INSERT statement becomes: INSERT INTO target_table SELECT * FROM my_files_view;.
3. When the schema changes, you only update the View definition once.
Summary Comparison
Goal | STRUCT | VARIANT / JSON
Maintenance | High (must update SQL/Table) | Zero (automatic)
Query Syntax | col.field | col.field or col->'field'
Performance | Best | Very Close (with Flat JSON enabled)
Flexibility | Rigid | High
Rocky's Recommendation: Use the VARIANT type. It allows your schema to evolve upstream in the Parquet files without requiring any manual intervention in StarRocks, or maintenance of, your StarRocks ingestion pipelines.
References
• sql-reference/data-types/semi_structured/JSON.md
• sql-reference/data-types/semi_structured/STRUCT.md
• loading/Json_loading.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.mdEliezer Talker
07/05/2026, 11:27 AMFILES() read and the CAST.
2. Your INSERT statement becomes: INSERT INTO target_table SELECT * FROM my_files_view;.
3. When the schema changes, you only update the View definition once.
what is this * FROM my_files_view;. ? how does it get its valueRocky
07/05/2026, 11:28 AMSELECT * FROM my_files_view actually does:
1. The "Gatekeeper" (The View)
Instead of putting the complex FILES() logic and the CAST inside your daily load script, you define it once in a View.
Example View Definition:
sql
CREATE VIEW my_files_view AS
SELECT
id,
-- This CAST is your "Approval List"
-- Only these 3 fields are allowed through.
-- If the Parquet file adds a 4th field "age", it is ignored.
CAST(parse_json(user_json_str) AS STRUCT<name STRING, city STRING, zip INT>) AS user_info,
load_time
FROM FILES(
"path" = "<s3://my-bucket/data/*.parquet>",
"format" = "parquet",
... credentials ...
);
*2. What is SELECT * FROM my_files_view?*
When you run INSERT INTO target_table SELECT * FROM my_files_view;, here is what happens:
* "How does it get its value?": A View is just a stored query. Every time you run the INSERT, StarRocks "unpacks" the View. It executes the FILES() function, reads the Parquet files, applies your CAST (the gatekeeper), and produces a temporary result set in memory.
* *"What does the * do?"*: The * refers to the columns defined inside the View definition.
* It does not mean "everything in the Parquet file."
* It means "exactly the columns I listed in the CREATE VIEW statement."
* Column Mapping: By default, StarRocks maps the View's columns to the Table's columns by position (1st column to 1st column).
_ Rocky's Tip: Use INSERT INTO target_table BY NAME SELECT _ FROM my_files_view (available in v3.4+) to map them by column name instead of position. This is even safer.
3. Why this solves your "Approval" problem
If your upstream Parquet file suddenly changes:
* Case A: New field added to Parquet. Your View remains the same. The CAST in the View only looks for the fields you specified. The new field is safely ignored. Your pipeline doesn't break and no "unapproved" data enters.
* Case B: You want to approve the new field.
1. You update your target_table to add the new field to the STRUCT.
2. You update your View definition (ALTER VIEW) to include the new field in the CAST.
3. Your INSERT statement never has to change. It still just says SELECT * FROM my_files_view.
Summary of Benefits
1. Security: Nothing enters the table unless it's in the View's CAST.
2. Clean Code: Your ingestion script is 1 line long (INSERT ... SELECT * FROM view).
3. Centralized Control: If you have 10 different tables loading from the same file type, you only update the View in one place.
References
• sql-reference/sql-statements/table_bucket_part_index/CREATE_VIEW.md
• sql-reference/sql-statements/loading_unloading/INSERT.md
• loading/InsertInto.md
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/data-types/semi_structured/STRUCT.md
References
• knowledge/usage/best-practice.md
• loading/InsertInto.md
• sql-reference/sql-statements/loading_unloading/INSERT.mdRocky
07/05/2026, 11:28 AMEliezer Talker
07/05/2026, 11:31 AMRocky
07/05/2026, 11:31 AMSTRUCT definition and how to maintain it without manual typing, here are the details on limitations and the "copy-paste" shortcut you requested.
1. Is there a limitation on the STRUCT definition size?
Technically, yes, but it is extremely high. You are unlikely to hit a "text length" limit for the CAST statement itself.
* Expression Complexity: StarRocks has a limit on the number of nested "children" in an expression (controlled by expr_children_limit), which defaults to 10,000. This means your struct could theoretically have thousands of fields before the SQL engine complains.
* Data Size: The actual data stored in a STRUCT column is limited by the maximum row size (typically several MBs), and individual VARCHAR fields within it can now be up to 1MB each.
* RPC Limits: If your SQL statement becomes massive (e.g., several megabytes of text), it might hit the thrift_rpc_max_body_size limit, but for a standard schema (even with 500+ fields), this is not an issue.
2. How to get the definition for copy-pasting
You do not need to manually type out the STRUCT<...> string. You can extract it directly from the database using three methods, ordered from "most manual" to "most automated":
Method A: The SHOW CREATE TABLE way (Best for manual copy)
Run this command:
sql
SHOW CREATE TABLE your_table_name;
In the output, look for your column. It will look exactly like this:
user_info STRUCT<id INT, name STRING, city STRING, ...>
You can simply copy everything after the column name.
Method B: The DESCRIBE way
Run:
sql
DESCRIBE your_table_name;
The Type column in the result will contain the full STRUCT<...> string. Most SQL editors (like DBeaver or MySQL Workbench) allow you to right-click that cell and "Copy Value."
Method C: The "Automated" way (Querying Metadata)
If you want just the type string without any extra text, query the `information_schema`:
sql
SELECT column_type
FROM information_schema.columns
WHERE table_name = 'your_table_name'
AND column_name = 'your_struct_column_name';
Result: struct<id:int(11),name:varchar(65533),city:varchar(65533)>
Rocky's Tip for "Zero-Touch" Maintenance
If you want to be even more advanced, you can use a Stored Procedure or a script to dynamically generate the View. However, for most teams, the View approach combined with the information_schema query is the best balance:
1. Change the Table: ALTER TABLE target_table MODIFY COLUMN ... (or add a field).
2. Get the Type: Run the information_schema query above.
3. Update the View: Copy the result into your ALTER VIEW statement.
This ensures that the "Gatekeeper" (the View) only allows the specific fields you have approved into your high-performance STRUCT column.
References
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.md
• sql-reference/sql-statements/table_bucket_part_index/SHOW_CREATE_TABLE.md
• sql-reference/information_schema/columns.md
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/System_limit.md
References
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.md
• sql-reference/data-types/semi_structured/STRUCT.md
• table_design/StarRocks_table_design.mdRocky
07/05/2026, 11:31 AMEliezer Talker
07/05/2026, 11:33 AMRocky
07/05/2026, 11:33 AMSTRUCT, ARRAY, and MAP).
* Example: STRUCT<level1 STRUCT<level2 STRUCT<level3 INT>>> counts as 3 levels.
* Most real-world Parquet or JSON schemas rarely exceed 5 or 6 levels, so you have plenty of room.
2. The Copy-Paste Shortcut (Metadata Query)
When your struct has multiple levels, typing it out manually is impossible. Use this query to get the exact string you need for your `CAST(...)`:
sql
SELECT COLUMN_TYPE
FROM information_schema.columns
WHERE TABLE_NAME = 'your_target_table'
AND COLUMN_NAME = 'your_nested_column';
Why this is perfect for you:
* It returns the entire tree structure in one long string (e.g., struct<id:int,info:struct<name:varchar,addr:struct<city:varchar,zip:int>>>).
* The types are already converted to StarRocks-compatible syntax (e.g., it converts STRING to varchar(65533)).
* You can copy this result directly into your View's CAST statement.
3. How the "Gatekeeper" View handles Nesting
Even with deep nesting, the View approach remains your best way to enforce "Approval."
If you have a 3-level nested struct and you add a field to the 3rd level in your Parquet file:
1. Before Approval: Your View's CAST still uses the "old" definition. The new field is ignored by the CAST and never reaches your table.
2. Approval Process:
* ALTER TABLE to add the field to your StarRocks table.
* Run the information_schema query above to get the new, long type string.
* ALTER VIEW to update the CAST with that new string.
Summary: Is it an issue?
* Performance: No. StarRocks stores nested fields columnarly. Querying user.address.city is nearly as fast as querying a top-level column.
* Storage: No. It is very efficient.
* Maintenance: Only if you do it manually. Using information_schema.columns to generate the CAST string makes it a 10-second task.
Rocky's Tip: If your nesting is so deep that the CAST string becomes hard to read, remember that you only have to look at it when you are "approving" a schema change. The rest of the time, it sits silently inside the View!
References
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/information_schema/columns.md
• sql-reference/data-types/semi_structured/Array.md
• sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.md
References
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/information_schema/columns.md
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.mdRocky
07/05/2026, 11:34 AMSTRUCT, ARRAY, and MAP).
* Example: STRUCT<level1 STRUCT<level2 STRUCT<level3 INT>>> counts as 3 levels.
* Most real-world Parquet or JSON schemas rarely exceed 5-6 levels, so 14 levels is usually more than enough.
2. How the "Gatekeeper" View handles Nesting
The "Gatekeeper" approach actually becomes more valuable as nesting depth increases.
If you have a 3-level nested struct and a new field is added deep inside the 3rd level in your Parquet file:
* Without the View: If you were using JSON or VARIANT without a schema, that new data would automatically flow into your table, potentially consuming space or causing unexpected behavior in downstream apps.
* With the View + CAST: Your CAST in the View only looks for the specific "approved" paths. The new field deep inside the nesting is simply ignored by the FILES() reader during the CAST process. No "unapproved" data reaches your table.
3. Copy-Paste Shortcut (How to manage long definitions)
You requested a way to avoid manually editing these long nested strings. You can query the database to give you the exact STRUCT string for your `CAST`:
sql
SELECT COLUMN_TYPE
FROM information_schema.columns
WHERE TABLE_NAME = 'your_target_table'
AND COLUMN_NAME = 'your_struct_column';
Why this is perfect for nested data:
* It returns the entire tree structure in one long string (e.g., struct<id:int,info:struct<name:varchar,addr:struct<city:varchar,zip:int>>>).
* The types are already converted to StarRocks-compatible syntax (e.g., it converts Parquet/JSON types to varchar(65533) or int).
* You can copy this result directly into your View's CAST statement.
4. Performance
Nested structs in StarRocks are stored columnarly. If you run a query like:
SELECT user.address.city FROM my_table;
StarRocks only reads the data for the city field. It does not have to read the entire user struct. This makes it significantly faster than processing raw JSON.
Summary
* Is it an issue? No.
* Limit: 14 levels.
* Maintenance: Use information_schema to "auto-generate" your copy-paste string whenever you approve a schema change.
References
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/information_schema/columns.md
• sql-reference/data-types/semi_structured/Array.md
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.md
References
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/data-types/semi_structured/Array.md
• sql-reference/data-types/semi_structured/VARIANT.mdMax Kemel
07/05/2026, 12:53 PMRocky
07/05/2026, 12:53 PMAUTO_INGEST is set to TRUE. There is no native "push-based" or "event-driven" trigger (like S3 Event Notifications to SQS) available in the stable releases (v3.2/v3.3) to bypass the poll interval entirely.
Current State of Pipe "Automatic" Ingestion
* Mechanism: StarRocks FE (Front End) periodically scans the S3 path you provided using the POLL_INTERVAL frequency.
* Default: 300 seconds.
* Your Setting: 60 seconds.
Can you achieve "no poll interval"?
As of the current version, no, you cannot achieve a true zero-poll event trigger. However, you can make it significantly more responsive:
1. Lower the `POLL_INTERVAL`: You can set the POLL_INTERVAL to a smaller value, such as 5 or 10 seconds. While this is still technically polling, it provides near-real-time ingestion.
_ _Note: Very frequent polling on large buckets with many files can increase S3 API costs and FE load.*
2. Roadmap (SQS Support): Support for using AWS SQS (Simple Queue Service) as a trigger for Pipe is a high-priority feature on the StarRocks 2024 Roadmap ("Continuous data ingestion from SQS with Pipe"). Once released, this will allow S3 to "push" notifications to StarRocks, eliminating the need for periodic folder scanning.
Recommendation
If 60 seconds is too slow, try reducing the interval to 5 or 10 seconds:
sql
ALTER PIPE my_s3_pipe SET ("POLL_INTERVAL" = "10");
Alternative for true "Continuous" streaming:
If your data volume is extremely high and requires sub-second ingestion without polling S3, the standard pattern is to land data in Kafka first and use Routine Load. Routine Load is natively event-driven and maintains a persistent connection to Kafka for immediate ingestion.
References
• loading/s3.md
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• GitHub Issue #39686: StarRocks Roadmap 2024
References
• loading/s3.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.mdMax Kemel
07/05/2026, 1:35 PMRocky
07/05/2026, 1:35 PMEventUid in your SQL, StarRocks' SQL analyzer automatically converts all unquoted column names to lowercase before trying to find them.
* Your SQL: s.EventUid $\rightarrow$ converted to s.eventuid
* Parquet File: Contains EventUid
* Result: eventuid (lowercase) does not match EventUid (mixed case) in the file metadata, leading to the "cannot be resolved" error.
2. The Solution: Use Backticks
To force StarRocks to preserve the exact casing and match the file metadata, you must wrap the column name in backticks:
sql
LEFT JOIN `1_s3_pipe` t ON s.`EventUid` = t.eventuid
3. Note on Table Names
I noticed your table name is 1_s3_pipe. In StarRocks (and most SQL databases), table names cannot start with a number unless they are also wrapped in backticks. This will likely be your next error if you don't wrap the table name as well.
Corrected PIPE Statement:
sql
CREATE PIPE my_s3_pipe
PROPERTIES (
"AUTO_INGEST" = "TRUE",
"POLL_INTERVAL" = "60"
)
AS
INSERT INTO `1_s3_pipe` -- Added backticks for number-start name
SELECT s.*
FROM FILES(
"path" = "<s3://pika1/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
) s
LEFT JOIN `1_s3_pipe` t ON s.`EventUid` = t.eventuid -- Exact case matching
WHERE t.eventuid IS NULL;
How to verify the exact case?
If the backticks don't work, it's possible the Parquet file has a slightly different name (e.g., all caps EVENTUID). You can check exactly what StarRocks "sees" in the file by running:
sql
DESC FILES(
"path" = "<s3://pika1/em-test/some_sample_file.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
);
Look at the Field column in the result—you must match that exact casing in your SELECT or JOIN using backticks.
References
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.md
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
References
• sql-reference/Error_code.md
• sql-reference/information_schema/events.mdMax Kemel
07/05/2026, 1:38 PMRocky
07/05/2026, 1:39 PMFILES function) is providing a case-sensitive name, OR because of an aliasing issue within the INSERT statement.
Since DESC FILES shows EventUid (mixed case), here is how to resolve this definitively:
1. The "Lowercase" Rule
Even though DESC shows mixed case, StarRocks often normalizes columns from FILES() to lowercase internally when they are used in a query.
Try using all lowercase for the column name without backticks first:
sql
LEFT JOIN `1_s3_pipe` t ON s.eventuid = t.eventuid
If the analyzer normalized the Parquet schema to lowercase, this will match.
2. Use a CTE (Recommended)
Sometimes the analyzer has trouble resolving aliases (s) directly on a table-valued function (FILES) inside a complex INSERT statement. Using a Common Table Expression (CTE) "flattens" the schema and usually fixes resolution errors.
3. Fixing the Table Name
Your table name 1_s3_pipe starts with a number. This must be wrapped in backticks throughout the entire query, or the analyzer may misinterpret the INSERT INTO or the JOIN.
Recommended Corrected SQL:
sql
CREATE PIPE my_s3_pipe
PROPERTIES (
"AUTO_INGEST" = "TRUE",
"POLL_INTERVAL" = "60"
)
AS
WITH source_data AS (
SELECT * FROM FILES(
"path" = "<s3://pika1/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
)
)
INSERT INTO `1_s3_pipe`
SELECT s.*
FROM source_data s
LEFT JOIN `1_s3_pipe` t ON s.eventuid = t.eventuid -- Try lowercase first
WHERE t.eventuid IS NULL;
If s.eventuid (lowercase) still fails with the CTE, try `s.`EventUid`` (backticked) inside the CTE version.
Why the CTE approach works:
* It forces StarRocks to "materialize" the schema of the files first.
* It eliminates ambiguity between the alias s and the FILES function.
* It ensures the INSERT context doesn't interfere with the LEFT JOIN logic.
One final check:
Ensure your S3 path in the PIPE definition does not contain the < and > brackets (e.g., use "<s3://pika1/em-test/*.parquet>" instead of "<s3://...>"). These are often used in documentation as placeholders but will cause the actual S3 driver to fail or return an empty set, which leads to "column not found" errors.
References
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.md
References
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• sql-reference/sql-statements/table_bucket_part_index/DESCRIBE.md
• sql-reference/sql-statements/data-manipulation/WITH.md
References
• sql-reference/sql-functions/table-functions/files.md
• loading/Loading_data_template.mdMax Kemel
07/05/2026, 1:48 PMRocky
07/05/2026, 1:48 PMSELECT * in an INSERT INTO statement.
When you run SELECT * FROM FILES(...), the FILES function detects all columns present in the Parquet file. In your case, it found 45 columns. Since your target table t1_s3_pipe only has 37 columns, the SQL analyzer throws a mismatch error because it doesn't know which 37 columns from the 45 should be mapped to the table.
To resolve this, you have two primary options:
1. Explicitly list the columns (Recommended)
This is the most reliable way. Since Parquet stores column names, StarRocks will map them correctly by name if you list them. If you only want the 37 columns that exist in your table, you must specify them in your query:
sql
INSERT INTO t1_s3_pipe (col1, col2, col3, ...) -- list your 37 table columns
SELECT col1, col2, col3, ... -- list the matching 37 file columns
FROM FILES(
"path" = "<s3://1st/em-test/*.parquet>",
"format" = "parquet",
...
);
*2. Use a CTE to filter the "SELECT " (Cleaner Syntax)
If you don't want to list all columns in the INSERT part, you can use a CTE to ensure the column count matches exactly.
sql
CREATE PIPE my_s3_pipe
AS
INSERT INTO t1_s3_pipe
SELECT
EventUid,
column2,
column3
-- ... list exactly 37 columns here
FROM FILES(
"path" = "<s3://1st/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
);
Why this happens with Parquet
Unlike CSV (where loading often skips trailing columns), Parquet is a schema-aware format. The FILES() function samples the Parquet metadata and creates a virtual table with the exact schema found in the file. An INSERT INTO Table SELECT * is a standard SQL operation that requires the source and destination to have the same number of columns.
New in v3.4: fill_mismatch_column_with
If your error was the other way around (Table has 45 columns but File only has 37), you could use a new property in StarRocks v3.4+ to fill missing columns with NULLs:
sql
"fill_mismatch_column_with" = "null"
However, since your File has more columns than the Table, the only solution is to explicitly select the columns you want to keep.
Note: Ensure you removed the < > brackets from your S3 path as well, as those are usually just placeholders in documentation and will cause the path to be invalid.
References
• faq/loading/Loading_faq.md
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• loading/Etl_in_loading.mdMax Kemel
07/05/2026, 1:50 PMRocky
07/05/2026, 1:50 PMINSERT statement to do so using the BY NAME clause.
By default, an INSERT INTO ... SELECT * uses positional matching. It expects the number of columns in the source (45) to exactly match the number of columns in the target (37).
The Solution: Use BY NAME
To make StarRocks match columns by their names and ignore any extra columns in the Parquet file that don't exist in your table, add BY NAME after the table name:
sql
CREATE PIPE my_s3_pipe
PROPERTIES (
"AUTO_INGEST" = "TRUE",
"POLL_INTERVAL" = "60"
)
AS
INSERT INTO t1_s3_pipe BY NAME -- <--- Add 'BY NAME' here
SELECT * FROM FILES(
"path" = "<s3://1st/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
);
Why this works:
1. Column Filtering: With BY NAME, StarRocks looks at the names of the 45 columns coming from the Parquet file. It only pulls the ones that match names in your t1_s3_pipe table (37 columns) and ignores the rest.
2. Order Independence: The columns in your Parquet file don't need to be in the same order as the columns in your StarRocks table.
3. Missing Columns: If the table has a column that is missing from the Parquet file, StarRocks will simply insert a NULL or the column's default value.
Important Note on Case Sensitivity
As you found earlier, Parquet matching is case-sensitive.
* If your table column is eventuid (lowercase).
* And your Parquet file column is EventUid (mixed case).
* Even with BY NAME, the matching might fail if the names don't match exactly.
If you still get an error after adding BY NAME, it is likely because of this case mismatch. In that case, you should explicitly alias the column in the `SELECT`:
sql
AS
INSERT INTO t1_s3_pipe BY NAME
SELECT
*,
`EventUid` AS eventuid -- Explicitly map the mixed-case file column to your lowercase table column
FROM FILES(...)
Version Note (4.1.1)
In version 4.1.1, BY NAME is the standard way to handle "Schema Evolution" style loading where the source and sink don't have identical structures. You can also use the property "fill_mismatch_column_with" = "null" inside the FILES() function properties if you want to ensure that any columns found in some files but not others are handled gracefully.
References
• loading/InsertInto.md
• sql-reference/sql-statements/loading_unloading/INSERT.md
• sql-reference/sql-functions/table-functions/files.md
References
• sql-reference/sql-statements/loading_unloading/INSERT.md
• loading/InsertInto.mdMax Kemel
07/05/2026, 1:53 PMRocky
07/05/2026, 1:54 PMBY NAME is powerful for mapping and handling missing columns (filling them with NULLs), it does not automatically "drop" extra columns that exist in your Parquet file but not in your StarRocks table when using SELECT *.
In StarRocks, the SELECT * part of the query expands to include all 45 columns from your Parquet file. When INSERT INTO ... BY NAME sees those 45 columns, it tries to find a matching destination in your table for every single one of them. Because your table only has 37 columns, it encounters dvcfqdn (and likely others), can't find a matching column in the table, and throws the Unknown column error.
To fix this, you have to be explicit about which columns you want to load. You have two options:
Option 1: Explicitly list the 37 columns (Most Stable)
You must replace SELECT * with the specific list of columns that exist in your table. This tells StarRocks exactly which data to pull from the Parquet file.
sql
CREATE PIPE my_s3_pipe
AS
INSERT INTO t1_s3_pipe BY NAME
SELECT
eventuid,
col2,
col3,
-- ... list all 37 columns here
col37
FROM FILES(
"path" = "<s3://1st/em-test/*.parquet>",
"format" = "parquet",
...
);
Option 2: Use a "Mapping" approach (v3.3+)
If you are on a recent version (v3.3 or higher), you can use the (column_list) syntax alongside FILES to map only what you need. However, for an INSERT INTO ... SELECT, the simplest way remains listing the columns in the SELECT clause.
Why doesn't it just "ignore" them?
In standard SQL, a SELECT * defines the "shape" of the data being moved. StarRocks' INSERT logic requires that every piece of data produced by the SELECT has a valid destination in the target table. It won't silently discard data (the extra 8 columns) unless you tell it to by omitting them from the SELECT list.
Summary of Rules for `BY NAME`:
1. Missing in File, Present in Table: StarRocks will auto-fill with NULL or default values.
2. Different Order: StarRocks will automatically reorder them to match.
3. Present in File, Missing in Table: StarRocks will NOT auto-drop them if they are in your SELECT list; it will throw an "Unknown column" error.
Recommendation: Update your SELECT statement to list only the 37 columns that match your table definition. This will resolve the Unknown column error and ignore the extra metadata columns like dvcfqdn.
References
• loading/InsertInto.md
• sql-reference/sql-statements/loading_unloading/INSERT.md
• sql-reference/sql-functions/table-functions/files.md
• loading/Etl_in_loading.md
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• faq/loading/Stream_load_faq.md
• loading/InsertInto.md
• sql-reference/sql-statements/loading_unloading/INSERT.mdEliezer Talker
07/05/2026, 1:56 PMRocky
07/05/2026, 1:56 PMSELECT *.
In StarRocks 4.1.1, the INSERT INTO ... BY NAME statement coupled with SELECT * only ignores extra columns if you enable Schema Push-down. Without this property, SELECT * expands to include every column in your Parquet file (45 columns), and the INSERT statement then fails because those extra columns don't exist in your table.
The Missing Piece: enable_push_down_schema
To make StarRocks automatically "trim" the extra columns from the Parquet file and only load the ones that match your table, you must add the enable_push_down_schema property to your INSERT statement:
sql
CREATE PIPE my_s3_pipe
PROPERTIES (
"AUTO_INGEST" = "TRUE"
)
AS
INSERT INTO t1_s3_pipe BY NAME
PROPERTIES (
"enable_push_down_schema" = "true" -- This is the key
)
SELECT * FROM FILES(
"path" = "<s3://1st/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
);
Why this property is required:
* Without it: SELECT * is handled by the FILES() function first. It sees 45 columns and returns them all. The INSERT statement then receives 45 columns and tries to match them to your 37-column table. It sees dvcfqdn, can't find it in your table, and throws the error.
* With it: StarRocks "pushes" your table's schema down into the FILES() function. Instead of the * expanding to the file's 45 columns, it expands only to the table's 37 columns. The extra columns in the Parquet file are ignored at the source level.
Summary for Version 4.1.1:
1. Extra columns in Parquet: Ignored only if enable_push_down_schema = true is used with BY NAME.
2. Missing columns in Parquet: Filled with NULL automatically when using BY NAME.
3. Column order: Handled automatically by BY NAME.
Note: If you still see the error after adding this property, please double-check that the column names in the Parquet file match your table's column names exactly (including case sensitivity).
References
• sql-reference/sql-functions/table-functions/files.md
• loading/InsertInto.md
• sql-reference/sql-statements/loading_unloading/INSERT.md
References
• loading/Etl_in_loading.md
• sql-reference/sql-functions/table-functions/files.mdEliezer Talker
07/05/2026, 3:10 PMCREATE PIPE my_s32_pipe
PROPERTIES (
"AUTO_INGEST" = "TRUE",
"POLL_INTERVAL" = "60"
)
AS
INSERT INTO type2_s3_pipe BY NAME
PROPERTIES ("enable_push_down_schema" = "true")
SELECT * FROM FILES(
"path" = "<s3://test/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
);
Query OK, 0 rows affected (0.404 sec)
and see:
kubectl exec -n dev1 starrocks-fe-0 -- grep -A5 "Generate exec plan failed.*pipe-my_s32_pipe" /opt/starrocks/fe/log/fe.log 2>/dev/null | tail -40
FROM FILES("format" = "parquet", "aws.s3.region" = "us-east-1", "path" = "<s3://test/em-test/20260705_093854_05578_rubau-692d2a6b-f666-4c50-b769-6ec7b9925f0f.parquet>", "aws.s3.use_aws_sdk_default_behavior" = "true"), err=SemanticException: Getting analyzing error. Detail message: Inserted target column count: 37 doesn't match select/value column count: 45., profile=Planner:
- -- Total[1] 80ms
- -- Analyzer[1] 76ms
- -- Lock[1] 0
Reason:
--
2026-07-05 15:02:21.048Z WARN (starrocks-taskrun-pool-6|1025) [StmtExecutor.logOptimizerTraceOnGenerateExecPlanFailure():902] Generate exec plan failed. Planner profile: query_id=019f32cd-418d-79df-a36f-b89bd1cb5c75, sql=INSERT INTO `type2_s3_pipe` WITH LABEL `pipe-my_s32_pipe-task-725470-0` SELECT *
FROM FILES("format" = "parquet", "aws.s3.region" = "us-east-1", "path" = "<s3://test/em-test/20260705_093854_05578_rubau-692d2a6b-f666-4c50-b769-6ec7b9925f0f.parquet>", "aws.s3.use_aws_sdk_default_behavior" = "true"), err=SemanticException: Getting analyzing error. Detail message: Inserted target column count: 37 doesn't match select/value column count: 45., profile=Planner:
- -- Total[1] 69ms
- -- Analyzer[1] 65ms
- -- Lock[1] 0
Reason:
--
2026-07-05 15:02:24.047Z WARN (starrocks-taskrun-pool-6|1025) [StmtExecutor.logOptimizerTraceOnGenerateExecPlanFailure():902] Generate exec plan failed. Planner profile: query_id=019f32cd-4d4c-7da6-9495-aba967c0856e, sql=INSERT INTO `type2_s3_pipe` WITH LABEL `pipe-my_s32_pipe-task-725470-0` SELECT *
FROM FILES("format" = "parquet", "aws.s3.region" = "us-east-1", "path" = "<s3://test/em-test/20260705_093854_05578_rubau-692d2a6b-f666-4c50-b769-6ec7b9925f0f.parquet>", "aws.s3.use_aws_sdk_default_behavior" = "true"), err=SemanticException: Getting analyzing error. Detail message: Inserted target column count: 37 doesn't match select/value column count: 45., profile=Planner:
- -- Total[1] 68ms
- -- Analyzer[1] 64ms
- -- Lock[1] 0
Reason:
--
2026-07-05 15:02:27.047Z WARN (starrocks-taskrun-pool-6|1025) [StmtExecutor.logOptimizerTraceOnGenerateExecPlanFailure():902] Generate exec plan failed. Planner profile: query_id=019f32cd-590b-75e6-9072-54cb7092d405, sql=INSERT INTO `type2_s3_pipe` WITH LABEL `pipe-my_s32_pipe-task-725470-0` SELECT *
FROM FILES("format" = "parquet", "aws.s3.region" = "us-east-1", "path" = "<s3://test/em-test/20260705_093854_05578_rubau-692d2a6b-f666-4c50-b769-6ec7b9925f0f.parquet>", "aws.s3.use_aws_sdk_default_behavior" = "true"), err=SemanticException: Getting analyzing error. Detail message: Inserted target column count: 37 doesn't match select/value column count: 45., profile=Planner:
- -- Total[1] 68ms
- -- Analyzer[1] 64ms
- -- Lock[1] 0
Reason:
--
2026-07-05 15:02:30.045Z WARN (starrocks-taskrun-pool-6|1025) [StmtExecutor.logOptimizerTraceOnGenerateExecPlanFailure():902] Generate exec plan failed. Planner profile: query_id=019f32cd-64ca-70c3-a8ba-1148ff203fd8, sql=INSERT INTO `type2_s3_pipe` WITH LABEL `pipe-my_s32_pipe-task-725470-0` SELECT *
FROM FILES("format" = "parquet", "aws.s3.region" = "us-east-1", "path" = "<s3://test/em-test/20260705_093854_05578_rubau-692d2a6b-f666-4c50-b769-6ec7b9925f0f.parquet>", "aws.s3.use_aws_sdk_default_behavior" = "true"), err=SemanticException: Getting analyzing error. Detail message: Inserted target column count: 37 doesn't match select/value column count: 45., profile=Planner:
- -- Total[1] 66ms
- -- Analyzer[1] 62ms
- -- Lock[1] 0
Reason:
--
2026-07-05 15:02:33.053Z WARN (starrocks-taskrun-pool-6|1025) [StmtExecutor.logOptimizerTraceOnGenerateExecPlanFailure():902] Generate exec plan failed. Planner profile: query_id=019f32cd-7089-7c45-9d9b-e0073012b0dd, sql=INSERT INTO `type2_s3_pipe` WITH LABEL `pipe-my_s32_pipe-task-725470-0` SELECT *
FROM FILES("format" = "parquet", "aws.s3.region" = "us-east-1", "path" = "<s3://test/em-test/20260705_093854_05578_rubau-692d2a6b-f666-4c50-b769-6ec7b9925f0f.parquet>", "aws.s3.use_aws_sdk_default_behavior" = "true"), err=SemanticException: Getting analyzing error. Detail message: Inserted target column count: 37 doesn't match select/value column count: 45., profile=Planner:
- -- Total[1] 74ms
- -- Analyzer[1] 71ms
- -- Lock[1] 0
Reason:
why doesn't it ignore the the unknown columns ?Rocky
07/05/2026, 3:10 PMINSERT INTO ... BY NAME statement supports schema evolution (dropping extra columns), your logs reveal that when this statement is wrapped inside a Pipe, the BY NAME and PROPERTIES clauses are being stripped out or ignored by the Pipe's internal task runner.
The Evidence from your Logs
Look closely at the SQL being executed in the error log you provided:
sql=INSERT INTO type2_s3_pipe WITH LABEL pipe-my_s32_pipe-task-725470-0 SELECT * FROM FILES(...)
The Pipe system generated an internal INSERT statement, but it omitted your BY NAME instruction and the enable_push_down_schema property. Because these instructions are missing, StarRocks defaults to positional matching, which fails because your file has 45 columns and your table only has 37.
Why this is happening
In current versions (including 4.1.1), the Pipe engine does not yet fully support the BY NAME clause or the PROPERTIES block inside its AS <INSERT_SQL> definition for some loading scenarios. While INSERT INTO ... BY NAME works as a standalone command, the Pipe component (which adds its own labels and manages retries) simplifies the SQL and loses these specific schema-mapping instructions.
The Workarounds
1. Explicitly list the columns (Recommended)
Since the Pipe is defaulting to positional matching, you must provide a SELECT list that exactly matches your table's 37 columns. This bypasses the need for BY NAME and is the most stable way to use Pipes today.
sql
CREATE PIPE my_s32_pipe
AS
INSERT INTO type2_s3_pipe
SELECT
column1,
column2,
... -- List all 37 columns here
column37
FROM FILES( ... );
2. Set the Global FE Configuration
If you want to try and force schema matching at the system level, you can try setting this global configuration on your FE, which might help FILES() filter columns more aggressively, though listing columns (Workaround 1) is still more reliable for Pipes:
sql
ADMIN SET FRONTEND CONFIG ("files_enable_insert_push_down_schema" = "true");
Summary
I apologize for the earlier guidance—while StarRocks can do name matching and ignore columns in a standard INSERT, the Pipe engine currently fails to pass those instructions through to the background tasks. For now, you should explicitly list your table columns in the Pipe's SELECT statement to ignore the extra 8 columns (like dvcfqdn) found in your Parquet files.
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• loading/InsertInto.md
• sql-reference/sql-functions/table-functions/files.md
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• sql-reference/information_schema/pipes.md
• sql-reference/sql-statements/loading_unloading/pipe/SHOW_PIPES.md
• loading/hdfs_load.mdEliezer Talker
07/05/2026, 3:25 PMRocky
07/05/2026, 3:26 PMcurl), the behavior regarding extra columns is as follows:
1. Automatic Matching (Default):
If you do not provide a columns header, StarRocks will attempt to match the columns in the Parquet file to the columns in your table by name.
* Extra columns in file: If the Parquet file contains 45 columns and your table only has 37, StarRocks will automatically ignore the 8 extra columns. It only extracts the fields that exist in your table.
* Missing columns in file: If a column exists in the table but not in the Parquet file, StarRocks will fill it with NULL.
2. Explicit Mapping (Using columns header):
If you use the -H "columns: col1, col2, ..." header, you are explicitly telling StarRocks which columns to take from the file.
* Any column in the file not listed in this header is ignored.
* If you list all 37 table columns here, the extra 8 in the Parquet file are ignored.
Why it works here but failed in your Pipe
The Pipe engine (and its internal task runner) has a known limitation where it often defaults to positional matching (column 1 to column 1, column 2 to column 2) when it generates the internal INSERT tasks. This is why you saw the "37 doesn't match 45" error—it was trying to shove all 45 columns into 37 slots.
The Stream Load API uses a different internal logic that preserves name-based mapping for schema-aware formats like Parquet.
Example Curl Command
You can test this with a single file using this command. It should succeed without needing to list the 37 columns:
bash
curl --location-trusted -u <user>:<password> \
-H "format: parquet" \
-T your_file.parquet \
http://<fe_ip>:<http_port>/api/<db>/type2_s3_pipe/_stream_load
Recommendation
If you have a large number of columns (37+) and frequently have "extra" metadata columns in your source files, Stream Load is much more resilient to these schema differences than Pipes are currently. If you must use Pipes, you are forced to list the columns explicitly in the SELECT statement until the Pipe engine's name-mapping support is improved.
References
• sql-reference/sql-statements/loading_unloading/STREAM_LOAD.md
• loading/Etl_in_loading.md
• faq/loading/Stream_load_faq.md
• loading/StreamLoad.md
• loading/InsertInto.md
• sql-reference/sql-statements/loading_unloading/INSERT.md
References
• sql-reference/sql-statements/loading_unloading/STREAM_LOAD.md
• faq/loading/Stream_load_faq.md
• loading/Etl_in_loading.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.mdRocky
07/05/2026, 3:26 PMcurl approach) versus the Pipe/SQL API for StarRocks v4.1.1.
1. Does the Stream Load API work for Parquet?
Yes. Starting from v3.3, the Stream Load HTTP API supports Parquet.
2. Does it automatically ignore extra columns?
Yes. When you use Stream Load for Parquet files, it behaves differently than the INSERT INTO ... SELECT * SQL statement:
* Stream Load Behavior: It uses the target table's schema as the primary guide. It looks for columns in the Parquet file that match your table's column names. If the Parquet file has 45 columns but your table only has 37, it simply ignores the extra 8 columns (like dvcfqdn) because they aren't part of the "target" it is looking for.
* Result: It will successfully load the 37 matching columns and ignore the rest without any errors.
3. Why did it fail in the Pipe but work here?
The failure you saw in the Pipe is due to how SQL Planning works vs. how Stream Load works:
* In your Pipe (SQL): You wrote SELECT _. In SQL, _ expands to "everything in the source." The scanner picks up all 45 columns. The INSERT then receives a 45-column row and realizes it only has 37 columns to put them in. As we saw in your logs, the Pipe engine failed to pass the BY NAME instruction to the background task, so it fell back to positional matching and crashed.
* In Stream Load (API): There is no SELECT * expansion. The loader simply maps the file's internal metadata names to your table names. It is "Name-Aware" by default for Parquet.
Summary of what works in v4.1.1:
Feature | Stream Load (HTTP API) | Pipe / INSERT INTO (SQL)
Parquet Support | Yes (v3.3+) | Yes
Extra Columns in File | Automatically Ignored | Errors (Unless you list columns or fix BY NAME properties)
Missing Columns in File | Fills with NULL | Fills with NULL
Mapping Method | Matches by Name | Matches by Position (unless BY NAME is used)
Double-Checked Recommendation:
If you want the "automatic ignore" behavior for your Parquet files without writing out all 37 column names, the Stream Load API is indeed more robust for this specific requirement than the current Pipe implementation in v4.1.1.
To use it, your curl command would look like this:
bash
curl --location-trusted -u <user>:<password> \
-H "format:parquet" \
-T your_file.parquet \
http://<fe_host>:<http_port>/api/<db>/<table1_s3_pipe>/_stream_load
(Note: Even though your table name has _pipe in it, you are bypassing the Pipe engine and using the Stream Load API directly).
References
• sql-reference/sql-statements/loading_unloading/STREAM_LOAD.md
• loading/StreamLoad.md
• loading/Etl_in_loading.md
• sql-reference/sql-statements/loading_unloading/INSERT.mdEliezer Talker
07/05/2026, 3:28 PMRocky
07/05/2026, 3:28 PMBY NAME clause, here are the two most reliable alternatives:
1. Direct INSERT INTO ... BY NAME (Synchronous)
This is the most direct alternative. Unlike the Pipe, which runs in the background and may strip certain clauses, a direct INSERT executed from your SQL client will respect the BY NAME instruction.
In version 4.1.1, when you use BY NAME, StarRocks maps columns from the Parquet file to the table by their metadata names. Columns in the Parquet file that do not exist in the table (like your extra 8 columns) will be ignored.
sql
INSERT INTO type2_s3_pipe BY NAME -- Matches by name, ignoring extras
SELECT * FROM FILES(
"path" = "<s3://test/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
);
* Pros: Simple, supports schema matching, easy to debug.
* Cons: Synchronous (the connection must stay open until it finishes). If you have 1TB of data, this isn't ideal.
2. Broker Load (Asynchronous)
Broker Load is the standard "heavy-duty" asynchronous loading method for S3. It runs in the background like a Pipe but uses a different execution engine that is very stable for large datasets.
To ignore the extra columns in Parquet, you simply don't list them, or you map the file's columns to your table columns explicitly.
sql
LOAD LABEL my_label_001
(
DATA INFILE("<s3://test/em-test/*.parquet>")
INTO TABLE type2_s3_pipe
FORMAT AS "parquet"
(col1, col2, col3, ...) -- List only the 37 columns you want to load
)
WITH BROKER
PROPERTIES (
"aws.s3.use_aws_sdk_default_behavior" = "true",
"aws.s3.region" = "us-east-1"
);
* Pros: Asynchronous (you get a Job ID and can walk away), handles massive data volumes (100GB+), extremely reliable.
* Cons: Slightly more verbose syntax than FILES().
Why these work better than your Pipe:
* Pipe Issue: As your logs showed, the Pipe engine was regenerating the SQL as INSERT INTO ... SELECT * and dropping your BY NAME property. This forced the system into positional matching, where 45 columns from the file must match 37 columns in the table, causing the "count mismatch" error.
* Direct/Broker Solution: Both of these methods ensure that the "name matching" or "explicit mapping" instructions are actually executed, allowing the system to safely discard the extra 8 columns.
Recommendation:
If you need a "set it and forget it" solution like Pipe but want it to work now, use Broker Load. If you are doing a one-time load or small batches, use the Direct INSERT but ensure you include BY NAME.
References
• loading/s3.mdRocky
07/05/2026, 3:28 PMINSERT INTO ... FROM FILES() (Recommended)
This is the standard SQL-based method. Since your Pipe failed due to the 45-column vs 37-column mismatch, you can run the INSERT statement manually. To ignore the unknown columns, you have two choices:
* Option A: List the columns explicitly (Safest)
Instead of SELECT *, list the 37 columns you want. This completely ignores the extra 8 columns in the Parquet file.
sql
INSERT INTO type2_s3_pipe
SELECT col1, col2, col3 ... col37
FROM FILES(
"path" = "<s3://test/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.region" = "us-east-1"
);
* Option B: Use BY NAME (Directly in SQL)
While BY NAME had issues inside your Pipe, it is fully supported in standard INSERT statements.
sql
INSERT INTO type2_s3_pipe BY NAME
SELECT * FROM FILES(
"path" = "<s3://test/em-test/*.parquet>",
"format" = "parquet",
"aws.s3.region" = "us-east-1"
);
2. Broker Load (Asynchronous)
Broker Load is a background job. It is better for very large datasets because it doesn't time out like a standard SQL query. It uses the s3a:// protocol.
To ignore the extra columns here, you define the columns you want to load in the ( ) block. StarRocks will map those names to the Parquet file and ignore anything else.
sql
LOAD LABEL my_s3_load_label
(
DATA INFILE("<s3a://test/em-test/*.parquet>")
INTO TABLE type2_s3_pipe
FORMAT AS "parquet"
(col1, col2, col3 ... col37) -- Only list the 37 columns you want
)
WITH BROKER
(
"aws.s3.region" = "us-east-1",
"aws.s3.access_key" = "...",
"aws.s3.secret_key" = "..."
);
3. External Tables (S3 Catalog)
You can "mount" your S3 bucket as an external table. This allows you to treat S3 as if it were a local table. You can then copy the data into your internal table.
1. Create the External Table:
sql
CREATE EXTERNAL TABLE s3_external_table (
col1 INT,
col2 STRING,
... -- Define only the 37 columns you want
)
ENGINE=file
PROPERTIES (
"path" = "<s3://test/em-test/>",
"format" = "parquet",
"aws.s3.region" = "us-east-1"
);
2. Load the data:
sql
INSERT INTO type2_s3_pipe SELECT * FROM s3_external_table;
The external table definition acts as a "filter"—it only sees the columns you defined in the CREATE EXTERNAL TABLE statement and ignores the rest.
Which one should you use?
* Use FILES() if you are doing a one-time load or a small script.
* Use Broker Load if you have hundreds of gigabytes and want to track the progress in the background (via SHOW LOAD).
* Use External Tables if you want to frequently query the S3 data directly without even loading it into StarRocks.
Summary for your column mismatch: All three methods work, but they require you to be explicit about the 37 columns you want (either by listing them or using BY NAME) to avoid the 45-column "count mismatch" error.
References
• loading/s3.md
• loading/BrokerLoad.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• sql-reference/sql-functions/table-functions/files.md
References
• sql-reference/sql-functions/table-functions/files.md
• loading/Loading_data_template.md
• loading/hdfs_load.mdEliezer Talker
07/05/2026, 3:43 PMBY NAME, column lists, and INSERT PROPERTIES when executing
StarRocks version: 4.1.1
Issue 1 — PIPE drops BY NAME and INSERT PROPERTIES:
When creating a PIPE with INSERT INTO target_table BY NAME PROPERTIES ("enable_push_down_schema" = "true") SELECT * FROM FILES(...), the PIPE internally serializes and re-executes the statement as a plain INSERT INTO target_table SELECT * FROM FILES(...) — stripping both BY NAME and PROPERTIES ("enable_push_down_schema" = "true").
This causes a column count mismatch error when the parquet file has more columns than the target table (e.g., 45 columns in parquet vs 37 in the table):
Inserted target column count: 37 doesn't match select/value column count: 45
Verified in FE logs — the actual SQL executed by the pipe has no BY NAME or `PROPERTIES`:
sql=INSERT INTO target_table WITH LABEL pipe-...-task-...-0 SELECT * FROM FILES(...)
Even specifying an explicit column list in the INSERT is stripped the same way.
Issue 2 — Broker Load cannot handle struct field mismatches:
When a parquet file contains a nested struct column with more fields than the target table's struct definition, Broker Load fails with:
Apache Arrow type (nested) utf8 does not match the type STRUCT in StarRocks
There is no way to map or filter nested struct fields in Broker Load.
Expected behavior: PIPE should preserve BY NAME, explicit column lists, and INSERT PROPERTIES so that schema mismatches (both top-level columns and nested struct fields) are handled correctly. Broker Load should support partial struct mapping or at least not fail on extra nested fields.
Workaround: The only working method is a direct INSERT INTO ... SELECT with explicit column lists and named_struct() to manually map struct fields. This cannot be automated via PIPE or Broker Load.Rocky
07/05/2026, 3:44 PMBY NAME and PROPERTIES
You have correctly identified a critical issue in how the Pipe engine handles its inner SQL.
* What's happening: When a Pipe is created, StarRocks stores the INSERT statement. However, the background task runner (which generates the actual execution plan and assigns the labels) is currently failing to preserve the BY NAME clause and the PROPERTIES block.
* Verification: This is closely related to a known bug (e.g., GitHub #70622) where INSERT INTO ... BY NAME was found to fallback to positional mapping under certain conditions. In the case of Pipes, it appears the serialization/deserialization logic for the background tasks is overly aggressive in stripping "extra" clauses.
* Status: This is a bug. The Pipe should act as a transparent wrapper for the SQL provided.
2. Confirmation of Issue 2: Broker Load Struct Mismatch
Broker Load uses an older scanner architecture (Broker Scanner) compared to the newer FILES() table function (Vectorized Scanner).
* The Problem: The Broker Load scanner is rigid when it comes to nested Parquet structures. It expects a 1:1 mapping between the Parquet Schema and the StarRocks STRUCT definition. If the Parquet file contains a field inside the struct that isn't in your StarRocks table, the Arrow-based reader fails because it cannot "subset" the struct fields during the scan phase.
* Limitation: Unlike top-level columns, where you can use the (col1, col2...) syntax to skip columns, there is currently no native syntax in Broker Load to "skip" or "filter" fields inside a STRUCT.
* Why INSERT ... SELECT works: The INSERT path uses the more modern scanner which allows for expression-based mapping (like named_struct), giving you the flexibility to explicitly pick which nested fields to keep.
Recommendation
Your bug report is very high quality. I recommend taking these exact points to the StarRocks GitHub Issues page.
Why it’s worth reporting:
1. Issue 1 is a regression in the Pipe's ability to handle modern INSERT syntax.
2. Issue 2 highlights the need for parity between FILES() and Broker Load for complex types.
Current "Best Path" for you:
As you noted, the manual INSERT INTO ... SELECT col1, col2, named_struct(...) is the only way to handle both the 45-to-37 column drop and the nested struct field filtering. Since Pipes are currently stripping those instructions, you may need to use a standard scheduled task (using StarRocks' built-in SUBMIT TASK or an external orchestrator) to run the explicit INSERT statement until the Pipe engine is patched to preserve the full SQL syntax.
References
• faq/loading/Broker_load_faq.md
• sql-reference/data-types/semi_structured/STRUCT.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• loading/BrokerLoad.mdEliezer Talker
07/05/2026, 4:06 PMRocky
07/05/2026, 4:06 PMBY NAME clause or the column list is a SQL rewriting bug inside the StarRocks Pipe manager.
* If you switch to CSV: CSV is positional. To load 45-column CSV files into a 37-column table, you must provide a column list in your INSERT statement to tell StarRocks which columns to pick. Since the Pipe strips that list, it will try to shove 45 columns into 37 slots and fail with the exact same "column count mismatch" error.
* If you switch to JSON: While JSON is flexible, if you want to map specific nested keys to specific columns, you usually need jsonpaths or a column list. If the Pipe strips these properties, the load will likely fail or result in all NULLs.
2. The Struct Mismatch (Issue 2)
* CSV: Does not support the STRUCT type natively. You would have to load the nested data as a string and then use complex SQL functions to parse it, which defeats the purpose of having a structured format.
* JSON: JSON handles "extra" fields better than Parquet in some loaders (it just ignores keys it doesn't recognize). However, StarRocks' Broker Load and Pipe still require a clear mapping. If the schema is complex, you'll run into the same Arrow-type mismatch errors because the background scanner still needs to reconcile the JSON structure with the StarRocks STRUCT definition.
Why Parquet is still your best bet
Parquet is the most efficient format for StarRocks. The issues you are seeing are not "Parquet issues"—they are "Pipe bugs" and "Broker Load limitations."
Better Alternatives to "Switching Formats"
Instead of changing your data pipeline to a less efficient format, I recommend one of these two paths to bypass the Pipe bug:
A. The "External Table" Buffer (The most stable workaround)
This bypasses the Pipe's SQL-stripping bug by moving the logic into the table definition itself.
1. Create an External File Table that matches your Parquet file exactly (all 45 columns).
2. Create a Task (instead of a Pipe) to move data:
sql
-- This works because the logic is in the SELECT, not a Pipe property
INSERT INTO target_table
SELECT col1, col2, ... col37 FROM s3_external_table_45_cols;
B. Use a Routine Load (If using Kafka)
If you can put this data into Kafka instead of S3, Routine Load is much more mature than Pipes. It handles column mapping and "extra columns" perfectly using the COLUMNS clause, and it does not strip its configuration like the Pipe engine currently does.
C. Manual Orchestration
Use a simple script or Airflow to run the INSERT INTO ... SELECT ... FROM FILES() command every few minutes. Since this is a direct SQL command and not a Pipe, StarRocks will not strip the column list or the BY NAME clause, and it will work perfectly.
Summary
* Does switching to CSV/JSON fix the Pipe bug? No. The Pipe will still strip your mapping instructions.
* Does it fix the Struct mismatch? CSV can't do structs; JSON might, but it's much slower and still prone to mapping errors in Broker Load.
* Recommendation: Stick with Parquet, but stop using Pipes until v4.1.2+ or whenever this serialization bug is fixed. Use a standard INSERT INTO ... FROM FILES() via a scheduled task.Eliezer Talker
07/05/2026, 4:09 PMRocky
07/05/2026, 4:09 PMFILES(), PIPE, and Broker Load.
* Behavior: Very similar to Parquet. It is a columnar format and highly efficient.
* Note: Since it is schema-based like Parquet, it will likely encounter the same "strict schema" issues you are seeing with your 45 vs 37 column mismatch if the Pipe engine continues to strip your mapping instructions.
2. Avro
* Method: Supported via FILES() and PIPE (as of v3.4.4+).
* Behavior: A row-based format. It is commonly used in streaming (Kafka) but is now supported for S3 file loads.
* Advantage: Avro is generally more flexible with "extra fields" than Parquet depending on the reader implementation, but StarRocks still prefers an explicit mapping for performance.
3. XML (Limited)
* StarRocks does not have a native format = 'xml' for the FILES() or Broker Load functions. If your data is in XML, you would typically have to load it as a single string column (via CSV with a custom delimiter) and then parse it using StarRocks' XML functions.
──────────
Would these solve your mismatch/struct problem?
Likely no.
The two issues you discovered are not caused by the Parquet format itself; they are caused by the StarRocks Load Engines:
1. The Pipe Engine Bug: As you identified, the Pipe is incorrectly stripping the BY NAME and column list clauses. It would do this to an ORC or Avro file too.
2. The Struct Mapping Limitation: The Broker Load/Arrow scanner is rigid with nested structures across all schema-based formats (Parquet, ORC, Avro).
The "Hidden" Format: External Tables (Unified Catalog)
Instead of a "file format," you can use Iceberg, Hudi, or Delta Lake formats on S3.
* StarRocks can treat these as External Catalogs.
* When you query a Hudi or Iceberg table, StarRocks uses its own metadata layer to handle schema evolution.
_ If your Parquet files are wrapped in an Iceberg/Hudi table, you can INSERT INTO your_table SELECT _ FROM iceberg_catalog.db.table. This is often more robust than raw file loading because the table format (Iceberg/Hudi) handles the "missing/extra columns" logic before the data even hits the StarRocks compute layer.
Summary: Your best alternative format is ORC, but it won't fix the Pipe bug you found. The most reliable fix for your current situation is to move the mapping logic into a Task or a View and avoid the Pipe engine until the serialization bug is resolved.
References
• sql-reference/sql-functions/table-functions/files.mdElla
07/06/2026, 7:27 AMRocky
07/06/2026, 7:27 AMsql=INSERT INTO ... SELECT * FROM FILES(...) without the BY NAME or PROPERTIES clause is definitive. The Pipe engine is responsible for "serializing" your original SQL into a background task. If that background task is missing your mapping instructions, it is a failure of the Pipe manager to correctly store or rebuild the SQL statement.
2. Related Known Issues: There was a documented bug in the v4.0/4.1 development cycle (e.g., GitHub #70774) specifically regarding BY NAME not correctly pushing down schemas for partial column sets. While that was reported for direct INSERT statements, the Pipe engine uses the same underlying logic but adds a serialization layer, which is where your specific "stripping" issue is occurring.
3. Broker Load Architecture: The error Apache Arrow type (nested) utf8 does not match the type STRUCT is a known limitation of the older Broker Load scanner. It uses a rigid Arrow reader that expects a schema match at the physical level for nested types. Unlike the modern FILES() table function, Broker Load does not yet have the sophisticated "Schema Evolution V2" or "Partial Struct Mapping" capabilities required to ignore extra nested fields.
Validation of your findings
* The Column Count Mismatch: (45 vs 37) is the direct result of the BY NAME being stripped. Without BY NAME, StarRocks defaults to positional mapping. Since the file has more columns than the table, the positional check fails before it even looks at column names.
* The Struct Error: This confirms you are hitting the "strict" legacy path in Broker Load, which cannot "subset" a Parquet struct on the fly.
Conclusion
Your deduction is correct. You have identified a regression in the Pipe engine (Issue 1) and a feature gap in Broker Load (Issue 2).
Since you are on v4.1.1, which is a very recent release, I strongly recommend reporting this on the StarRocks GitHub. You have already done 90% of the work by capturing the FE logs showing the stripped SQL.
Until it is fixed: Your "Workaround" using a direct INSERT INTO ... SELECT (not wrapped in a Pipe) is the only way to ensure the full SQL syntax is respected by the query optimizer and correctly executed by the FE.Ella
07/06/2026, 7:31 AMRocky
07/06/2026, 7:32 AMSTRUCT is treated as a fixed schema. If your Parquet file has 10 fields in a struct but your table only defines 8, the low-level scanner often fails because it tries to map the entire "physical" struct to the "logical" definition.
If you want a more flexible approach that leverages native FILES() lookups while supporting schema evolution, here are the two best strategies:
1. The "JSON Column" Strategy (Best for Nested Evolution)
Instead of defining a rigid STRUCT, define that column as a JSON data type in your table.
* How it works: When StarRocks loads a Parquet STRUCT into a JSON column, it automatically converts the entire object into a JSON blob.
* Why it solves your issue:
* Flexibility: If the Parquet struct grows from 5 fields to 50 fields, the JSON column doesn't care. It stores whatever is there.
* No Mismatches: You will never get a "type mismatch" or "count mismatch" error for nested fields because the mapping is dynamic.
* Performance: You can still query nested fields using column->'field' or get_json_string(). StarRocks also supports JSON sub-column optimization (it can physically speed up queries on specific JSON keys).
2. The fill_mismatch_column_with Strategy (Best for Top-Level Evolution)
For your top-level column mismatch (the 45 vs 37 issue), you can use a specific property in the FILES() function introduced in recent versions (v3.4+):
sql
SELECT * FROM FILES(
"path" = "<s3://bucket/data.parquet>",
"format" = "parquet",
"fill_mismatch_column_with" = "null" -- This is key!
);
* Behavior: If a column exists in the file but not in your target table (or vice versa), StarRocks will gracefully handle it by filling missing values with NULL or ignoring the extra columns, rather than throwing an error.
* Limitation: This works beautifully with the FILES() table function, but it may still be ignored if you use the Pipe engine due to the serialization bug we discussed earlier.
3. "Auto-Schema" via CTAS
If your schema changes so frequently that you don't want to manage the table at all, you can use CREATE TABLE AS SELECT (CTAS) with FILES().
sql
CREATE TABLE my_new_batch AS
SELECT * FROM FILES(...)
StarRocks will infer the schema of the files and create a matching table on the fly. However, this creates a new table, so it's usually better for batch processing than for a continuous stream.
Recommended Path Forward
To get the best of both worlds (performance + flexibility):
1. Change nested STRUCT columns to JSON type. This removes the "rigid" requirement for your complex data.
2. Avoid the Pipe engine for now. Use a Task (Scheduled INSERT INTO ... SELECT) or a simple script that calls INSERT INTO target_table SELECT * FROM FILES(...).
3. Use BY NAME mapping in your SELECT statement to ensure that only the columns you want are pulled, even if the file grows.
Summary: The STRUCT type is great for performance when the schema is 100% stable. The JSON type is the "native" way to handle schema evolution in StarRocks without breaking your ingestion pipeline every time a new field is added.
References
• sql-reference/sql-functions/table-functions/files.md
• loading/Loading_data_template.mdElla
07/06/2026, 7:35 AMRocky
07/06/2026, 7:35 AMBY NAME only as part of a different ingestion method that avoids the bugged Pipe engine.
The Distinction
* The Pipe Engine (Bugged): When you use CREATE PIPE, StarRocks saves your SQL and runs it in the background. As you discovered in your FE logs, the Pipe manager currently has a bug where it "strips" the mapping instructions (BY NAME or column lists) when it serializes that task. This forces a positional match, which causes your 45 vs 37 column error.
* Direct INSERT or TASKS (Working): If you run INSERT INTO table BY NAME SELECT * FROM FILES(...) manually, or put that exact same SQL into a Scheduled Task, the BY NAME clause is not stripped. It is executed correctly by the FE.
Why this is the "Native" solution for Evolution
If you move your logic from a Pipe to a Task (or a simple script), you can use the combination that handles schema evolution perfectly:
1. Top-Level Evolution: Use INSERT INTO ... BY NAME. This allows the Parquet file to have columns in any order, or have extra columns that the table doesn't have, without failing.
2. Nested Evolution: Change your STRUCT columns to the JSON data type. This prevents the "rigid struct" error you saw with Broker Load. A JSON column will simply ingest the entire Parquet struct as a flexible object, regardless of how many new fields are added to it later.
Summary Recommendation
To keep using StarRocks' native file lookup while supporting evolution:
* Drop the Pipe: It is the source of the "stripped SQL" bug.
* Use a Task: Create a CREATE TASK that runs your INSERT INTO ... BY NAME every minute (or as needed).
* Use JSON: Switch the target column from STRUCT to JSON to handle the nested field changes.
This setup uses the modern FILES() scanner (which is faster and smarter than Broker Load) but bypasses the specific serialization bug currently affecting the Pipe engine in v4.1.1.
References
• sql-reference/sql-functions/table-functions/files.md
• sql-reference/sql-statements/loading_unloading/INSERT.mdElla
07/06/2026, 7:39 AMRocky
07/06/2026, 7:39 AMBY NAME clause, here are the two best "automatic" alternatives that still offer retries and error tracking (DLQ-like features).
1. The "View + Pipe" Workaround (Highly Recommended)
If you want to keep the Pipe's automatic file tracking but bypass the BY NAME stripping bug, you can wrap your file logic in a Logical View.
* The Idea: The Pipe engine often fails to serialize complex clauses inside its own definition, but it handles SELECT * FROM view correctly.
* Step 1: Create a View that handles the "rigid struct" and column mapping:
sql
CREATE VIEW s3_file_source_v AS
SELECT
col1,
col2,
CAST(my_struct_column AS JSON) as my_json_col -- Solves the "rigid struct" issue
FROM FILES(
"path" = "<s3://bucket/*.parquet>",
"format" = "parquet",
"fill_mismatch_column_with" = "null"
);
* Step 2: Create the Pipe targeting the view:
sql
CREATE PIPE my_pipe AS
INSERT INTO target_table
SELECT * FROM s3_file_source_v;
* Why this works: The Pipe only "sees" SELECT * FROM view. The BY NAME logic and the struct-to-JSON conversion are safely tucked away inside the view definition, where the Pipe's serialization bug won't touch them.
2. Scheduled Tasks (Idempotent-ish)
StarRocks TASKS can automate SQL execution with built-in retries, but they do not track files natively.
* Retries: Tasks have PROPERTIES ("retry_type" = "ALL", "retry_interval" = "10s").
* DLQ: You can set log_rejected_record_num = -1. This will log every failed row. You can find the "DLQ" data by querying:
sql
SELECT REJECTED_RECORD_PATH FROM information_schema.loads
WHERE STATE = 'CANCELLED' ORDER BY CREATE_TIME DESC;
* Missing Piece: It doesn't know which files it finished. You would need to move files to a "processed" folder using an external tool (like a Lambda or script) after the task runs.
3. StarRocks "Stream Load" with S3 Events
If you want a true production-grade DLQ and automatic ingestion without the Pipe bug:
* Use an S3 Event Notification (ObjectCreated) to trigger an AWS Lambda.
* The Lambda calls the StarRocks Stream Load API.
* Retries/DLQ: You get this via AWS Lambda's native retry policy and Dead Letter Queues (SQS/SNS).
* Schema Evolution: Stream Load supports the same jsonpaths and mapping logic as FILES().
Summary Comparison
Feature | Pipe (with View workaround) | Scheduled Tasks | External (Lambda + Stream Load)
File Tracking | Native & Automatic | None (manual) | External (S3 Events)
Retries | Automatic (Pipe Manager) | Built-in property | AWS Lambda native
DLQ Support | REJECTED_RECORD_PATH | REJECTED_RECORD_PATH | Full SQS/SNS support
Schema Evolution | High (via View/JSON) | High | High
My Recommendation: Try the View + Pipe approach first. It keeps your architecture simple and "native" while protecting the BY NAME and JSON conversion logic from the Pipe engine's serialization bug.
References
• sql-reference/sql-statements/loading_unloading/STREAM_LOAD.md
• loading/loading_introduction/troubleshooting_loading.md
• sql-reference/System_variable.mdRocky
07/06/2026, 7:39 AMEliezer Talker
07/07/2026, 7:37 PMRocky
07/07/2026, 7:38 PMWHERE clause)
Since a Pipe is defined by an INSERT INTO ... SELECT FROM FILES(...) statement, you can use any standard SQL logic in the SELECT portion. This includes the WHERE clause.
* Example: Only ingest records where a certain status is 'active' or a value is above a threshold.
sql
CREATE PIPE my_pipe AS
INSERT INTO target_table
SELECT * FROM FILES(...)
WHERE status = 'active' AND price > 100;
2. Partial Updates (Primary Key Tables)
If your target table is a Primary Key table, you can perform partial updates (updating only specific columns for an existing key) through a Pipe.
* Requirements:
* StarRocks version v3.3.1 or later.
* You must specify a column list in the INSERT INTO statement.
* Behavior: When you specify a column list, StarRocks treats the ingestion as a partial update. If the primary key already exists, it only updates the columns you listed; if it doesn't exist, it inserts a new row (filling missing columns with NULL or default values).
* Example:
sql
CREATE PIPE partial_update_pipe AS
INSERT INTO my_pk_table (id, current_price, last_updated) -- Only these columns
SELECT id, price, now()
FROM FILES(...);
3. Deletions via Pipe?
Standard INSERT statements (which Pipes use) do not natively support the DELETE operation. However, there is a workaround for Primary Key tables:
* *The *op Column:* In StarRocks' loading architecture, a hidden column named *op controls whether a row is an UPSERT (0) or a DELETE (1).
* For Pipes: While INSERT is usually for adding data, if your Pipe targets a Primary Key table and you include the __op logic in your mapping, you can trigger deletions from your source files.
* Example:
sql
CREATE PIPE cdc_pipe AS
INSERT INTO my_pk_table (id, name, __op)
SELECT
id,
name,
CASE WHEN action = 'DELETE' THEN 1 ELSE 0 END as __op
FROM FILES(...);
Summary Table
Feature | Pipe Support | Requirement
Conditional Ingestion | Yes | Use a WHERE clause in the SELECT statement.
Partial Update | Yes | Use a Primary Key Table + Column List (v3.3.1+).
Conditional Update | Limited | Achieved via CASE WHEN logic inside the SELECT list.
Deletions | Yes | Use a Primary Key Table + mapping an __op column.
Important Note: As we discussed earlier, if you are using v4.1.1, the Pipe engine has a bug where it might strip the column list (e.g., (id, name)) or the BY NAME clause during serialization. If your partial update fails with a "column count mismatch," you should use the View workaround (defining the logic in a View and having the Pipe select from that View) to protect your SQL syntax from being stripped.
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• loading/InsertInto.md
• loading/Load_to_Primary_Key_tables.md
• table_design/table_types/primary_key_table.md
References
• loading/InsertInto.md
• sql-reference/sql-statements/loading_unloading/INSERT.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.mdRocky
07/07/2026, 7:38 PMINSERT (UPSERT) jobs, they don't support arbitrary UPDATE SET ... WHERE ... syntax, but they do support the most common industrial update patterns if your target table is a Primary Key table.
Here is how you handle different "conditional" scenarios within a Pipe:
1. Partial Updates (Updating only specific columns)
Yes, this works. If your target is a Primary Key table, you can update only a subset of columns.
* How to do it: In your `PIPE`'s INSERT statement, simply list only the columns you want to update.
* Configuration: Ensure the session variable enable_insert_partial_update is set to true (it is true by default in v3.3+). You can also use a hint in the Pipe's SQL:
sql
CREATE PIPE my_pipe AS
INSERT /_+ SET_VAR(enable_insert_partial_update = true) _/ INTO target_table (id, status)
SELECT id, status FROM FILES(...);
_This will update the status for the given id while leaving other columns (like created_at) untouched._
2. Conditional Updates (Version Tracking / Idempotency)
If you want to ensure that an "older" file doesn't overwrite "newer" data already in the table, you use a merge_condition.
* How to do it: Use the properties("merge_condition" = "column") clause in your INSERT statement.
sql
CREATE PIPE my_pipe AS
INSERT INTO target_table
PROPERTIES("merge_condition" = "updated_at") -- Only update if file's updated_at >= table's updated_at
SELECT * FROM FILES(...);
3. Conditional Deletes
If your source files contain "delete" events, you can handle them via a Pipe.
* How to do it: StarRocks uses a hidden __op column for this.
sql
CREATE PIPE my_pipe AS
INSERT INTO target_table (id, data, __op)
SELECT
id,
data,
CASE WHEN action = 'DELETE' THEN 1 ELSE 0 END -- 1 = Delete, 0 = Upsert
FROM FILES(...);
4. Row-level Logic (CASE WHEN)
If you mean "update Column A if X is true, else Column B," you do this via standard SQL logic in the SELECT part of your Pipe:
sql
CREATE PIPE my_pipe AS
INSERT INTO target_table (id, final_price)
SELECT
id,
CASE WHEN discount > 0 THEN price * 0.9 ELSE price END
FROM FILES(...);
Important Limitations to Remember
* Pipes only run `INSERT`: You cannot put a DELETE FROM table WHERE... or UPDATE table SET... statement inside a Pipe. It must be an INSERT statement.
* Primary Key Table Required: For any actual "updating" or "deleting" to happen (as opposed to just appending new rows), your target table must be a Primary Key table. On Duplicate Key or Aggregate tables, INSERT simply appends or merges according to the table's internal rules.
* Complex Joins: Pipes generally should be simple SELECT FROM FILES. If you need to join against another large table to determine "conditions" before inserting, the Pipe may become slow or unstable. For complex ETL, Scheduled Tasks are better than Pipes.
Summary: You can achieve almost any conditional update logic by combining Primary Key tables, the __op column, and CASE statements within the Pipe's INSERT INTO ... SELECT query.
References
• loading/Load_to_Primary_Key_tables.mdEliezer Talker
07/07/2026, 7:50 PMRocky
07/07/2026, 7:50 PMBY NAME clause. However, there is a technical difference in how StarRocks handles these two things:
The Short Answer: No
A manual column list—like *`INSERT INTO target_table (col1, col2)`*—is a core part of standard SQL that the Pipe engine's internal serializer is built to handle.
The BY NAME clause is a much newer, "shorthand" feature. The current bug in v4.1.1 is specifically that the Pipe manager’s code does not yet know how to "save" that specific keyword when it stores your job in the system. Because it doesn't "save" the keyword, it defaults to a positional match when it actually runs.
Why the column list is safer:
1. Serialization Support: The Pipe engine has supported explicit column lists since it was introduced (v3.2). It treats the column list as a fundamental part of the target table definition.
2. No "Defaulting" Risk: Unlike BY NAME, which is an instruction to dynamically find columns, a list like (id, status) is static. The Pipe saves this list into its metadata. When it runs the task, it explicitly tells the execution engine: "Only fill these two columns."
3. Partial Updates: If you use a column list to perform a partial update on a Primary Key table, the FE (Frontend) generates a specific plan that identifies it as a partial update. This plan is what the Pipe runs.
How to be 100% sure (The "View" trick again)
If you are still worried about the Pipe engine messing with your SQL, the View Workaround I mentioned earlier is your "insurance policy."
By putting your column mapping and logic inside a View, you move the complexity out of the Pipe's hands:
1. The View handles the column selection: CREATE VIEW my_source AS SELECT colA, colB FROM FILES(...)
2. The Pipe does the simplest possible thing: INSERT INTO target_table SELECT * FROM my_source
Because the Pipe only sees SELECT *, there is nothing for it to "strip" or misinterpret. The StarRocks query planner will look "inside" the view at runtime and find your correct column mapping every time.
Recommendation: If you want to use the Pipe's automatic file tracking, use the View method. It is the most robust way to ensure your column logic remains intact regardless of Pipe engine bugs.
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• sql-reference/sql-statements/loading_unloading/INSERT.md
• loading/Load_to_Primary_Key_tables.md
• sql-reference/sql-functions/table-functions/files.md
functions/table-functions/files)
-functions/files)
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• release_notes/release-3.3.md
• loading/InsertInto.md
• loading/s3.md
• loading/hdfs_load.mdEliezer Talker
07/07/2026, 7:52 PMBY NAME, column lists, and INSERT PROPERTIES when executing -> also column list -> check if there is any bug on thisRocky
07/07/2026, 7:52 PMINSERT statement.
The Specific Bugs
You likely encountered or saw reports related to Issue #70774 and #70621, which were addressed in the v3.5 and v4.x release cycles. These bugs cause the following when used inside a CREATE PIPE statement:
1. BY NAME Stripping: The keyword is lost during serialization. The background task then defaults to "positional matching," which causes data to land in the wrong columns or fail with type mismatches.
2. Column List Stripping: If you specify INSERT INTO table (col1, col3), the Pipe may save it internally as just INSERT INTO table.
* The Result: It tries to insert into all columns of the table. If your SELECT only has two columns, the job fails with: "Target column count doesn't match source value column count."
3. PROPERTIES Loss: Clauses like PROPERTIES("merge_condition" = "ts") or strict_mode can also be lost. This prevents partial updates or version-based merges from working correctly in background tasks.
Is there a bug on the Column List?
Yes. While many reports focus on BY NAME, the root cause is the same: the Pipe manager's internal "Task" generator does not always preserve the "outer" parts of the INSERT statement when it reconstructs the SQL for background execution.
How to verify it on your cluster
You can see exactly what the Pipe is trying to run by checking the metadata:
sql
-- This will show the "reconstructed" SQL the Pipe is actually using
SELECT * FROM information_schema.pipes WHERE PIPE_NAME = 'your_pipe_name'\G
If the DEFINITION or LAST_ERROR field shows an INSERT statement that is missing your column list or properties, you are hitting this serialization bug.
The Bulletproof Workaround: Use a Logical View
To avoid these bugs in v4.1.1, do not put any complex logic or column mapping directly in the PIPE definition. Instead, "hide" it inside a View.
1. Create a View that handles the column list and mapping:
sql
CREATE VIEW pipe_source_v AS
SELECT
id,
status,
CAST(data AS JSON) as data -- Handle complex types here
FROM FILES(...)
/_ BY NAME logic is handled here by your explicit SELECT list _/;
2. Create the Pipe using the simplest possible SQL:
sql
CREATE PIPE my_pipe AS
INSERT INTO target_table (id, status, data) -- Pipe handles this list better
SELECT * FROM pipe_source_v;
Why this works:
The Pipe engine has no trouble serializing SELECT * FROM view. The complexity (like BY NAME, struct-to-JSON conversion, or specific column filtering) is safely stored in the View's metadata, which is managed by a different part of the system that does not have this "stripping" bug.
This remains the most reliable way to use Pipes for "set and forget" ingestion in your current version while still getting the benefits of partial updates and automatic file tracking.
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• loading/InsertInto.md
• sql-reference/information_schema/pipes.md
• releasenotes/release-3.5.md
References
• sql-reference/information_schema/pipes.md
• integrations/streaming/pipe.mdx
• sql-reference/sql-functions/meta-functions/inspect_all_pipes.mdEliezer Talker
07/07/2026, 8:09 PMFILES() directly as the source:
Bad pipe statement: 'only support FileTableFunction'
Rocky
07/07/2026, 8:10 PMFROM clause must directly call the FILES() table function. It cannot read from a View, even if that View contains a FILES() call.
This creates a difficult situation when combined with the "stripping" bug you've noticed. If the Pipe manager is removing your column lists and PROPERTIES, you lose the ability to trigger partial or conditional updates.
Here is the current state of workarounds for this specific bug in v4.1.x:
1. Confirm the "Stripping" Bug
First, check if your version actually has the fix for the metadata serialization bug (Issue #70774). Run this query:
sql
SELECT * FROM information_schema.pipes WHERE PIPE_NAME = 'your_pipe_name'\G
Look at the DEFINITION field. If the SQL there is missing your BY NAME, (col1, col2) list, or PROPERTIES, then you are indeed hitting the bug where the Pipe "forgets" these clauses during background execution.
2. The Best Workaround: Use "Tasks" instead of "Pipes"
Since the Pipe engine is currently failing to store the correct SQL metadata, the most reliable alternative is to use the Scheduled Task engine.
* Why? Tasks do not have the FileTableFunction restriction and they do not strip your SQL. They store and execute the statement exactly as you write it.
* The Trade-off: You lose the Pipe's automatic tracking of "new" vs "already loaded" files. You must either handle this in the S3 path (e.g., loading by date) or have the task delete/move files after processing.
Example Task for Partial/Conditional Updates:
sql
CREATE TASK my_ingest_task
SCHEDULE CRON '*/5 * * * *' -- Runs every 5 minutes
AS
INSERT INTO target_table (id, status, updated_at)
PROPERTIES("merge_condition" = "updated_at") -- This will NOT be stripped
SELECT id, status, updated_at
FROM FILES(
"path" = "<s3://bucket/data/year=2024/month=10/day=25/*.parquet>",
"format" = "parquet",
...
);
3. If you must use Pipes (Full-Table Mapping)
If you require the Pipe's automatic file-tracking and cannot use Tasks, the only way to perform an "update" while the column list is being stripped is to make your SELECT match the full schema of the table:
* Logic in SELECT: You can use CASE WHEN to decide what value to insert.
* Conditional Deletes: You can include the hidden __op column in your SELECT list. Even if the Pipe strips the INSERT INTO (...) part, it usually preserves the SELECT projection.
sql
CREATE PIPE my_pipe AS
INSERT INTO target_table -- Column list stripped? We provide ALL columns.
SELECT
id,
val1,
val2,
...
CASE WHEN status = 'deleted' THEN 1 ELSE 0 END AS __op
FROM FILES(...);
4. Check for Patch v4.1.2+
The bug where Pipe strips BY NAME and properties was a high-priority issue (linked to #70774).
* If you are on v4.1.1, check if v4.1.2 or a later patch is available for your environment. These patches specifically address the metadata serialization logic in the Frontend (FE).
Summary for your question:
If you use a Pipe and it strips the column list, you cannot do a partial update (it will either fail or map columns to the wrong positions). You should switch to a Scheduled Task to ensure your PROPERTIES and column mappings are preserved exactly as written.
References
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• sql-reference/information_schema/pipes.md
• loading/InsertInto.md
• administration/management/task_management.mdRocky
07/07/2026, 8:10 PMEliezer Talker
07/07/2026, 8:12 PMRocky
07/07/2026, 8:12 PMv4.1.2 tag, it means the code for that version is finalized. Based on recent activity in the StarRocks repository, v4.1.2 is specifically intended to fix the Pipe issues you are describing.
Why v4.1.2 is the version you want:
The "stripping" bugs you found are tracked under issues like #70622 and #70774. Here is the status of those fixes:
* The Bug: BY NAME and explicit column lists in FILES() were being ignored during background execution, causing the Pipe to default to positional matching.
* The Fix: This has been merged into the 4.1 branch. The fix ensures that the "Outer" INSERT state (including your column list and properties) is correctly serialized into the Pipe's metadata.
* Release Status: The fix was officially included in the v4.0.12 and v4.1.2 patch cycles (released late June 2026).
What you can do now:
1. Wait for the official release: The website usually updates within 48–72 hours of the GitHub tag. Once it's live, you can download the 4.1.2 binaries or update your container image.
2. Verify the fix (if you upgrade): After moving to v4.1.2, you can recreate your pipe and run:
sql
SELECT * FROM information_schema.pipes WHERE PIPE_NAME = '...'\G
If the DEFINITION field now correctly shows your (col1, col2) list and BY NAME keyword, the bug is resolved for your cluster.
3. The "Task" Fallback: If you cannot wait for the 4.1.2 release to be fully vetted by your team, switching to a Scheduled Task remains the safest way to ingest data with complex column mappings on your current v4.1.1 version, as Tasks do not suffer from this specific serialization bug.
Summary: You found the right trail. v4.1.2 is the patch that addresses the "Pipe stripping" behavior. Keep an eye on the StarRocks Releases page over the next few days.Eliezer Talker
07/07/2026, 8:15 PMRocky
07/07/2026, 8:15 PMv4.1.2 appears on the StarRocks GitHub, it usually triggers a CI/CD pipeline that builds and pushes the Docker images to Docker Hub. The website and official release notes often lag behind by 24–48 hours while the team performs final validation and documentation updates.
How to check for the 4.1.2 image
You can check if the images have landed by attempting to pull them directly. Use the standard StarRocks naming convention:
* FE Image: docker pull starrocks/fe-ubuntu:4.1.2
* BE Image: docker pull starrocks/be-ubuntu:4.1.2
* All-In-One (if used): docker pull starrocks/allin1-ubuntu:4.1.2
If these commands succeed, the images are ready for use. If they return a "manifest not found" error, the build process is likely still running or the images are in a staging repository.
Should you use it?
Since you are hitting a critical bug in v4.1.1 (the Pipe/FILES "stripping" issue), v4.1.2 is the exact version you need. The fixes for Issue #70774 and #70621 (preserving BY NAME, column lists, and PROPERTIES) are verified to be in the 4.1.2 patch.
Wait for the Website?
If the image is available but the website isn't updated, it means you are using the "bleeding edge" of that patch. For a production environment, it is generally safer to wait for the official release notes to appear on the StarRocks Releases page, which confirms the build has passed all regression tests.
Verification after upgrade
Once you have the 4.1.2 image running, verify the fix by recreating your pipe and checking the metadata:
sql
SELECT * FROM information_schema.pipes WHERE PIPE_NAME = 'your_pipe_name'\G
If the DEFINITION column now correctly shows your BY NAME clause or explicit (col1, col2, ...) list, the serialization bug is resolved.
Quick Reminder
If the image is not yet available, the Scheduled Task workaround mentioned earlier is your best alternative, as Tasks do not use the problematic Pipe serialization logic.Eliezer Talker
07/07/2026, 8:17 PMRocky
07/07/2026, 8:17 PMBY NAME (Issue #70622)
This bug caused INSERT INTO ... BY NAME FROM FILES() to ignore the BY NAME instruction and fall back to positional mapping during background execution. This is why your columns were mapping to the wrong indices. This was officially fixed and backported to the v4.1.2 release.
2. Fix for Partial Column Lists (Issue #70774)
This issue addressed the failure to push down the schema correctly when using a partial set of columns in the INSERT statement. In v4.1.1, the Pipe often "forgot" your explicit column list, but v4.1.2 ensures that partial column sets are correctly serialized and maintained in the Pipe's definition.
3. Fix for Property Loss / merge_condition (Issue #73371)
You likely noticed that PROPERTIES("merge_condition" = "...") was also being dropped. Issue #73371 specifically addresses the loss of the merge_condition during the commit process, which is critical for the Primary Key model's conditional updates.
How to verify the fix in v4.1.2:
Once you upgrade, you must recreate the Pipe (drop and create) to ensure the metadata is stored using the new, fixed serialization logic. You can then verify it by running:
sql
SELECT * FROM information_schema.pipes WHERE PIPE_NAME = 'your_pipe_name'\G
In v4.1.2, the DEFINITION column should now preserve your SQL exactly as written:
* BY NAME will remain present.
* Your explicit column list (col1, col2, ...) will be preserved.
* The PROPERTIES block will no longer be stripped.
Release Note Summary:
These fixes were bundled into the patch cycle released around June 25, 2026. If you see the 4.1.2 tag on GitHub, that code contains these fixes. Asynchronous Load/Pipe stability improvements to the Pipe and FILES() engine.
References
• releasenotes/release-4.1.md
• sql-reference/sql-statements/loading_unloading/pipe/CREATE_PIPE.md
• loading/InsertInto.md
• sql-reference/information_schema/pipes.md
• integrations/streaming/pipe.mdxRocky
07/07/2026, 8:17 PMBY NAME, and PROPERTIES are "stripped" or ignored during Pipe execution is addressed by several recent fixes in the v4.1 branch. Here are the specific details:
1. The "BY NAME" and Column Mapping Fix (#70774)
The bug where INSERT INTO ... BY NAME from FILES() failed to correctly push down the schema for partial column sets was tracked and resolved under Issue #70774. This issue caused the Pipe engine to essentially "forget" that you wanted to map columns by name, often leading to positional mapping errors or failed loads when your file and table schemas didn't match perfectly.
2. Nullable Column Inference Fix (#70621)
Another related issue (#70621) addressed a bug where NOT NULL constraints were incorrectly pushed down into the schema inferred from FILES(). This often caused loads to fail if the input data contained nulls for columns that StarRocks mistakenly thought should be strictly non-nullable during the inference phase.
3. Release Status of v4.1.2
The code for these fixes was merged into the release branch in late June 2026.
* The Tag: The v4.1.2 tag you see on GitHub confirms that the code is finalized and the build process has started.
* The Website/Images: There is typically a 2–3 day delay between the GitHub tag and the official release notes/Docker images appearing. If you are using Docker, you can try pulling starrocks/fe-ubuntu:4.1.2 to see if the image is already available on the registry.
Summary of what to expect in v4.1.2:
Once you upgrade to v4.1.2, your Pipe should correctly preserve the following:
* BY NAME keyword: Ensures columns match by header rather than position.
* Partial column lists: INSERT INTO table (colA, colB) SELECT ... will no longer be stripped.
* PROPERTIES clause: Clauses like "merge_condition" = "version_col" for partial updates will be correctly persisted in the Pipe metadata.
Note on your VIEW approach:
As you discovered, the error only support FileTableFunction is an intentional restriction of the Pipe engine. It must read directly from FILES() so it can manage file-level state (tracking which files are finished vs. pending). The fix in v4.1.2 solves this by ensuring that the FILES() call inside the Pipe actually respects the mapping you provide.Eliezer Talker
07/07/2026, 8:19 PMRocky
07/07/2026, 8:20 PMmerge_condition property.
In StarRocks, Primary Key and Unique Key tables normally use "Upsert" (replace) semantics. However, you can use a Conditional Update to prevent an existing row from being overwritten.
How to do it in a Pipe
Because v4.1.2 fixes the bug that was "stripping" properties from your Pipe definition, you can now define your pipe like this:
sql
CREATE PIPE my_pipe AS
INSERT INTO my_table
PROPERTIES (
"merge_condition" = "your_timestamp_or_version_column"
)
SELECT * FROM FILES(...)
How the logic works:
* The Condition: When you specify a merge_condition, StarRocks only updates an existing row if the incoming record's value for that column is greater than or equal to the existing one.
* To "Skip if Exists": If your incoming data has a "version" or "timestamp" and you want to ensure that once a row is written, it is never changed by an older or duplicate record, this property handles it.
* Strict "Insert Ignore": If you want to strictly prevent any update regardless of values (i.e., "first record wins"), you can point merge_condition to a column that represents the record's creation time. If a record with the same primary key already exists and has an equal or greater timestamp, the incoming record will effectively be ignored for that row.
Why v4.1.2 is critical for this:
In v4.1.1 and earlier 4.x versions, even if you wrote the PROPERTIES("merge_condition" = "...") clause inside your CREATE PIPE statement, the Pipe engine would often strip it out during the internal background execution. This meant your "conditional update" would revert to a standard "overwrite everything," which is exactly what you are trying to avoid.
In summary: Once you move to v4.1.2, the Pipe will correctly respect the merge_condition property, allowing you to control whether existing rows are updated or skipped based on the column you choose column.
References
• sql-reference/sql-functions/table-functions/files.md