One of the quieter but genuinely useful features in Oracle 23ai is SQL Domains. It doesn’t have the headline appeal of AI Vector Search, but for development teams building new applications on Oracle, it addresses a real and persistent problem: the same validation logic duplicated across dozens of table columns, the same formatting rules re-implemented in every application layer, the same data type decisions made inconsistently by different developers over time.
SQL Domains are Oracle’s answer to this — a first-class database object that encodes business rules about a data type, reusable across any number of columns.
The Problem With How We Define Columns Today
In a typical Oracle schema built over several years, you find patterns like this:
sql
-- Table 1: email stored as VARCHAR2(100) with a CHECK constraintCREATE TABLE customers ( email VARCHAR2(100) CONSTRAINT chk_cust_email CHECK (email LIKE '%@%.%'));-- Table 2: email stored as VARCHAR2(255), different constraintCREATE TABLE suppliers ( contact_email VARCHAR2(255) CONSTRAINT chk_sup_email CHECK (contact_email LIKE '%_@_%.__%'));-- Table 3: email stored as VARCHAR2(200), no constraintCREATE TABLE employees ( work_email VARCHAR2(200));
Three tables, three different implementations of “email address.” Different max lengths, different validation rules, one has no validation. This is the reality of schemas built incrementally by multiple developers. SQL Domains eliminate this by defining the rules once.
Creating and Using SQL Domains
sql
-- Define a domain for email addressesCREATE DOMAIN email_address AS VARCHAR2(255) DEFAULT ON NULL 'unknown@example.com' CONSTRAINT valid_email CHECK (VALUE LIKE '%_@_%.__%') DEFERRABLE INITIALLY IMMEDIATE DISPLAY LOWER(VALUE) ORDER LOWER(VALUE) ANNOTATIONS ( 'Description' IS 'Standard email address field', 'UI_LABEL' IS 'Email Address' );-- Use it in multiple tablesCREATE TABLE customers ( customer_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR2(100) NOT NULL, email email_address -- domain applied here);CREATE TABLE suppliers ( supplier_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, company_name VARCHAR2(200) NOT NULL, contact email_address -- same domain, consistent rules);CREATE TABLE employees ( employee_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, full_name VARCHAR2(200) NOT NULL, work_email email_address -- same domain again);
Now every email column in your schema has the same validation constraint, the same display transformation (lowercase), and the same ordering behavior. Change the domain definition and it propagates to all columns using it.
Domain Properties — What Each One Does
CONSTRAINT: Validation rule. Fires on INSERT and UPDATE. The VALUE keyword refers to the column value being validated. Supports deferred constraint checking.
DEFAULT ON NULL: Value substituted when NULL is inserted. More specific than column-level DEFAULT — applies whenever NULL arrives, whether explicitly inserted or from a missing column in INSERT.
DISPLAY: Expression for display transformation. DOMAIN_DISPLAY(column_name) returns the display value without changing stored data. Useful for consistent formatting in reports.
sql
-- DOMAIN_DISPLAY returns the display expression resultSELECT customer_id, DOMAIN_DISPLAY(email) formatted_email, -- runs LOWER(VALUE) email raw_email -- stored value unchangedFROM customers;
ORDER: Expression for sorting. DOMAIN_ORDER(column_name) returns the sort key. For case-insensitive sorting: ORDER LOWER(VALUE) means ORDER BY DOMAIN_ORDER(email) sorts case-insensitively without the developer remembering to add LOWER().
ANNOTATIONS: Metadata about the domain. Key-value pairs stored in the data dictionary. Useful for documentation, UI generation, and tooling integration.
Numeric and Date Domains
Domains aren’t just for VARCHAR2. They’re particularly useful for numeric business rules:
sql
-- Domain for monetary amountsCREATE DOMAIN monetary_amount AS NUMBER(15,2) CONSTRAINT positive_amount CHECK (VALUE >= 0) CONSTRAINT max_amount CHECK (VALUE <= 9999999999999.99) ANNOTATIONS ('currency' IS 'USD', 'format' IS 'FM$999,999,999,990.00');-- Domain for percentage valuesCREATE DOMAIN percentage AS NUMBER(5,2) CONSTRAINT valid_pct CHECK (VALUE BETWEEN 0 AND 100) DEFAULT 0 DISPLAY TO_CHAR(VALUE, 'FM990.00') || '%';-- Domain for future dates onlyCREATE DOMAIN future_date AS DATE CONSTRAINT must_be_future CHECK (VALUE > SYSDATE);-- Using them togetherCREATE TABLE contracts ( contract_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, value monetary_amount NOT NULL, discount_pct percentage, expiry_date future_date NOT NULL);
Querying Domain Metadata
sql
-- View all domains in a schemaSELECT owner, name, data_type, data_length, default_on_null, nullableFROM all_domainsWHERE owner = 'MY_SCHEMA';-- View domain constraintsSELECT owner, domain_name, constraint_name, search_condition, statusFROM all_domain_constraintsWHERE owner = 'MY_SCHEMA';-- Find all columns using a specific domainSELECT owner, table_name, column_name, domain_owner, domain_nameFROM all_tab_columnsWHERE domain_name = 'EMAIL_ADDRESS'AND domain_owner = 'MY_SCHEMA'ORDER BY owner, table_name;
Enum Domains — Replacing Magic Numbers and String Constants
A particularly clean use case: enumerated values that currently live as application constants or undocumented CHECK constraints.
sql
-- Instead of: CHECK (status IN ('PENDING', 'ACTIVE', 'CLOSED', 'CANCELLED'))-- scattered across multiple tables...CREATE DOMAIN order_status AS VARCHAR2(20) CONSTRAINT valid_status CHECK (VALUE IN ('PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CANCELLED', 'RETURNED')) ANNOTATIONS ('description' IS 'Valid order lifecycle statuses');-- Now the valid values are self-documenting in the catalog-- Any table tracking order status uses this domain
The business rule lives once, in the database, visible to anyone querying the data dictionary. No hunting through application code to understand what values a status column accepts.

Yorum bırakın