Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 3 – Data types and built-in functions

Data types and type conversion

Choose SQL types for numbers, text, dates and booleans, convert values with CAST, and predict what PostgreSQL, MySQL and SQLite do with input that does not fit.

  • Beginner
  • 25 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Choose suitable types for integers, decimals, text, dates and booleans in each engine
  • Convert values with CAST, and with :: in PostgreSQL
  • Predict what each engine does with input that does not fit the type
  • Inspect the type of a value with typeof() in SQLite and pg_typeof() in PostgreSQL

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose suitable types for integers, decimals, text, dates and booleans in each engine
  • Convert values with CAST, and with :: in PostgreSQL
  • Predict what each engine does with input that does not fit the type
  • Inspect the type of a value with typeof() in SQLite and pg_typeof() in PostgreSQL

Before you start

On this page

Every value in SQL has a data type: a whole number, a decimal, text, a date, true or false. A column’s type decides what the database stores, how it compares and sorts the values, and what happens to input that does not fit. Type conversion turns a value of one type into another, written CAST(value AS type) in all three engines of this track. The engines agree on clean input and disagree on messy input: given the text '12abc' for a whole number, PostgreSQL stops with an error, MySQL converts what it can and warns, and SQLite either converts what it can or keeps the text as it is. This lesson shows the types to choose, how to look at a value’s type, and what each engine does with input that does not fit.

The examples below run in your browser on SQLite 3.49.1, the engine inside sql.js 1.14.2. For PostgreSQL 18 and MySQL 8.4, the tables summarise what their manuals specify.

The types you will use most

Common data types in PostgreSQL, MySQL and SQLite
Criterion PostgreSQL 18MySQL 8.4SQLite 3.49
Whole numbers integer (4 bytes), bigint (8 bytes)INT (4 bytes), BIGINT (8 bytes)INTEGER (up to 8 bytes)
Exact decimals, such as money numeric(p, s)DECIMAL(p, s)None: store whole paise as INTEGER
Approximate numbers double precision, realDOUBLE, FLOATREAL (8-byte floating point)
Text text, varchar(n)VARCHAR(n), TEXTTEXT
True or false booleanBOOLEAN, a synonym for TINYINT(1)INTEGER 0 or 1
Dates and times date, timestamp, timestamptzDATE, DATETIME, TIMESTAMPTEXT in year-month-day form, or numbers

Three choices cover most tables. Counts and identifiers that you do arithmetic on go in an integer type: bigint when the values can pass about 2.1 billion, the limit of a 4-byte integer. Money goes in an exact type, numeric or DECIMAL, or as whole paise in an integer column, never in a floating-point type; the next lesson shows why. Codes that only look like numbers, such as phone numbers, postal codes and account numbers, go in a text column, because nobody adds them up and their leading zeros matter.

True and false are the odd ones out. Only PostgreSQL has a real boolean type. In MySQL BOOLEAN is another name for a one-byte integer, and TRUE and FALSE are the numbers 1 and 0; SQLite stores the same 1 and 0 in an ordinary integer. A filter such as WHERE is_paid therefore works in all three engines, but WHERE is_paid = 'yes' does not.

Look at the type of a value

SQLite’s typeof() names the storage class of any value: integer, real, text, blob or null. PostgreSQL has pg_typeof() for the same job, and it answers with the full type name, such as integer or numeric. MySQL has no such function; SHOW CREATE TABLE shows the declared types of a table’s columns instead.

The storage class of eight values SQL · typeof_values.sql
-- typeof() names the storage class of a value: integer, real, text, blob or null.
-- The column v has no declared type, so it keeps every value exactly as it was written.
CREATE TABLE samples (written TEXT, v);
INSERT INTO samples VALUES
  ('42', 42), ('4.2', 4.2), ('''42''', '42'), ('NULL', NULL), ('x''2A''', x'2A'),
  ('TRUE', TRUE), ('3 > 2', 3 > 2), ('''2026-03-31''', '2026-03-31');

SELECT written, typeof(v) AS type
FROM samples;

Output

┌──────────────┬─────────┐
│   written    │  type   │
├──────────────┼─────────┤
│ 42           │ integer │
│ 4.2          │ real    │
│ '42'         │ text    │
│ NULL         │ null    │
│ x'2A'        │ blob    │
│ TRUE         │ integer │
│ 3 > 2        │ integer │
│ '2026-03-31' │ text    │
└──────────────┴─────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < typeof_values.sql

Two results are worth a second look. TRUE and the result of a comparison are integers, because SQLite has no separate boolean class. And '2026-03-31' is text: SQLite has no date type at all, and its date functions work on text written in that year-month-day form, as the lesson on dates explains.

Declared types in SQLite are preferences

In PostgreSQL and MySQL a column’s type is a rule. In an ordinary SQLite table it is a preference, called the column’s type affinity. SQLite converts an incoming value to the preferred class when nothing is lost, and otherwise stores the value as it arrived. Here the same text goes into five columns:

One phone number and two values in five column types SQL · declared_types.sql
-- The same text goes into five columns with different declared types.
CREATE TABLE contacts (
  phone_text   TEXT,
  phone_int    INTEGER,
  phone_string STRING,         -- not a type SQLite knows: the column gets NUMERIC affinity
  price        DECIMAL(10, 2),
  qty          INTEGER
);
INSERT INTO contacts VALUES ('02226540000', '02226540000', '02226540000', '19.99', '12abc');

SELECT phone_text,   typeof(phone_text)   AS t1,
       phone_int,    typeof(phone_int)    AS t2,
       phone_string, typeof(phone_string) AS t3
FROM contacts;

SELECT price, typeof(price) AS t4,
       qty,   typeof(qty)   AS t5
FROM contacts;

Output

┌─────────────┬──────┬────────────┬─────────┬──────────────┬─────────┐
│ phone_text  │  t1  │ phone_int  │   t2    │ phone_string │   t3    │
├─────────────┼──────┼────────────┼─────────┼──────────────┼─────────┤
│ 02226540000 │ text │ 2226540000 │ integer │ 2226540000   │ integer │
└─────────────┴──────┴────────────┴─────────┴──────────────┴─────────┘
┌───────┬──────┬───────┬──────┐
│ price │  t4  │  qty  │  t5  │
├───────┼──────┼───────┼──────┤
│ 19.99 │ real │ 12abc │ text │
└───────┴──────┴───────┴──────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < declared_types.sql

The phone number keeps its leading zero only in the TEXT column. INTEGER turned it into a number, and so did STRING, a type name SQLite does not know, which gives the column NUMERIC affinity. The price in the DECIMAL(10, 2) column became a REAL, an approximate floating-point number, because SQLite has no exact decimal type behind that name. And '12abc' stayed text inside an INTEGER column, since converting it would lose the letters. A query that adds up qty later meets a text value in a column everybody believed held numbers.

SQLite works out the affinity from the declared type’s name, one test after another:

SQLite checks a declared type name for INT, then CHAR, CLOB or TEXT, then BLOB, then REAL, FLOA or DOUB; anything else is NUMERIC.Declared type nameof a columnContains INT?INTEGER affinityContains CHAR, CLOB or TEXT?TEXT affinityContains BLOB, or no type at all?BLOB affinityContains REAL, FLOA or DOUB?REAL affinityAnything elseNUMERIC affinitynononono

How SQLite picks a column's type affinity from its declared type

Text description of the diagram

The diagram is a chain of five tests, read from the top. The first test that matches decides the column's affinity, and the tests below it are never reached.

  1. The name contains INT: INTEGER affinity. INT, INTEGER, BIGINT and also FLOATING POINT land here, the last one because it contains the letters INT.
  2. Otherwise, the name contains CHAR, CLOB or TEXT: TEXT affinity. VARCHAR(20), NCHAR(10) and TEXT land here.
  3. Otherwise, the name contains BLOB, or the column has no type at all: BLOB affinity, which stores values as they come.
  4. Otherwise, the name contains REAL, FLOA or DOUB: REAL affinity. REAL, FLOAT and DOUBLE land here.
  5. Anything else gets NUMERIC affinity: NUMERIC, DECIMAL(10, 2), BOOLEAN, DATE, DATETIME and also STRING.

The test ignores upper and lower case. Each arrow from one test to the next is labelled "no". A column with NUMERIC affinity turns text that looks like a number into a number, which is how a phone number with a leading zero loses its zero.

The tests run in order, and the first match wins, so a column declared FLOATING POINT gets INTEGER affinity because the name contains the letters INT. To keep text exactly as it is, declare it TEXT or VARCHAR(n).

STRICT tables check every value

Since version 3.37.0, a SQLite table created with the keyword STRICT after its column list checks types as PostgreSQL does. Its columns must use one of the types INT, INTEGER, REAL, TEXT, BLOB or ANY, and a value that cannot be converted without loss is refused:

A STRICT table accepts '42' and refuses '12abc' SQL · strict_table.sql
-- A STRICT table checks every value against the column's type.
CREATE TABLE stock (sku TEXT, qty INTEGER) STRICT;

INSERT INTO stock VALUES ('DAL-1KG', '42');     -- text that is a clean integer: stored as 42
SELECT sku, qty, typeof(qty) AS type FROM stock;

INSERT INTO stock VALUES ('GHEE-1L', '12abc');  -- refused

Output (exit status 1)

┌─────────┬─────┬─────────┐
│   sku   │ qty │  type   │
├─────────┼─────┼─────────┤
│ DAL-1KG │ 42  │ integer │
└─────────┴─────┴─────────┘

Printed as an error (standard error)

Runtime error near line 7: cannot store TEXT value in INTEGER column stock.qty

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < strict_table.sql

The text '42' was accepted and stored as the integer 42, because the conversion is exact. The second INSERT failed with an error and stored nothing. New SQLite tables are worth declaring STRICT unless you need the flexible behaviour; the only cost is that names such as DECIMAL or DATE are not allowed, so dates go in TEXT and money in INTEGER.

Converting values with CAST

CAST(expression AS type) converts a value explicitly. It is standard SQL and works in all three engines; PostgreSQL also accepts its own shorter form, expression::type. Here is what SQLite makes of eleven inputs:

Eleven inputs cast to INTEGER and REAL SQL · cast_inputs.sql
-- What CAST makes of messy input in SQLite. The table only holds the inputs.
CREATE TABLE inputs (label TEXT, v);
INSERT INTO inputs VALUES
  ('clean',          '42'),
  ('padded',         ' 42 '),
  ('plus sign',      '+7'),
  ('trailing text',  '12abc'),
  ('no digits',      'abc'),
  ('decimal text',   '3.9'),
  ('number 3.9',     3.9),
  ('number -3.9',    -3.9),
  ('exponent',       '1e3'),
  ('hexadecimal',    '0x1A'),
  ('missing',        NULL);

SELECT label, quote(v) AS input,
       CAST(v AS INTEGER) AS as_integer,
       CAST(v AS REAL)    AS as_real
FROM inputs;

Output

┌───────────────┬─────────┬────────────┬─────────┐
│     label     │  input  │ as_integer │ as_real │
├───────────────┼─────────┼────────────┼─────────┤
│ clean         │ '42'    │ 42         │ 42.0    │
│ padded        │ ' 42 '  │ 42         │ 42.0    │
│ plus sign     │ '+7'    │ 7          │ 7.0     │
│ trailing text │ '12abc' │ 12         │ 12.0    │
│ no digits     │ 'abc'   │ 0          │ 0.0     │
│ decimal text  │ '3.9'   │ 3          │ 3.9     │
│ number 3.9    │ 3.9     │ 3          │ 3.9     │
│ number -3.9   │ -3.9    │ -3         │ -3.9    │
│ exponent      │ '1e3'   │ 1          │ 1000.0  │
│ hexadecimal   │ '0x1A'  │ 0          │ 0.0     │
│ missing       │ NULL    │            │         │
└───────────────┴─────────┴────────────┴─────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < cast_inputs.sql

SQLite reads text from the left and keeps the longest prefix that forms a number, ignoring leading spaces. That is why '12abc' becomes 12, 'abc' becomes 0, the exponent in '1e3' is dropped when the target is INTEGER, and '0x1A' stops at the letter x. A real number cast to INTEGER loses its fraction and moves toward zero, so 3.9 becomes 3, and -3.9 becomes -3. Only NULL stays NULL.

The other two engines treat the same inputs differently:

Casting to a whole number, engine by engine
Criterion PostgreSQL 18MySQL 8.4SQLite 3.49
How to write it CAST(x AS integer) or x::integerCAST(x AS SIGNED)CAST(x AS INTEGER)
Text '12abc' Error: invalid input syntax12, with warning 129212
Text 'abc' Error0, with a warning0
Text '3.9' Error3, with a warning3
Number 3.9 4 (rounded)4 (rounded)3 (fraction dropped)
Number -3.9 -4-4-3
NULL NULLNULLNULL

PostgreSQL accepts only text that is a valid integer and stops the whole statement otherwise. MySQL converts as much as it can and records a warning, such as 1292 “Truncated incorrect INTEGER value”, which a program sees only if it asks for warnings. Its CAST also has a short list of target types: a whole number is SIGNED or UNSIGNED, and CAST(x AS INT) is a syntax error. Numbers with a fraction are rounded by PostgreSQL and MySQL but cut toward zero by SQLite, so the same query returns 4 in two engines and 3 in the third. When you need one behaviour everywhere, say it: round() or floor() first, then the cast.

When a value does not fit a column

Writing data shows the same split, with one twist in MySQL. PostgreSQL refuses an INSERT of '12abc' into an integer column with the same “invalid input syntax” error. MySQL 8.4 runs in strict mode by default (STRICT_TRANS_TABLES is part of its default SQL mode), so it refuses the row as well; without strict mode it would store 12 with a warning. Strict mode applies to writing data, not to SELECT, so a cast in a query still converts and warns. SQLite stores the text unchanged, as the declared-types example showed, unless the table is STRICT.

The safe habit is the same everywhere: check or convert data before it reaches a typed column, and in SQLite declare new tables STRICT. The exercise below builds such a check.

Comparing values of different types

A comparison between values of different types needs a conversion, and each engine has its own rules. SQLite’s depend on where the values come from:

'10' against 9, three ways SQL · mixed_compare.sql
-- The same comparison with values of different types.
CREATE TABLE stock (qty INTEGER, code TEXT);
INSERT INTO stock VALUES (10, '10');

SELECT '10' > 9  AS text_vs_number,       -- two literals: any text sorts after any number
       qty > '9'  AS integer_column,       -- the column's INTEGER affinity turns '9' into 9
       code > 9   AS text_column           -- the column's TEXT affinity turns 9 into '9'
FROM stock;

SELECT CAST(code AS INTEGER) > 9 AS cast_first
FROM stock;

Output

┌────────────────┬────────────────┬─────────────┐
│ text_vs_number │ integer_column │ text_column │
├────────────────┼────────────────┼─────────────┤
│ 1              │ 1              │ 0           │
└────────────────┴────────────────┴─────────────┘
┌────────────┐
│ cast_first │
├────────────┤
│ 1          │
└────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < mixed_compare.sql

Two literals have no affinity, and SQLite then orders every number before every text, so '10' > 9 is true without any arithmetic. Against an INTEGER column, the text '9' is converted to the number 9 first. Against a TEXT column, the number 9 becomes the text '9', and '10' sorts before '9' the way words do, so the comparison is false. An explicit CAST gives the answer you meant in every case.

The other engines go further in both directions. MySQL converts the text to a number for such a comparison, so a VARCHAR column compared with the number 0 matches every value that does not begin with a number, such as 'pending', and its manual notes that an index on that column cannot then be used to find the rows. PostgreSQL refuses to compare a text column with an integer at all (“operator does not exist”) until you add a cast. Compare like with like: quote the value when the column holds text, and cast deliberately when it does not.

SQLite Online (SQL Playground) Create a table with the column types of your own data and check what typeof() reports for each value.

Your turn

Exercise · Easy · SQL

Flag the quantities that are clean integers

A supplier sends stock counts as text. They arrive in the table raw_quantities, with the columns id and qty_text (TEXT). Before loading them into an INTEGER column, flag the rows that are safe to convert.

Write a query that returns each row's id, its qty_text and a column is_clean:

- 1 when qty_text is an integer written exactly the way SQLite writes that integer back as text: an optional minus sign followed by digits, with no spaces, no plus sign, no decimal point and no extra leading zeros (0 itself is clean); - 0 for everything else, including NULL.

So 42 and -7 are clean, while 42, +42, 007, 3.5, 12abc and an empty string are not. A value too large for a 64-bit integer is not clean either. The order of the rows does not matter. The sample tests include spaces, signs, leading zeros, Devanagari digits and the largest 64-bit integer.

Starter code · clean_flags.sql

-- Flag each row of raw_quantities: is_clean is 1 when qty_text is an integer
-- written exactly as SQLite writes it, and 0 otherwise (NULL included).
SELECT id,
       qty_text,
       1 AS is_clean
FROM raw_quantities;
The sample tests · tests.yaml
# Sample tests: each one builds a small raw_quantities table, runs your query on it and compares the rows it returns
# (in any order).
tests:
  - name: flags clean integers, text, decimals and NULL
    setup: |
      CREATE TABLE raw_quantities (id INTEGER PRIMARY KEY, qty_text TEXT);
      INSERT INTO raw_quantities VALUES (1, '42'), (2, '12abc'), (3, '3.5'), (4, '-7'), (5, NULL);
    expected:
      columns: [id, qty_text, is_clean]
      rows:
        - [1, '42', 1]
        - [2, '12abc', 0]
        - [3, '3.5', 0]
        - [4, '-7', 1]
        - [5, null, 0]
  - name: refuses spaces, plus signs and an empty string
    setup: |
      CREATE TABLE raw_quantities (id INTEGER PRIMARY KEY, qty_text TEXT);
      INSERT INTO raw_quantities VALUES (1, ' 42'), (2, '42 '), (3, '+42'), (4, '- 7'), (5, ''), (6, '0');
    expected:
      columns: [id, qty_text, is_clean]
      rows:
        - [1, ' 42', 0]
        - [2, '42 ', 0]
        - [3, '+42', 0]
        - [4, '- 7', 0]
        - [5, '', 0]
        - [6, '0', 1]
  - name: refuses leading zeros, other digits and values beyond 64 bits
    setup: |
      CREATE TABLE raw_quantities (id INTEGER PRIMARY KEY, qty_text TEXT);
      INSERT INTO raw_quantities VALUES
        (1, '007'), (2, '-0'), (3, '४२'), (4, '9223372036854775807'), (5, '9223372036854775808'), (6, '1e3');
    expected:
      columns: [id, qty_text, is_clean]
      rows:
        - [1, '007', 0]
        - [2, '-0', 0]
        - [3, '४२', 0]
        - [4, '9223372036854775807', 1]
        - [5, '9223372036854775808', 0]
        - [6, '1e3', 0]
A hint

Convert the text to an integer and back again: CAST(CAST(qty_text AS INTEGER) AS TEXT). For a clean value you get the same text back; for ' 42', '007' or '12abc' you get something else. Compare the round trip with the original inside a CASE, and remember that a comparison with NULL is unknown, so ELSE must give 0.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does this query return in SQLite?

    Read the code, then choose one answer.

    SELECT CAST('7 apples' AS INTEGER) AS n;
    Show the answer to question 1

    Answer: 7

    SQLite reads the text from the left and keeps the longest prefix that is a number, here 7, and ignores the rest. PostgreSQL would stop with an "invalid input syntax" error, and MySQL would return 7 with a warning.

  2. Question 2 of 6 In an ordinary (not STRICT) SQLite table, a column is declared STRING. What happens to the text '0221234567' inserted into it?

    Choose one answer.

    Show the answer to question 2

    Answer: It becomes the integer 221234567, because STRING gives the column NUMERIC affinity

    SQLite derives the affinity from the type's name: STRING contains none of INT, CHAR, CLOB, TEXT, BLOB, REAL, FLOA or DOUB, so it falls through to NUMERIC, and text that looks like an integer is converted. The leading zero is lost. Declare such columns TEXT.

  3. Question 3 of 6 What does this query return in SQLite?

    Read the code, then choose one answer.

    SELECT CAST(-3.9 AS INTEGER) AS n;
    Show the answer to question 3

    Answer: -3

    SQLite casts a real number to an integer by dropping the fraction, which moves it toward zero: -3.9 becomes -3. PostgreSQL and MySQL round instead and return -4.

  4. Question 4 of 6 Which of these converts the text '42' to a whole number in MySQL 8.4?

    Choose one answer.

    Show the answer to question 4

    Answer: CAST('42' AS SIGNED)

    MySQL's CAST takes a short list of target types, and a whole number is SIGNED or UNSIGNED; CAST(x AS INT) is a syntax error there. The :: form is PostgreSQL's.

  5. Question 5 of 6 A table is created with CREATE TABLE t (qty INTEGER) STRICT. What does INSERT INTO t VALUES ('42') do?

    Choose one answer.

    Show the answer to question 5

    Answer: It stores the integer 42, because the text converts to an integer without loss

    A STRICT table still converts values with the usual affinity rules; it refuses only values that cannot be converted without loss, such as '12abc', with "cannot store TEXT value in INTEGER column".

  6. Question 6 of 6 How many of these four values does SQLite's typeof() report as integer: 42, '42', 4.2 and TRUE?

    Type a number.

    Show the answer to question 6

    Answer: 2

    42 and TRUE are integers (TRUE is just another way to write 1). '42' is text, because it is quoted, and 4.2 is real.

Interview questions

Fresher · What is type affinity in SQLite, and how is it different from a column type in PostgreSQL?

In PostgreSQL a column’s type is a rule: every value in an integer column is an integer, and input that cannot be converted is an error. In an ordinary SQLite table the declared type only sets the column’s affinity, the storage class it prefers. SQLite converts a value to that class when nothing is lost, so the text '42' becomes the integer 42 in an INTEGER column, and otherwise keeps the value as it came, so '12abc' stays text. The affinity comes from the type’s name: INT gives INTEGER; CHAR, CLOB or TEXT give TEXT; BLOB or no type give BLOB; REAL, FLOA or DOUB give REAL; anything else, such as STRING or DECIMAL, gives NUMERIC. A table declared STRICT, available since SQLite 3.37.0, checks types the way PostgreSQL does.

Key takeaways

  • Choose integer types for counts and identifiers you calculate with, exact types or whole paise for money, and text for codes that only look like numbers.
  • typeof() in SQLite and pg_typeof() in PostgreSQL show the type of any value; use them when a result surprises you.
  • An ordinary SQLite column prefers a type but accepts anything, so '12abc' can sit in an INTEGER column; STRICT tables refuse it.
  • CAST(x AS type) works everywhere; PostgreSQL also has x::type, and MySQL casts to SIGNED, not INT.
  • On messy text PostgreSQL raises an error, MySQL converts with a warning and SQLite converts silently; numbers with a fraction are rounded by PostgreSQL and MySQL but cut toward zero by SQLite.
  • Compare values of the same type, or cast explicitly, because mixed comparisons follow different rules in each engine.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.