OPENROWSET#
Reads data that is not a registered table and exposes it as a table in the FROM clause — either by
querying a source through a provider, or by bulk-reading external files.
Provider form#
OPENROWSET ( 'provider', 'connection_string', 'query' )
Runs query against the source identified by provider and connection_string.
SELECT * FROM OPENROWSET('csv', 'provider_string', 'SELECT * FROM data');
Bulk form#
OPENROWSET ( BULK 'data_file' [ , <option> [ , ...n ] ] ) [ WITH ( <schema> ) ] AS alias
Reads one or more Excel workbooks (.xlsx / .xls / .xlsb) from a registered Excel connection as a
rowset; a correlation name (AS alias) is required. DATA_SOURCE names the connection (which locates the
files) and WITH (...) declares the result columns — optional, since without it the schema is derived from
the first workbook the statement resolves to. Read several workbooks by listing them
(BULK ('a.xlsx', 'b.xlsx')) or with a glob. The worked walkthrough — worksheet and range selection,
header-name binding, column ordinals, and globbing — is in Reading Excel workbooks (BULK) below.
The same bulk form reads PDF documents from a registered File connection when you add FORMAT = 'PDF'. A
PDF read returns the built-in PdfDocuments columns — including the extracted Text and the detected
Tables (JSON) — selected by name, with no WITH clause. See Reading PDF documents (BULK) below.
It also reads Parquet files from a File connection with FORMAT = 'PARQUET'. Parquet files carry their
own schema, so the WITH clause is optional. See Reading Parquet files (BULK) below.
And it reads delimited text files from a File connection with FORMAT = 'CSV'. The schema is derived
from the data - a header row names the columns and the sampled values decide their types - so the WITH
clause is optional here too. A list or a glob that matches several files is read as one rowset, with each file
matched onto the one schema. See Reading delimited files (BULK) below.
Supported options#
Options follow the BULK clause as NAME = value:
Option |
Meaning and behavior |
|---|---|
|
Names the registered connection that locates the file(s). File paths are resolved relative to the connection’s directory, and a path that escapes it is rejected. |
|
Selects the file reader for a File connection — |
|
Carries reader options as a TOML |
|
The 1-based first and last data rows of the read window, applied when the |
|
|
The WITH clause also accepts an explicit 1-based source-column ordinal after a column’s type, pinning a
column to a specific sheet column (see Bind columns by header name).
Reading Excel workbooks (BULK)#
The bulk form reads .xlsx, .xls and .xlsb workbooks from a registered Excel connection. Point
DATA_SOURCE at the connection and declare the result columns with WITH (...):
SELECT id, amount, qty
FROM OPENROWSET(BULK 'sales.xlsx', DATA_SOURCE = 'excel_sales')
WITH (id bigint, amount decimal(28,2), qty int) AS data;
The WITH columns map positionally to sheet columns A, B, C, … in declaration order, and each cell is
coerced to the declared type. The read starts at the top of the sheet and treats the first row as data; skip a
header row with FIRSTROW = 2, or bind to it by name with HEADER_ROW (see below).
A file reference is resolved under the connection’s directory, and a reference that escapes it is rejected, so a query cannot read arbitrary files on the server. Address a workbook by a path relative to that directory; an absolute path is accepted only when it already points inside it.
Excel is a distinct provider: this form requires an Excel connection. To read PDF, text, XML or email
files, use a File connection with the FORMAT option instead — see Reading PDF documents (BULK)
below.
Worksheet and range#
Reader options specific to Excel — the worksheet and range, and the multi-file error mode — are passed as a
small TOML options table inside the standard FORMATFILE_DATA_SOURCE parameter; packing them into a
Microsoft-recognised parameter keeps the statement parseable by standard T-SQL client tools. sheet selects the
worksheet and an optional A1 cell range; the standard FIRSTROW and LASTROW bound the rows when no range
is given (an A1 range wins if both are supplied):
-- worksheet 'Q1', restricted to the range B2:D100
SELECT region, q1, q2
FROM OPENROWSET(BULK 'sales.xlsx', DATA_SOURCE = 'excel_sales',
FORMATFILE_DATA_SOURCE = 'options = { sheet = "Q1!B2:D100" }')
WITH (region nvarchar(50), q1 float, q2 float) AS data;
Worksheet names are matched case-insensitively; no other difference is ignored, so sheet = "m2" does not
name a tab spelled with a superscript two. Naming a worksheet a workbook does not carry raises an error
identifying the sheet and the file, even under on_error = "skip" — a mistyped sheet name is not an unreadable
workbook. Over a set of workbooks the check is made as each workbook is read, so every workbook the statement
reads must carry the sheet; leave sheet out to read each workbook’s first worksheet.
Bind columns by header name#
With HEADER_ROW = TRUE the first row of the read window is treated as a header, and each WITH column
binds to the sheet column whose header matches its name instead of by position — so you can omit or reorder
columns:
-- sheet headers: id | name | amount; read only amount and id, by name
SELECT amount, id
FROM OPENROWSET(BULK 'sales.xlsx', DATA_SOURCE = 'excel_sales', HEADER_ROW = TRUE)
WITH (amount decimal(28,2), id bigint) AS data;
Header matching is case-insensitive, and the header row is excluded from the data. A column whose name a
workbook’s header does not carry reads NULL for that workbook’s rows rather than failing the statement, so a
workbook that drops or renames a column does not fail a read the rest of the set answers — the same rule the
delimited format follows. A duplicate header raises a clear error, because binding by name through it would be
ambiguous. To pin a specific sheet column regardless of the header, give the column an explicit 1-based sheet
ordinal after its type — for example WITH (id bigint 1, note nvarchar(200) 3) reads sheet column 1 into
id and sheet column 3 into note. An ordinal below 1 names no sheet column and is refused while the
statement is planned.
Warning
The other face of that rule: a misspelled column name is indistinguishable from a column the workbook does
not carry, so it reads NULL instead of reporting the mistake. Check the result’s columns when one comes
back entirely empty.
Which workbooks carry a header row#
Whether a workbook carries a header row cannot be read out of its cells — it is a property of how the set of
workbooks was produced, so the statement states it. HEADER_ROW takes three values:
|
Meaning |
Effect per workbook |
|---|---|---|
|
every workbook carries a header row |
each workbook’s window-top row is consumed as that workbook’s header, and its cells are claimed by name, so a workbook whose columns are in another order still answers the same schema |
|
no workbook carries one |
nothing is consumed as a header; every workbook’s cells are claimed by position |
|
only the first resolved workbook carries one |
the first workbook’s header row is consumed and describes the whole set; the workbooks after it are pure data, and their cells are claimed through that one header |
'FIRST_FILE' is for the split-export shape — a header written once, continuation workbooks data-only:
-- sales_a.xlsx id | amount (the header, then its data rows)
-- 1 | 10.5
-- sales_b.xlsx 2 | 20.5 (a continuation workbook, with no header of its own)
SELECT id, amount
FROM OPENROWSET(BULK 'sales_*.xlsx', DATA_SOURCE = 'excel_sales', HEADER_ROW = 'FIRST_FILE')
WITH (id bigint, amount float) AS data;
Under HEADER_ROW = TRUE that second workbook’s first data row would be consumed as a header and matched
against nothing. With a single workbook TRUE and 'FIRST_FILE' say the same thing, which is why the
distinction only appears once a statement spans several workbooks. Omitted, HEADER_ROW means that no
workbook carries one; a value that is none of the three is refused rather than guessed at.
Two rules follow from that one header being the layout of the whole set:
The first resolved workbook cannot be stepped over. If it cannot be read the statement fails even under
on_error = "skip", which governs the workbooks after it — whose layout is already settled. This is the rule that already governs the schema when noWITHclause is given, and under'FIRST_FILE'it holds whether or not the statement declares one.A continuation workbook narrower than the header — one that simply stops before a column the header names — reads
NULLfor the columns it does not reach, rather than failing. The anchor may also carry nothing but its header row; it then contributes no data of its own and still states the layout.
Let the workbook state the schema#
Leave the WITH clause out and the result columns come from the workbook itself: with HEADER_ROW = TRUE
or HEADER_ROW = 'FIRST_FILE' the window’s first row names them, and the cells beneath it decide their
types — a text column being declared nvarchar(max) whatever those cells hold (see below). Without a header
the columns are named c0, c1, … in sheet order.
-- sheet headers: id | name | amount - the result has those three columns, typed from the data
SELECT * FROM OPENROWSET(BULK 'sales.xlsx', DATA_SOURCE = 'excel_sales', HEADER_ROW = TRUE) AS data;
The schema is derived from the first workbook the statement resolves to, over the same window the read uses
— the worksheet, the A1 range or FIRSTROW/LASTROW. A list or a glob matching several workbooks is still
one rowset answering that one schema, and with HEADER_ROW = TRUE every later workbook is matched onto it by
its own header name, so a workbook whose columns sit in another order still lines up; under
HEADER_ROW = 'FIRST_FILE' the later workbooks carry no header and are matched through the first one’s.
Because the first workbook is the schema, it is the one file that cannot be stepped over: if it cannot be
read, the statement fails even under on_error = "skip", which governs the read of the workbooks after it.
Declare the columns with WITH (...) when you want the schema fixed by the statement rather than by a file.
A derived text column is declared nvarchar(max), not sized to the longest value the first workbook holds.
The workbooks after it were never read while the schema was derived, so a width measured in one of them would
refuse a longer value in any of the others — a statement that ran yesterday would start failing because a vendor
sent a longer name. This matches what SQL Server describes for a schema it derives from data, where a text
column’s width comes from the type and never from the cells.
Note
Two things a derived schema cannot promise, both of which answer NULL rather than failing:
a value in a later workbook that does not fit the type the first workbook’s cells implied — a word in a column the anchor held only numbers in, for example;
a column a later workbook does not carry at all.
Neither is reported, and neither can be told apart from a cell that is genuinely empty. Declare the columns
with WITH (...) when the statement has to answer for the shape of the data rather than follow it.
Read several workbooks (globbing)#
List workbooks explicitly, or match them with a glob pattern relative to the connection’s directory:
-- an explicit list
SELECT id FROM OPENROWSET(BULK ('jan.xlsx', 'feb.xlsx'), DATA_SOURCE = 'excel_sales')
WITH (id bigint) AS data;
-- a glob: every .xlsx in the connection directory (top level)
SELECT id FROM OPENROWSET(BULK '*.xlsx', DATA_SOURCE = 'excel_sales')
WITH (id bigint) AS data;
* matches within a single path segment (the top level only); ** spans subdirectories, so
2024/**/*.xlsx matches every workbook under 2024/ at any depth. The matched workbooks are read and
concatenated.
Workbooks are resolved by the same rules as every other BULK read, described under What a wildcard
reaches below: names beginning with . or _ are not reached by a wildcard, letter case follows the
server’s file system, a folder path reads that folder one level deep, the matches are taken in name order, and a
pattern matching nothing returns no row with a warning - or, with no WITH clause, is rejected, because there
is then no workbook to take the schema from. What stays Excel’s own is what a workbook is: a wildcard reaches
only the extensions the connection declares, never Excel’s ~$ lock files. An absolute path that already
points inside the connection’s directory is accepted too, and read as the path relative to it.
Add on_error to the FORMATFILE_DATA_SOURCE options payload to control how an unreadable
workbook is handled — on_error = "skip" skips it and continues with the rest, while the default
on_error = "fail" raises an error instead. A workbook that can be read but lacks the worksheet named in
sheet is not skipped; it fails the statement under either value:
-- skip workbooks that cannot be read, and read the rest
SELECT id FROM OPENROWSET(BULK '2024/**/*.xlsx', DATA_SOURCE = 'excel_sales',
FORMATFILE_DATA_SOURCE = 'options = { on_error = "skip" }')
WITH (id bigint) AS data;
See Microsoft Excel for registering an Excel connection and data-type inference.
Reading PDF documents (BULK)#
The bulk form also reads .pdf files from a registered File connection. Add FORMAT = 'PDF', point
DATA_SOURCE at the connection, and select the built-in PdfDocuments columns by name — no WITH clause
is needed, and a correlation name (AS alias) is required. File references are resolved under the
connection’s directory, and a reference that escapes it is rejected.
The FORMAT option is required on a File connection and selects the reader: PDF, TEXT, XML or
EMAIL. Excel workbooks (.xlsx / .xls / .xlsb) are not a File format — they are read through
an Excel connection with no FORMAT; see Reading Excel workbooks (BULK) above.
SELECT Path, NumberOfPages, Text
FROM OPENROWSET(BULK 'report.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF') AS docs;
Two columns carry extracted content: Text (the document text, via PdfPig) and Tables (tables detected in the document, as a JSON array). Each is produced only when selected, and the table engine’s Java runtime starts lazily on the first read of Tables. The full PdfDocuments schema is in PDF.
-- metadata for every PDF in the connection directory
SELECT Path, Title, Author, NumberOfPages
FROM OPENROWSET(BULK '*.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF') AS docs;
-- detected tables as JSON
SELECT Path, Tables
FROM OPENROWSET(BULK 'invoice.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF') AS docs;
Extraction options#
Extraction is steered by options passed as a TOML options table in FORMATFILE_DATA_SOURCE. Each option
also has a connection-level default that applies to every read (including the built-in PdfDocuments table); a
per-query value overrides it.
Option |
Applies to |
Values (default in bold) |
|---|---|---|
|
Text |
blocks, words, letters |
|
Text |
default, docstrum, xycut — page-segmentation algorithm for |
|
Text |
none, rendering, unsupervised |
|
Text |
true, false — drop duplicated overlapping glyphs |
|
Text |
points cropped from each page edge (default 0) |
|
Tables |
auto (ruled first, stream fallback), lattice (ruled only), stream (borderless) |
|
Tables |
|
|
Text and Tables |
1-based page range, e.g. |
|
Text and Tables |
password for an encrypted PDF (default: none) |
|
file selection |
fail, skip — how a multi-file read handles an unreadable PDF |
Examples#
-- text as space-separated words instead of reading-order blocks
SELECT Text
FROM OPENROWSET(BULK 'report.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { text_mode = "words" }') AS docs;
-- only pages 1-3 and 5
SELECT Text
FROM OPENROWSET(BULK 'report.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { pages = "1-3,5" }') AS docs;
-- an encrypted document
SELECT Text
FROM OPENROWSET(BULK 'secured.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { password = "s3cret" }') AS docs;
-- ruled tables only (no stream fallback)
SELECT Tables
FROM OPENROWSET(BULK 'grid.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { engine = "lattice" }') AS docs;
-- borderless tables via stream detection
SELECT Tables
FROM OPENROWSET(BULK 'columns.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { engine = "stream" }') AS docs;
-- restrict extraction to a rectangle (top, left, bottom, right in PDF points, top-left origin)
SELECT Tables
FROM OPENROWSET(BULK 'invoice.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { engine = "lattice", area = [72, 40, 760, 560] }') AS docs;
-- combine text and table options in one read
SELECT Path, Text, Tables
FROM OPENROWSET(BULK 'report.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { pages = "2-4", engine = "lattice", text_mode = "words" }') AS docs;
-- every PDF under 2024/ at any depth, skipping any that cannot be read
SELECT Path, Text
FROM OPENROWSET(BULK '2024/**/*.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { on_error = "skip" }') AS docs;
See PDF for the full PdfDocuments schema and guidance on choosing the extraction options, and Files and data services for the File provider and connection setup.
Reading Parquet files (BULK)#
The bulk form reads .parquet and .parq files from a registered File connection. Point
DATA_SOURCE at the connection and add FORMAT = 'PARQUET'; a correlation name (AS alias) is required,
and file references are resolved under the connection’s directory. Because a Parquet file carries its own
schema, the WITH clause is optional — omit it to return every column:
SELECT id, customer, amount
FROM OPENROWSET(BULK 'sales.parquet', DATA_SOURCE = 'files', FORMAT = 'PARQUET') AS p;
When the path ends in .parquet or .parq, the format is inferred and FORMAT may be omitted:
SELECT * FROM OPENROWSET(BULK 'sales.parquet', DATA_SOURCE = 'files') AS p;
Add WITH (...) to project and order a subset of columns; on Parquet it binds by column name, so the
listed columns may be a subset in any order:
SELECT amount, id
FROM OPENROWSET(BULK 'sales.parquet', DATA_SOURCE = 'files', FORMAT = 'PARQUET')
WITH (amount decimal(18,2), id bigint) AS p;
A WITH name must match the file’s column name exactly, case included: WITH (AMOUNT decimal(18,2))
over a column named amount is a column the file does not contain. A WITH column the file does not contain
is returned as all-NULL (not an error), and a column that is present is read as the declared type: a
declared bigint widens a 32-bit column, a shorter declared fractional scale truncates, and a declared time
or datetimeoffset recovers a value the Parquet schema cannot describe. A declared type the file’s own type
cannot be read as is refused with an error naming both and suggesting a type that works. Nested-field projection
with a '$.path' is not supported for Parquet yet; declare top-level columns only.
Read several files as one rowset by listing them or matching a glob — * within a folder, ** across
subfolders. The matched files must share the same schema:
-- an explicit list
SELECT id FROM OPENROWSET(BULK ('jan.parquet', 'feb.parquet'), DATA_SOURCE = 'files') AS p;
-- every Parquet file under 2024/ at any depth
SELECT id FROM OPENROWSET(BULK '2024/**/*.parquet', DATA_SOURCE = 'files') AS p;
A folder named key=value is a folder, not data: a partitioned layout such as year=2024/month=01/ adds
no year or month column to the result, and a WITH column named after a folder key reads as
all-NULL.
See Parquet for the Parquet type mapping and the imported virtual-table form.
Reading delimited files (BULK)#
The bulk form reads delimited text files from a registered File connection. Point DATA_SOURCE at
the connection and add FORMAT = 'CSV' (never inferred); a correlation name (AS alias) is required, and
every file reference is resolved under the connection’s directory. The schema is derived from the data: a header
row names the columns and the sampled values decide their types, so the WITH clause is optional:
SELECT id, name, price
FROM OPENROWSET(BULK 'sales.csv', DATA_SOURCE = 'files', FORMAT = 'CSV') AS sales;
A glob only reaches files the connection calls delimited or text - the extensions configured on the File
connection for delimited files and for text files. That keeps a broad pattern such as * from handing a PDF
or an archive to the delimited reader. A connection that declares no such extensions rejects a glob outright:
declare the extensions, or name the file.
SELECT * FROM OPENROWSET(BULK 'exports/*.txt', DATA_SOURCE = 'files', FORMAT = 'CSV') AS data;
A literal path is never filtered: it names one exact file and FORMAT already states how to read it, so
an explicitly named file is read whatever its extension.
SELECT * FROM OPENROWSET(BULK 'exports/quarterly.dat', DATA_SOURCE = 'files', FORMAT = 'CSV') AS data;
What a wildcard reaches#
A * matches within one folder or file name and never crosses a /; a ** segment spans folders. A
name beginning with . or _ is not reached by a wildcard - that is the convention the tools writing
partitioned folders follow, so _SUCCESS, _delta_log and .crc entries stay out of a data read. A
pattern segment that spells the character out reaches them: _* matches _SUCCESS.
Matching follows the server’s own file system, so it ignores letter case on Windows and respects it on Linux,
exactly as naming a file does. A ? and a [ are ordinary characters in a file name, not wildcards.
A path that ends in /, and a path that names a folder, read that folder’s files one level deep - the same
names skipped, and no recursion. The connection’s Find recursive setting governs how deep a wildcard may
reach: with it off, a wildcard sees only the files directly in the connection’s folders.
When nothing matches, the statement succeeds and returns no row, with a warning naming the expression. The one
exception is a statement with no WITH clause over a format whose schema comes from a file: there is then no
schema to describe, and the statement is rejected.
Reading several files as one rowset#
A list of paths, or a glob that matches more than one file, is read as one rowset: every matched file contributes its rows to the same result, in file order.
The files are resolved in a fixed, host-independent order - the paths in the order the statement wrote them, and each glob’s matches in name order. A file reached twice, named explicitly and matched by a glob as well, is read once.
That order is also what decides the result schema when the statement declares none:
Statement shape |
Result schema |
|---|---|
|
the declared columns are the schema |
no |
the columns of the first resolved file, as though you had written them as |
Naming a file ahead of the glob therefore pins which file the schema comes from. The anchor resolves first, and resolving again through the glob does not read it twice:
-- 'sales-2026-01.csv' resolves first, so the result schema is that file's schema;
-- the glob then contributes the remaining monthly exports
SELECT * FROM OPENROWSET(BULK ('sales-2026-01.csv', 'sales-*.csv'),
DATA_SOURCE = 'files', FORMAT = 'CSV') AS sales;
Only the files a pattern matches are opened, and they are opened one at a time as the read reaches them: a
month-scoped glob never touches the rest of the folder, and a TOP-limited read stops opening files as soon
as it has the rows it was asked for.
Because a file is opened only when the read reaches it, a file removed or made unreadable after the statement started fails that statement with an error naming the file.
How each file is matched onto the schema#
There is one schema and one matcher. Every file, the first one included, is matched onto the result schema in
its own right, so files that disagree about field order, about width, or about whether they carry a header row
all answer the same schema. Which files carry a header row is stated by HEADER_ROW - see Which files carry
a header row below. For each column of the schema, against each file, in this order:
The column |
Reads from |
|---|---|
states a source ordinal in |
that field of the file, counting from 1 |
otherwise, and a header row is in force for that file |
the field whose header name equals the column’s name, compared case-insensitively |
otherwise (no header row for that file) |
the field at the column’s own position in the schema |
matched nothing, or matched a field past the file’s last one |
|
Two consequences follow, and together they are what keeps a statement stable over a file set that changes. A
field no column of the schema claims is ignored, so a vendor that adds a column does not change what an
existing statement returns. And a column a file does not supply reads as NULL for that file’s rows, so an
older, narrower file does not fail the statement either.
Note
A mismatch is answered with NULL, never with an error, and that NULL is indistinguishable from a
value the file supplies as empty. So a file whose fields match no column of the schema yields rows of
NULL, silently, and a source column someone renamed reads as NULL rather than raising anything.
Under SELECT * the schema is the first resolved file’s, which for date-named files is the oldest and
narrowest one, so columns added later stay invisible. Declare the columns with WITH whenever the shape of
the result matters: it is the mechanism that enforces one.
Header, first row and separators#
Per-statement options layer over the connection’s delimited-file settings — a stated option wins, an absent one leaves the connection’s setting in force:
Option |
Meaning and behavior |
|---|---|
|
|
|
The 1-based number of the first row to read, applied in every file of the read; the rows before it are
skipped as physical lines, so a preamble whose shape differs from the data is fine. With |
|
The field separator ( |
|
The encoding, as a code page number or an encoding name, binding every file of the read. A byte-order mark in a file overrides a stated encoding — the mark is that file’s own statement of what it is. See Encoding, file by file below. |
|
Accepted only as the double quote ( |
An option this read cannot honour is rejected rather than ignored — ROWTERMINATOR, LASTROW,
DATAFILETYPE, MAXERRORS, ERRORFILE, ROWS_PER_BATCH, FORMATFILE and the
SINGLE_BLOB/SINGLE_CLOB/SINGLE_NCLOB forms all refuse with a clear error, so a statement never
quietly returns something other than what it asked for.
-- a two-line export banner precedes the header
SELECT id, name
FROM OPENROWSET(BULK 'export.csv', DATA_SOURCE = 'files', FORMAT = 'CSV',
FIRSTROW = 3, HEADER_ROW = TRUE) AS sales;
-- semicolon-separated, Central European encoding
SELECT *
FROM OPENROWSET(BULK 'orders.csv', DATA_SOURCE = 'files', FORMAT = 'CSV',
FIELDTERMINATOR = ';', CODEPAGE = '1250') AS orders;
Which files carry a header row#
Whether a file carries a header row cannot be read out of its bytes - it is a property of how the file set was
produced, so the statement states it. HEADER_ROW takes three values:
|
Meaning |
Effect per file |
|---|---|---|
|
every file carries a header row |
each file’s first row is consumed as that file’s header, and its fields are claimed by name, so a file whose columns are in another order still answers the same schema |
|
no file carries one |
nothing is consumed as a header; every file’s fields are claimed by position |
|
only the first resolved file carries one |
the first file’s header row is consumed and describes the whole set; the files after it are pure data, and their fields are claimed through that one header |
'FIRST_FILE' is for the split-export shape - a header written once, continuation files data-only:
-- sales-1.csv id,name
-- 1,widget
-- sales-2.csv 2,gadget (a continuation file, with no header of its own)
SELECT id, name
FROM OPENROWSET(BULK 'sales-*.csv', DATA_SOURCE = 'files', FORMAT = 'CSV',
HEADER_ROW = 'FIRST_FILE') AS sales;
Under HEADER_ROW = TRUE that second file’s only data row would be consumed as a header, matched against
nothing, and the file would read as all-NULL. With a single file TRUE and 'FIRST_FILE' say the same
thing, which is why the distinction only appears once a statement spans several files. Omitted, HEADER_ROW
falls back to the connection’s Column names in first row setting, which answers TRUE or FALSE and
never 'FIRST_FILE'; a value that is none of the three is refused rather than guessed at.
Leading rows in each file#
FIRSTROW and the connection’s Skip first # rows setting both apply per file - each file loses its own
leading rows - and they act at different points in the read:
FIRSTROWis pre-parser: it hides physical lines, counting blank and comment lines, and survives a preamble the parser cannot read - which is its reason to exist.Skipis post-parser: it consumes parsed records, so comment and blank lines do not count, and the skipped rows must be parseable under the data’s column count (a ragged preamble aborts it).Stating
FIRSTROWzeroesSkipfor the read.
The two combine with the header convention rather than replacing it: the leading-row skip applies in every file,
while the header is consumed wherever HEADER_ROW says one is. Under 'FIRST_FILE' the first file loses its
preamble and its header, and the files after it lose their preamble only.
Encoding, file by file#
The encoding is resolved when each file is opened, not once for the whole read, so a set whose files were produced by different servers is read correctly without splitting the statement. Detection runs per file, and each file’s own byte-order mark has the last word over what was detected.
CODEPAGE states the encoding for every file of the read - a stated encoding is an instruction, not a
hint, and it turns detection off. A byte-order mark still overrides it, per file, because the mark is that
file’s own statement of what it is.
Where the connection’s file format asks for the delimiter to be auto-detected, the separator is detected per
file exactly as the encoding is; stating FIELDTERMINATOR fixes one separator for every file of the read,
the way CODEPAGE fixes one encoding.
The WITH clause#
Add WITH (...) to state the result columns yourself. The declared columns are the schema of the whole
read, and each file is matched onto them by the rules above - by a stated ordinal wherever one is stated, else
by name, case-insensitively, where a header row is in force, else by the column’s own position in the list - so
the list may name a subset of the columns in any order, and each column keeps the type derived from the data:
SELECT price, id
FROM OPENROWSET(BULK 'sales.csv', DATA_SOURCE = 'files', FORMAT = 'CSV')
WITH (price decimal(18,2), id int) AS sales;
A WITH column that names no column of a file reads as NULL for that file’s rows and keeps the type the
statement declared for it, matching the Parquet form. Note that this is indistinguishable from a column the
file supplies as empty: a renamed source column therefore reads as NULL rather than as an error.
The WITH clause also accepts an explicit 1-based source-column ordinal after a column’s type. It pins
the column to that field of every file and overrides the by-name matching:
SELECT label, id
FROM OPENROWSET(BULK 'sales.csv', DATA_SOURCE = 'files', FORMAT = 'CSV')
WITH (label varchar(50) 2, id int 1) AS sales;
An ordinal below 1 is rejected while the statement is planned. An ordinal past a file’s last field is not:
it reads as NULL for that file’s rows, exactly as an unmatched name does, because how wide a file is
differs from file to file.
See CSV for the connection’s delimited-file settings and the imported virtual-table form.
File metadata functions#
Two functions answer, per row, which file the row came from. They are called on the correlation name of an
OPENROWSET (BULK ...) source and are available for every format a File connection reads - 'CSV',
'PARQUET', 'TEXT', 'XML', 'PDF' and 'EMAIL':
Call |
Returns |
|---|---|
|
the file’s own name, without its folders |
|
the file’s full path on the Querona server |
|
the text the |
Every one of them returns nvarchar(1024), and the two names are written in lower case: FILENAME()
is an ordinary function call, not this one.
An Excel connection answers them as well, per workbook: filename() is the workbook’s name, and a filter on
filepath(n) is decided per workbook in the same way, except under HEADER_ROW = 'FIRST_FILE', where the
first workbook carries the header every later one is read through.
SELECT r.filepath(1) AS [year], r.filepath(2) AS [month], r.filename() AS [file], COUNT_BIG(*) AS [rows]
FROM OPENROWSET(BULK 'events/year=*/month=*/*.parquet', DATA_SOURCE = 'files') AS r
WHERE r.filepath(1) = '2025'
GROUP BY r.filepath(1), r.filepath(2), r.filename();
This is how a partitioned folder layout is queried: the folder names are not columns (see above), and these functions are what brings them into the result.
The values are ordinary columns from there on, so they may be selected, filtered, grouped, ordered, wrapped in
other functions, used inside a view and inserted into a table. They are not included by SELECT *, which
returns the file’s own columns only.
What each wildcard captures#
Each * is one wildcard and captures the characters it consumed within one folder or file name; a **
segment is one wildcard and captures the folders it spanned, joined by /, or the whole remainder when it
ends the pattern. Wildcards are counted left to right across the whole path.
Pattern |
Matched file |
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
An ordinal past the number of wildcards the statement declares is rejected while the statement is planned. When
a path list mixes patterns of different wildcard counts, a row from a pattern with fewer answers NULL for
the ordinals that pattern does not have. A literal path and a folder path declare no wildcard at all, so
filepath(n) over them is rejected; filename() and filepath() still answer.
Differences from SQL Server#
filepath() returns the file’s full path on the server, with or without a wildcard in the pattern. SQL Server
reads its files from object storage and returns the path as the statement wrote it for a literal file; a local
folder has no equivalent there, so Querona answers one path for every query rather than a relative one for
some and an absolute one for others.
Querona also captures the ** forms it accepts beyond SQL Server’s final /**, as described above. SQL
Server rejects those patterns outright, so no query that runs on both products answers differently.
The functions are answered for the text, XML, PDF and e-mail reads too, where one file is one row. SQL Server has no such formats, so nothing a script can run on both products answers differently there either. The same holds for an Excel connection’s workbooks.