Published: 2026-08-25

How to Import Excel into a SQL Database: 3 Ways (SQL Server, PostgreSQL, MySQL)

There is no single "import Excel" button in SQL. What there is: a converter that turns a sheet into INSERT statements, a desktop client that maps a workbook onto a real table, and each engine's own bulk loader. They are good at genuinely different things, and picking the wrong one is how a fifteen-minute job becomes an afternoon. This guide walks all three — with the Excel-side traps that break imports before any SQL runs.

The short version

 Browser converterImport wizardNative bulk loader
Best forA one-off sheet, a seed script, a test fixtureLoading into a live table when the mapping is the hard partScripted, scheduled, or very large loads
Reads .xlsx directlyYesYesOnly SQL Server, only on Windows, only with the ACE provider
Needs a DB connectionNoYesYes
Size ceiling5,000 rows per sheet, with a warning100 MB workbook; 100,000 rows per run on the free Personal licenceThe file and the server
Per-column controlInferred types you edit by hand afterwardsInclude / exclude, target column, type, date order, fixed valueColumn list, format file, SET expressions
Bad rowsYour problem — you run the SQLAbort and roll back, or skip with an error reportMAXERRORS/ERRORFILE, warnings, or hard abort
Repeatable next monthRe-upload, re-copyRe-run the wizardIt is already a script

First: what Excel has already done to your data

Most failed spreadsheet imports fail on the Excel side, before a single statement runs. These are the ones that actually show up:

  • Leading zeros are gone. A zip code or SKU typed into a numeric cell is stored as a number: 00742 became 742 the moment it was entered. No import tool can recover it. Fix the column format in Excel (or re-source the data) before exporting.
  • Dates are serial numbers wearing a costume. Excel stores a date as a day count and renders it through the cell's format. Export to CSV and you get the rendering3/1/24 on a US machine, 01/03/2024 on a UK one, from the same workbook. Either format the column as yyyy-mm-dd before exporting, or import from the .xlsx itself, where the cell still carries a real date value.
  • Formulas export their last cached result. The file stores the value Excel computed when it was last saved. If a formula last evaluated to #DIV/0! or #N/A, that error text is what lands in the CSV — and no numeric column will take it.
  • Long numbers are already rounded. Excel keeps 15 significant digits. A 16-digit payment card number, or a 19-digit snowflake-style id, typed into a numeric cell has had its trailing digits quietly zeroed — and a 16-digit value typically renders as 1.23457E+15 on export, which is its own kind of surprise. These belong in text columns from the start.
  • Merged cells leave holes. The value lives in the top-left cell of the merge; every other cell in the block is empty. A "grouped" report sheet full of merges is not a table, and importing it produces a table full of NULLs.
  • The sheet ceiling is 1,048,576 rows × 16,384 columns. If someone handed you a workbook built from a bigger extract, Excel already truncated it. Go back to the source.
  • "CSV" is not one format. Excel's plain CSV (Comma delimited) writes the system ANSI code page, which mangles anything outside it; CSV UTF-8 writes UTF-8 with a byte-order mark. And in locales where the list separator is a semicolon, Excel writes semicolons, not commas — while your FIELDTERMINATOR = ',' keeps insisting otherwise.

The last one is the single best argument for skipping the CSV round-trip entirely when you can: an .xlsx cell carries its own type and its own text, with no locale, delimiter, or code page in between.

Way 1 — The free browser converter (one-off, no connection)

The fastest path when you have a sheet and no particular urge to connect a client to a database: drop the workbook into the Excel to SQL converter and copy out either a CREATE TABLE script, the INSERT statements, or both. Concretely, what it does:

  • Parses the workbook in the browser with ExcelJS, vendored on the page rather than pulled from a CDN. The file is never uploaded; disconnect after the page loads and it keeps working.
  • .xlsx and .xlsm only. The legacy binary .xls from Excel 97–2003 is not supported — open it and re-save as .xlsx first.
  • Multi-sheet workbooks get a sheet picker; switching sheets regenerates the SQL without re-uploading.
  • Reads each cell's real type rather than re-guessing from displayed text, then scans every value in a column to pick the narrowest type that fits: INT promoted to BIGINT past the 32-bit range, DECIMAL(p,s) sized from the widest value, DATE vs. the engine's datetime type depending on whether a time component exists, and a text bucket (50 / 100 / 255 / 1000, then MAX/TEXT/CLOB) sized to the longest value.
  • Emits for SQL Server, PostgreSQL, MySQL, Oracle, or SQLite, with each engine's identifier quoting — which matters more than it sounds, because spreadsheet headers are full of First Name and Order #.

The honest limits. Output is capped at 5,000 data rows per sheet; past that it emits the first 5,000 and says so rather than truncating quietly. INSERT rows are batched 500 per statement — SQL Server hard-caps a single VALUES list at 1,000 row expressions (error 10738), and MySQL statements that big start bumping into max_allowed_packet. Oracle before 23ai has no multi-row VALUES at all, so it gets one INSERT per row. And inference is inference: no primary keys, no foreign keys, no indexes are guessed. Treat the DDL as a first draft.

Use it when the sheet is small, you want a seed script or a test fixture in version control, or you are working somewhere you would rather not point a database client at anything. Skip it when the load repeats, the file is large, or the target table already exists with types you have to honour.

Way 2 — A desktop import wizard (repeatable, into a live table)

When the destination is a real table on a real server, the hard part stops being "turn cells into SQL" and becomes mapping, coercion, and what to do about the eleven rows that will not convert. That is the job of an import wizard. Jam SQL Studio's Data Import is a workspace tab with four steps — Source · Target · Columns · Import — and behaves identically across SQL Server, PostgreSQL, MySQL, Oracle, and SQLite. (It still carries a Beta badge; the numbers and behaviours below are what it does today.)

Data Import wizard Source step in Jam SQL Studio showing a parsed file in a capped preview grid with detected column kinds beside the parsing-options panel

Source — and the three Excel-shaped controls that matter

Pick the workbook and it is parsed immediately. Format reads Excel (XLSX); CSV / TSV / TXT and JSON / NDJSON come through the same step. Three controls exist specifically because spreadsheets are messy:

  • Sheet. A picker listing every worksheet; the first is selected by default, and switching re-runs the analysis against the new sheet.
  • HeaderFirst row or No header. Delimited files get header auto-detection; Excel assumes a header unless you say otherwise.
  • Skip lines before header. The answer to the export nobody asked for: a title row, a blank, a "Generated 2026-08-19" line, then the actual column names. Set it to 3 and the reader skips the first three non-empty rows before reading headers. There is no A1-style range picker — if your data starts at C7 and stops before column Q, delete the surroundings in Excel first.

The encoding picker (UTF-8, UTF-16 LE/BE, Latin-1, Windows-1250/1252, ISO-8859-2, GBK, Shift-JIS) is offered for delimited text and hidden for xlsx, because an .xlsx is a zip of XML with the encoding declared inside — there is nothing to guess. For CSV, detection is BOM-based plus that manual picker, not universal charset sniffing; a mojibake heuristic suggests you re-pick but never silently reinterprets your data.

The preview grid shows the first 100 rows, sortable, and a Filter rows control narrows the file by condition — with a scope segment reading Preview only or Preview + import, so you can explore without accidentally changing what gets loaded.

Type inference over the whole file, not a sample

This is the part that earns the wizard its keep against OPENROWSET. Every column is evaluated as a streaming lattice of candidate types — boolean, int32, int64, decimal, float64, date and timestamp in ISO / DMY / MDY, timestamptz, time, uuid, json — across every row in the file, keeping a failure count and up to three row-numbered example values per candidate. The narrowest survivor wins. Contrast with the ACE provider's 8-row guess, which quietly NULLs the disagreeing values at row 900.

Better still: a column where both DMY and MDY survive is not silently resolved. 01/03/2024, 05/06/2024, 11/12/2024 are all valid either way, so the wizard refuses to guess and raises a blocking banner until you pick a Date order. That is precisely the Excel trap from the first section, caught before it becomes three months of transposed dates. Because the failure counts are already computed, changing a type or a date order costs zero rescans.

Target

Two modes, and only two: Create new table or Add rows to existing table. New-table mode pre-fills the name from the filename, infers each column's engine-native type, and shows a live schema preview — names, types, generated primary key — before you commit. If the name already exists it says so and offers a one-click switch to appending. Existing-table mode gives a searchable table picker and a read-only view of the schema you are appending to.

The destination cascade is Connection → Database → Schema, with the schema field hidden on SQLite and MySQL and relabelled Schema (user) on Oracle. Azure Data Explorer connections are read-only and never appear.

Data Import wizard Columns step in Jam SQL Studio showing the column mapping grid with per-column include checkboxes, target columns, and editable types

Columns

In new-table mode, clicking a column header opens an editor with the column name, the type (per-engine presets plus free-text for anything exotic), Date order for temporal columns, and checkboxes for Nullable, Primary key, and Include in import. Unchecking include is how you drop a column.

In existing-table mode it becomes a mapping problem. Auto-matching lowercases both names and strips everything that is not a letter or digit, so First Name finds firstname and Order # finds order — which is exactly the shape spreadsheet headers arrive in. Each source column is claimed at most once. A target with no source shows — Not mapped — and can take Use fixed value… instead — a batch id, an import timestamp, a source-system tag — stored as raw text end to end, so a 19-digit id never round-trips through a JavaScript number. A coverage chip in the command bar counts the state out loud: 7 of 9 covered · 2 need a value, then All 9 covered.

Columns the database fills in are marked unavailable with the reason spelled out — Computed by the database, Generated identity — filled by the database, SQLite rowid — filled by the database — rather than failing at row 40,000. So are fixed values that cannot work: A repeated constant would violate the key on a primary or unique column with more than one row.

Import

The If a row fails choice is made before anything runs: Stop at the first error (roll back the transaction and report the failing row — see the SQLite caveat below) or Skip invalid rows (import the good ones; every skipped row lands in a CSV error report with columns row_number, column, value, reason, budgeted at 10,000 rows). If a mapped column is a key, each option gains a line saying what a duplicate key will do.

Under the hood the run holds one dedicated connection on four of the five engines and writes typed, parameterized batches inside a single transaction — SQL Server's new-table path uses real bulk copy; PostgreSQL sizes each batch to stay under the 65,535 bind-parameter cap; MySQL respects both the bind cap and an estimated max_allowed_packet budget; SQLite replays one prepared statement per row. Literal SQL appears in exactly one place: Generate script instead of executing, which hands you DDL plus INSERT statements and writes nothing.

SQLite is the exception to both of those. It has no connection pool — the app owns a single shared handle and an import borrows that rather than opening its own. And when the file happens to be open on the bundled sql.js fallback rather than better-sqlite3, every write rewrites the whole database, so BEGIN/COMMIT buy no atomicity at all: the session reports supportsRollback: false and the runner declines to claim a rollback it cannot perform. Stop at the first error still stops, but rows already written stay written. On better-sqlite3 — the normal case — rollback is real.

Existing-table mode can Empty the table first (TRUNCATE), behind a confirmation naming what it is about to delete — the actual count when the table's row count is already known, otherwise just all existing rows. On MySQL and Oracle only, that confirmation also warns that the truncate cannot be rolled back if the import then fails.

The honest limits

  • An .xlsx is loaded into memory in full, capped at 100 MB. Over that you get a straight refusal telling you to export to CSV. Delimited files are genuinely streamed; workbooks are not, and the cap exists to bound the memory that costs. (The streaming reader was tried and abandoned: on workbooks whose zip stores shared strings and styles after the worksheet — which Excel-adjacent writers routinely produce — it returned null for text cells and raw serial numbers for dates, with no error at all. A slower correct parse beats a fast wrong one.)
  • Formulas are never evaluated — the cached result is used, and a cell with no cached result, or one holding #N/A / #REF!, becomes NULL.
  • .xls is not supported. Only OOXML .xlsx. Re-save first.
  • Existing-table mode is append-only. No upsert, no update-if-exists, no skip-if-key-present — the review card says Append only — do not update existing rows rather than implying otherwise.
  • A new table's CREATE runs outside the row transaction on every engine. Cancel or fail partway and an empty table is left behind; a Delete partial table action cleans it up, but that is cleanup, not rollback.
  • PostgreSQL and Oracle identity columns are not offered as targets. The schema snapshot cannot tell GENERATED ALWAYS from BY DEFAULT, so it under-offers rather than failing at insert. SQL Server is the only engine where SET IDENTITY_INSERT is issued for you.
  • The free Personal licence caps a run at 100,000 rows. Bigger loads need Pro — or the native bulk loader below, which has no such limit.

After a successful new-table import — not on an existing-table append, and not on a run that failed — there is one step most tools skip: an optional card offering loose foreign key, JSON, and enum declarations inferred from what just landed. Spreadsheet data is exactly where those show up — a status column with six distinct values, an id column that clearly points at the lookup sheet you imported ten minutes ago — and declaring them costs nothing and changes no schema. Import another file then resets the file while keeping the destination and your column overrides, which is what makes "the same report, every Monday" bearable.

Use it when the load repeats, the target already exists, or you need to see the mapping before anything is written. Skip it when the load belongs in a deployment pipeline — that is what the next section is for.

Way 3 — Native SQL, engine by engine

The fastest and most scriptable option, and the only one that belongs in a scheduled job. The catch: with one Windows-only exception, no SQL engine reads .xlsx. Save the sheet as CSV first — CSV UTF-8, not plain CSV — and re-read the Excel gotchas above, because the CSV round-trip is where they all cash in.

SQL Server: read the workbook directly (Windows only)

SQL Server is the only one of the five that can query an .xlsx without an intermediate file, via the ACE OLE DB provider:

-- One-time, requires sysadmin. Understand the exposure before enabling this.
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'Ad Hoc Distributed Queries', 1;
RECONFIGURE;

-- The trailing $ on the sheet name is required.
SELECT *
INTO   dbo.sales_import
FROM   OPENROWSET(
         'Microsoft.ACE.OLEDB.12.0',
         'Excel 12.0 Xml;HDR=YES;IMEX=1;Database=C:\data\sales.xlsx',
         'SELECT * FROM [Sheet1$]');

Four things go wrong here, in roughly this order:

  • The path is the server's, not yours. C:\data\sales.xlsx is resolved on the machine running SQL Server, under the service account. Your laptop's C:\ is not involved.
  • The ACE provider must be installed on that server, at matching bitness — a 64-bit instance needs the 64-bit Access Database Engine redistributable. This is the source of the classic "Cannot create an instance of OLE DB provider 'Microsoft.ACE.OLEDB.12.0'".
  • There is no ACE on Linux or Azure SQL Database. If your instance is either, this route does not exist; go to CSV.
  • IMEX=1 is doing real work — but it is a hint, not a guarantee. ACE decides each column's type by sampling the first 8 rows (the TypeGuessRows registry value) and hands back NULL for later values that disagree — so a numeric column with a stray N/A at row 900 silently loses it, with no error. IMEX=1 usually gets mixed columns back as text instead, but the actual behaviour also depends on the provider's TypeGuessRows and ImportMixedTypes registry settings on the server. Spot-check a suspect column after the load rather than assuming.

SQL Server: BULK INSERT from CSV

CREATE TABLE dbo.sales_import (
    order_id   INT            NOT NULL,
    customer   NVARCHAR(200)  NULL,
    order_date DATE           NULL,
    amount     DECIMAL(12, 2) NULL
);

BULK INSERT dbo.sales_import
FROM 'C:\data\sales.csv'
WITH (
    FORMAT          = 'CSV',     -- SQL Server 2017 (14.x) and later
    FIELDQUOTE      = '"',       -- SQL Server 2017 (14.x) and later
    CODEPAGE        = '65001',   -- UTF-8; pair with Excel's "CSV UTF-8" export
    FIRSTROW        = 2,         -- see the caveat below
    FIELDTERMINATOR = ',',
    ROWTERMINATOR   = '0x0a',    -- LF; omit for Windows-authored CRLF files
    TABLOCK
);

Notes that are easy to lose an hour to:

  • FORMAT = 'CSV' and FIELDQUOTE arrived in SQL Server 2017. On 2016 and earlier you need a format file, or a CSV with no quoted fields at all.
  • Microsoft's own documentation is blunt that FIRSTROW is not a header-skip feature — skipped rows are counted by field terminators, not validated. It works for a simple one-line header; it will not save you from a header that contains an embedded newline.
  • The default ROWTERMINATOR is '\r\n'. Excel on Windows writes CRLF, so leave it out; a file that has been through a Mac, a Linux box, or Git with line-ending normalization needs '0x0a'.
  • Permissions are INSERT plus ADMINISTER BULK OPERATIONS. And the file, again, is read by the server.

To load a file that lives on your machine instead, use bcp, which runs client-side:

bcp dbo.sales_import in sales.csv \
  -S localhost -d salesdb -T \
  -c -t "," -F 2 -C 65001

With one real caveat: bcp in character mode has no FIELDQUOTE equivalent. -t "," splits on every comma, including the ones inside quoted cells — so a file with "Acme, Inc." in it, exactly the case the BULK INSERT snippet above handles correctly, will land shifted by a column. If your CSV has quoted fields containing the delimiter, get the file somewhere the server can read it and use BULK INSERT … FORMAT = 'CSV', or load it through a client that parses CSV properly.

PostgreSQL: COPY, and why you probably want \copy

CREATE TABLE sales_import (
    order_id   integer NOT NULL,
    customer   text,
    order_date date,
    amount     numeric(12, 2)
);

From psql, on the client machine, needing no special role:

\copy sales_import FROM 'sales.csv' WITH (FORMAT csv, HEADER match, ENCODING 'UTF8')

Server-side, reading a file the server can see:

COPY sales_import
FROM '/var/lib/postgresql/import/sales.csv'
WITH (
    FORMAT     csv,
    HEADER     true,
    ENCODING   'UTF8',
    FORCE_NULL (amount, order_date)
);
  • \copy vs COPY is the distinction that trips everyone. \copy is a psql meta-command that streams the file over the connection from the client. COPY … FROM 'file' makes the server process open the path, and requires superuser or the pg_read_server_files role. If you are importing a spreadsheet from a laptop, you want \copy.
  • HEADER match (PostgreSQL 15+) is worth the upgrade. It verifies that the CSV's header names and count match the table, and errors out if they do not — instead of HEADER true, which just discards line one and cheerfully loads your columns in the wrong order when someone reorders the spreadsheet.
  • FORCE_NULL is what fixes empty Excel cells. Excel writes an empty cell as an empty field; if the column is quoted, that arrives as "", which CSV mode treats as an empty string and a numeric column rejects. FORCE_NULL on those columns turns it into NULL. PostgreSQL 16 also added a DEFAULT option for a sentinel string meaning "use the column default".

MySQL: LOAD DATA

CREATE TABLE sales_import (
    order_id   INT NOT NULL,
    customer   VARCHAR(200),
    order_date DATE,
    amount     DECIMAL(12, 2)
);

LOAD DATA LOCAL INFILE 'sales.csv'
INTO TABLE sales_import
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 LINES
(order_id, customer, @order_date, @amount)
SET order_date = STR_TO_DATE(NULLIF(@order_date, ''), '%m/%d/%Y'),
    amount     = NULLIF(@amount, '');

The user-variable trick in the column list is the whole reason to reach for LOAD DATA over a client: read the raw field into @order_date, then convert it in SET. That is where you undo Excel's locale-formatted dates and its empty-string-instead-of-NULL habit, without touching the file.

  • local_infile has defaulted to OFF since MySQL 8.0.2, and the client library opts in separately. Both sides must agree: SET GLOBAL local_infile = 1; on the server, mysql --local-infile=1 on the client. Getting exactly one of them right produces an error that reads like a permissions problem.
  • Without LOCAL the statement needs the FILE privilege and the file must live inside the directory named by secure_file_priv (and if that variable is NULL, LOAD DATA from a file is disabled outright).
  • LOCAL downgrades data errors to warnings and keeps loading. That is convenient right up until it isn't: always run SHOW WARNINGS; afterwards and compare the row count you expected against the row count you got.
  • Excel's CSV UTF-8 writes a byte-order mark. Because it sits on line one, IGNORE 1 LINES disposes of it along with the header. If you are loading a headerless file, strip the BOM first or your first column's first value carries three invisible bytes.

Oracle and SQLite, briefly

  • Oracle — SQL*Loader (sqlldr) with a control file, or an external table over the CSV. Neither reads .xlsx. Watch NLS_DATE_FORMAT: a date literal that works in your session may not work in the job's.
  • SQLite — in the sqlite3 CLI, .import --csv --skip 1 sales.csv sales_import appends to an existing table. If the table does not exist, drop the --skip 1 and .import creates it using the header row for column names — every column typed TEXT, which is usually not what you want for anything you plan to sort or aggregate.

So which one?

  1. One sheet, one time, and you want the SQL in a file. Browser converter. Thirty seconds, nothing installed, nothing connected.
  2. Into a live table, and you need to see the mapping first. Import wizard. The value is not the insert — it is the type preview, the per-column overrides, and knowing in advance what happens to the rows that will not convert.
  3. It runs again next month without you. Native bulk loader. Write it once, put it in the repo, and accept the CSV round-trip as the price.

A pattern worth naming: do the first load with a wizard and the repeat loads natively. The wizard is where you discover that Order # has an embedded newline on row 812 and that the amount column has three values formatted as text. Once you know, the BULK INSERT or \copy is easy to write — and the wizard's Generate script instead of executing toggle will even hand you the DDL to start from.

Quick Answers

Short answers to the questions that come up most often.

Q: How do I import an Excel file into SQL Server?

A: Three practical routes. For a one-off sheet, convert it to INSERT statements in the browser and run them. For a repeatable load into a live database, use a client with an import wizard that maps columns and validates types. For large or scheduled loads, save the sheet as CSV and use BULK INSERT or bcp — or, if SQL Server runs on Windows and the ACE OLE DB provider is installed there, read the .xlsx directly with OPENROWSET. Azure SQL Database and SQL Server on Linux have no ACE provider, so CSV is the only native path there.

Q: Can PostgreSQL or MySQL read an .xlsx file directly?

A: No. Neither engine can open an Excel workbook. COPY and LOAD DATA read delimited text, so an .xlsx has to become CSV first — either by exporting from Excel, or by having a client tool parse the workbook and send rows over the connection. SQL Server is the only one of the five engines with a native way to query a workbook, and only through the ACE OLE DB provider on Windows.

Q: Why do my leading zeros and dates break when I import from Excel?

A: Because Excel stores a display format, not the text you see. A zip code typed into a numeric cell loses its leading zeros before the file is ever saved. A date is stored as a serial number and exported using whatever format the cell had, so the same workbook produces 3/1/24 on a US machine and 01/03/2024 on a UK one. Format the column as text or as an unambiguous yyyy-mm-dd date before exporting, or import from the .xlsx itself, where each cell still carries a real type.

Q: How many rows can I import from a spreadsheet?

A: An Excel worksheet holds at most 1,048,576 rows and 16,384 columns, so anything larger was already truncated before you got it. The free browser converter on this site caps output at 5,000 data rows per sheet and warns instead of silently cutting. Jam SQL Studio's import wizard reads workbooks up to 100 MB and imports up to 100,000 rows per run on the free Personal licence. A native bulk loader is bounded only by the file and the server.

Q: Should I use BULK INSERT, COPY, or an import wizard?

A: Use the native bulk loader when the load is scripted, scheduled, or measured in millions of rows — it is the fastest path and the only one that belongs in a deployment pipeline. Use an import wizard when the mapping is the hard part: unfamiliar column names, types you want to review before the table exists, rows you expect to fail. Use the browser converter when you just need a seed script and do not want to connect anything to anything.

Q: Why does LOAD DATA LOCAL INFILE fail with an error about local_infile?

A: The local_infile server system variable has defaulted to OFF since MySQL 8.0.2, and the client library has to opt in separately. Both sides must agree: set GLOBAL local_infile = 1 on the server, and start the client with --local-infile=1. Without LOCAL, the statement needs the FILE privilege and the file must sit inside the directory named by secure_file_priv — which is why LOCAL is usually the easier route from a laptop.

The takeaway

"Import Excel into SQL" is three different jobs wearing one sentence. The converter is for getting SQL out of a sheet. The wizard is for getting a sheet into a table you have to live with afterwards. The bulk loader is for doing it again next month without you. Pick by which of those you are actually doing — and spend the first five minutes on the spreadsheet, not the SQL, because that is where the import was going to fail anyway.

Spreadsheet in, Table out

Import .xlsx, CSV, and JSON into SQL Server, PostgreSQL, MySQL, Oracle, or SQLite — column mapping, type preview, error policy, and a generate-script escape hatch. Free for personal use.

Related