StringToolsStringTools

CREATE TABLE Generator for Four SQL Dialects

Describe a table once in the column editor and read the statement PostgreSQL, MySQL, SQLite or SQL Server actually expects. Flip the dialect and the type names, the identifier quoting and the auto-increment syntax all change with it — because the same table really is different SQL in each one.

Table & dialect

Changing the dialect rewrites the type names, the identifier quoting, and the auto-increment syntax — the same table, spelled the way that database expects it.

Columns

Statement options

PostgreSQL SQL

-- PostgreSQL — generated from a column editor. Review before running it.

CREATE TABLE IF NOT EXISTS "users" (
    "id" BIGINT GENERATED ALWAYS AS IDENTITY,
    "email" VARCHAR(255) NOT NULL UNIQUE,
    "full_name" VARCHAR(120),
    "status" VARCHAR(20) NOT NULL DEFAULT 'active',
    "is_active" BOOLEAN NOT NULL,
    "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT "pk_users" PRIMARY KEY ("id")
);

COMMENT ON COLUMN "users"."email" IS 'Login identity';

CREATE INDEX IF NOT EXISTS "idx_users_full_name" ON "users" ("full_name");

PostgreSQL will not refresh updated_at on its own — it needs a trigger (or you set it in your UPDATE statements). Only MySQL can do it in the column definition.

This is a starting point, not validated SQL. Nothing here is parsed or executed against a database, so it is not checked for correctness. Dialects and versions differ — a clause that is fine on PostgreSQL 16 may not be on 9.6, and MySQL and MariaDB drift apart. Read the statement, adjust it for your schema and version, and run it on a scratch database before production.

Everything runs in your browser. Nothing you type is uploaded, and no database is contacted — this tool only generates text.

TL;DR

Copying a PostgreSQL CREATE TABLE into MySQL is how people lose an afternoon. Four things change between dialects and all four are in the first few lines: identifier quoting ("name" vs `name` vs [name]), type names (nothing outside PostgreSQL knows JSONB, TIMESTAMPTZ or BYTEA), auto-increment syntax, and which IF NOT EXISTS clauses exist at all. Pick the dialect first, then read the output — and review it before you run it, because nothing here is validated.

One table, four genuinely different statements

Below is the same five-column users table — an auto-incrementing key, a unique email carrying a comment, a status with a text default, a boolean flag and a created timestamp — as each of the four dialects wants it written. Nothing about the design changed. Almost every line did.

PostgreSQL

CREATE TABLE IF NOT EXISTS "users" (
    "id" BIGINT GENERATED ALWAYS AS IDENTITY,
    "email" VARCHAR(255) NOT NULL UNIQUE,
    "status" VARCHAR(20) NOT NULL DEFAULT 'active',
    "is_active" BOOLEAN NOT NULL,
    "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT "pk_users" PRIMARY KEY ("id")
);

COMMENT ON COLUMN "users"."email" IS 'Login identity';

Double-quoted names, an identity column, a real BOOLEAN and a zone-aware TIMESTAMPTZ. Column comments become separate COMMENT ON COLUMN statements.

MySQL / MariaDB

CREATE TABLE IF NOT EXISTS `users` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `email` VARCHAR(255) NOT NULL UNIQUE COMMENT 'Login identity',
    `status` VARCHAR(20) NOT NULL DEFAULT 'active',
    `is_active` TINYINT(1) NOT NULL,
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT `pk_users` PRIMARY KEY (`id`)
);

Backtick names, AUTO_INCREMENT, TINYINT(1) standing in for a boolean, and the comment folded into the column definition itself.

SQLite

CREATE TABLE IF NOT EXISTS "users" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "email" TEXT NOT NULL UNIQUE,  -- Login identity
    "status" TEXT NOT NULL DEFAULT 'active',
    "is_active" INTEGER NOT NULL,
    "created_at" TEXT NOT NULL DEFAULT (datetime('now'))
);

AUTOINCREMENT is only legal inline on one INTEGER PRIMARY KEY, so the table-level constraint disappears. VARCHAR and BOOLEAN both collapse to TEXT and INTEGER.

SQL Server

IF OBJECT_ID(N'[users]', N'U') IS NULL
BEGIN
    CREATE TABLE [users] (
        [id] BIGINT IDENTITY(1,1) NOT NULL,
        [email] NVARCHAR(255) NOT NULL UNIQUE,  -- Login identity
        [status] NVARCHAR(20) NOT NULL DEFAULT 'active',
        [is_active] BIT NOT NULL,
        [created_at] DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
        CONSTRAINT [pk_users] PRIMARY KEY ([id])
    );
END;

Bracketed names, IDENTITY(1,1), BIT for booleans, DATETIME2 for timestamps — and no IF NOT EXISTS, so the whole statement is wrapped in an OBJECT_ID guard.

Count the differences: three quoting styles, four auto-increment spellings, four boolean representations, four timestamp types, two places a column comment can live, and one dialect that cannot say IF NOT EXISTS at all. That is why a snippet from a PostgreSQL answer on Stack Overflow rarely survives contact with MySQL.

The type translation table

This is the mapping the generator applies when you switch dialects. It is worth keeping around even when you are writing the SQL by hand — the rows that surprise people most are boolean, JSON and UUID.

What you wantPostgreSQLMySQLSQLiteSQL Server
Auto-increment keyBIGINT GENERATED ALWAYS AS IDENTITYBIGINT AUTO_INCREMENTINTEGER PRIMARY KEY AUTOINCREMENTBIGINT IDENTITY(1,1)
BooleanBOOLEANTINYINT(1)INTEGERBIT
Text, capped lengthVARCHAR(n)VARCHAR(n)TEXTNVARCHAR(n)
Text, unboundedTEXTTEXTTEXTNVARCHAR(MAX)
Date + time, no zoneTIMESTAMPDATETIMETEXTDATETIME2
Date + time with zoneTIMESTAMPTZTIMESTAMPTEXTDATETIMEOFFSET
UUIDUUIDCHAR(36)TEXTUNIQUEIDENTIFIER
JSONJSONBJSONTEXTNVARCHAR(MAX)
Exact decimalNUMERIC(p,s)DECIMAL(p,s)NUMERIC(p,s)DECIMAL(p,s)
Approximate floatDOUBLE PRECISIONDOUBLEREALFLOAT
Binary blobBYTEABLOBBLOBVARBINARY(MAX)

Two rows deserve a footnote. SQLite’s column looks lazy because SQLite uses type affinity: VARCHAR(50), DATE and DATETIME are all accepted, all stored as text, and the length is never enforced — the declared type is mostly documentation. And MySQL’s CHAR(36) UUID costs 36 bytes per row where PostgreSQL and SQL Server store 16; if the table gets large, consider BINARY(16) with UUID_TO_BIN() instead of what this tool emits.

Quoting, auto-increment, and the clauses that do not exist

These three columns break more pasted SQL than the types do, because they break on line one rather than line nine.

DialectIdentifier quotingAuto-increment clauseCREATE TABLE IF NOT EXISTSCREATE INDEX IF NOT EXISTS
PostgreSQL"users"GENERATED ALWAYS AS IDENTITY (PG 10+), or SERIAL / BIGSERIALYesYes
MySQL / MariaDB`users`AUTO_INCREMENT (must be a key)YesNo on MySQL; MariaDB allows it
SQLite"users"AUTOINCREMENT — inline only, on one INTEGER PRIMARY KEYYesYes
SQL Server[users]IDENTITY(1,1)No— use an IF OBJECT_ID guardNo

The generator handles each of these for you rather than pretending they are the same. Tick IF NOT EXISTS on SQL Server and it wraps the whole statement in IF OBJECT_ID(N'[users]', N'U') IS NULL BEGIN … END;, and says so in a note. Ask for AUTOINCREMENT on a SQLite table whose key is a BIGINT, or part of a composite key, and it drops the keyword and explains why — SQLite requires the literal words INTEGER PRIMARY KEY AUTOINCREMENT on one column. Turn the index block on for MySQL or SQL Server and it warns that re-running those CREATE INDEX statements will error, because neither accepts IF NOT EXISTS there. Rerunnable migrations are a per-dialect problem, not a checkbox.

Why the default value has an “expr” toggle

A default is either data or SQL, and the database cannot tell which you meant from the characters alone. DEFAULT 'active' stores six letters. DEFAULT CURRENT_TIMESTAMP calls a function every time a row is inserted. Same slot in the grammar, completely different behaviour — which is why each column here has a small toggle that flips between 'text' and expr.

Guessing wrong fails in two different ways, and only one of them is loud:

  • Quoting something that should be an expression gives you DEFAULT 'CURRENT_TIMESTAMP'. On a date column that errors immediately. On a text column it succeeds — and every row silently gets the literal string CURRENT_TIMESTAMP. You find out weeks later.
  • Leaving a literal unquoted gives you DEFAULT active, which the parser reads as an identifier, not a word. That one at least fails at CREATE TABLE time.

The tool nudges you when the two disagree: type 0 or NOW() into a quoted default and it offers to switch the column to an expression, and vice versa. It also flags booleans — SQLite and SQL Server have no TRUE / FALSE keyword in a default, so those want 1 and 0. And when you enable the timestamps helper it writes a different expression per dialect: NOW() on PostgreSQL, CURRENT_TIMESTAMP on MySQL, (datetime('now')) on SQLite, SYSUTCDATETIME() on SQL Server. Only MySQL can keep updated_at fresh from the column definition itself, with ON UPDATE CURRENT_TIMESTAMP; the other three need a trigger or an explicit assignment in every UPDATE, and the tool says so instead of quietly leaving you with a column that never changes.

Choosing the primary key type

This is the one decision on the page that is genuinely hard to reverse, so it is worth two minutes before you tick the auto-increment box.

A bigint identity key is eight bytes, sorts in insert order, is trivial to read in a log line, and is the right default for a single database. Its costs are real but narrow: ids can only be minted by the database, so you cannot generate one client-side before the insert; and the numbers are guessable, so exposing /orders/1042 in a URL tells the world roughly how many orders you have taken.

A UUID key fixes exactly those two problems and charges you for it. It is 16 bytes natively — 36 as CHAR(36) on MySQL — and every secondary index carries a copy. The bigger cost is ordering: a random UUID v4 lands in a different place in the index on every insert, which is painful in MySQL InnoDB and SQL Server where the table is physically stored in primary key order. If you want UUIDs, use the time-ordered UUID v7, which puts a millisecond timestamp in the leading bits so new rows still append to the end of the index. Our UUID generator goes into the v4 versus v7 trade-off properly and will produce either.

A practical middle path many teams settle on: keep the bigint identity as the internal primary key and add a separate indexed UUID column as the public identifier. You pay for one extra index instead of paying on every index, and nothing in your URLs leaks row counts. Whichever you pick, tick more than one column as Primary key here and the generator emits a composite CONSTRAINT "pk_table" PRIMARY KEY (a, b) — the normal shape for a join table.

What this generator does not do

The output is a starting point, not validated SQL. Read it before you run it against a real database — ideally run it against a scratch one first. Concretely:

  • Nothing is parsed or executed. The statement is assembled as text and never checked for correctness, so a typo in a default expression sails straight through.
  • Versions differ. Identity columns need PostgreSQL 10 or newer, DROP TABLE IF EXISTS needs SQL Server 2016 or newer, and MySQL and MariaDB have drifted apart. The dialect picker has no version picker.
  • No foreign keys, check constraints, partitioning or storage options. It builds columns, one primary key, unique flags, defaults, comments and simple single-column indexes. Anything else you add by hand.
  • DROP TABLE really drops the table. The optional drop line destroys the existing table and every row in it, with no undo. The tool warns you; the database will not.
  • Comments are not portable. MySQL stores them inline and PostgreSQL via COMMENT ON COLUMN. SQLite has no column comments at all, and SQL Server keeps descriptions in extended properties, so for those two your text is emitted as -- comments that the schema does not retain.
  • Nothing leaves your browser. No database is ever contacted, no connection string is asked for, and your column names are not uploaded anywhere. This tool only generates text, and there are no accounts and no saving.

Frequently asked questions

Why does the same CREATE TABLE fail on MySQL after working on PostgreSQL?

Because almost every part of the statement is spelled differently. PostgreSQL quotes identifiers with double quotes and MySQL uses backticks, PostgreSQL writes GENERATED ALWAYS AS IDENTITY where MySQL writes AUTO_INCREMENT, and types like TIMESTAMPTZ, JSONB and BYTEA do not exist in MySQL at all. A Postgres snippet pasted into MySQL usually fails on the first line that contains a quoted name.

Does SQL Server support CREATE TABLE IF NOT EXISTS?

No. PostgreSQL, MySQL and SQLite all accept CREATE TABLE IF NOT EXISTS, but SQL Server has never added it. The standard workaround is to wrap the statement in an IF OBJECT_ID(N'table', N'U') IS NULL guard, which is exactly what this generator emits when you tick IF NOT EXISTS on the SQL Server dialect.

Why is my SQLite AUTOINCREMENT being ignored?

SQLite only accepts AUTOINCREMENT on a single column declared inline as INTEGER PRIMARY KEY AUTOINCREMENT. It cannot be a BIGINT, it cannot be part of a composite key, and it cannot be attached to a table-level PRIMARY KEY constraint. If your table does not fit that shape the generator drops the keyword and tells you why. In practice you rarely need it: a plain INTEGER PRIMARY KEY is already an alias for the rowid and assigns ids on its own.

Should I quote a default value or not?

Quote it when it is data and leave it bare when it is SQL. DEFAULT 'active' stores the six characters, while DEFAULT CURRENT_TIMESTAMP calls a function at insert time. Getting it backwards is quiet: quoting CURRENT_TIMESTAMP either errors on a date column or silently stores the literal text on a text column. The tool has an expr toggle per column and warns when the value looks like the other kind.

Should my primary key be a bigint or a UUID?

A bigint identity is smaller, faster to index and easier to read in logs, so it is the right default for a single database. A UUID is worth the cost when ids must be generated outside the database, merged across shards, or exposed publicly without leaking row counts. If you choose a UUID, prefer the time-ordered UUID v7 over the fully random v4, because random keys scatter inserts across a clustered index.

Is the generated SQL safe to run straight away?

Treat it as a starting point rather than validated SQL. Nothing is parsed or executed, so it is not checked for correctness, and dialects drift between versions: identity columns need PostgreSQL 10 or newer, DROP TABLE IF EXISTS needs SQL Server 2016 or newer, and MySQL and MariaDB no longer agree on everything. Read the statement, adjust it, and run it against a scratch database first.