Loading learning content...
As databases grow from dozens of tables to hundreds or thousands, organization becomes critical. How do you separate sales tables from HR tables from audit tables? How do you allow developers to work on experimental features without affecting production tables? How do you grant permission to see customer data but not salary information?
The answer is schemas.
In SQL, a schema is a container—a namespace—that holds database objects like tables, views, functions, and sequences. Schemas provide:
Understanding schema operations is essential for managing databases beyond the simplest use cases.
By the end of this page, you will master CREATE SCHEMA, ALTER SCHEMA, and DROP SCHEMA commands. You'll understand schema qualification for object naming, the search_path mechanism for default schema resolution, and best practices for schema organization in enterprise environments.
A schema is a named collection of database objects within a database. Think of it as a folder within the database that can contain its own tables, views, sequences, functions, and other objects.
The database hierarchy:
Database Cluster
└── Database (e.g., 'company_db')
├── Schema (e.g., 'sales')
│ ├── Table: customers
│ ├── Table: orders
│ └── View: customer_orders_summary
├── Schema (e.g., 'hr')
│ ├── Table: employees
│ ├── Table: departments
│ └── Function: calculate_bonus
└── Schema (e.g., 'public')
└── Table: shared_config
Each database typically has a default schema named public (PostgreSQL) or dbo (SQL Server) where objects are created if no schema is specified.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
-- ============================================-- Fully Qualified Object Names-- ============================================-- Without schema qualification (uses default/search_path):SELECT * FROM customers; -- With schema qualification (explicit):SELECT * FROM sales.customers; -- With full qualification (database.schema.table):-- Note: Cross-database queries have limited supportSELECT * FROM company_db.sales.customers; -- SQL Server style -- ============================================-- Objects in Different Schemas Can Share Names-- ============================================-- This is perfectly valid:CREATE TABLE sales.config (setting TEXT, value TEXT);CREATE TABLE hr.config (setting TEXT, value TEXT);CREATE TABLE audit.config (setting TEXT, value TEXT); -- Each schema has its own 'config' table—no conflictSELECT * FROM sales.config; -- Gets sales configurationSELECT * FROM hr.config; -- Gets HR configurationSELECT * FROM audit.config; -- Gets audit configuration -- ============================================-- Default Schema Behavior-- ============================================-- PostgreSQL: Objects go to first schema in search_path-- Usually 'public' by default CREATE TABLE my_table (id INT); -- Creates: public.my_table (if search_path starts with public) -- Check current search_pathSHOW search_path; -- Output: "$user", public -- The "$user" means: schema matching current username-- If user 'alice' creates a table, it goes to 'alice' schema (if exists)-- Otherwise falls through to 'public'Don't confuse schemas with databases. A database is the top-level container with its own files, connections, and transaction management. A schema is a namespace WITHIN a database. You can have multiple schemas in one database. Cross-schema queries within a database are easy; cross-database queries are limited or require special syntax.
| Database | Default Schema | Schema Syntax | Notes |
|---|---|---|---|
| PostgreSQL | public | schema.table | Rich schema support with search_path |
| MySQL | N/A (database = schema) | database.table | Uses 'database' term for schemas |
| SQL Server | dbo | schema.table | Full schema support with ownership |
| Oracle | User-named | schema.table | Schema typically matches username |
| SQLite | N/A | No schema support | Database is single namespace |
The CREATE SCHEMA statement creates a new schema (namespace) within the current database. When executed, the database:
The basic syntax and variations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
-- ============================================-- Basic CREATE SCHEMA-- ============================================-- Create an empty schemaCREATE SCHEMA sales; -- Create schema if it doesn't exist (PostgreSQL 9.3+)CREATE SCHEMA IF NOT EXISTS sales; -- Create schema with specific ownerCREATE SCHEMA hr AUTHORIZATION admin_user; -- Create schema with owner matching schema name (SQL standard)CREATE SCHEMA AUTHORIZATION alice; -- Creates schema named 'alice' owned by 'alice' -- ============================================-- CREATE SCHEMA with Initial Objects-- ============================================-- SQL Standard: Create schema with contained objectsCREATE SCHEMA inventory CREATE TABLE products ( product_id INTEGER PRIMARY KEY, product_name VARCHAR(200) NOT NULL, unit_price DECIMAL(10, 2) ) CREATE TABLE warehouse_stock ( warehouse_id INTEGER, product_id INTEGER REFERENCES products(product_id), quantity INTEGER DEFAULT 0, PRIMARY KEY (warehouse_id, product_id) ) CREATE VIEW low_stock_products AS SELECT p.product_name, ws.quantity FROM products p JOIN warehouse_stock ws ON p.product_id = ws.product_id WHERE ws.quantity < 10; -- All objects created in 'inventory' schema atomically -- ============================================-- Multi-Tenant Schema Pattern-- ============================================-- Create isolated schemas for each tenantCREATE SCHEMA tenant_001;CREATE SCHEMA tenant_002;CREATE SCHEMA tenant_003; -- Create identical structure in each-- (Usually done via a script or function)CREATE TABLE tenant_001.users ( user_id SERIAL PRIMARY KEY, username VARCHAR(100) NOT NULL);CREATE TABLE tenant_001.orders ( order_id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES tenant_001.users(user_id)); -- Repeat for each tenant... -- ============================================-- Development/Environment Schemas-- ============================================CREATE SCHEMA staging;CREATE SCHEMA development;CREATE SCHEMA production; -- Same tables, different environmentsCREATE TABLE staging.features (id INT, data TEXT);CREATE TABLE production.features (id INT, data TEXT); -- Developers work in staging, changes promoted to production -- ============================================-- Check Existing Schemas-- ============================================-- PostgreSQL: List all schemasSELECT schema_name, schema_ownerFROM information_schema.schemata; -- Or use psql shortcut-- dn -- SQL Server: List schemas-- SELECT name, principal_id FROM sys.schemas;Choose schema names that clearly communicate purpose: sales, hr, audit, staging, archive. Avoid generic names like schema1 or new_schema. For multi-tenant systems, use consistent patterns like tenant_001 or customer_acme. Schema names should be lowercase with underscores for maximum portability.
When you reference a table without schema qualification (e.g., SELECT * FROM customers), how does the database know which schema's customers table to use?
PostgreSQL uses the search_path—an ordered list of schemas to search when resolving unqualified object names. The first schema where the object is found wins.
Search path mechanics:
$user: Replaced with current username12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
-- ============================================-- Viewing and Setting Search Path-- ============================================-- Check current search pathSHOW search_path;-- Default output: "$user", public -- Set search path for current sessionSET search_path TO sales, hr, public; -- Now unqualified references search: sales → hr → publicSELECT * FROM customers; -- Finds: sales.customers if exists, else hr.customers, else public.customers -- Set search path permanently for a userALTER USER developer SET search_path = development, public; -- Set search path permanently for a databaseALTER DATABASE company_db SET search_path = app, shared, public; -- ============================================-- Search Path in Practice-- ============================================-- Setup: Tables in different schemasCREATE SCHEMA app;CREATE SCHEMA audit; CREATE TABLE app.settings (key TEXT, value TEXT);CREATE TABLE audit.settings (key TEXT, value TEXT);CREATE TABLE public.settings (key TEXT, value TEXT); -- With default search path:SET search_path = "$user", public;SELECT * FROM settings; -- Gets public.settings -- Switch to application context:SET search_path = app, audit, public;SELECT * FROM settings; -- Gets app.settingsSELECT * FROM audit.settings; -- Explicit always works -- ============================================-- The "$user" Schema-- ============================================-- If you connect as user 'alice':-- "$user" is replaced with "alice"-- So search_path of "$user", public becomes: alice, public -- This enables per-user private tablesCREATE SCHEMA alice AUTHORIZATION alice;CREATE TABLE alice.my_notes (note TEXT); -- Alice's private table -- When Alice queries:SET search_path = "$user", public;SELECT * FROM my_notes; -- Gets alice.my_notes -- ============================================-- Security Implications of Search Path-- ============================================-- DANGER: Malicious schema injection-- If an attacker can create objects in a schema that's searched-- before the intended schema, they can hijack function calls! -- Example vulnerability:-- search_path = public, app (public is searched FIRST)-- If attacker creates public.important_function()... -- BEST PRACTICE: Always list application schemas FIRSTSET search_path = app, public; -- Safe: app's objects found first -- Or use explicit schema qualification for security-sensitive operationsSELECT app.sensitive_function(data) FROM app.private_table; -- ============================================-- Schema Qualification in Queries-- ============================================-- Fully qualified: Not affected by search pathSELECT s.order_id, s.customer_id, h.employee_nameFROM sales.orders sJOIN hr.employees h ON s.salesperson_id = h.employee_id; -- Mixed: Unqualified tables use search pathSET search_path = sales, hr;SELECT orders.order_id, -- Found in sales (first in path with 'orders') employees.name -- Found in hr (has 'employees')FROM ordersJOIN employees ON orders.salesperson_id = employees.employee_id;Never put public first in search_path if untrusted users can create objects in public schema. An attacker could create a function with the same name as a system function, and your code would call the malicious version. Always list trusted schemas before public, or explicitly qualify all object references.
Once created, schemas can be renamed or have their ownership transferred. ALTER SCHEMA provides these capabilities. Note that altering a schema doesn't affect the objects within it—they remain and retain their names.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
-- ============================================-- Renaming Schemas-- ============================================-- Rename a schema (PostgreSQL)ALTER SCHEMA old_sales RENAME TO sales_archive; -- All objects inside retain their names-- What was old_sales.customers is now sales_archive.customers -- ============================================-- Changing Schema Ownership-- ============================================-- Transfer ownership to another user/roleALTER SCHEMA sales OWNER TO sales_manager; -- The new owner gets full control over the schema-- Including ability to create, alter, and drop objects within it -- ============================================-- Practical Examples-- ============================================-- Scenario 1: Archiving a schema-- Rename to indicate archived statusALTER SCHEMA legacy_app RENAME TO legacy_app_archived_2024; -- Scenario 2: Transferring responsibility-- New DBA taking over maintenanceALTER SCHEMA inventory OWNER TO new_dba; -- Scenario 3: Merging teams-- HR department absorbed by People OperationsALTER SCHEMA hr RENAME TO people_ops;ALTER SCHEMA hr_archive RENAME TO people_ops_archive; -- ============================================-- Limitations of ALTER SCHEMA-- ============================================-- You CANNOT:-- - Move objects between schemas with ALTER SCHEMA-- - Change the schema's default privileges (must use ALTER DEFAULT PRIVILEGES)-- - Merge two schemas -- Moving objects requires ALTER TABLE/VIEW/etc:ALTER TABLE old_schema.customers SET SCHEMA new_schema;ALTER VIEW old_schema.customer_summary SET SCHEMA new_schema;ALTER SEQUENCE old_schema.customer_id_seq SET SCHEMA new_schema; -- ============================================-- Batch Moving Objects Between Schemas-- ============================================-- Generate ALTER statements to move all tablesSELECT 'ALTER TABLE ' || schemaname || '.' || tablename || ' SET SCHEMA new_schema;'FROM pg_tablesWHERE schemaname = 'old_schema'; -- Execute the generated statements-- (In practice, save to file and run as script) -- ============================================-- Checking Schema Ownership-- ============================================SELECT nspname AS schema_name, pg_get_userbyid(nspowner) AS ownerFROM pg_namespaceWHERE nspname NOT LIKE 'pg_%' AND nspname != 'information_schema';Renaming a schema is a metadata operation—fast and non-disruptive. However, any code using qualified names (sales.customers) will break after rename. Views, functions, and stored procedures referencing the old schema name may fail. Always update application code and database objects that use explicit schema qualification.
DROP SCHEMA removes a schema from the database. Like DROP TABLE, the behavior depends on whether the schema contains objects and whether CASCADE is specified.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
-- ============================================-- Basic DROP SCHEMA-- ============================================-- Drop an empty schemaDROP SCHEMA empty_schema; -- Conditional drop (avoids error if doesn't exist)DROP SCHEMA IF EXISTS maybe_exists; -- Drop schema with all contents (DANGEROUS)DROP SCHEMA populated_schema CASCADE;-- This deletes ALL objects in the schema: tables, views, functions, etc. -- ============================================-- RESTRICT vs CASCADE-- ============================================-- Default behavior (RESTRICT): Fail if schema contains objectsDROP SCHEMA sales;-- ERROR: cannot drop schema sales because other objects depend on it-- DETAIL: table sales.customers depends on schema sales-- table sales.orders depends on schema sales-- view sales.order_summary depends on schema sales -- RESTRICT is explicit but same as defaultDROP SCHEMA sales RESTRICT;-- Same error -- CASCADE: Drop schema AND all contentsDROP SCHEMA sales CASCADE;-- NOTICE: drop cascades to table sales.customers-- NOTICE: drop cascades to table sales.orders-- NOTICE: drop cascades to view sales.order_summary-- Schema and all objects are gone -- ============================================-- Safe DROP SCHEMA Patterns-- ============================================-- Pattern 1: Check contents before droppingSELECT table_type, table_nameFROM information_schema.tablesWHERE table_schema = 'schema_to_drop'; -- Pattern 2: Count objects to get sense of impactSELECT COUNT(*) AS table_count, SUM(CASE WHEN table_type = 'VIEW' THEN 1 ELSE 0 END) AS view_countFROM information_schema.tablesWHERE table_schema = 'schema_to_drop'; -- Pattern 3: Rename before drop (reversible step)ALTER SCHEMA old_app RENAME TO old_app_pending_delete;-- Wait a week for any issues to surfaceDROP SCHEMA old_app_pending_delete CASCADE; -- ============================================-- Dependencies on Schema Objects-- ============================================-- Objects in OTHER schemas might reference this schema's objects -- Check for cross-schema dependencies before dropping:SELECT dependent_ns.nspname AS dependent_schema, dependent_obj.relname AS dependent_object, source_ns.nspname AS depends_on_schema, source_obj.relname AS depends_on_objectFROM pg_depend dJOIN pg_class dependent_obj ON d.objid = dependent_obj.oidJOIN pg_namespace dependent_ns ON dependent_obj.relnamespace = dependent_ns.oidJOIN pg_class source_obj ON d.refobjid = source_obj.oidJOIN pg_namespace source_ns ON source_obj.relnamespace = source_ns.oidWHERE source_ns.nspname = 'schema_to_drop' AND dependent_ns.nspname != source_ns.nspname; -- If results: Those objects in OTHER schemas will break! -- ============================================-- Cannot Drop Special Schemas-- ============================================-- These will fail:DROP SCHEMA pg_catalog CASCADE; -- System catalogDROP SCHEMA information_schema; -- SQL standard metadata-- ERROR: cannot drop schema pg_catalog because it is required by the database system -- Public CAN be dropped (but usually shouldn't)DROP SCHEMA public CASCADE; -- Works...-- But breaks default behavior—recreate immediately if needed:CREATE SCHEMA public;DROP SCHEMA CASCADE is one of the most destructive commands in SQL. It removes the schema and EVERY object it contains—potentially hundreds of tables, views, functions, and sequences in a single, irreversible operation. Always enumerate contents and verify the impact before executing.
Schemas serve as permission boundaries. You can grant or revoke access at the schema level, controlling who can see or modify all objects within. This is more maintainable than granting permissions on individual tables.
Schema permission types:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
-- ============================================-- Granting Schema Access-- ============================================-- Allow a role to access objects in a schemaGRANT USAGE ON SCHEMA sales TO readonly_user; -- USAGE alone doesn't give access to objects—it's the gateway-- User still needs SELECT/INSERT/etc on actual tables -- ============================================-- Permission Levels Explained-- ============================================-- To access sales.customers, user needs:-- 1. USAGE on schema 'sales' (to enter the schema)-- 2. SELECT on table 'sales.customers' (to query the table) -- Grant schema accessGRANT USAGE ON SCHEMA sales TO analyst; -- Grant table accessGRANT SELECT ON sales.customers TO analyst;GRANT SELECT ON sales.orders TO analyst; -- Or grant on all tables in schemaGRANT SELECT ON ALL TABLES IN SCHEMA sales TO analyst; -- ============================================-- Default Privileges for Future Objects-- ============================================-- Grant permissions that apply to objects created in the futureALTER DEFAULT PRIVILEGES IN SCHEMA sales GRANT SELECT ON TABLES TO readonly_role; -- Now any new table created in 'sales' automatically grants SELECT to readonly_role -- Default privileges for other object typesALTER DEFAULT PRIVILEGES IN SCHEMA sales GRANT EXECUTE ON FUNCTIONS TO app_role; ALTER DEFAULT PRIVILEGES IN SCHEMA sales GRANT USAGE ON SEQUENCES TO app_role; -- ============================================-- Creating Objects in Schema-- ============================================-- Allow a role to create objects in a schemaGRANT CREATE ON SCHEMA development TO developer_role; -- Developer can now create tables, views, etc. in 'development' -- ============================================-- Revoking Permissions-- ============================================-- Remove schema accessREVOKE USAGE ON SCHEMA sales FROM former_employee; -- Remove all privileges on all objectsREVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA sales FROM former_employee; -- Remove default privileges (for future objects)ALTER DEFAULT PRIVILEGES IN SCHEMA sales REVOKE SELECT ON TABLES FROM former_employee; -- ============================================-- Schema Ownership and ALL Privileges-- ============================================-- Schema owner has implicit ALL privileges-- Transfer ownership to grant full controlALTER SCHEMA hr OWNER TO hr_admin; -- hr_admin now controls all aspects of 'hr' schema -- ============================================-- Common Permission Patterns-- ============================================-- Pattern 1: Read-only access for analystsCREATE ROLE analyst_role;GRANT USAGE ON SCHEMA reporting TO analyst_role;GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO analyst_role;ALTER DEFAULT PRIVILEGES IN SCHEMA reporting GRANT SELECT ON TABLES TO analyst_role; -- Pattern 2: Full access for applicationCREATE ROLE app_role;GRANT USAGE ON SCHEMA app TO app_role;GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA app TO app_role;GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_role;ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT ALL ON TABLES TO app_role; -- Pattern 3: Schema ownership delegationCREATE ROLE sales_team;CREATE SCHEMA sales_data AUTHORIZATION sales_team;-- sales_team owns and manages everything in sales_data -- ============================================-- Checking Schema Permissions-- ============================================-- PostgreSQL: Check user's schema privilegesSELECT nspname AS schema_name, pg_catalog.has_schema_privilege(current_user, nspname, 'USAGE') AS has_usage, pg_catalog.has_schema_privilege(current_user, nspname, 'CREATE') AS has_createFROM pg_namespaceWHERE nspname NOT LIKE 'pg_%'; -- Check which roles have access to a schemaSELECT grantee, privilege_typeFROM information_schema.role_usage_grantsWHERE object_schema = 'sales';Remember: schema USAGE is necessary but not sufficient. It's like needing a key to enter a building (USAGE) AND a key to a specific room (SELECT on table). Without USAGE, users can't even see that objects exist in the schema, providing an extra security layer.
Well-organized schemas make databases easier to understand, maintain, and secure. Here are proven patterns for schema organization in production environments:
sales, hr, finance, inventory. Each department owns its data.erp, crm, ecommerce, analytics. Prevents naming conflicts and enables independent access control.staging, production, archive schemas for data pipeline management. Staging receives raw imports, production holds verified data.public_api, internal, restricted schemas based on sensitivity. Simplifies security audits.tenant_acme, tenant_bigcorp. Enables tenant isolation with shared infrastructure.api_v1, api_v2. Maintain backward compatibility while evolving schema.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
-- ============================================-- Pattern 1: Business Domain Organization-- ============================================CREATE SCHEMA sales; -- Customer-facing sales dataCREATE SCHEMA inventory; -- Warehouse and stockCREATE SCHEMA finance; -- Accounting and billingCREATE SCHEMA hr; -- Employee data (restricted)CREATE SCHEMA shared; -- Cross-domain reference data -- Domain-specific tablesCREATE TABLE sales.customers (...);CREATE TABLE sales.orders (...);CREATE TABLE inventory.products (...);CREATE TABLE inventory.warehouses (...);CREATE TABLE finance.invoices (...);CREATE TABLE hr.employees (...);CREATE TABLE shared.countries (...);CREATE TABLE shared.currencies (...); -- ============================================-- Pattern 2: Layered Architecture (Data Warehouse)-- ============================================CREATE SCHEMA raw; -- Raw ingested data (exact copy from sources)CREATE SCHEMA staging; -- Cleaned, validated data in progressCREATE SCHEMA dimensional; -- Star schema facts and dimensionsCREATE SCHEMA reporting; -- Views optimized for BI toolsCREATE SCHEMA ml; -- Feature tables for machine learning -- Data flows through layers:-- raw → staging → dimensional → reporting-- ↘ ml -- ============================================-- Pattern 3: Multi-Tenant Isolation-- ============================================-- Template schema with base structureCREATE SCHEMA tenant_template;CREATE TABLE tenant_template.users (...);CREATE TABLE tenant_template.orders (...); -- Per-tenant schemas cloned from templateCREATE SCHEMA tenant_acme;CREATE TABLE tenant_acme.users (LIKE tenant_template.users INCLUDING ALL);CREATE TABLE tenant_acme.orders (LIKE tenant_template.orders INCLUDING ALL); -- Tenant isolation at connection level-- Each tenant's app configures: SET search_path = tenant_acme; -- ============================================-- Pattern 4: API Versioning-- ============================================CREATE SCHEMA api_v1; -- Legacy, deprecatedCREATE SCHEMA api_v2; -- Current stableCREATE SCHEMA api_v3; -- Beta/development -- Views expose different structures per versionCREATE VIEW api_v1.customers AS SELECT id, name, email FROM core.customers; -- Simple CREATE VIEW api_v2.customers AS SELECT id, name, email, phone, created_at FROM core.customers; -- Extended CREATE VIEW api_v3.customers AS SELECT id, full_name as name, email, phone_numbers, created_at, updated_at, status FROM core.customers; -- New structure -- ============================================-- Naming Conventions-- ============================================-- Good schema names:-- ✓ sales, hr, inventory (domain names)-- ✓ staging, production (lifecycle stages)-- ✓ tenant_001, tenant_acme (tenant identifiers)-- ✓ api_v1, api_v2 (versioned interfaces) -- Bad schema names:-- ✗ schema1, new_schema (meaningless)-- ✗ johns_stuff (personal, unclear)-- ✗ SALES, HR (case sensitivity issues)-- ✗ data-warehouse (hyphens cause quoting issues) -- ============================================-- Documentation via Comments-- ============================================COMMENT ON SCHEMA sales IS 'Customer-facing sales data including orders, customers, and transactions. Owner: Sales Engineering. Contact: sales-eng@company.com'; COMMENT ON SCHEMA hr IS 'RESTRICTED: Employee data, compensation, reviews. Access requires HR approval. Owner: HR Systems.';Your schema organization reflects (and enforces) your data architecture. Plan it intentionally during initial design. Changing schema organization later—moving tables between schemas, renaming schemas—is disruptive and requires updating all dependent code. Get it right early.
Schemas provide the organizational structure that transforms a flat collection of tables into a well-architected database. They enable namespace separation, security boundaries, and logical grouping. Let's consolidate the key concepts:
Module Complete:
You have now completed the Data Definition Language (DDL) module. You've mastered the foundational commands for defining and managing database structures:
These DDL commands form the structural foundation upon which all database applications are built. With this knowledge, you can design, implement, and maintain database schemas that are correct, performant, secure, and maintainable.
Congratulations! You have mastered Data Definition Language. You can create and modify tables, manage constraints, handle schema organization, and apply production-safe practices for schema evolution. This foundation enables you to design databases that serve applications efficiently and securely.