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.
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
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 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:
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 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
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
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:
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 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
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.
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.
-
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?
-
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?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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, ...).
-
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.
-
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.
-
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_AScollation ignores case; SQL Server pads trailing spaces for=, and Oracle pads onlyCHARvalues and literals. 7 / 2is 3 in T-SQL and 3.5 in Oracle, and an OracleDATEalways holds a time, where SQL Server separatesdate,datetime2anddatetime.- Both engines accept
OFFSET … FETCH, SQL Server addsTOP (n), and both offerWITH 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
- SQL Language Reference 26ai: Nulls (Oracle)
- SQL Language Reference 26ai: Concatenation Operator (Oracle)
- SQL Language Reference 26ai: Data Type Comparison Rules (Oracle)
- SQL Language Reference 26ai: Data Types (Oracle)
- SQL Language Reference 26ai: SELECT (Oracle)
- SQL Language Reference 26ai: constraint (Oracle)
- NLS_COMP (Oracle)
- ORA-00001: unique constraint violated (Oracle)
- ORA-02290: check constraint violated (Oracle)
- + (String concatenation) (Transact-SQL) (Microsoft)
- Database engine error severities (Microsoft)
- LEN (Transact-SQL) (Microsoft)
- Collation and Unicode support (Microsoft)
- = (String comparison or assignment) (Microsoft)
- / (Division) (Transact-SQL) (Microsoft)
- datetime (Transact-SQL) (Microsoft)
- datetime2 (Transact-SQL) (Microsoft)
- TOP (Transact-SQL) (Microsoft)
- ORDER BY clause (Transact-SQL) (Microsoft)
- CREATE TABLE (Transact-SQL) (Microsoft)
- Unique constraints and check constraints (Microsoft)
- Database engine events and errors (0 to 999) (Microsoft)
- Database engine events and errors (2000 to 2999) (Microsoft)
- Quickstart: Run SQL Server Linux container images with Docker (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress