Loading learning content...
If SELECT defines what data to retrieve, FROM defines where to retrieve it from. The FROM clause is the foundation upon which every query is built—it specifies the tables, views, subqueries, and other data sources that provide the raw material for your query.
In the logical processing order of SQL, FROM is actually evaluated first, before WHERE, GROUP BY, or SELECT. This means FROM determines the universe of data available to all subsequent clauses. Understanding FROM deeply is essential for:
By the end of this page, you will understand the FROM clause comprehensively—from basic table references through aliasing, schema qualification, derived tables, and the critical relationship between FROM and the rest of the query. You'll be equipped to specify data sources correctly in any SQL context.
At its simplest, the FROM clause names a single table from which to retrieve data. However, the full syntax supports far more sophisticated data source specifications.
Basic syntax:
FROM table_reference [, table_reference ...]
Where each table_reference can be:
Key concept: The virtual table
The FROM clause constructs a virtual table that serves as input to all subsequent processing. When FROM contains a single table, this virtual table is simply that table's contents. When FROM contains multiple tables or joins, the virtual table is the result of combining them.
12345678910111213141516171819202122
-- Simple single-table referenceSELECT * FROM employees; -- Schema-qualified table referenceSELECT * FROM hr.employees; -- Database + schema + table (fully qualified)SELECT * FROM company_db.hr.employees; -- Multiple tables (implicit cross join - rarely what you want!)SELECT * FROM employees, departments;-- Returns: every employee paired with every department -- With meaningful join condition (old-style)SELECT * FROM employees, departmentsWHERE employees.department_id = departments.id; -- Modern explicit JOIN syntax (preferred)SELECT * FROM employeesJOIN departments ON employees.department_id = departments.id;Listing multiple tables separated by commas (FROM a, b) creates a cross join—every row from a paired with every row from b. For tables with 1000 rows each, this produces 1,000,000 rows! Always use explicit JOIN syntax for clarity and to prevent accidental cross joins.
FROM clause processing model:
This processing happens conceptually before anything else in the query. Even though you write SELECT first, the database logically starts with FROM.
Table aliases provide shorthand names for tables referenced in the FROM clause. While aliases might seem like a convenience feature, they are actually essential for certain query patterns and dramatically improve query readability.
Why table aliases matter:
employee_performance_metrics_historical as epmh improves readabilitye.name is clearer than employees.name1234567891011121314151617181920212223242526272829303132333435363738
-- Basic alias syntax (AS keyword optional but recommended)SELECT e.first_name, e.last_name, e.salaryFROM employees AS e; -- Without AS (valid but less explicit)SELECT e.first_name, e.last_name, e.salaryFROM employees e; -- Multiple table aliasesSELECT e.employee_id, e.first_name, d.department_name, m.first_name AS manager_nameFROM employees AS eJOIN departments AS d ON e.department_id = d.department_idJOIN employees AS m ON e.manager_id = m.employee_id; -- Self-join: finding employees and their managers-- MUST use aliases to distinguish the two employee table referencesSELECT emp.first_name AS employee, emp.last_name AS emp_last_name, mgr.first_name AS manager, mgr.last_name AS mgr_last_nameFROM employees AS empLEFT JOIN employees AS mgr ON emp.manager_id = mgr.employee_id; -- Once aliased, you MUST use the alias (original name is hidden)SELECT e.first_name, employees.last_name -- ERROR in most databases! Alias 'e' hides 'employees'FROM employees AS e; -- Schema-qualified with aliasSELECT o.order_id, o.order_dateFROM sales.orders AS oWHERE o.status = 'pending';| Practice | Explanation | Example |
|---|---|---|
| Single letter for simple queries | Quick to write, easy to read for simple joins | FROM employees e |
| Meaningful abbreviations for complex queries | Helps readers understand which table is which | FROM customer_orders co |
| Consistent conventions | Use same alias for same table across queries | Always e for employees |
| Always use AS keyword | More explicit, some databases require it for column aliases | FROM employees AS e |
| Avoid reserved words | Some databases reject reserved words as aliases | Avoid FROM table t, order o |
Once you define a table alias in FROM, that alias is available in SELECT, WHERE, GROUP BY, HAVING, and ORDER BY. In most databases, once aliased, the original table name cannot be used within that query—you must use the alias.
In enterprise database environments, tables are organized within schemas and catalogs (databases). Understanding how to reference tables across these organizational boundaries is essential for working with complex database deployments.
Naming hierarchy:
catalog.schema.table
Or in some terminology:
database.schema.table
Different database systems have varying terminology and defaults:
| Database | Format | Example | Default Schema |
|---|---|---|---|
| PostgreSQL | database.schema.table | mydb.public.employees | public |
| MySQL | database.table | mydb.employees | Current database |
| SQL Server | database.schema.table | mydb.dbo.employees | dbo |
| Oracle | schema.table | hr.employees | Current user's schema |
| SQLite | database.table | main.employees | main |
123456789101112131415161718192021222324252627282930313233343536
-- Unqualified reference uses current/default schemaSELECT * FROM employees; -- Schema-qualified referenceSELECT * FROM hr.employees; -- Fully qualified reference (SQL Server syntax)SELECT * FROM CompanyDB.hr.employees; -- Cross-database query (SQL Server)SELECT p.product_name, s.quantityFROM ProductDB.inventory.products AS pJOIN SalesDB.orders.order_items AS s ON p.product_id = s.product_id; -- PostgreSQL cross-schema querySELECT e.employee_name, p.project_nameFROM hr.employees AS eJOIN projects.assignments AS a ON e.id = a.employee_idJOIN projects.projects AS p ON a.project_id = p.id; -- Setting default schema (varies by DBMS)-- PostgreSQLSET search_path TO hr, public; -- SQL ServerALTER USER current_user WITH DEFAULT_SCHEMA = hr; -- MySQLUSE sales_database; -- After setting default, unqualified names resolve to defaultSELECT * FROM employees; -- Now resolves to hr.employeesAlways fully qualify table names in: (1) Stored procedures and functions that may be called from different schema contexts, (2) Cross-database or cross-schema queries, (3) Production deployment scripts, (4) Documentation and training materials. Use unqualified names for ad-hoc queries in a known context.
Portability considerations:
Schema qualification syntax varies significantly across database systems. When writing portable SQL:
A derived table is a subquery that appears in the FROM clause and acts as a virtual table for the outer query. This powerful technique allows you to:
Syntax:
FROM (subquery) AS alias
The alias is mandatory—unlike regular tables, derived tables must be named.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
-- Simple derived table: pre-filteringSELECT dt.employee_id, dt.full_name, dt.salaryFROM ( SELECT employee_id, CONCAT(first_name, ' ', last_name) AS full_name, salary FROM employees WHERE status = 'active') AS dtWHERE dt.salary > 50000; -- Derived table for aggregation before joinSELECT d.department_name, dept_stats.employee_count, dept_stats.avg_salaryFROM departments AS dJOIN ( SELECT department_id, COUNT(*) AS employee_count, AVG(salary) AS avg_salary FROM employees GROUP BY department_id) AS dept_stats ON d.department_id = dept_stats.department_id; -- Working around WHERE limitation on aggregates-- Problem: "Find employees earning more than their department's average"-- Cannot use aggregate in WHERE directly -- Solution: Derived table with department averagesSELECT e.employee_id, e.first_name, e.salary, dept_avg.avg_salaryFROM employees AS eJOIN ( SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id) AS dept_avg ON e.department_id = dept_avg.department_idWHERE e.salary > dept_avg.avg_salary; -- Nested derived tables (use sparingly - CTEs often clearer)SELECT top_earners.dept_name, top_earners.max_salaryFROM ( SELECT d.department_name AS dept_name, dept_salaries.max_sal AS max_salary FROM departments AS d JOIN ( SELECT department_id, MAX(salary) AS max_sal FROM employees GROUP BY department_id ) AS dept_salaries ON d.department_id = dept_salaries.department_id) AS top_earnersWHERE top_earners.max_salary > 100000;Derived tables and Common Table Expressions (CTEs) solve similar problems. CTEs (WITH clause) are often more readable, especially when the subquery is referenced multiple times or when queries become deeply nested. Consider CTEs for complex derived table scenarios.
FROM (SELECT ...) AS dt — the alias is requiredViews are stored queries that act as virtual tables. From the perspective of the FROM clause, a view is indistinguishable from a regular table—you reference it by name and can alias, join, and filter it exactly like a table.
Why views matter:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
-- Creating a view (for context)CREATE VIEW employee_directory ASSELECT e.employee_id, CONCAT(e.first_name, ' ', e.last_name) AS full_name, e.email, d.department_name, e.hire_dateFROM employees AS eJOIN departments AS d ON e.department_id = d.department_idWHERE e.status = 'active'; -- Using the view in FROM (exactly like a table)SELECT * FROM employee_directory; -- Filtering a viewSELECT full_name, department_nameFROM employee_directoryWHERE department_name = 'Engineering'; -- Joining views with tablesSELECT ed.full_name, ed.department_name, p.project_nameFROM employee_directory AS edJOIN project_assignments AS pa ON ed.employee_id = pa.employee_idJOIN projects AS p ON pa.project_id = p.project_id; -- Joining multiple viewsSELECT ed.full_name, ds.total_salary_budgetFROM employee_directory AS edJOIN department_statistics AS ds ON ed.department_name = ds.department_name; -- View with aggregationCREATE VIEW department_summary ASSELECT d.department_id, d.department_name, COUNT(e.employee_id) AS employee_count, AVG(e.salary) AS average_salary, SUM(e.salary) AS total_salaryFROM departments AS dLEFT JOIN employees AS e ON d.department_id = e.department_idGROUP BY d.department_id, d.department_name; -- Using aggregation viewSELECT department_name, employee_countFROM department_summaryWHERE employee_count > 10ORDER BY average_salary DESC;When you query a view, the database typically merges the view's definition with your outer query for optimization. This means predicates you apply to a view can often be pushed down into the view's underlying query. However, complex views (those with DISTINCT, GROUP BY, or UNION) may require materialization before your outer predicates apply, potentially impacting performance.
| View Type | Description | FROM Usage |
|---|---|---|
| Simple view | Single table, no aggregation | Fully transparent—filter pushdown works |
| Join view | Multiple tables joined | Usually transparent—join conditions preserved |
| Aggregate view | Contains GROUP BY | May block filter pushdown |
| Materialized view | Pre-computed and stored | Nearly identical to table access |
| Updatable view | Allows INSERT/UPDATE/DELETE | Same as simple view for SELECT |
Some database systems support table-valued functions (TVFs)—functions that return a table rather than a scalar value. These can appear in the FROM clause like any other table source, enabling powerful programmatic data generation and transformation.
Built-in table-valued functions:
Many databases provide built-in functions that return tables:
GENERATE_SERIES() in PostgreSQLSTRING_SPLIT() in SQL ServerJSON_TABLE() in MySQL/Oracle for parsing JSONUNNEST() in PostgreSQL for expanding arraysXMLTABLE() for parsing XML1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
-- PostgreSQL: GENERATE_SERIES for number sequencesSELECT numFROM GENERATE_SERIES(1, 10) AS num;-- Returns: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 -- Date series for calendar generationSELECT date::date AS calendar_dateFROM GENERATE_SERIES( '2024-01-01'::date, '2024-12-31'::date, '1 day'::interval) AS date; -- SQL Server: STRING_SPLIT for parsing delimited stringsSELECT value AS tagFROM STRING_SPLIT('red,green,blue,yellow', ',');-- Returns: red, green, blue, yellow (as separate rows) -- Using TVF in joinsSELECT p.product_id, p.product_name, t.value AS tagFROM products AS pCROSS APPLY STRING_SPLIT(p.tags, ',') AS t; -- PostgreSQL: UNNEST for array expansionSELECT order_id, itemFROM ordersCROSS JOIN LATERAL UNNEST(item_array) AS item; -- MySQL: JSON_TABLE for parsing JSONSELECT jt.*FROM orders,JSON_TABLE( order_json, '$.items[*]' COLUMNS ( product_id INT PATH '$.product_id', quantity INT PATH '$.quantity', price DECIMAL(10,2) PATH '$.price' )) AS jt; -- User-defined table-valued function (SQL Server)CREATE FUNCTION GetEmployeesByDepartment(@DeptId INT)RETURNS TABLEASRETURN ( SELECT employee_id, first_name, last_name, salary FROM employees WHERE department_id = @DeptId); -- Using UDF-TVF in FROMSELECT * FROM GetEmployeesByDepartment(5); -- Joining with UDF-TVFSELECT d.department_name, e.first_name, e.salaryFROM departments AS dCROSS APPLY GetEmployeesByDepartment(d.department_id) AS e;SQL Server uses CROSS APPLY to reference outer query columns in a table-valued function. PostgreSQL uses LATERAL for the same purpose. Standard SQL specifies LATERAL JOIN. These allow row-by-row processing where each row from the left side is used to parameterize the table-valued function on the right.
In standard SQL, the FROM clause is technically optional when you're selecting only constants or expressions that don't reference table data. However, different databases handle this inconsistently.
The Oracle DUAL table:
Oracle historically required a FROM clause for every SELECT. To accommodate queries that don't need table data, Oracle provides a special one-row, one-column table called DUAL. This has become a common pattern across many databases.
| Database | FROM Optional? | Workaround |
|---|---|---|
| PostgreSQL | Yes | SELECT 1 works |
| MySQL | Yes | SELECT 1 works, DUAL also supported |
| SQL Server | Yes | SELECT 1 works |
| SQLite | Yes | SELECT 1 works |
| Oracle | No | Must use SELECT 1 FROM DUAL |
| DB2 | No | Must use SELECT 1 FROM SYSIBM.SYSDUMMY1 |
12345678910111213141516171819202122232425262728293031323334353637
-- PostgreSQL/MySQL/SQL Server: No FROM neededSELECT 1 + 1;-- Returns: 2 SELECT CURRENT_TIMESTAMP;-- Returns: current date/time SELECT 'Hello' || ' ' || 'World';-- Returns: Hello World (PostgreSQL) -- Oracle: DUAL requiredSELECT 1 + 1 FROM DUAL;SELECT SYSDATE FROM DUAL;SELECT 'Hello' || ' ' || 'World' FROM DUAL; -- MySQL: DUAL optional but supported for Oracle compatibilitySELECT 1 + 1 FROM DUAL; -- WorksSELECT 1 + 1; -- Also works -- Practical uses of FROM-less queries:-- 1. Testing database connectivitySELECT 1; -- 2. Getting current timestampSELECT CURRENT_TIMESTAMP AS now; -- 3. Evaluating expressionsSELECT POWER(2, 10) AS result; -- 1024 -- 4. String manipulation testingSELECT UPPER('test') AS upper_result; -- 5. Generating single-row results for UNIONSELECT 'Header' AS category, 0 AS sort_orderUNION ALLSELECT category_name, 1 FROM categoriesORDER BY sort_order, category;For maximum portability, consider using SELECT expression FROM (SELECT 1) AS dummy which works across all major databases. However, in most modern databases (except Oracle), a simple SELECT expression without FROM is perfectly valid and preferred for clarity.
The FROM clause is the foundation of every SELECT statement—specifying where your data originates. Despite being written after SELECT, it is processed first and creates the virtual table from which all other operations draw.
Let's consolidate the key points:
What's next:
With a solid understanding of SELECT and FROM, we'll next examine column selection in detail—how to precisely specify which data elements appear in your result set, including wildcards, expressions, and complex selection patterns.
You now understand the FROM clause comprehensively—from simple table references through derived tables, views, and table-valued functions. This foundation is essential for all query construction, especially when we explore joins in detail later.