cancel
Showing results for 
Search instead for 
Did you mean: 
Data Engineering
Join discussions on data engineering best practices, architectures, and optimization strategies within the Databricks Community. Exchange insights and solutions with fellow data engineers.
cancel
Showing results for 
Search instead for 
Did you mean: 

Oracle NUMBER → DecimalType(38,10) on ingestion, and silver layer best practices

faruk
New Contributor III

Hello everyone,

I've just started a new position and my goal is to build a Data Lakehouse with Databricks. I have no previous experience with Databricks and I'm being helped by an external company. The goal is to centralize all our data in Databricks. I have two problems and I hope you can help me.

1. Ingestion: how to handle Oracle NUMBER becoming DecimalType(38,10)?

I'm ingesting all our tables from Oracle databases into the bronze layer. I managed to ingest every table, but the problem is that many columns are declared as plain NUMBER in Oracle (no precision, no scale), so Databricks reads them as DecimalType(38,10). Even an ID stored as 1250 ends up as 1250.0000000000. I don't know how to handle this inside the pipeline.

I already tried doing it with the JDBC connector in a notebook: I run two queries, one to get the data and one to get the metadata, then I keep only the decimal columns, check if the precision is null, and cast those to integer. It works, but it becomes overwhelming because I have to provide the primary key and the new table name for every single table.

Important: we are not allowed to change the data types on the Oracle side. That's a hard restriction from headquarters, so the fix has to happen on the Databricks side.

Is there a way to handle the type conversion in the ingestion pipeline itself? Or a way to automate this that scales to a large number of tables?

2. Silver layer: should I clean into tables, or directly in the views?

The company that is training me told me I don't need to clean all the bronze tables, even when I use them in a silver view — instead I should rename and clean the columns directly in the view.

This doesn't match what I learned in the Databricks training. My understanding was that every table I use in a silver view or in an intermediate silver table should first exist as a cleaned silver table, so I don't have to redo the renaming and the type casting everywhere it's used.

Which approach is actually recommended, and why?

3. Will the pipeline redo all the renaming and casting on every run?

This is related to the previous question. When my pipeline runs, will it rename and reprocess everything in the silver layer every single time? My data needs to be refreshed every 4 hours, so I want to be sure the transformations only apply to new or changed rows.

Thank you for your help and advice

2 ACCEPTED SOLUTIONS

Accepted Solutions

data_pulse
New Contributor II

@faruk Good question and worth exploring the options.

1. Handling Oracle NUMBER types

If you are using JDBC and unqualified Oracle NUMBER columns are landing as Decimal(38,10), there are two sensible approaches:

Option A: Keep Bronze close to source and standardize in Silver
This is the cleaner medallion pattern. Let Bronze preserve the source representation, then cast based on metadata/business meaning in Silver.

Eg:

SELECT
  CAST(id AS BIGINT) AS id,
  CAST(amount AS DECIMAL(18,2)) AS amount
FROM bronze.orders;

This keeps ingestion simple and avoids changing source semantics too early.

Option B: Make the casts metadata-driven
For a large number of Oracle tables, this is usually the more scalable approach. Instead of manually maintaining casts for every table, read Oracle metadata from ALL_TAB_COLUMNS / DBA_TAB_COLUMNS, generate the target type mappings, and store only exceptions in a small Databricks control mapping table.

Eg:

source_table | source_column | oracle_type   | target_type
----------------------------------------------------------
ORDERS       | ORDER_ID      | NUMBER        | BIGINT
ORDERS       | AMOUNT        | NUMBER(18,2)  | DECIMAL(18,2)
CUSTOMER     | STATUS_CODE   | NUMBER        | INT

Then dynamically generate silver layer as :

exprs = [
    f"CAST({col} AS {dtype}) AS {col}"
    for col, dtype in mappings.items()
]
silver_df = spark.table("bronze.orders").selectExpr(*exprs)

This avoids maintaining separate notebooks or hand-written casts for hundreds of tables.

One caution: plain Oracle NUMBER does not always mean integer, so don't cast every such column to BIGINT unless the business meaning or actual data confirms it.

Databricks’ medallion guidance supports keeping Bronze minimally transformed and doing type normalization in Silver.

2. Silver layer: tables or views

Again, two practical approaches.

Option A : Materialize reusable cleaned datasets
If multiple downstream systems/pipelines use the same renamed/cast dataset, create a Silver table/streaming table once.

Eg:

CREATE OR REFRESH STREAMING TABLE silver.orders AS
SELECT
  CAST(order_id AS BIGINT) AS order_id,
  TRIM(customer_name) AS customer_name,
  CAST(amount AS DECIMAL(18,2)) AS amount
FROM STREAM(bronze.orders);

This gives you one reusable definition and avoids repeating the same casts everywhere.

Option B: Use views for one-off transformations

If the transformation is lightweight and only used by one downstream object, a view is completely reasonable.

CREATE VIEW silver.orders_view AS
SELECT
  CAST(order_id AS BIGINT) AS order_id,
  TRIM(customer_name) AS customer_name
FROM bronze.orders;

Databricks documents views, streaming tables and materialized views as different dataset choices for Lakeflow pipelines.

3. Will everything be reprocessed every 4 hours?

That depends on which object type you choose.

Option A: Streaming table for incremental processing
Best when the transformation is row-level and you only want new data processed.  So, New rows arrive and streaming table processes only new input and checkpoint/state advances.

Option B: Materialized view
Useful for joins/aggregations. Pipeline can refresh incrementally when the query supports it, but some queries may fall back to re-computation.

Option C: Normal view
Nothing is stored, the cast/rename logic is evaluated every time the view is queried.

For a large Oracle related ingestion pipelines, I would lean toward metadata-driven type mappings + reusable Silver tables where needed, rather than manually maintaining casts table by table.

The good news is that Databricks now provides a managed Lakeflow Connect Oracle integrated CDC pipeline. It is currently in Beta, but it can simplify Oracle ingestion compared with maintaining custom JDBC logic yourself. worth checking here.

Also another community post for reference on Oracle ingestion here

View solution in original post

balajij8
Esteemed Contributor II

@faruk 

Handling Oracle NUMBER in the ingestion pipeline

The Oracle JDBC driver maps an NUMBER (no precision, no scale) to DecimalType(38,10) as it must pick a fixed scale for a type system that has no equivalent to Oracle's unconstrained decimal. You can fix it in the silver layer, where type correction, renaming and cleaning belongs. The bronze layer should ingest raw data exactly as the driver delivers it and this preserves a actual copy of the source and keeps the ingestion step simple and uniform across tables. To scale the type correction without specifying a primary key and target name for every table manually, you can build a configuration based approach - maintain a small meta table or YAML file in Unity Catalog that maps each (catalog.schema.table, column_name, target_type) eg, default.bronze.customers, customer_id, BIGINT. You can use a generic silver layer transformation then reads the config and applies the appropriate CAST to each column listed. It allows you add or adjust column types by editing one row in a config table rather than rewriting per table ingestion code. In a Spark Declarative Pipelines (SDP), the silver streaming table reads FROM STREAM of bronze table and applies these casts inline directly - the pipeline engine handles it and you only declare the transformation logic once.

Silver tables vs views

The consulting company advice has carries significant drawbacks. View is just a saved SQL definition - every time any downstream query, dashboard or gold-layer table references that view, the entire transformation (all the casts, renames, filters) is re-evaluated against the full bronze table from scratch. With hundreds of tables refreshed every four hours, this means the same cleaning work is repeated on every read, wasting cost and increasing response time. Views cannot support incremental processing as they have no checkpoint, so there is no way to apply transformations only to new or changed data. Materialized silver tables serve as a quality checkpoint in the pipeline - you can attach data-quality expectations (not-null constraints, range checks, uniqueness) to a streaming table, and the pipeline will report violations and block bad data from flowing downstream. You cannot do this with a plain view.

You can keep bronze streaming table (raw as ingested) to silver streaming table (cleaned, typed, renamed, with data quality checks) to gold materialized view or streaming table (aggregated, business-ready). Views have a role for read-only, light reshaping on top of already-materialized tables, but the core cleaning and data correction should live in materialized silver tables.

Pipeline reprocess

No full reprocess every time if you use streaming tables, which is the default SDP approach for bronze and silver layers. A streaming table maintains an internal checkpoint that records exactly which rows it has already consumed from its source. On each pipeline update, the silver streaming table reads FROM STREAM of bronze table and the engine returns only the rows appended to bronze since the last successful run. The transformations like casting Decimal Type(38,10) to BIGINT, renaming columns, filtering invalid rows are applied only to those new rows and the result is appended to the silver table. The existing silver data is untouched. Its a main advantage of materialized silver tables over views as the view recomputes the entire transformation every time it is queried while a streaming table processes incrementally and stores the result.

View solution in original post

2 REPLIES 2

data_pulse
New Contributor II

@faruk Good question and worth exploring the options.

1. Handling Oracle NUMBER types

If you are using JDBC and unqualified Oracle NUMBER columns are landing as Decimal(38,10), there are two sensible approaches:

Option A: Keep Bronze close to source and standardize in Silver
This is the cleaner medallion pattern. Let Bronze preserve the source representation, then cast based on metadata/business meaning in Silver.

Eg:

SELECT
  CAST(id AS BIGINT) AS id,
  CAST(amount AS DECIMAL(18,2)) AS amount
FROM bronze.orders;

This keeps ingestion simple and avoids changing source semantics too early.

Option B: Make the casts metadata-driven
For a large number of Oracle tables, this is usually the more scalable approach. Instead of manually maintaining casts for every table, read Oracle metadata from ALL_TAB_COLUMNS / DBA_TAB_COLUMNS, generate the target type mappings, and store only exceptions in a small Databricks control mapping table.

Eg:

source_table | source_column | oracle_type   | target_type
----------------------------------------------------------
ORDERS       | ORDER_ID      | NUMBER        | BIGINT
ORDERS       | AMOUNT        | NUMBER(18,2)  | DECIMAL(18,2)
CUSTOMER     | STATUS_CODE   | NUMBER        | INT

Then dynamically generate silver layer as :

exprs = [
    f"CAST({col} AS {dtype}) AS {col}"
    for col, dtype in mappings.items()
]
silver_df = spark.table("bronze.orders").selectExpr(*exprs)

This avoids maintaining separate notebooks or hand-written casts for hundreds of tables.

One caution: plain Oracle NUMBER does not always mean integer, so don't cast every such column to BIGINT unless the business meaning or actual data confirms it.

Databricks’ medallion guidance supports keeping Bronze minimally transformed and doing type normalization in Silver.

2. Silver layer: tables or views

Again, two practical approaches.

Option A : Materialize reusable cleaned datasets
If multiple downstream systems/pipelines use the same renamed/cast dataset, create a Silver table/streaming table once.

Eg:

CREATE OR REFRESH STREAMING TABLE silver.orders AS
SELECT
  CAST(order_id AS BIGINT) AS order_id,
  TRIM(customer_name) AS customer_name,
  CAST(amount AS DECIMAL(18,2)) AS amount
FROM STREAM(bronze.orders);

This gives you one reusable definition and avoids repeating the same casts everywhere.

Option B: Use views for one-off transformations

If the transformation is lightweight and only used by one downstream object, a view is completely reasonable.

CREATE VIEW silver.orders_view AS
SELECT
  CAST(order_id AS BIGINT) AS order_id,
  TRIM(customer_name) AS customer_name
FROM bronze.orders;

Databricks documents views, streaming tables and materialized views as different dataset choices for Lakeflow pipelines.

3. Will everything be reprocessed every 4 hours?

That depends on which object type you choose.

Option A: Streaming table for incremental processing
Best when the transformation is row-level and you only want new data processed.  So, New rows arrive and streaming table processes only new input and checkpoint/state advances.

Option B: Materialized view
Useful for joins/aggregations. Pipeline can refresh incrementally when the query supports it, but some queries may fall back to re-computation.

Option C: Normal view
Nothing is stored, the cast/rename logic is evaluated every time the view is queried.

For a large Oracle related ingestion pipelines, I would lean toward metadata-driven type mappings + reusable Silver tables where needed, rather than manually maintaining casts table by table.

The good news is that Databricks now provides a managed Lakeflow Connect Oracle integrated CDC pipeline. It is currently in Beta, but it can simplify Oracle ingestion compared with maintaining custom JDBC logic yourself. worth checking here.

Also another community post for reference on Oracle ingestion here

balajij8
Esteemed Contributor II

@faruk 

Handling Oracle NUMBER in the ingestion pipeline

The Oracle JDBC driver maps an NUMBER (no precision, no scale) to DecimalType(38,10) as it must pick a fixed scale for a type system that has no equivalent to Oracle's unconstrained decimal. You can fix it in the silver layer, where type correction, renaming and cleaning belongs. The bronze layer should ingest raw data exactly as the driver delivers it and this preserves a actual copy of the source and keeps the ingestion step simple and uniform across tables. To scale the type correction without specifying a primary key and target name for every table manually, you can build a configuration based approach - maintain a small meta table or YAML file in Unity Catalog that maps each (catalog.schema.table, column_name, target_type) eg, default.bronze.customers, customer_id, BIGINT. You can use a generic silver layer transformation then reads the config and applies the appropriate CAST to each column listed. It allows you add or adjust column types by editing one row in a config table rather than rewriting per table ingestion code. In a Spark Declarative Pipelines (SDP), the silver streaming table reads FROM STREAM of bronze table and applies these casts inline directly - the pipeline engine handles it and you only declare the transformation logic once.

Silver tables vs views

The consulting company advice has carries significant drawbacks. View is just a saved SQL definition - every time any downstream query, dashboard or gold-layer table references that view, the entire transformation (all the casts, renames, filters) is re-evaluated against the full bronze table from scratch. With hundreds of tables refreshed every four hours, this means the same cleaning work is repeated on every read, wasting cost and increasing response time. Views cannot support incremental processing as they have no checkpoint, so there is no way to apply transformations only to new or changed data. Materialized silver tables serve as a quality checkpoint in the pipeline - you can attach data-quality expectations (not-null constraints, range checks, uniqueness) to a streaming table, and the pipeline will report violations and block bad data from flowing downstream. You cannot do this with a plain view.

You can keep bronze streaming table (raw as ingested) to silver streaming table (cleaned, typed, renamed, with data quality checks) to gold materialized view or streaming table (aggregated, business-ready). Views have a role for read-only, light reshaping on top of already-materialized tables, but the core cleaning and data correction should live in materialized silver tables.

Pipeline reprocess

No full reprocess every time if you use streaming tables, which is the default SDP approach for bronze and silver layers. A streaming table maintains an internal checkpoint that records exactly which rows it has already consumed from its source. On each pipeline update, the silver streaming table reads FROM STREAM of bronze table and the engine returns only the rows appended to bronze since the last successful run. The transformations like casting Decimal Type(38,10) to BIGINT, renaming columns, filtering invalid rows are applied only to those new rows and the result is appended to the silver table. The existing silver data is untouched. Its a main advantage of materialized silver tables over views as the view recomputes the entire transformation every time it is queried while a streaming table processes incrementally and stores the result.