SQL Advanced Topics Complete Guide
SQL Advanced Topics Complete Guide
150+ Questions | DDL, DML, Advanced Queries, Transactions, Views, Indexes & More
Generated on: August 04, 2026
Table of Contents
Explanation: PRIMARY KEY uniquely identifies each row. Automatically creates a clustered index. NULL values
not allowed. Only one PRIMARY KEY per table.
Explanation: FOREIGN KEY maintains referential integrity. References PRIMARY KEY in another table. Prevents
orphan records.
Explanation: UNIQUE allows NULL values (multiple NULLs are allowed). Unlike PRIMARY KEY, multiple UNIQUE
constraints per table possible.
Q4. Add a CHECK constraint for salary > 0.
Query:
ALTER TABLE Employee ADD CONSTRAINT CHK_Salary CHECK (Salary > 0);
Explanation: CHECK constraint validates data before insert/update. Enforces business rules like salary must be
positive.
Explanation: DEFAULT provides automatic value if none specified. Reduces NULL values and ensures
consistency.
Explanation: Removes PRIMARY KEY constraint. Any FOREIGN KEYs referencing this must be dropped first.
Q7. Rename a table.
Query:
EXEC sp_rename 'Employee', 'EmployeeData'; -- MySQL: RENAME TABLE Employee TO EmployeeData;
Explanation: Changes table name. Different syntax for SQL Server (sp_rename) vs MySQL (RENAME TABLE).
Explanation: Renames column. SQL Server uses sp_rename, MySQL uses ALTER TABLE CHANGE. Updates all
references.
Explanation: Changes existing column type. May lose data if converting from larger to smaller type. Check
compatibility first.
Q10. Add a new column to an existing table.
Query:
ALTER TABLE Employee ADD PhoneNumber VARCHAR(15);
Explanation: Adds new column. Can specify DEFAULT to populate existing rows. NULL for current rows if no
default.
Explanation: Permanently removes column and all data. Cannot undo. Ensure column is not referenced by
views/constraints.
Explanation: Removes all rows without logging individual row deletes. Faster than DELETE. Identity seed resets
(SQL Server). Cannot use WHERE clause.
Q13. Delete all records without deleting the table.
Query:
DELETE FROM Employee;
Explanation: Removes all records but keeps table structure. Slower than TRUNCATE (logs each delete). Allows
WHERE clause. Identity NOT reset.
Explanation: Removes table structure and data completely. Cannot be recovered (unless backup). Removes
associated constraints/indexes.
Explanation: DELETE: DML, removes rows with WHERE, slower, logs rows, identity unchanged. TRUNCATE:
DDL, removes all rows, faster, no WHERE, identity reset (SQL Server). DROP: DDL, removes table structure.
INSERT, UPDATE & DELETE (16-30)
Explanation: Single INSERT with multiple VALUES clauses. More efficient than separate INSERTs. Values must
match column order/count.
Explanation: Inserts results of SELECT query. Target table must exist. Column order/types must match. Great for
backups/archiving.
Explanation: Updates all rows since no WHERE clause. Uses expression to calculate new value. Affects all
records.
Q19. Update salary only for HR department.
Query:
UPDATE Employee SET Salary = Salary * 1.10 WHERE Department = 'HR';
Explanation: WHERE clause limits update to specific rows. Only HR department salaries increased.
Q20. Increase salary by ■5000 for employees with experience > 5 years.
Query:
UPDATE Employee SET Salary = Salary + 5000 WHERE DATEDIFF(YEAR, HireDate, GETDATE()) > 5;
Explanation: Uses DATEDIFF to calculate years of service. Combines condition on calculated field. Rewards
experience.
Explanation: WHERE clause targets specific rows. Only employees with salary < 20000 deleted. Logs each delete.
Q22. Delete customers who never placed orders.
Query:
DELETE FROM Customer WHERE CustomerID NOT IN (SELECT DISTINCT CustomerID FROM Orders);
Explanation: Uses subquery to find customers with no orders. Subquery identifies inactive customers.
Explanation: IS NULL checks for missing values. Assigns default manager to employees without one.
Explanation: Fills NULL values with default salary. Ensures all employees have salary data.
Q25. Copy only Chennai employees into another table.
Query:
INSERT INTO ChennaiEmployees SELECT * FROM Employee WHERE City = 'Chennai';
Explanation: Copies filtered data. WHERE clause selects only Chennai records. Useful for city-specific analysis.
Explanation: Inserts old employee records to archive table. SELECT determines which rows to insert.
Explanation: Keeps MIN(EmployeeID) per name/salary combo, deletes rest. Assumes duplicate = same name &
salary.
Explanation: Different updates for different categories. Can have multiple UPDATE statements for different groups.
Explanation: ROLLBACK undoes changes within transaction. COMMIT saves changes. Demo: delete then rollback
restores data.
CASE Statement (31-45)
Explanation: CASE with multiple WHEN conditions. Assigns category based on salary range. ELSE provides
default.
Explanation: Simple CASE with one condition. >= 40 passes, below fails. Typical pass threshold.
Explanation: Multiple slabs with different bonus percentages. Higher earners get higher bonus percentage.
Hierarchical conditions.
Q34. Convert gender code into Male/Female.
Query:
SELECT EmployeeName, CASE WHEN GenderCode = 'M' THEN 'Male' WHEN GenderCode = 'F' THEN 'Female'
ELSE 'Other' END as Gender FROM Employee;
Explanation: Converts codes to readable values. Makes reports user-friendly. Handles unknown codes with ELSE.
Explanation: IS NULL vs IS NOT NULL logic. NULL ResignationDate = still working. Useful for employee analytics.
Explanation: Combines DATEDIFF for calculated field in CASE. Classifies by years of service.
Q37. Find tax percentage using CASE.
Query:
SELECT EmployeeName, Salary, CASE WHEN Salary > 1000000 THEN 0.30 WHEN Salary > 500000 THEN
0.20 WHEN Salary > 250000 THEN 0.10 ELSE 0 END as TaxRate FROM Employee;
Explanation: Tax brackets using CASE. Realistic tax slab scenario. ELSE for lowest bracket.
Explanation: Grading system with CASE. Common academic use case. Ranges for each grade.
Explanation: CASE or COALESCE both work. COALESCE is simpler for single replacement. CASE more flexible
for multiple conditions.
Explanation: DATEPART(WEEKDAY) returns day number. 1=Sunday, 7=Saturday (SQL Server). Different across
databases.
Explanation: DATEPART extracts hour from time. Determines shift based on login hour.
Explanation: Combines GROUP BY with CASE. Customer segmentation by spending. Aggregate function in
CASE.
Explanation: Uses subquery in CASE. Rates based on project count. Correlated subquery for each employee.
EXISTS / NOT EXISTS (46-55)
Explanation: EXISTS checks if subquery returns any rows. If EXISTS true, outer query returns employee. More
efficient than IN for large datasets.
Explanation: NOT EXISTS finds employees with no matching projects. Returns unassigned employees.
Explanation: EXISTS efficient for checking existence. Returns only customers who ordered.
Q49. Find customers without orders.
Query:
SELECT * FROM Customer c WHERE NOT EXISTS (SELECT 1 FROM Orders o WHERE [Link] =
[Link]);
Explanation: Identifies sold products. EXISTS faster than JOIN for this check.
Explanation: Finds unsold inventory. Useful for removing products from catalog.
Q52. Find departments having employees.
Query:
SELECT * FROM Department d WHERE EXISTS (SELECT 1 FROM Employee e WHERE [Link] =
[Link]);
Explanation: Managers who have subordinates. Uses self-join logic with EXISTS.
Q55. Find employees who are not managers.
Query:
SELECT * FROM Employee e WHERE NOT EXISTS (SELECT 1 FROM Employee e2 WHERE [Link] =
[Link]);
Explanation: UNION removes duplicates. Returns unique customer names from both tables.
Explanation: UNION automatically removes duplicates. Each city listed once even if in both tables.
Q58. Combine all records including duplicates.
Query:
SELECT City FROM Employees UNION ALL SELECT City FROM Customers;
Explanation: UNION ALL keeps duplicates. Faster than UNION (no deduplication). City appears multiple times if
duplicate.
Explanation: INTERSECT returns only rows in both queries. Employees in both branches. Works on names.
Explanation: EXCEPT returns rows in first query but not second. Exclusive to Branch A. Branch B exclusive
employees not shown.
Q61. Find products sold online and offline.
Query:
SELECT ProductID FROM OnlineSales UNION SELECT ProductID FROM OfflineSales;
Explanation: Combines historical data. UNION ALL preserves all records from each year.
Q64. Display common cities between customers and employees.
Query:
SELECT City FROM Customer INTERSECT SELECT City FROM Employee;
Explanation: CTE (AvgSalary) calculates average once. Main query uses CTE result. More readable than
subquery.
Q67. Use CTE to calculate department totals.
Query:
WITH DeptTotal AS (SELECT Department, SUM(Salary) as TotalSal, COUNT(*) as EmpCount FROM
Employee GROUP BY Department) SELECT Department, TotalSal, TotalSal/EmpCount as AvgSal FROM
DeptTotal;
Explanation: CTE stores grouped data. Main query uses CTE for calculations. Cleaner logic separation.
Explanation: Named result set for clarity. Reusable in query. Alternative to nested subqueries.
Explanation: Recursive CTE: base case (top managers), recursive case (adds subordinates). Shows org hierarchy
with levels.
Q70. Generate numbers from 1 to 100 using recursive CTE.
Query:
WITH RECURSIVE Numbers AS (SELECT 1 as N UNION ALL SELECT N+1 FROM Numbers WHERE N < 100)
SELECT * FROM Numbers;
Explanation: Simple recursive CTE for number generation. Useful for generating sequences without actual table
data.
Explanation: Recursive CTE with indentation. Shows tree structure. REPLICATE for visual hierarchy.
Explanation: CTE with window function. Shows running total. ORDER BY OrderDate ensures sequence.
Q73. Display running balance.
Query:
WITH RunningBalance AS (SELECT TransactionDate, Amount, SUM(Amount) OVER (ORDER BY
TransactionDate) as Balance FROM BankTransactions) SELECT * FROM RunningBalance;
Explanation: Running sum for account balance. Each row shows cumulative total to that point.
Explanation: CTE with ROW_NUMBER to identify duplicates. Rows with rn > 1 are duplicates. Based on Email
uniqueness.
Explanation: Uses CTE in DELETE statement. Keeps first occurrence (rn=1), deletes rest. Clean duplicate removal.
Transactions (76-85)
Explanation: BEGIN TRANSACTION starts atomic unit. COMMIT saves changes. All-or-nothing execution.
Ensures data consistency.
Explanation: COMMIT finalizes changes. After COMMIT, changes permanent. Before COMMIT, changes visible
only in transaction.
Explanation: ROLLBACK undoes all changes. Update never applied. Returns to pre-transaction state. Used for
error handling.
Q79. Use savepoints.
Query:
BEGIN TRANSACTION; INSERT INTO Employee VALUES (5, 'NewEmp', 30000); SAVE TRANSACTION SP1;
UPDATE Employee SET Salary = 40000; ROLLBACK TRANSACTION SP1;
Explanation: Savepoint marks checkpoint. ROLLBACK TO SP1 undoes only changes after SP1. Partial rollback
within transaction.
Explanation: Returns to savepoint state. INSERT kept, DELETE undone. COMMIT applies remaining changes.
Explanation: Multiple statements in one transaction. All succeed or all fail. Maintains consistency across multiple
updates.
Q82. Transfer money between accounts.
Query:
BEGIN TRANSACTION; UPDATE BankAccount SET Balance = Balance - 1000 WHERE AccountID = 1; UPDATE
BankAccount SET Balance = Balance + 1000 WHERE AccountID = 2; COMMIT;
Explanation: Classic transaction example. Both updates must succeed. If second fails, both ROLLBACK. No partial
transfer.
Explanation: ROLLBACK restores deleted records. Delete never persisted. Data recovery within transaction scope.
Explanation: ROWLOCK prevents concurrent access. Ensures data isolation during update. Other transactions
wait.
Q85. Explain ACID properties using SQL examples.
Query:
A(Atomicity): All-or-nothing transaction. C(Consistency): Data valid before/after.
I(Isolation): Transactions separate. D(Durability): Committed data permanent.
Explanation: Atomicity: BEGIN...COMMIT either all succeed or all fail. Consistency: constraints maintained.
Isolation: one transaction doesn't affect others. Durability: committed data survives failures.
Views (86-95)
Explanation: Virtual table based on query. Query executes when view accessed. Simplifies complex queries.
Stored query definition.
Explanation: Aggregated view with GROUP BY. Provides summary statistics. Hides complex grouping logic.
Q88. Update records through view.
Query:
UPDATE EmployeeView SET Salary = 50000 WHERE EmployeeID = 1;
Explanation: If view based on single table, updates work on underlying table. Updates through view must affect
base table directly.
Explanation: Removes view definition. Underlying tables unchanged. Removes view from database.
Explanation: WITH CHECK OPTION restricts inserts/updates. Records must satisfy view WHERE conditions (if
exists). Enforces view logic.
Q91. Create view joining multiple tables.
Query:
CREATE VIEW EmployeeDeptView AS SELECT [Link], [Link], [Link], [Link]
FROM Employee e INNER JOIN Department d ON [Link] = [Link];
Explanation: View with JOIN. Simplifies joining frequent table combinations. Query complex joins transparently.
Explanation: View with TOP clause. Provides top earners easily. Encapsulates ranking logic.
Explanation: View excludes sensitive columns (Salary, SSN). Users query view, not base table. Security through
obscurity.
Q94. Replace existing view.
Query:
ALTER VIEW EmployeeView AS SELECT EmployeeID, EmployeeName, Department, Salary, HireDate FROM
Employee;
Explanation: ALTER VIEW modifies view definition. Adds HireDate to previous version. Maintains view
name/permissions.
Explanation: View: Virtual, latest data always. Materialized View: Actual table, faster access, potentially stale.
Tradeoff: freshness vs performance.
Indexes (96-105)
Explanation: Non-clustered index on EmployeeName. Speeds up searches/sorts by name. Creates index structure.
Q97. Create composite index.
Query:
CREATE INDEX IDX_DeptSalary ON Employee(Department, Salary);
Explanation: Index on multiple columns. Efficient for searches on both Department & Salary. Column order matters.
Explanation: Removes index. Frees storage. Slows down queries that used this index. Speeds up updates/inserts.
Explanation: sys.dm_db_index_usage_stats shows index usage. Queries: seeks, scans, lookups count. Identifies
unused indexes.
Q100. Compare clustered vs non-clustered index.
Query:
Clustered: One per table, determines row order, PRIMARY KEY default. Non-clustered: Multiple
per table, separate structure, faster for specific columns.
Explanation: Clustered: Physical table order, fastest range queries. Non-clustered: Pointer structure, multiple
possible. Clustered usually on PK, non-clustered on frequent search columns.
Explanation: Queries consuming most CPU. Top 10 by total_worker_time. Candidates for optimization/indexing.
Explanation: Strategies: indexing, query rewrites, query plan analysis. Remove unnecessary columns/rows. Use
EXISTS over COUNT(*). Proper indexes on WHERE/JOIN columns.
Q103. Create unique index.
Query:
CREATE UNIQUE INDEX IDX_Email ON Employee(Email);
Explanation: Unique index enforces uniqueness like UNIQUE constraint. Prevents duplicate values. Useful for
optional unique fields.
Explanation: Index includes non-key columns. Query satisfied entirely from index. No lookup to base table. Faster
queries.
Explanation: Seek: Targeted lookup like EmployeeID = 5. Scan: Searches every row like Salary > 50000 without
index. Seek better for large tables.
Advanced Scenarios (106-120)
Explanation: Groups by HireDate, counts employees. HAVING > 1 shows dates with multiple hires. Shows
recruitment patterns.
Explanation: LAG gets previous salary. Calculates difference. Shows salary progression. Ordered by EmployeeID.
Explanation: LEN() calculates name length. ORDER BY DESC, TOP 1 gets longest. Uses function in ORDER BY.
Q109. Find shortest employee name.
Query:
SELECT TOP 1 EmployeeName, LEN(EmployeeName) as NameLength FROM Employee ORDER BY
LEN(EmployeeName) ASC;
Explanation: ORDER BY ASC finds shortest. Reverse of longest query. Could have multiple employees with same
length.
Explanation: REVERSE() flips string. Palindrome equals its reverse. Rare in names but interesting data quality
check.
Explanation: Same phone for multiple employees. Data quality issue. May indicate data entry errors or family
members.
Q112. Find employees having birthdays this month.
Query:
SELECT * FROM Employee WHERE MONTH(DateOfBirth) = MONTH(GETDATE());
Explanation: MONTH() extracts month. Compares to current month. Useful for birthday greetings/celebrations.
Explanation: Feb 29 only exists in leap years. DAY() = 29 and MONTH() = 2. Very rare dates.
Explanation: LEFT JOIN with date condition. WHERE ProductID IS NULL means no sales in last year. Candidates
for removal.
Q115. Find inactive customers.
Query:
SELECT c.* FROM Customer c LEFT JOIN Orders o ON [Link] = [Link] AND [Link] >=
DATEADD(YEAR, -1, GETDATE()) WHERE [Link] IS NULL;
Explanation: No orders in last year. Reactivation targets. Potential for win-back campaigns.
Explanation: CTE with ROW_NUMBER and PARTITION BY. Top 1 per month. Shows seasonal top products.
Explanation: Department with highest total payroll. Budget allocation indicator. Usually largest department.
Q118. Find customer with highest average order value.
Query:
SELECT TOP 1 CustomerID, CustomerName, AVG(Amount) as AvgOrderValue FROM Customer c INNER JOIN
Orders o ON [Link] = [Link] GROUP BY [Link], [Link] ORDER BY
AVG(Amount) DESC;
Explanation: High-value customer per order (not total spend). VIP candidates. Average order value metric.
Explanation: Subquery for total salary. Calculates percentage per department. Shows salary distribution.
Explanation: Concatenates parts: prefix + year + month + sequence. ROW_NUMBER for sequential numbering per
month. Generates unique invoice IDs.
Bonus Real Interview Questions (121-150)
Explanation: ROW_NUMBER ranks salaries. Median is middle value(s). For odd count: middle value. For even
count: avg of two middle values.
Explanation: Mode = most frequent value. GROUP BY salary, count occurrences. ORDER BY count DESC, TOP 1
= most frequent.
Explanation: Subquery calculates average. WHERE equality finds exact matches. May return 0 rows if no exact
match.
Q124. Find employees whose salary is a prime number.
Query:
This is complex SQL depending on database. May require recursive CTE to check primality or
number theory calculations.
Explanation: Advanced scenario requiring prime number logic. Rarely needed in practice. Tests advanced SQL
logic.
Explanation: DISTINCT ProductID avoids counting duplicates. GROUP BY customer, HAVING >= 3. Identifies
multi-product buyers.
Explanation: Products with customer count = total customers. Must be bought by every customer. Very rare
scenario.
Q127. Find departments where all employees earn above ■50,000.
Query:
SELECT Department FROM Employee GROUP BY Department HAVING MIN(Salary) > 50000;
Explanation: MIN(Salary) ensures minimum in dept > 50k. If min > 50k, all > 50k. Efficient check.
Explanation: DATEPART(WEEKDAY) = 1 for Sunday, 7 for Saturday (SQL Server). Different on MySQL. Unlikely
hiring day.
Explanation: Year totals calculated. LAG gets previous year. Growth = (current - previous) / previous * 100. Shows
trends.
Q130. Find employees with gaps in employment history.
Query:
SELECT [Link], [Link], [Link] FROM EmploymentHistory e1 INNER JOIN
EmploymentHistory e2 ON [Link] = [Link] AND [Link] > DATEADD(DAY, 1,
[Link]);
Explanation: Self-join on employment records. Finds gaps between end and next start date. Identifies career
breaks.
Explanation: Monthly totals per customer. Self-join compares consecutive months. Sales must increase
month-over-month.
Explanation: Complex logic: gap-and-island pattern. Identifies consecutive date sequences. Streak = unbroken
sequence.
Q133. Find missing dates in sales records.
Query:
WITH Dates AS (SELECT DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1, (SELECT
MIN(SaleDate) FROM Sales)) as D FROM Sales), MissedDates AS (SELECT D FROM Dates WHERE D NOT IN
(SELECT DISTINCT CAST(SaleDate as DATE) FROM Sales)) SELECT * FROM MissedDates;
Explanation: Generates all dates in range. Finds dates with no sales. Identifies gaps in sales records. Date audit.
Explanation: PIVOT rotates months to columns. Shows customer sales by month. Useful for dashboards/reports.
Explanation: UNPIVOT converts columns to rows. Converts denormalized data to normalized. Opposite of PIVOT.
Q136. Calculate moving average of sales.
Query:
SELECT SaleDate, Amount, AVG(Amount) OVER (ORDER BY SaleDate ROWS BETWEEN 6 PRECEDING AND
CURRENT ROW) as MovingAvg7 FROM Sales;
Explanation: Window function with ROWS BETWEEN. 7-day moving average (current + 6 previous). Smooths data
trends.
Explanation: PERCENTILE_CONT calculates 90th percentile. Employees at/above 90th percentile = top 10%.
Statistical approach.
Explanation: LOWER() converts to lowercase for comparison. Case-insensitive duplicates. Groups on lowercase
version.
Q139. Split comma-separated values into rows.
Query:
SELECT EmployeeID, value FROM Employee CROSS APPLY STRING_SPLIT(Skills, ',');
Explanation: STRING_SPLIT breaks delimited string into rows. One skill per row. Normalizes denormalized data.
Explanation: Identifies duplicates by email. Merges to lowest ID. Updates foreign keys. Deletes duplicates.
Transaction for safety.
Q141. Find products with declining sales for three consecutive months.
Query:
WITH MonthlySales AS (SELECT ProductID, YEAR(SaleDate) as Y, MONTH(SaleDate) as M,
SUM(Quantity) as Qty, LAG(SUM(Quantity)) OVER (PARTITION BY ProductID ORDER BY YEAR(SaleDate),
MONTH(SaleDate)) as PrevQty FROM Sales GROUP BY ProductID, YEAR(SaleDate), MONTH(SaleDate))
SELECT ProductID FROM MonthlySales WHERE Qty < PrevQty GROUP BY ProductID HAVING COUNT(*) >= 3;
Explanation: LAG compares consecutive months. Declining = current < previous. 3+ declining months. Problem
products.
Q142. Identify employees whose salary increased every year.
Query:
WITH YearlySalary AS (SELECT EmployeeID, YEAR(ReviewDate) as Y, Salary, LAG(Salary) OVER
(PARTITION BY EmployeeID ORDER BY YEAR(ReviewDate)) as PrevSal FROM EmployeeSalaryHistory)
SELECT DISTINCT EmployeeID FROM YearlySalary WHERE Salary > PrevSal GROUP BY EmployeeID HAVING
COUNT(*) = (SELECT COUNT(DISTINCT YEAR(ReviewDate)) FROM EmployeeSalaryHistory) - 1;
Explanation: Salary history table required. Every year higher than previous. All increases = consistent growth.
Explanation: Recursive CTE generates date range. DATENAME, MONTH, DAY, WEEK extract components.
Useful for date joins.
Q145. Calculate business days between two dates.
Query:
SELECT COUNT(*) as BusinessDays FROM (WITH Dates AS (SELECT DATEADD(DAY, ROW_NUMBER() OVER
(ORDER BY (SELECT NULL)) - 1, @StartDate) as D FROM Sales) SELECT D FROM Dates WHERE D <=
@EndDate AND DATEPART(WEEKDAY, D) NOT IN (1, 7)) T;
Explanation: Generates all dates in range. Filters weekdays (WEEKDAY NOT IN 1,7). Counts business days only.
Excludes weekends.
Explanation: FIRST_VALUE returns first value in window. PARTITION BY groups. ROWS BETWEEN defines
frame. Or COALESCE with multiple columns.
Explanation: Gap-and-island pattern. Identifies missing sequences. Islands = consecutive ranges, gaps = missing
numbers.
Q148. Build an employee hierarchy using recursive queries.
Query:
WITH RECURSIVE Hierarchy AS (SELECT EmployeeID, EmployeeName, ManagerID, 0 as Level FROM
Employee WHERE ManagerID IS NULL UNION ALL SELECT [Link], [Link], [Link],
[Link] + 1 FROM Employee e INNER JOIN Hierarchy h ON [Link] = [Link]) SELECT * FROM
Hierarchy ORDER BY Level, EmployeeName;
Explanation: Base case: top managers (NULL ManagerID). Recursive: adds subordinates at each level. Shows org
structure with levels.
Explanation: Recursive CTE builds path. Detects cycles = employee is own manager indirectly. Path contains
loops. Data quality issue.
Q150. Write a query to compare two tables and return only the changed rows.
Query:
SELECT COALESCE([Link], [Link]) as ID, [Link] as OldValue, [Link] as NewValue FROM Table1 t1
FULL OUTER JOIN Table2 t2 ON [Link] = [Link] WHERE [Link] != [Link] OR [Link] IS NULL OR [Link]
IS NULL;
Explanation: FULL OUTER JOIN shows all rows from both tables. WHERE filters to changed/missing rows.
OldValue from t1, NewValue from t2.