Also in the app Edgely Database Manager: Help, then Documentation (F1), Partitioning Lab
A database keeps each table in files on the server's disk. When a table grows to millions of rows, reading it costs more and more. Partitioning (also called horizontal fragmentation) splits one table into several parts by the values of a column, for example one part per month or one per country. A query that filters on that column then reads only the part it needs (the engine "prunes" the others), and old data can be dropped one part at a time. A query that does not filter on the column still reads every part, and can get slower.
The Partitioning Lab shows how this works on your own tables. On PostgreSQL, MySQL and MariaDB it shows how a table is stored and how it is already partitioned, and helps you choose a partition key: it measures the columns you pick, suggests a strategy and previews the partitions it would make (see Choosing a partition key), then builds a partitioned copy of the table beside it, leaving the original as it is (see Trying a partitioning safely), measures your queries on both (see Reading the results), and can put the copy in the table's place, keeping the original as <table>_old (see Replacing the original). It can also split a table's columns into two tables sharing its primary key (see Splitting columns). On ClickHouse, SQL Server and Oracle it does the same, each with its own rules for the key and the copy.
Opening the Lab
- Right-click a table in the explorer and choose Partitioning Lab. On a partition (under a table's Partitions group) the same entry opens the Lab of its table.
- Or choose Tools > Partitioning Lab in the ribbon: the Lab opens on the connection of the current tab, with the default schema (
publicon PostgreSQL,dboon SQL Server, your login's own schema in capitals on Oracle, the connection's database on MySQL, MariaDB and ClickHouse).
There is one Lab tab per connection; opening it again selects it and shows the table you picked.
What the Lab shows
On the left, type a Schema and a Table and click Show storage (or press Enter). Below, Partitioned tables in this schema lists the tables that are already partitioned, with their number of partitions; click one to show it.
On the right:
| Part | What it says |
|---|---|
| Kind | Table, partitioned table, a partition of another table, view or foreign table |
| Rows | The server's estimate, from its statistics (SQL Server keeps an exact count per partition). Tick Count rows exactly to count them instead (this reads the whole table, so it can take time on a large one). "No statistics yet" means the server has not analysed the table: count the rows or run ANALYZE (on Oracle, gather the table's statistics) |
| Size on disk | Data and indexes together; for a partitioned table, the sum of its partitions. On Oracle the table's own segments only (indexes not counted), read from DBA_SEGMENTS, or for your own tables from USER_SEGMENTS; a partition no row has reached yet takes no space (0 B) |
| Engine | MySQL and MariaDB: the storage engine (InnoDB, MyISAM, ...); ClickHouse: the table engine (MergeTree, ReplacingMergeTree, Log, ...) |
| On disk | The file that holds the table. PostgreSQL gives a path relative to the server's data directory (base/16384/16385); MySQL and MariaDB give the .ibd file, one per partition (orders#p#p0.ibd on MySQL, orders#P#p0.ibd on MariaDB). ClickHouse gives the table's data directory, which holds the parts of every partition. SQL Server gives the data files (.mdf, .ndf) of the filegroup that holds the table or each partition. Oracle gives the data files (.dbf) of the tablespace that holds it, to a login that may read DBA_DATA_FILES. These are paths on the database server, shown for information only |
| Partitioned by | The strategy (LIST, RANGE, HASH, KEY, RANGE COLUMNS, ...) and the key, as the server reports them. ClickHouse has no strategy: its PARTITION BY expression (toYYYYMM(created)) is shown. SQL Server partitions by range only, through a partition function: RANGE RIGHT (order_date) puts each boundary value in the partition above it, RANGE LEFT in the one below. Oracle shows list, range, hash and composite partitioning, and an interval-partitioned table with its interval (RANGE (D) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH'))) |
| Partitions | One row per partition: what it holds (its bound), its rows, its size and its file. A sub-partition is indented under its partition; the default partition takes the rows no other partition accepts. On ClickHouse a partition holds the rows whose key expression has its value (toYYYYMM(created) = 202401), and its rows and size add up its active parts; ClickHouse reports no file per partition. SQL Server partitions have numbers, not names, and their bound is written from the partition function ([order_date] >= '2024-01-01' AND [order_date] < '2024-02-01'). Oracle writes a date bound as VALUES LESS THAN (DATE '2025-01-01') |
When no file is shown, the Lab says why: a managed service may not report it, MySQL and MariaDB show InnoDB file names only to a login with the PROCESS privilege, a MyISAM table has other files, a table in a shared tablespace has no file of its own, and on Oracle only a login that may read DBA_DATA_FILES sees the files.
Partitions in the explorer
A partitioned table gets a Partitions group under its Columns, Indexes and Foreign Keys, listing each partition with its bound and its row count. On PostgreSQL a partition is itself a table: it is listed under its parent's Partitions group and no longer beside it in the Tables group, and the partitioned table, which used to be missing from the list, now shows. On SQL Server the partitions are listed by number (1, 2, 3, ...) with the values each holds.
Choosing a partition key
Under a table that is not partitioned yet, Design a partitioning helps you choose how to split it. Nothing is written: the Lab only reads the table.
- Partition key, in order. Click the column (or columns) most of your queries filter on, for example
region, then a date column. The number on each chip is its place in the key. The Lab reads, for each column, its distinct values, its NULLs, its smallest and largest value and its most frequent values. - The suggestion. From what it read about the first column, the Lab proposes a strategy and selects it:
- List when the column holds few distinct values (20 or fewer): one partition per value, for example one per country.
- Range when it holds dates (one partition per month, or per year when the dates span more than ten years) or growing numbers that are nearly all different, such as an id (equal intervals between the smallest and the largest value).
- Hash otherwise: the rows are spread evenly over a number of partitions you choose.
- Try another strategy. Click List, Range or Hash; the Lab keeps your choice when you change the key. Range is offered for dates and numbers only. For a range choose one per month or per year (dates) or the number of intervals (numbers); for a hash, the number of partitions. Default partition for other values and NULL adds a partition for the rows no other partition takes. With two or more key columns, Then hash by splits each list or range partition again by a hash of the second column (sub-partitions).
- The preview lists each future partition with what it would hold, its estimated rows and its share of the table, with a bar. On a large table (more than 200,000 rows by the server's estimate) the Lab reads a sample instead of the whole table and says so ("Estimated from a 20 % sample"); the estimated rows are then the sample's shares applied to the server's estimate of the table's rows, which MySQL and MariaDB can be off by a good margin. ClickHouse is always read whole: it scans quickly, and its own sampling needs a sampling key the table may not have. Cancel stops a read that takes too long; SQL Server, Oracle and ClickHouse cannot interrupt a running read from the Lab, so it finishes on the server and its answer is ignored.
- Each engine places NULL its own way, and the preview follows it: in a range, MySQL, MariaDB and SQL Server put NULL keys in the lowest partition, Oracle in the last one (
MAXVALUE), ClickHouse in a partition of their own, and PostgreSQL in the default partition. SQL Server cuts a list as range boundaries and ClickHouse makes one partition per value as rows arrive, so on those two every value has a partition and no default partition is offered. SQL Server partitions by one column and has no sub-partitions, so Then hash by is not offered there.
Below the preview the Lab warns about what would make the split cost more than it pays:
| Warning | What it means and what to do |
|---|---|
| Too many partitions | More than 8,192: MySQL and MariaDB refuse it, and PostgreSQL, SQL Server and Oracle plan every query more slowly as partitions add up. On ClickHouse the limit is 100: ClickHouse advises far fewer partitions (no finer than by month), one insert may write to at most 100 partitions by default, and a list key makes one partition per distinct value. Choose a coarser range (per year rather than per month) or fewer hash partitions |
| Nearly empty partitions | Some partitions would hold almost nothing (less than a tenth of an even share). They still cost a file and planning time; widen the ranges |
| Skewed split | One partition would hold far more than its share (more than twice an even share). Queries on that value gain nothing; with a hash, a single frequent value always lands in one partition |
| NULL with nowhere to go | Some rows have a NULL key and no partition takes them, so inserting them would fail. Tick the default partition. On MySQL a range puts NULL in its lowest partition, and a hash takes NULL anyway |
| Values no partition lists | A list without a default partition would refuse the values it does not name (the list shows the 20 most frequent values). Tick the default partition |
| No default list partition on MySQL | MySQL has no default partition for a list (MariaDB has one): every value, NULL included, must be listed |
| One column for a PostgreSQL list | PostgreSQL's list partitioning takes one column; the other columns can only sub-partition |
| One column on SQL Server | A SQL Server partition function takes one column: the other key columns are not used |
| Primary key or unique index without the key | PostgreSQL, MySQL and SQL Server (for an index aligned with the partitions) require every primary key and unique index of a partitioned table to contain all the partition key's columns. Add the key to them, or choose a key they already contain. Oracle keeps such a key in a global index and ClickHouse enforces no uniqueness, so neither is warned about |
| Foreign keys on MySQL | MySQL and MariaDB refuse foreign keys on a partitioned InnoDB table: the table's foreign keys would have to go |
Trying a partitioning safely
Below the design, Build a partitioned copy turns it into a real table you can look at and query, without touching the original. The copy is named after the table with _part at the end (orders_part for orders) and sits beside it in the same schema.
- Read the SQL. The SQL that builds it shows every statement the copy runs: on PostgreSQL a
CREATE TABLE ... (LIKE orders ...) PARTITION BY ...and oneCREATE TABLE ... PARTITION OF ...per partition; on MySQL and MariaDB aCREATE TABLE ... LIKE ordersand anALTER TABLE ... PARTITION BY ...; on SQL Server a partition function (CREATE PARTITION FUNCTION), a partition scheme on it, an empty copy of the columns (SELECT ... INTO) and a clustered index built on the scheme; on Oracle aCREATE TABLE ... PARTITION BY ... AS SELECTthat takes no row; on ClickHouse aCREATE TABLE ... AS orders ENGINE = MergeTree PARTITION BY ...; then anINSERT ... SELECTthat copies the rows. Copy the SQL puts it on the clipboard, to run it yourself or keep it. - Check the room. The copy takes about as much space as the table, shown under the SQL. The Lab cannot see the server's free disk space, so make sure the server has room before copying a large table.
- Build the copy. The Lab asks first, naming the copy, the table, its rows and its size. On a Production connection the Production question comes before it. The progress line shows the statement running and then "Copying the rows"; Cancel stops the copy on the server. SQL Server, Oracle and ClickHouse cannot interrupt a running statement from the Lab: the line then says the copy stops after the running statement, and once it ends the copy is dropped.
- Look at it. Once built, the Lab says how many rows were copied and how long it took. Show the copy opens it in the Lab: its partitions, their rows and files, as for any partitioned table. Back to orders returns to the original, with the design, the copy's buttons and Before and after as you left them.
- Drop the copy when you are done (on the design, or on the copy itself). The Lab asks first; the copy and all its partitions are dropped (on SQL Server its partition function and scheme too, on Oracle without going to the recycle bin), the original stays as it is.
- Or replace the original by the copy (PostgreSQL, MySQL and MariaDB): see Replacing the original.
What the copy holds on PostgreSQL, MySQL and MariaDB: the table's columns, defaults, check constraints and indexes, and all its rows. It has no foreign keys (neither engine copies them), and a generated or identity column becomes a plain column holding the copied values on PostgreSQL. List and range partition on the key's first column; hash on all its columns; with Then hash by, each list or range partition is split again on the second column.
The other three engines copy less, and the panel says what:
| Engine | What the copy holds | How it is partitioned |
|---|---|---|
| SQL Server | The columns and all the rows, clustered on the partition key; no key, other index, default or constraint (an identity column holds the copied values) | By range on one column: a list becomes one partition per value (and one below the first), a hash becomes an extra column edgely_hash holding a checksum of the key's columns, one partition per value |
| Oracle | The columns, their NOT NULL constraints and all the rows; no key, index or default | List (with a DEFAULT partition when ticked), range up to MAXVALUE (NULL keys go there), hash, and hash sub-partitions with Then hash by |
| ClickHouse | The columns and all the rows, in a MergeTree ordered by the table's primary key columns | By an expression: the column itself for a list (one partition per value as rows arrive), the number of the interval for a range (NULL keys in a partition of their own), a hash of the key's columns for a hash |
On Oracle, partitioning is an option of some editions only. The Lab asks the database (V$OPTION): when it says Partitioning is not enabled, the panel says so and offers no Build; the SQL is still shown. The Lab reports what the database says, nothing more: it is not a statement about your licence.
The Lab refuses to build when the server would refuse the copy or some rows would have no partition: a primary key or unique index without the key (not on SQL Server, whose copy has no unique index), more than 8,192 partitions (100 on ClickHouse), NULLs or values no partition takes, or on MySQL a list that does not name every value (MySQL has no default list partition). The SQL is still shown. It also refuses while a transaction is open on the connection: commit or roll it back first, so the copy does not join your work. When a statement fails, you cancel, or you close the Lab tab during the build, the Lab drops what the copy had created: no half-built table stays behind. A copy already built stays on the server when the tab closes; drop it from the Lab before closing, or with DROP TABLE. If a table named like the copy already exists, the build stops at the first statement and leaves that table alone.
Reading the results
Once the copy is built, Before and after under it measures what the partitioning changes for your own queries.
- Your queries. The box starts with two queries on the original table: one that filters on the partition key (with one of its values, the least frequent of its common ones) and one that does not. Replace them with the queries you really run, written against the original table; end each one with a semicolon. The Lab runs each query on the original as you wrote it, and on the copy with the table's name replaced by the copy's (bare, quoted or with its schema, also as a column prefix such as
orders.id; text in quotes and comments is left alone). - The copy's version, optional. When a name replacement could hit the wrong word (a column named like the table, for example), write the copy's queries yourself in the second box, one for each query above, in the same order; an empty box means "replace the name".
- Compare. Each query runs through the Benchmark runner, with the Benchmark tab's settings: 5 warm-up runs, then 100 timed runs, a 30-second timeout per run, first on the original, then on the copy. The line beside the button says which run is going. On a Production connection the Production question comes first when the queries write.
- Read each query's card:
- Reads 1 of 6 partitions: how many of the copy's partitions the server's plan reads. PostgreSQL lists one scan per partition it keeps (partitions it prunes, while planning or when the query starts, are left out); MySQL and MariaDB name them in the plan's
partitionscolumn. SQL Server reports them in the actual plan only, so the Lab runs the copy's query once more to get it. Oracle gives them in the plan'sPstartandPstopcolumns; when they readKEYthe partitions are chosen as the query runs and the card shows no count. ClickHouse counts parts rather than partitions, so the count is shown only while each partition of the copy is one part (as just after it is built), and aCOUNT(*)ClickHouse answers from its metadata reads no partition and shows no count. - Average: the mean time of one run on the original and on the copy, and how many times faster or slower the copy is ("About the same" within 10 %).
- Side by side opens the Benchmark comparison of the two runs (average, median, percentiles, throughput, histogram); Plan on the copy opens the copy's plan.
- Reads 1 of 6 partitions: how many of the copy's partitions the server's plan reads. PostgreSQL lists one scan per partition it keeps (partitions it prunes, while planning or when the query starts, are left out); MySQL and MariaDB name them in the plan's
- Find it again later. Both runs of every query are saved in the Benchmark history, like any benchmark: open a Benchmark tab on the same connection to reload, compare or export them (HTML, CSV, JSON).
What to expect: a query that filters on the key reads only the partitions that hold its values, so it reads less and gets faster. A query that does not filter on the key reads every partition, one after the other, and can be a little slower than on the plain table: the server plans and opens each partition. That is the trade-off of partitioning: choose the key your most frequent or most expensive queries filter on. A small table, or one that already fits in memory, shows little difference either way; a query served by an index on the original may gain little too.
The copy's partitions are counted for one partitioned table: on MySQL, MariaDB and SQL Server a query that joins two partitioned tables adds both tables' partitions.
Replacing the original
Once the copy is built and compared, Replace the original (beside Drop the copy, on the design or on the copy itself) puts the copy in the table's place. Nothing is dropped: the original is renamed with _old at the end (orders_old for orders) and the copy takes the table's name, so your queries now run on the partitioned table, and the original rows stay in orders_old until you drop it yourself.
- The checks. On a Production connection the Production question comes first. The Lab then refuses while a transaction is open on the connection (commit or roll it back first: the rename would join it on PostgreSQL and commit it on MySQL) and when a table, view or index named
orders_oldalready exists (the Lab names it and never overwrites it: rename or drop it first). - What depends on the table. The Lab reads what depends on
ordersand lists it in the question, as it will be after the rename:- PostgreSQL ties views, foreign keys, grants, triggers, row security policies and owned sequences (a
serialor identity column's) to the table itself, not to its name, so all of them stay on `orders_old`: the views readorders_old, the foreign keys of other tables point at it, and the grants, triggers and policies apply to it. The partitionedordersstarts with none of them. Aserialcolumn's default on it still draws from the sequenceorders_oldowns, so move the sequence (ALTER SEQUENCE ... OWNED BY orders.id) before you droporders_old. - MySQL and MariaDB move foreign keys and triggers with the table, so other tables' foreign keys and the table's triggers stay on `orders_old`; views and table grants name the table, so they now reach the partitioned `orders`. MySQL and MariaDB refuse foreign keys on a partitioned table: those foreign keys cannot be moved to
orders, only kept onorders_oldor dropped. - The table's own foreign keys stay on
orders_oldon every engine (the copy has none).
- PostgreSQL ties views, foreign keys, grants, triggers, row security policies and owned sequences (a
- Replace. The question names the table, the copy and that list. Replace renames both in one step: on PostgreSQL the two
ALTER TABLE ... RENAMErun in one transaction (aDOblock), on MySQL and MariaDB oneRENAME TABLE orders TO orders_old, orders_part TO orders. Either both names change or neither does. If the server refuses, the Lab says so in its words and the copy stays built, so you can still drop it. - After. The Lab shows
orders, now partitioned, with its partitions, and above it what still depends onorders_old(and, on MySQL and MariaDB, what now reaches the partitionedorders). Move what can move toorders, then droporders_oldyourself once nothing needs it: the Lab never drops it. The partitions keep the copy's names (orders_part_p0, ...) on PostgreSQL.
Replace is not offered on SQL Server, Oracle and ClickHouse: rename the tables yourself there.
Splitting columns
Partitioning splits a table's rows. Database courses teach the other way too, vertical fragmentation: a wide table whose queries use a few columns often is split by its columns into two tables that share the primary key. A query on the often-used columns then reads a much smaller table; a query that needs a column of the other half has to join the two.
On PostgreSQL, MySQL and MariaDB, under a table that is not partitioned yet, choose Split columns (beside Split rows (partitions), which shows the partition design above).
- Columns your queries use often. The primary key's columns are shown as (key): they go in both halves. Click the other columns your frequent queries use, for example
nameandemailin auserstable whosebioandphotoare large and rarely read. - The SQL that builds it lists the statements:
<table>_hotgets the key and the columns you ticked,<table>_coldthe key and the others, each with its rows and its primary key, then the view<table>_vsplit, which joins them back on the key, with the original's columns in their order, so the queries you wrote for the table run on it unchanged. Copy the SQL puts it on the clipboard. Other indexes, defaults and constraints are not copied. - Build the split asks first (on a Production connection the Production question comes before), then creates the two tables and the view beside the original, which is only read. It is refused inside an open transaction; a statement that fails, or Cancel, drops what the split had created. The two halves take as much room as the table.
- Before and after then works as for a partitioned copy, with one difference: each query runs on
<table>_hotwhen it uses only the key and the ticked columns, otherwise on the view, which pays the join. The second box, The split's version, lets you choose the table or view yourself, one query for each query above. The box starts with two queries: one on a ticked column and one on a column of the other half. No partition is counted. - Drop the split removes the view and both tables; the original is not changed.
The Lab refuses to split a table that has no primary key (there would be no safe way to join the halves back, and it does not invent one), a split with no column ticked outside the key, and one with every column ticked (nothing would be left for the second table); each reason is written under the columns.
What to expect: the query on the narrow table reads less and gets faster, often by several times on a wide table; the query through the view joins both halves on the key and is slower than on the original. Split by columns when your frequent queries need only a few columns and the rest are large.
Good to know
- Measuring and designing only read. Building and dropping the copy write, and only to the copy (
<table>_part), and building and dropping a column split only to<table>_hot,<table>_coldand<table>_vsplit; the original is only read. Before and after runs the queries you typed, on the original and on the copy or the split, as they are. - On PostgreSQL, inside an open transaction, each read runs in a savepoint: a refused read (an exact count on a table you may not read) leaves your transaction usable.
- A table you may not see is reported as "not found, or this login may not see it".