HTML Table to SQL Converter

Take a published table into a database. Column types come from reading whole columns, merged cells are resolved first, and the identifier quoting matches the dialect you picked.

To convert an HTML table to SQL, paste the markup above and choose a dialect. Every table on the page is listed; the one you pick becomes a CREATE TABLE with types inferred from all the values in each column, followed by INSERT statements batched at a size you set. Empty cells become NULL and identifiers are quoted the way your dialect spells them.

Want to clean the rows before you load them? Open the app

The reference data is published and you need it queryable

Postal code ranges, tax bands, country codes, airport identifiers, holiday calendars, allocation tables. Somebody authoritative publishes them as a page, and your application needs to join against them.

The alternatives are all worse than they sound. Writing a scraper is a maintenance commitment for something that changes once a year. Copying into a spreadsheet and importing loses the types and takes as long. Typing it out invites the transposition error that surfaces in production three months later.

A SQL script you can read before you run it is the shortest path that stays reviewable, which matters when the load is going anywhere near production.

Merged cells, resolved before anything is typed

Published tables are full of merged cells, because they were laid out for a reader. A category label spanning three rows, a group heading spanning four columns. A converter that ignores rowspan produces rows that are one cell short, and the values after the gap shift left into the wrong columns.

That failure is silent and it is the worst kind: the INSERT statements are syntactically perfect and every value is in the wrong field. You find out when a query returns a quarter label where a revenue figure should be.

Here a colspan repeats its value across the columns it covers and a rowspan fills it down, so the rectangle is correct before a single type is decided. rowspan="0" and a rowspan overshooting the last row are both handled.

Worked example, in SQLite

A published table with a merged label:

<table>
  <caption>regions</caption>
  <tr><th>Region</th><th>Code</th><th>Quarter</th><th>Revenue</th></tr>
  <tr><td rowspan="2">North Coast</td><td rowspan="2">01</td><td>Q1</td><td>1840</td></tr>
  <tr><td>Q2</td><td></td></tr>
</table>

And the script:

CREATE TABLE "regions" (
  "Region" TEXT,
  "Code" TEXT,
  "Quarter" TEXT,
  "Revenue" NUMERIC
);

INSERT INTO "regions" ("Region", "Code", "Quarter", "Revenue") VALUES
  ('North Coast', '01', 'Q1', 1840),
  ('North Coast', '01', 'Q2', NULL);

The merged cells were filled down, so the second row is complete. The empty revenue cell is NULL rather than '', which is what a nullable numeric column needs and what a competing converter on this term gets wrong. Code is text because of the padded zero.

Types, dialects and the traps in both

  • A column is numeric only when every value round trips exactly. One padded code makes the column text, which is what keeps it joinable.
  • Integer versus decimal follows the values, so a column with no fractional part anywhere gets the exact type.
  • MySQL gets DECIMAL(20,0) rather than a bare NUMERIC, which there means DECIMAL(10,0) and silently refuses an eleven-digit value.
  • Identifier quoting is double quotes in Postgres and SQLite, backticks in MySQL, brackets in SQL Server. Header cells from a page often have spaces in them, so this is load bearing rather than decorative.
  • No VARCHAR sized from a sample. A width fitted to the rows you happened to paste runs today and truncates later.
  • Duplicate headers are renamed, so two columns both called 2024 become 2024 and 2024_2 rather than one of them being lost to a collision.

Before you run it

Read the CREATE TABLE. It is four lines and it is the part worth checking, because it encodes every assumption the conversion made about your data. If a column you know is a date came out as TEXT, changing the word is a two-second edit and the load will be better for it.

The CREATE TABLE can also be switched off entirely when the table exists and you are refreshing it. Rows batch five hundred at a time by default, which is well inside what every client will send.

Frequently Asked Questions

Does it handle merged cells before generating the INSERTs?

Yes, and it is the most important thing on this page. A rowspan fills down and a colspan repeats across, so the rectangle is correct first. Ignoring spans produces syntactically perfect INSERTs with every value one column out, which is a silent and expensive failure.

Which dialects are available?

Postgres, MySQL, SQLite and SQL Server. The choice changes the identifier quote, the boolean literal, the text type and the exact-number type, all of which fail the first statement if they are wrong.

What happens to an empty cell?

NULL, in every column type, which is what a nullable column is for. Writing an empty string instead turns "we do not know" into "we know it is nothing", and a competing converter on this term does exactly that.

My table has two columns with the same header. What happens?

The second is renamed, so 2024 and 2024 become 2024 and 2024_2, with a note. Left colliding, the second column would overwrite the first and you would lose a column of data without being told.

Are text columns sized from my data?

No. No VARCHAR(27) fitted to the rows you pasted, because that runs fine now and truncates the moment the full table arrives. Text columns are TEXT, or VARCHAR(MAX) on SQL Server.

Can I skip the CREATE TABLE?

Yes, one switch gives INSERTs only. That is the usual choice when the table already exists and you are refreshing published reference data rather than creating it.

Make the published table queryable

Spans resolved, real column types, batched INSERTs, four dialects.

Back to the converter