Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

PL/SQL & T-SQL (Oracle and SQL Server)  Module 1 – Start here: two engines, versions and a verified lab

The practice schema and dialect basics

Load the practice schema into Oracle and SQL Server, then predict where the dialects differ: empty strings, case, trailing spaces, division and dates.

  • Beginner
  • 10 minutes
  • Examples run with oracle-free 23.26.3.0.0, mssql 17.0.5005.3 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Load the practice schema of customers, accounts, ledger, stock and orders into both engines
  • Predict how Oracle and SQL Server differ on empty strings, case, trailing spaces, division and dates
  • Name every key, check and reference so that error messages stay the same from one rebuild to the next
  • Find each engine's documentation for an error number

Before you start

What you’ll learn · Before you start

What you will learn

  • Load the practice schema of customers, accounts, ledger, stock and orders into both engines
  • Predict how Oracle and SQL Server differ on empty strings, case, trailing spaces, division and dates
  • Name every key, check and reference so that error messages stay the same from one rebuild to the next
  • Find each engine's documentation for an error number

Before you start

On this page

7 / 2 is 3.5 on Oracle and 3 on SQL Server, and that is one of the milder surprises: an empty string, a capital letter or a trailing space can change which rows a query returns when code moves from one engine to the other. Every later lesson in this track runs on one small practice schema, so it pays to load it once and see these differences on data you know.

You will load five tables with named constraints into both engines, then work through the five differences that break moved code most often, and learn to find what an error number means in each engine’s documentation.

One practice schema, written for both engines

The schema is small on purpose: four customers, four accounts, five ledger rows, three stock items and three orders, all made up, with rupee amounts kept in exact decimal columns. Each key, check and reference has a name of its own, the habit the last section of this lesson explains. Step through the tables in the order the scripts load them:

Interactive · step by step The practice schema, one table at a time Step through the five tables in the order the scripts create and fill them, with the named constraints of each.
The practice schema, one table at a time, step 6 of 6. Showing customers (4 rows, customers_pk), accounts (4 rows, 2 checks), ledger (5 rows, amount <> 0), orders (3 rows, 2 references), stock (3 rows, 2 checks). In focus: customers, accounts, ledger, stock, orders.customer_idaccount_idcustomer_idskucustomers4 rows, customers_pkaccounts4 rows, 2 checksledger5 rows, amount <> 0orders3 rows, 2 referencesstock3 rows, 2 checks

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Customers come first. customers holds four made-up people from Pune, Kochi, Lucknow and Chennai. Its key, customers_pk, is an identity column, and customers_email_uq keeps two customers from sharing an e-mail address.
  2. Accounts belong to customers. Each account points at its customer through accounts_customer_fk. accounts_kind_ck allows only savings and current, and accounts_balance_ck refuses a negative balance.
  3. Every ledger row moves money. A ledger row records one movement of money on one account. ledger_amt_nonzero refuses an amount of 0, because a movement of nothing is always a mistake in the caller.
  4. Stock has a price and a count. stock is keyed by its SKU, a short text code. stock_price_ck wants a price above 0 and stock_on_hand_ck a count of 0 or more; the ghee is out of stock with 0 on hand.
  5. Orders join customer and item. An order points at a customer and at an item, through two foreign keys. orders_qty_ck wants at least one item, and orders_status_ck allows new, paid, shipped and cancelled.
  6. Parents are loaded first. A foreign key refuses a row whose parent does not exist yet, so both scripts fill customers, accounts and ledger, then stock and orders. They end by counting 4, 4, 5, 3 and 3 rows.

The two scripts drop the five tables if they exist, create them again, fill them with the same rows and end by counting them, so running one again always brings back these rows. Run the one for each engine in your lab, the Oracle one as the user lab in FREEPDB1 and the SQL Server one in the database lab:

The practice schema in each dialect

Oracle · schema/schema_oracle.sql

-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- MySmartCoPilot's practice schema: five tables whose keys, checks and references all carry names, loaded with
-- made-up data (no real people, phone numbers or account numbers). Amounts are rupees in NUMBER(14,2).
SET FEEDBACK OFF

-- Children first: drop the five tables if an earlier run left them, so the script can run again at any time.
DROP TABLE IF EXISTS orders PURGE;
DROP TABLE IF EXISTS ledger PURGE;
DROP TABLE IF EXISTS stock PURGE;
DROP TABLE IF EXISTS accounts PURGE;
DROP TABLE IF EXISTS customers PURGE;

CREATE TABLE customers (
  customer_id NUMBER(10) GENERATED ALWAYS AS IDENTITY CONSTRAINT customers_pk PRIMARY KEY,
  full_name   VARCHAR2(80)  NOT NULL,
  city        VARCHAR2(40)  NOT NULL,
  email       VARCHAR2(120) CONSTRAINT customers_email_uq UNIQUE
);

CREATE TABLE accounts (
  account_id  NUMBER(10) GENERATED ALWAYS AS IDENTITY CONSTRAINT accounts_pk PRIMARY KEY,
  customer_id NUMBER(10)    NOT NULL CONSTRAINT accounts_customer_fk REFERENCES customers (customer_id),
  kind        VARCHAR2(10)  NOT NULL CONSTRAINT accounts_kind_ck CHECK (kind IN ('savings', 'current')),
  balance     NUMBER(14, 2) DEFAULT 0 NOT NULL CONSTRAINT accounts_balance_ck CHECK (balance >= 0)
);

CREATE TABLE ledger (
  entry_id   NUMBER(10) GENERATED ALWAYS AS IDENTITY CONSTRAINT ledger_pk PRIMARY KEY,
  account_id NUMBER(10)    NOT NULL CONSTRAINT ledger_account_fk REFERENCES accounts (account_id),
  amount     NUMBER(14, 2) NOT NULL CONSTRAINT ledger_amt_nonzero CHECK (amount <> 0),
  posted_at  DATE          NOT NULL,
  memo       VARCHAR2(60)
);

CREATE TABLE stock (
  sku        VARCHAR2(12)  CONSTRAINT stock_pk PRIMARY KEY,
  item_name  VARCHAR2(60)  NOT NULL,
  unit_price NUMBER(10, 2) NOT NULL CONSTRAINT stock_price_ck CHECK (unit_price > 0),
  on_hand    NUMBER(6)     NOT NULL CONSTRAINT stock_on_hand_ck CHECK (on_hand >= 0)
);

CREATE TABLE orders (
  order_id    NUMBER(10) GENERATED ALWAYS AS IDENTITY CONSTRAINT orders_pk PRIMARY KEY,
  customer_id NUMBER(10)   NOT NULL CONSTRAINT orders_customer_fk REFERENCES customers (customer_id),
  sku         VARCHAR2(12) NOT NULL CONSTRAINT orders_sku_fk REFERENCES stock (sku),
  qty         NUMBER(4)    NOT NULL CONSTRAINT orders_qty_ck CHECK (qty > 0),
  ordered_on  DATE         NOT NULL,
  status      VARCHAR2(10) DEFAULT 'new' NOT NULL
              CONSTRAINT orders_status_ck CHECK (status IN ('new', 'paid', 'shipped', 'cancelled'))
);

INSERT INTO customers (full_name, city, email) VALUES ('Asha Rao', 'Pune', 'asha.rao@example.com');
INSERT INTO customers (full_name, city, email) VALUES ('Vikram Nair', 'Kochi', 'vikram.nair@example.com');
INSERT INTO customers (full_name, city, email) VALUES ('Farida Shaikh', 'Lucknow', NULL);
INSERT INTO customers (full_name, city, email) VALUES ('Meera Iyer', 'Chennai', 'meera.iyer@example.com');

INSERT INTO accounts (customer_id, kind, balance) VALUES (1, 'savings', 2500.00);
INSERT INTO accounts (customer_id, kind, balance) VALUES (1, 'current', 18000.00);
INSERT INTO accounts (customer_id, kind, balance) VALUES (2, 'savings', 760.25);
INSERT INTO accounts (customer_id, kind, balance) VALUES (4, 'savings', 41250.50);

INSERT INTO ledger (account_id, amount, posted_at, memo) VALUES (1, 2500.00, DATE '2026-03-02', 'opening deposit');
INSERT INTO ledger (account_id, amount, posted_at, memo) VALUES (2, 18000.00, DATE '2026-03-02', 'opening deposit');
INSERT INTO ledger (account_id, amount, posted_at, memo) VALUES (3, 1000.00, DATE '2026-03-05', 'opening deposit');
INSERT INTO ledger (account_id, amount, posted_at, memo) VALUES (3, -239.75, DATE '2026-03-09', 'card payment');
INSERT INTO ledger (account_id, amount, posted_at, memo) VALUES (4, 41250.50, DATE '2026-03-11', 'opening deposit');

INSERT INTO stock (sku, item_name, unit_price, on_hand) VALUES ('TEA-250', 'Assam tea 250 g', 180.00, 40);
INSERT INTO stock (sku, item_name, unit_price, on_hand) VALUES ('RICE-5K', 'Sona masoori rice 5 kg', 420.00, 25);
INSERT INTO stock (sku, item_name, unit_price, on_hand) VALUES ('GHEE-500', 'Ghee 500 g', 420.00, 0);

INSERT INTO orders (customer_id, sku, qty, ordered_on, status) VALUES (1, 'TEA-250', 2, DATE '2026-03-12', 'paid');
INSERT INTO orders (customer_id, sku, qty, ordered_on, status) VALUES (2, 'RICE-5K', 1, DATE '2026-03-12', 'new');
INSERT INTO orders (customer_id, sku, qty, ordered_on, status) VALUES (4, 'TEA-250', 5, DATE '2026-03-13', 'shipped');
COMMIT;

SET FEEDBACK ON
SELECT 'customers' AS table_name, COUNT(*) AS row_count FROM customers
UNION ALL SELECT 'accounts', COUNT(*) FROM accounts
UNION ALL SELECT 'ledger', COUNT(*) FROM ledger
UNION ALL SELECT 'stock', COUNT(*) FROM stock
UNION ALL SELECT 'orders', COUNT(*) FROM orders;

SQL Server · schema/schema_sqlserver.sql

-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab at compatibility level 170, default collation
-- SQL_Latin1_General_CP1_CI_AS. Client: sqlcmd. MySmartCoPilot's practice schema: five tables whose keys, checks and
-- references all carry names, loaded with made-up data. Amounts are rupees in decimal(14,2).
SET NOCOUNT ON;

-- Children first: drop the five tables if an earlier run left them, so the script can run again at any time.
DROP TABLE IF EXISTS dbo.orders, dbo.ledger, dbo.stock, dbo.accounts, dbo.customers;

CREATE TABLE dbo.customers (
  customer_id int IDENTITY(1,1) NOT NULL CONSTRAINT customers_pk PRIMARY KEY,
  full_name   nvarchar(80)  NOT NULL,
  city        nvarchar(40)  NOT NULL,
  email       nvarchar(120) NULL CONSTRAINT customers_email_uq UNIQUE
);

CREATE TABLE dbo.accounts (
  account_id  int IDENTITY(1,1) NOT NULL CONSTRAINT accounts_pk PRIMARY KEY,
  customer_id int           NOT NULL CONSTRAINT accounts_customer_fk REFERENCES dbo.customers (customer_id),
  kind        nvarchar(10)  NOT NULL CONSTRAINT accounts_kind_ck CHECK (kind IN (N'savings', N'current')),
  balance     decimal(14,2) NOT NULL CONSTRAINT accounts_balance_df DEFAULT 0
                                     CONSTRAINT accounts_balance_ck CHECK (balance >= 0)
);

CREATE TABLE dbo.ledger (
  entry_id   int IDENTITY(1,1) NOT NULL CONSTRAINT ledger_pk PRIMARY KEY,
  account_id int           NOT NULL CONSTRAINT ledger_account_fk REFERENCES dbo.accounts (account_id),
  amount     decimal(14,2) NOT NULL CONSTRAINT ledger_amt_nonzero CHECK (amount <> 0),
  posted_at  datetime2(0)  NOT NULL,
  memo       nvarchar(60)  NULL
);

CREATE TABLE dbo.stock (
  sku        varchar(12)   NOT NULL CONSTRAINT stock_pk PRIMARY KEY,
  item_name  nvarchar(60)  NOT NULL,
  unit_price decimal(10,2) NOT NULL CONSTRAINT stock_price_ck CHECK (unit_price > 0),
  on_hand    int           NOT NULL CONSTRAINT stock_on_hand_ck CHECK (on_hand >= 0)
);

CREATE TABLE dbo.orders (
  order_id    int IDENTITY(1,1) NOT NULL CONSTRAINT orders_pk PRIMARY KEY,
  customer_id int          NOT NULL CONSTRAINT orders_customer_fk REFERENCES dbo.customers (customer_id),
  sku         varchar(12)  NOT NULL CONSTRAINT orders_sku_fk REFERENCES dbo.stock (sku),
  qty         int          NOT NULL CONSTRAINT orders_qty_ck CHECK (qty > 0),
  ordered_on  date         NOT NULL,
  status      nvarchar(10) NOT NULL CONSTRAINT orders_status_df DEFAULT N'new'
                                    CONSTRAINT orders_status_ck CHECK (status IN (N'new', N'paid', N'shipped', N'cancelled'))
);

INSERT INTO dbo.customers (full_name, city, email) VALUES
  (N'Asha Rao', N'Pune', N'asha.rao@example.com'),
  (N'Vikram Nair', N'Kochi', N'vikram.nair@example.com'),
  (N'Farida Shaikh', N'Lucknow', NULL),
  (N'Meera Iyer', N'Chennai', N'meera.iyer@example.com');

INSERT INTO dbo.accounts (customer_id, kind, balance) VALUES
  (1, N'savings', 2500.00), (1, N'current', 18000.00), (2, N'savings', 760.25), (4, N'savings', 41250.50);

INSERT INTO dbo.ledger (account_id, amount, posted_at, memo) VALUES
  (1, 2500.00, '2026-03-02', N'opening deposit'),
  (2, 18000.00, '2026-03-02', N'opening deposit'),
  (3, 1000.00, '2026-03-05', N'opening deposit'),
  (3, -239.75, '2026-03-09', N'card payment'),
  (4, 41250.50, '2026-03-11', N'opening deposit');

INSERT INTO dbo.stock (sku, item_name, unit_price, on_hand) VALUES
  ('TEA-250', N'Assam tea 250 g', 180.00, 40),
  ('RICE-5K', N'Sona masoori rice 5 kg', 420.00, 25),
  ('GHEE-500', N'Ghee 500 g', 420.00, 0);

INSERT INTO dbo.orders (customer_id, sku, qty, ordered_on, status) VALUES
  (1, 'TEA-250', 2, '2026-03-12', N'paid'),
  (2, 'RICE-5K', 1, '2026-03-12', N'new'),
  (4, 'TEA-250', 5, '2026-03-13', N'shipped');

SELECT 'customers' AS table_name, COUNT(*) AS row_count FROM dbo.customers
UNION ALL SELECT 'accounts', COUNT(*) FROM dbo.accounts
UNION ALL SELECT 'ledger', COUNT(*) FROM dbo.ledger
UNION ALL SELECT 'stock', COUNT(*) FROM dbo.stock
UNION ALL SELECT 'orders', COUNT(*) FROM dbo.orders;
GO
The practice schema, loaded in each lab

Oracle · schema_oracle_session.py

"""schema_oracle.sql in the lab: SQL*Plus runs it as LAB, with nothing in its schema yet."""

from oracle_session import Lab

with Lab(schema=False) as lab:
    lab.sqlplus("schema/schema_oracle.sql")

Output

$ sqlplus -s lab@//localhost/FREEPDB1 @schema/schema_oracle.sql
TABLE_NAM  ROW_COUNT
--------- ----------
customers          4
accounts           4
ledger             5
stock              3
orders             3

5 rows selected.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 schema_oracle_session.py

SQL Server · schema_sqlserver_session.py

"""schema_sqlserver.sql in the lab: sqlcmd runs it as sa in the empty database lab."""

from mssql_session import Lab

with Lab(schema=False) as lab:
    lab.sqlcmd("schema/schema_sqlserver.sql")

Output

$ sqlcmd -S localhost -U sa -C -d lab -i schema/schema_sqlserver.sql
table_name row_count
---------- ---------
customers          4
accounts           4
ledger             5
stock              3
orders             3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 schema_sqlserver_session.py

Most lines read the same in both. The differences are the ones you will meet in every lesson: an identity column: A numeric key column whose values the database assigns to new rows by itself, 1, 2, 3 and so on: GENERATED ... AS IDENTITY in Oracle, IDENTITY(seed, increment) in SQL Server. is GENERATED ALWAYS AS IDENTITY in Oracle and IDENTITY(1,1) in SQL Server, text is VARCHAR2 against nvarchar, money is NUMBER(14, 2) against decimal(14,2), and a defaulted column in SQL Server can name its default too. One rule hides in the customers table: SQL Server allows only one NULL in a column with a UNIQUE constraint, while Oracle allows any number, so a second customer without an e-mail address would load into Oracle and fail on SQL Server.

Make more rows when you need them

When a later exercise needs a bigger table, generate made-up rows with the mock data generator and turn a CSV file into INSERT statements with the CSV to SQL converter, rather than copying real customer data.

An empty string is NULL in Oracle and a value in SQL Server

Oracle’s SQL reference is plain about it: for now, a character value of length zero is a null in Oracle. So in Oracle LENGTH('') is NULL, not 0, '' behaves as NULL in every comparison, and COALESCE(NULL, '', 'x') skips the empty string and returns 'x'. Oracle adds that this may change in a future release and recommends not treating empty strings as nulls in your own logic. SQL Server keeps the two apart: LEN(N'') is 0, N'' is not NULL, and the same COALESCE returns the empty string. Concatenation follows from it. Oracle’s 'a' || NULL is 'a', because a null joined to text gives the text back, while SQL Server’s 'a' + NULL is NULL under its default session settings.

Expression Oracle SQL Server
Length of an empty string LENGTH('') is NULL LEN(N'') is 0
Is the empty string NULL? Yes No
COALESCE(NULL, '', 'x') 'x' the empty string

These two scripts ask the same six questions of each engine, starting with this one; the rest of the lesson goes through their answers:

Six questions with different answers in each dialect

Oracle · traps/traps_oracle.sql

-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1, after schema_oracle.sql.
-- Client: SQL*Plus or SQLcl. Six questions whose answers differ between Oracle and SQL Server.
SET SERVEROUTPUT ON
SET NULL '(null)'

-- 1. An empty string
SELECT LENGTH('') AS len_empty,
       NVL2('', 'not null', 'null') AS empty_is,
       COALESCE(NULL, '', 'x') AS first_non_null
  FROM dual;

-- 2. Upper and lower case
SELECT CASE WHEN 'abc' = 'ABC' THEN 'equal' ELSE 'different' END AS case_test FROM dual;

-- 3. Trailing spaces: two literals, then VARCHAR2 and CHAR variables
SELECT CASE WHEN 'a' = 'a   ' THEN 'equal' ELSE 'different' END AS literals FROM dual;
DECLARE
  v1 VARCHAR2(10) := 'a';
  v2 VARCHAR2(10) := 'a   ';
  c1 CHAR(4)      := 'a';
  c2 CHAR(4)      := 'a   ';
BEGIN
  DBMS_OUTPUT.PUT_LINE('VARCHAR2: ' || CASE WHEN v1 = v2 THEN 'equal' ELSE 'different' END);
  DBMS_OUTPUT.PUT_LINE('CHAR:     ' || CASE WHEN c1 = c2 THEN 'equal' ELSE 'different' END);
END;
/

-- 4. Dividing two whole numbers
SELECT 7 / 2 AS seven_by_two, TRUNC(7 / 2) AS truncated FROM dual;

-- 5. A date plus one twenty-fourth
SELECT TO_CHAR(DATE '2026-03-15' + 1 / 24, 'YYYY-MM-DD HH24:MI:SS') AS plus_one_hour FROM dual;

-- 6. The most expensive item, with ties
SELECT sku, unit_price FROM stock ORDER BY unit_price DESC FETCH FIRST 1 ROW WITH TIES;

SQL Server · traps/traps_sqlserver.sql

-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab (compatibility level 170, collation
-- SQL_Latin1_General_CP1_CI_AS), after schema_sqlserver.sql. Client: sqlcmd. The same six questions as the Oracle
-- script, in T-SQL.
SET NOCOUNT ON;

-- 1. An empty string
SELECT LEN(N'') AS len_empty,
       CASE WHEN N'' IS NULL THEN N'null' ELSE N'not null' END AS empty_is,
       DATALENGTH(COALESCE(NULL, N'', N'x')) AS bytes_of_first_non_null;

-- 2. Upper and lower case, under the database's default collation
SELECT CASE WHEN 'abc' = 'ABC' THEN 'equal' ELSE 'different' END AS case_test;

-- 3. Trailing spaces: two literals, then two varchar variables
SELECT CASE WHEN 'a' = 'a   ' THEN 'equal' ELSE 'different' END AS literals;
DECLARE @v1 varchar(10) = 'a', @v2 varchar(10) = 'a   ';
SELECT CASE WHEN @v1 = @v2 THEN 'equal' ELSE 'different' END AS varchar_test,
       DATALENGTH(@v2) AS bytes_in_v2;

-- 4. Dividing two whole numbers
SELECT 7 / 2 AS seven_by_two, 7 / 2.0 AS with_a_decimal;

-- 5. A date plus one hour
SELECT DATEADD(hour, 1, CAST('2026-03-15' AS datetime2(0))) AS plus_one_hour;

-- 6. The most expensive item, with ties
SELECT TOP (1) WITH TIES sku, unit_price FROM dbo.stock ORDER BY unit_price DESC;
GO
The six questions, asked in each lab

Oracle · traps_oracle_session.py

"""traps_oracle.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""

from oracle_session import Lab

with Lab() as lab:
    lab.sqlplus("traps/traps_oracle.sql")

Output

$ sqlplus -s lab@//localhost/FREEPDB1 @traps/traps_oracle.sql
 LEN_EMPTY EMPT F
---------- ---- -
(null)     null x


CASE_TEST
---------
different


LITERALS
---------
different

VARCHAR2: different
CHAR:     equal

PL/SQL procedure successfully completed.


SEVEN_BY_TWO  TRUNCATED
------------ ----------
         3.5          3


PLUS_ONE_HOUR
-------------------
2026-03-15 01:00:00


SKU          UNIT_PRICE
------------ ----------
RICE-5K             420
GHEE-500            420

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 traps_oracle_session.py

SQL Server · traps_sqlserver_session.py

"""traps_sqlserver.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice schema."""

from mssql_session import Lab

with Lab() as lab:
    lab.sqlcmd("traps/traps_sqlserver.sql")

Output

$ sqlcmd -S localhost -U sa -C -d lab -i traps/traps_sqlserver.sql
len_empty empty_is bytes_of_first_non_null
--------- -------- -----------------------
        0 not null                       0
case_test
---------
equal
literals
--------
equal
varchar_test bytes_in_v2
------------ -----------
equal                  4
seven_by_two with_a_decimal
------------ --------------
           3       3.500000
plus_one_hour
-------------------
2026-03-15 01:00:00
sku      unit_price
-------- ----------
GHEE-500     420.00
RICE-5K      420.00

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 traps_sqlserver_session.py

Storing an empty string to mean no value

Wrong: WHERE email = '' to find customers without an e-mail address, written on SQL Server and moved to Oracle, where '' is NULL and the condition is never true. Right: store NULL for no value and test email IS NULL, which works the same in both engines.

Checkpoint

Check: The practice schema · Empty strings and NULL

  1. Question 1 of 3 The orders table has foreign keys to customers and to stock. Why do the scripts fill orders last?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: A foreign key refuses a row whose parent row does not exist yet

    Load parents before children: customers, then accounts and ledger, then stock, then orders. Deleting goes the other way round.

  2. Question 2 of 3 What does LENGTH('') return in Oracle?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: NULL

    In Oracle '' is NULL. Functions of NULL usually return NULL, so LENGTH('') is NULL and NVL2('', …) takes its NULL branch.

  3. Question 3 of 3 A report written on SQL Server finds customers without an e-mail address with WHERE email = ''. On Oracle it returns no rows. Which condition works in both engines?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: WHERE email IS NULL, with NULL stored for no address

    In Oracle email = '' compares with NULL, which is never true. Store NULL for a missing value and test it with IS NULL; that behaves the same on both engines.

Case and trailing spaces decide what is equal

'abc' = 'ABC' is false in Oracle and true in SQL Server’s lab. Oracle compares text with binary collation by default, byte by byte, so case matters; its NLS_COMP parameter defaults to BINARY. SQL Server compares with the column’s or database’s collation: The rules a database uses to compare and sort text, such as whether case and accents matter; SQL Server's container default SQL_Latin1_General_CP1_CI_AS ignores case., and the container’s default, SQL_Latin1_General_CP1_CI_AS, is case-insensitive (CI) and accent-sensitive (AS). A login check written on SQL Server that compares user names with = therefore stops matching mixed-case names on Oracle.

Trailing spaces follow their own rules. SQL Server follows the ANSI SQL-92 rule and pads the shorter string with spaces before an = comparison, so 'a' = 'a ' is true even between varchar values. Oracle uses a blank-padded comparison: A text comparison that first pads the shorter value with spaces, so values that differ only in trailing spaces are equal. Oracle uses it only for CHAR, NCHAR and literals; SQL Server for every = comparison. only when both sides are CHAR, NCHAR or text literals; as soon as one side is VARCHAR2, the comparison is nonpadded and 'a' and 'a ' differ:

Comparison Oracle SQL Server (CI_AS)
'abc' = 'ABC' different equal
Two literals, 'a' = 'a ' equal in a WHERE clause, different in the script’s CASE equal
Two VARCHAR2 or varchar values, 'a' and 'a ' different equal
Two CHAR(4) values, 'a' and 'a ' equal equal

Oracle’s documentation counts text literals among the blank-padded cases, and the lab’s 26ai keeps to that in a WHERE clause, yet the same two literals printed different inside the trap script’s CASE expression. Code that has to behave the same on both engines therefore never relies on padding between literals.

Key idea

Before you trust an equality on text, know three things about each engine: whether an empty string is NULL, whether case matters under its collation, and whether trailing spaces are padded away.

Division and dates do not mean the same in both

7 / 2 is the classic. Microsoft documents that when an integer is divided by an integer in T-SQL, the result is an integer with the fraction truncated, so 7 / 2 is 3, and 7 / 2.0 is 3.500000 because one side is a decimal. Oracle does arithmetic on its NUMBER type, so 7 / 2 is 3.5, and you write TRUNC(7 / 2) when you want 3. A rate or an average moved from Oracle to SQL Server can lose its fraction without any error.

Dates differ in what they hold. An Oracle DATE stores a year, month, day, hour, minute and second, always, and Oracle treats a number added to a date as a number of days: adding 1 gives the same time tomorrow and adding 1 / 24 adds one hour. SQL Server keeps separate types: date for a day alone, datetime2(n) for a date and time with n digits of fractions of a second, from 0 to 7, and the older datetime, which rounds its time to increments of .000, .003 or .007 seconds. To add an hour there you call DATEADD(hour, 1, …).

Question Oracle SQL Server
7 / 2 3.5 3
A day without a time DATE (its time is midnight) date
Add one hour + 1 / 24 DATEADD(hour, 1, …)
Fractions of a second TIMESTAMP datetime2(n)

Comparing an Oracle DATE with a day

Wrong: WHERE posted_at = DATE '…' for every entry of a day, when the column holds times. Right: a range, posted_at >= DATE '…' AND posted_at < DATE '…' + 1, which finds every time on that day and still uses an index.

Checkpoint

Check: Case and trailing spaces · Division and dates

  1. Question 1 of 3 A login check compares user names with WHERE user_name = 'Asha'. Which rows match a stored name 'ASHA' on each engine with its default settings?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: SQL Server matches it, Oracle does not

    Equality on text depends on the collation. Decide explicitly, for example by storing names in one case, rather than relying on a default that differs between engines.

  2. Question 2 of 3 What does SELECT 7 / 2 return on SQL Server?

    Type a number.

    Show the answer: Question 2 of 3

    Answer: 3

    Both operands are integers, and T-SQL then truncates the fraction, so 7 / 2 is 3. Oracle's NUMBER arithmetic gives 3.5, and 7 / 2.0 gives 3.500000 on SQL Server.

  3. Question 3 of 3 In Oracle, a DATE holds 09:00 on some day. What does adding 1 / 24 to it give?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: 10:00 on the same day

    Oracle date arithmetic counts in days: + 1 is a day, + 1 / 24 an hour and + 10 / 1440 ten minutes. SQL Server uses DATEADD with a named unit instead.

Both engines limit rows with OFFSET and FETCH

Taking “the first n rows” is a daily task, and here the two engines meet halfway. Both accept the standard form, ORDER BY … OFFSET m ROWS FETCH NEXT n ROWS ONLY, which skips m rows and returns the next n; SQL Server requires the ORDER BY. Each also has its own form for the same question. SQL Server’s TOP (n) goes after SELECT, and Oracle writes FETCH FIRST n ROWS ONLY without an offset. Both can include ties: TOP (1) WITH TIES and FETCH FIRST 1 ROW WITH TIES return every row whose sort key equals that of the last row taken.

Task Oracle SQL Server
The first n rows FETCH FIRST n ROWS ONLY TOP (n)
Skip m rows, then take n OFFSET m ROWS FETCH NEXT n ROWS ONLY the same, after ORDER BY
The first n rows and their ties FETCH FIRST n ROWS WITH TIES TOP (n) WITH TIES

The sixth question of the trap scripts uses the last row of that table. The rice and the ghee both cost 420.00, the highest price in stock, so the most expensive item with ties is two rows in both engines, RICE-5K and GHEE-500, while FETCH FIRST 1 ROW ONLY or TOP (1) alone would return one of them, and which one is not defined.

Always sort before you limit

A row limit without ORDER BY returns whichever rows the engine reaches first, which can change after an index or a statistics update. Name the sort, and add a unique column such as the key when ties must not decide.

Named constraints keep error messages stable

When you leave out a constraint’s name, each engine makes one up: Oracle documents names of the form SYS_Cn, and SQL Server assigns a system-generated name. Those names change every time the schema is rebuilt, and Microsoft notes that the constraint name appears in every error message about a violation, so a test or a log search that looks for it breaks with each rebuild. Named constraints, such as ledger_amt_nonzero, give the same message every time. These two scripts break two of them on purpose:

Two inserts that break named constraints

Oracle · errors/errors_oracle.sql

-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1, after schema_oracle.sql.
-- Client: SQL*Plus or SQLcl, which goes on after an error. Two inserts that break named constraints: the error
-- messages name LEDGER_AMT_NONZERO and CUSTOMERS_EMAIL_UQ, the same on every rebuild of the schema.
INSERT INTO ledger (account_id, amount, posted_at, memo) VALUES (1, 0, DATE '2026-03-15', 'zero entry');

INSERT INTO customers (full_name, city, email) VALUES ('Asha R.', 'Pune', 'asha.rao@example.com');

ROLLBACK;

SQL Server · errors/errors_sqlserver.sql

-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab, after schema_sqlserver.sql. Client: sqlcmd without -b,
-- which goes on after an error. Two inserts that break named constraints: the messages name ledger_amt_nonzero and
-- customers_email_uq, the same on every rebuild of the schema.
SET NOCOUNT ON;

INSERT INTO dbo.ledger (account_id, amount, posted_at, memo) VALUES (1, 0, '2026-03-15', N'zero entry');

INSERT INTO dbo.customers (full_name, city, email) VALUES (N'Asha R.', N'Pune', N'asha.rao@example.com');
GO
The two refused inserts, run in each lab

Oracle · errors_oracle_session.py

"""errors_oracle.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""

from oracle_session import Lab

with Lab() as lab:
    lab.sqlplus("errors/errors_oracle.sql", errors=2)

Output

$ sqlplus -s lab@//localhost/FREEPDB1 @errors/errors_oracle.sql
INSERT INTO ledger (account_id, amount, posted_at, memo) VALUES (1, 0, DATE '2026-03-15', 'zero entry')
*
ERROR at line 1:
ORA-02290: check constraint (LAB.LEDGER_AMT_NONZERO) violated
Help: https://docs.oracle.com/error-help/db/ora-02290/


INSERT INTO customers (full_name, city, email) VALUES ('Asha R.', 'Pune', 'asha.rao@example.com')
*
ERROR at line 1:
ORA-00001: unique constraint (LAB.CUSTOMERS_EMAIL_UQ) violated on table
LAB.CUSTOMERS columns (EMAIL)
ORA-03301: (ORA-00001 details) row with column values
(EMAIL:'asha.rao@example.com') already exists
Help: https://docs.oracle.com/error-help/db/ora-00001/



Rollback complete.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_oracle_session.py

SQL Server · errors_sqlserver_session.py

"""errors_sqlserver.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice schema."""

from mssql_session import Lab

with Lab() as lab:
    lab.sqlcmd("errors/errors_sqlserver.sql", errors=2)

Output

$ sqlcmd -S localhost -U sa -C -d lab -i errors/errors_sqlserver.sql
Msg 547, Level 16, State 1, Server lab-mssql, Line 6
The INSERT statement conflicted with the CHECK constraint "ledger_amt_nonzero". The conflict occurred in database "lab", table "dbo.ledger", column 'amount'.
The statement has been terminated.
Msg 2627, Level 14, State 1, Server lab-mssql, Line 8
Violation of UNIQUE KEY constraint 'customers_email_uq'. Cannot insert duplicate key in object 'dbo.customers'. The duplicate key value is (asha.rao@example.com).
The statement has been terminated.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_sqlserver_session.py

The two engines report the same failures in different shapes:

Failure Oracle SQL Server
A check fails ORA-02290, check constraint (owner.name) violated Msg 547, level 16: the statement conflicted with the CHECK constraint
A unique key repeats ORA-00001, unique constraint violated, with the table and columns Msg 2627, level 14: violation of a UNIQUE KEY constraint
Where the number is explained docs.oracle.com/error-help/db/ora-02290/, one page per error Microsoft’s lists of database engine errors, by number

An Oracle error is ORA- and five digits, and each has a help page with its cause and action at the address shown, with the number in lower case. A SQL Server error carries a number, a severity level from 0 to 25 and a state; the number finds it in Microsoft’s lists, which are split into ranges such as 0 to 999 and 2000 to 2999.

Checkpoint

Check: Limiting rows · Named constraints and error numbers

  1. Question 1 of 2 Two items share the highest price, 420.00. What does SELECT TOP (1) WITH TIES sku FROM dbo.stock ORDER BY unit_price DESC return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Both items, RICE-5K and GHEE-500

    FETCH FIRST 1 ROW WITH TIES in Oracle returns the same two rows. Without WITH TIES, which of the tied rows comes back is not defined.

  2. Question 2 of 2 A test checks that a zero ledger entry fails with a message naming SYS_C008715. After the schema is rebuilt, the test fails although the check still works. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The constraint had no name, so Oracle made up a new SYS_Cn name on the rebuild

    Name the constraint, CONSTRAINT ledger_amt_nonzero CHECK (amount <> 0), and the message is the same on every rebuild, in Oracle and in SQL Server.

Practise: predict each engine’s answer

Each question shows one snippet and asks what Oracle or SQL Server makes of it. Answer from the rules of the sections above, then read why each choice is right or wrong:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 How does SQL Server write the identity column that Oracle writes as GENERATED ALWAYS AS IDENTITY?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: int IDENTITY(1,1)

    The two scripts differ in such small spellings: GENERATED ALWAYS AS IDENTITY against IDENTITY(1,1), VARCHAR2 against nvarchar, NUMBER against decimal.

  2. Question 2 of 12 A fifth customer without an e-mail address is added, so two rows have a NULL email under customers_email_uq. What happens in each engine?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: Oracle accepts it; SQL Server refuses it, because its UNIQUE constraints allow only one NULL

    The same constraint means two different things for missing values. Test every UNIQUE rule on a nullable column in both engines before data moves between them.

  3. Question 3 of 12 What do Oracle's 'Mr ' || NULL and SQL Server's 'Mr ' + NULL return, with default settings?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: Oracle returns 'Mr ', SQL Server returns NULL

    Labels built from optional columns, such as a title and a name, come out different in the two engines, so test them with a NULL in the data.

  4. Question 4 of 12 The column full_name is NOT NULL in both schemas. You insert an empty string into it. What happens?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Oracle refuses it, because '' is NULL; SQL Server stores a zero-length name

    To refuse empty names on SQL Server too, add a check such as CHECK (LEN(full_name) > 0), and give it a name.

  5. Question 5 of 12 What does CI mean in the collation name SQL_Latin1_General_CP1_CI_AS?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: Case-insensitive

    Collation names list their rules: CI or CS for case, AI or AS for accents. The container's default is CI_AS.

  6. Question 6 of 12 A PL/SQL procedure compares a VARCHAR2 parameter with a VARCHAR2 column holding 'TEA-250 ' with three trailing spaces. The parameter is 'TEA-250'. Are they equal in Oracle, and would they be in SQL Server?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Not in Oracle, where VARCHAR2 comparisons are nonpadded; yes in SQL Server, which pads before =

    Trailing spaces in VARCHAR2 data are a classic porting bug. Trim on input, or compare with RTRIM on both sides while you clean the data.

  7. Question 7 of 12 A T-SQL report computes paid_orders / total_orders for 1 paid order out of 3, both int columns. What does it show, and how do you fix it?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: 0; multiply by 1.0 or cast one side to decimal first

    Oracle would show 0.333…, so the same report ported from Oracle loses its fraction silently. Written as 1.0 * paid_orders / total_orders, the division has a decimal operand and keeps the fraction on SQL Server too.

  8. Question 8 of 12 To what does SQL Server's legacy datetime type round the time of day?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Increments of .000, .003 or .007 seconds

    New code uses datetime2(n), with the number of fractional digits stated, or date when there is no time.

  9. Question 9 of 12 Which row-limiting form do both engines accept?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: ORDER BY … OFFSET m ROWS FETCH NEXT n ROWS ONLY

    Use the standard form in code that should read the same on both engines; SQL Server needs the ORDER BY for it.

  10. Question 10 of 12 A nightly job takes the "first 100 orders" with FETCH FIRST 100 ROWS ONLY and no ORDER BY. It processed different orders after an index was added. What went wrong?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: Without ORDER BY the engine returns whichever rows it reaches first, which a new index can change

    Sort by a column that matches the meaning ("first" by order_id or ordered_on), and add the key when ties are possible, so the job takes the same rows every time.

  11. Question 11 of 12 Where does Oracle explain the cause and action of ORA-02290?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: On its error help page, docs.oracle.com/error-help/db/ora-02290/

    ORA-02290 is check constraint violated. SQL Server's numbers, such as 547 for a failed check, are in Microsoft's lists of database engine errors, grouped in ranges.

  12. Question 12 of 12 SQL Server reports "Msg 2627, Level 14, State 1" for a repeated e-mail address. What is the 14?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: The severity level of the error

    Microsoft documents error 2627 at severity 14, a violation of a PRIMARY KEY or UNIQUE KEY constraint. The number finds the message in Microsoft's lists, and the severity says how serious it is.

Interview questions

Warm-up (fresher to 2–5 years): how do Oracle and SQL Server treat an empty string?

Oracle treats a character value of length zero as NULL. LENGTH('') is NULL, '' IS NULL is true, a NOT NULL column rejects '', and COALESCE(NULL, '', 'x') returns 'x'. Oracle’s documentation adds that this may change in a future release and advises against relying on it. SQL Server keeps the empty string as a real value of length zero: LEN(N'') is 0, it is not NULL, and a NOT NULL column accepts it. Concatenation differs too: Oracle’s 'a' || NULL gives 'a', while SQL Server’s 'a' + NULL gives NULL by default. Code that moves between the engines should store NULL for “no value” and test it with IS NULL, never compare with ''.

Two longer questions about moving code and about constraint names follow; work out your answer before you open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A customer search ported from SQL Server to Oracle no longer finds customers without a phone number, and a load that worked on SQL Server now fails on a NOT NULL column. What changed, and how do you fix the code and the data?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to senior

    A CI job compares the error messages of a schema's tests with recorded ones, and the comparison breaks on every fresh build: one run says SYS_C008715, the next SYS_C008731. What is happening, and what would you change?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is an empty string in each engine?

    Oracle's SQL reference says a character value of length zero is a null, so LENGTH('') is NULL and '' behaves as NULL in every comparison. SQL Server keeps it as a real value: LEN(N'') is 0 and N'' is not NULL. Store NULL for no value and test IS NULL.

  2. What does joining text to NULL give in each engine?

    Oracle's 'a' || NULL is 'a', because a null joined to text gives the text back. SQL Server's 'a' + NULL is NULL under its default session settings.

  3. Is 'abc' = 'ABC' true, and is 'a' = 'a ' true?

    Oracle compares text with binary collation by default, so 'abc' and 'ABC' differ; the lab's SQL Server collation SQL_Latin1_General_CP1_CI_AS is case-insensitive, so they are equal. SQL Server pads trailing spaces for =, while Oracle pads only CHAR values and literals, so two VARCHAR2 values with different trailing spaces differ.

  4. What is 7 / 2 in T-SQL and in Oracle?

    3 in T-SQL, because Microsoft documents integer division as truncating the fraction, and 3.5 in Oracle, which does the arithmetic on its NUMBER type. 7 / 2.0 is 3.500000 in T-SQL, and TRUNC(7 / 2) gives 3 in Oracle. A rate moved from Oracle can lose its fraction with no error.

  5. How do the two engines hold dates and add an hour?

    An Oracle DATE always holds a year, month, day, hour, minute and second, and a number added to it is a number of days, so + 1 / 24 adds an hour. SQL Server separates date, datetime2(n) with 0 to 7 fractional digits and the older datetime, and adds an hour with DATEADD(hour, 1, ...).

  6. How does each engine take the first n rows, with ties?

    Both accept the standard ORDER BY ... OFFSET m ROWS FETCH NEXT n ROWS ONLY, and SQL Server requires the ORDER BY. SQL Server also has TOP (n) after SELECT, Oracle FETCH FIRST n ROWS ONLY, and both add WITH TIES to keep every row whose sort key equals the last one taken.

  7. Why does every key, check and reference in the practice schema have a name of its own?

    Without one each engine makes a name up (Oracle's SYS_Cn form, SQL Server a system-generated name) that changes with every rebuild, and Microsoft notes the constraint name appears in every violation message. A named constraint gives the same message each time, so tests and log searches keep working.

  8. Where do you look up an error number in each engine?

    An Oracle error is ORA- and five digits, and each has a help page with its cause and action at docs.oracle.com/error-help/db/, with the number in lower case. A SQL Server error carries a number, a severity from 0 to 25 and a state; the number finds it in Microsoft's lists of database engine errors, split into ranges.

Key takeaways

  • The practice schema has five tables with named keys, checks and references, and the same rows in both engines, loaded parents first.
  • Oracle treats an empty string as NULL, while SQL Server keeps it as a value of length zero, so store NULL for “no value” and test it with IS NULL.
  • Oracle compares text by binary collation, case-sensitively, while SQL Server’s default CI_AS collation ignores case; SQL Server pads trailing spaces for =, and Oracle pads only CHAR values and literals.
  • 7 / 2 is 3 in T-SQL and 3.5 in Oracle, and an Oracle DATE always holds a time, where SQL Server separates date, datetime2 and datetime.
  • Both engines accept OFFSET … FETCH, SQL Server adds TOP (n), and both offer WITH TIES; sort before you limit.
  • Named constraints keep error messages stable, and ORA- numbers have help pages while SQL Server errors carry a number, a severity and a state.

Terms in this lesson

blank-padded comparison
A text comparison that first pads the shorter value with spaces, so values that differ only in trailing spaces are equal. Oracle uses it only for CHAR, NCHAR and literals; SQL Server for every = comparison.
collation
The rules a database uses to compare and sort text, such as whether case and accents matter; SQL Server's container default SQL_Latin1_General_CP1_CI_AS ignores case.
identity column
A numeric key column whose values the database assigns to new rows by itself, 1, 2, 3 and so on: GENERATED ... AS IDENTITY in Oracle, IDENTITY(seed, increment) in SQL Server.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.