テーブルをテストデータで埋める方法:手書きのINSERTから、SQLで行を生成するまで
テストデータ開発MySQLMariaDBPostgreSQLSQLiteSQL ServerOracleClickHouse
誰もが読めるようにAIで翻訳しています。原文はスペイン語です。 原文を読む
私の知っているDBAのほとんどは、もともと開発者でした。今も開発者だという人もいます。私もデータベースの仕事に移る前はコードを書いていました。Calíopeの最初のバージョンを書き始めたのは2008年で、2016年からはDBAとして働いています。開発者だったころのことで覚えているのが、ある問題です。少しも高度なものではなく、誰もが経験したことのある問題です。新しいテーブルを作ると、アプリケーションが動くところを見るために、そこにデータが必要になります。
この記事では、私がその問題を解決してきた方法を、覚えた順に紹介します。例はMySQLで書いてあり、どのコードブロックにも、そのままで動くエンジンを示しています。
どの例も、このテーブルを使います。
CREATE TABLE clientes (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL,
saldo DECIMAL(10,2) NOT NULL DEFAULT 0,
alta DATE NOT NULL
);ほかのエンジンで変わるのは自動増分だけです。PostgreSQLとOracleではGENERATED ALWAYS AS IDENTITY、SQL ServerではIDENTITY(1,1)、SQLiteではINTEGER PRIMARY KEYです。ClickHouseには自動増分がありません。
1. INSERTを1つずつ
最初のころは、経験がなく、ほかのやり方があるとも知らなかったので、データを手で打ち込んでいました。INSERTを1つ、もう1つ、さらにもう1つ。
INSERT INTO clientes (nombre, email, saldo, alta)
VALUES ('Ana Mora', 'ana@ejemplo.com', 150.00, '2010-03-01');
INSERT INTO clientes (nombre, email, saldo, alta)
VALUES ('Luis Rojas', 'luis@ejemplo.com', 80.50, '2010-03-02');
INSERT INTO clientes (nombre, email, saldo, alta)
VALUES ('Sofía Vargas', 'sofia@ejemplo.com', 0.00, '2010-03-05');Oracleでは、日付はDATE '2010-03-01'と書きます。ただの文字列はセッションの日付書式で読み取られ、デフォルトの書式では失敗します。
これでも動きますが、問題が2つあります。数行より多く書くのはとても手間がかかりますし、データはいつも同じです。顧客が3人では、ページ送りが壊れていることにも、一覧が遅いことにも、長い名前で画面が崩れることにも気づきません。
2. 自分自身をコピーするテーブル
やがて、INSERTにはVALUESがなくてもよく、後ろにSELECTを続けられることを知りました。そして、そのSELECTが挿入先と同じテーブルを読むと、テーブルは2倍になります。
INSERT INTO clientes (nombre, email, saldo, alta)
SELECT nombre, email, saldo, alta
FROM clientes;実行するたびに行数が2倍になります。最初の3行が6行になり、次に12行、24行…と増え、18回実行するとすでに786,432行です。id列はリストに入っていないので、コピーにはエンジンが番号を振ります。
問題は、データが相変わらず同じ3行の繰り返しであることです。しかもemailに一意インデックスがあれば、2回目の実行で失敗します。解決策は、コピーそのものの中で値を変えることです。
INSERT INTO clientes (nombre, email, saldo, alta)
SELECT CONCAT(nombre, ' ', id),
CONCAT(id, '.', email),
ROUND(RAND() * 1000, 2),
alta + INTERVAL FLOOR(RAND() * 365) DAY
FROM clientes;ほかのエンジンでは、乱数の関数と日付の計算が変わります。
- PostgreSQL:
random()とalta + (random() * 365)::int。 - SQLite:
abs(random()) % 1000とdate(alta, '+' || (abs(random()) % 365) || ' days')。 - SQL Server:
RAND()は、文のすべての行に同じ値を返します。行ごとに違う値を得るにはABS(CHECKSUM(NEWID())) % 1000を使います。 - Oracle:
DBMS_RANDOM.VALUE(0, 1000)。 - ClickHouse:
rand() % 1000。
注意があります。増え方は指数関数的で、トランザクションログに書かれる量も同じように増えます。2回多く実行しただけで、テーブルは4倍の大きさになります。
3. SQLで行を生成する
年月を重ねるうちに、すでにある行から始める必要はないことを知りました。エンジン自身が数の連番を生成でき、その数の1つ1つが1行になります。MySQLでは再帰CTEで書きます。
SET SESSION cte_max_recursion_depth = 100000;
INSERT INTO clientes (nombre, email, saldo, alta)
WITH RECURSIVE serie (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM serie WHERE n < 100000
)
SELECT CONCAT('Cliente ', n),
CONCAT('cliente', n, '@ejemplo.com'),
ROUND(RAND() * 10000, 2),
DATE '2020-01-01' + INTERVAL FLOOR(RAND() * 2000) DAY
FROM serie;1行目が必要なのは、MySQLがデフォルトで再帰を1000段で止めるからです。
連番の生成方法はエンジンごとに違い、ずっと短く書けるものもあります。
SELECT seq FROM seq_1_to_100000;SELECT n FROM generate_series(1, 100000) AS n;SELECT value FROM GENERATE_SERIES(1, 100000);SELECT LEVEL FROM dual CONNECT BY LEVEL <= 100000;SELECT number + 1 FROM numbers(100000);SQLiteと、2022より前のSQL Serverでは、再帰CTEを使います。
WITH RECURSIVE serie (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM serie WHERE n < 100000
)
SELECT n FROM serie;SQL ServerはRECURSIVEを受け付けず、再帰を100段で止めます。最後にオプションを付けると、この上限がなくなります。
WITH serie (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM serie WHERE n < 100000
)
SELECT n FROM serie
OPTION (MAXRECURSION 0);いつからできるのか
2008年の私は、こんなことができるとは知りませんでした。実際、私が使っていたMySQLではできなかったのです。古いものから新しいものへ、年表にするとこうなります。
- PostgreSQL 8.0、2005年1月:
generate_series。再帰CTEはその後、8.4(2009年)で入りました。 - SQL Server 2005、2005年11月:再帰CTE。
GENERATE_SERIESが来たのはSQL Server 2022になってからです。 - Oracle:
dualに対するCONNECT BY LEVELは、Oracle 10g(2004年)のころにはすでに行の生成に使われていました。標準の再帰CTEは11g R2(2009年)で入りました。 - SQLite 3.8.3、2014年2月:
WITH RECURSIVE。 - MariaDB 10.0(2014年):Sequenceエンジンと、その
seq_1_to_Nテーブル。再帰CTEはMariaDB 10.2(2017年)で入りました。 - ClickHouse:
numbers()は、オープンソースとして公開された2016年からあります。 - MySQL 8.0、2018年4月:再帰CTE。それ以前は、1つの文で連番を生成する方法はありませんでした。
つまり2008年には、PostgreSQL、SQL Server、Oracleではすでにできていました。MySQLはさらに10年かかり、その間に残っていたのは、テーブルを2倍にしていくか、ループのあるプロシージャを書くことでした。
4. テーブルを読み取るプロシージャ
少しプロらしくなってからは、その作業を代わりにやってくれるストアドプロシージャを書きました。information_schemaからテーブルの列とその型を読み取り、それぞれに合ったランダムな値を入れるINSERTを組み立てるものです。
DELIMITER //
CREATE PROCEDURE poblar(IN p_tabla VARCHAR(64), IN p_filas INT)
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE v_columnas TEXT;
DECLARE v_valores TEXT;
SELECT GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY ordinal_position),
GROUP_CONCAT(
CASE
WHEN data_type IN ('int', 'bigint', 'smallint', 'tinyint')
THEN 'FLOOR(RAND() * 1000)'
WHEN data_type IN ('decimal', 'double', 'float')
THEN 'ROUND(RAND() * 1000, 2)'
WHEN data_type IN ('date', 'datetime', 'timestamp')
THEN 'CURRENT_DATE - INTERVAL FLOOR(RAND() * 2000) DAY'
ELSE CONCAT('LEFT(SHA2(RAND(), 256), ',
LEAST(IFNULL(character_maximum_length, 32), 32), ')')
END
ORDER BY ordinal_position)
INTO v_columnas, v_valores
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = p_tabla
AND extra NOT LIKE '%auto_increment%';
SET @sql = CONCAT('INSERT INTO `', p_tabla, '` (', v_columnas, ') VALUES (', v_valores, ')');
PREPARE sentencia FROM @sql;
WHILE i < p_filas DO
EXECUTE sentencia;
SET i = i + 1;
END WHILE;
DEALLOCATE PREPARE sentencia;
END //
DELIMITER ;
CALL poblar('clientes', 1000);ほかのエンジンには、それぞれの列のカタログ(PostgreSQLとSQL Serverではinformation_schema、OracleではALL_TAB_COLUMNS、SQLiteではpragma_table_info)と、それぞれの手続き型言語があるので、エンジンごとに書き直す必要があります。
5. 今は、Calíopeで
あのプロシージャは、Calíopeのある機能の祖先です。カタログのテーブル一覧で、テーブルのコンテキストメニューに「ダミーデータを投入」があります(Macでは右クリック、iPadでは長押し)。作りたい行数を1から5000までで選ぶと、Calíopeは次のことをします。
- サーバのカタログから列とその型を読み取ります。
ENUMの値も読み取ります。 - 自動増分の列と計算列は飛ばします。これらはエンジン自身が埋めます。
- 列の型ごとに有効なサンプル値を生成し、始める前にその1つを見せます。
- 50行ずつのバッチで挿入し、進行状況バーを表示します。バッチが失敗したときは、範囲外の値、
CHECK制約、重複など、その理由を伝えます。
外部キーの列でも、値はランダムです。MySQL、MariaDB、PostgreSQLでは、Calíopeは投入の間だけチェックを無効にするので、存在しない親を指す行ができることがあります。これは、一致する行のないJOINをアプリケーションがどう扱うかを試すのに役立ちます。SQL ServerとOracleではチェックは有効なままで、親テーブルにない値を含むバッチは、その警告とともに失敗します。
MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、ClickHouseで使えます。
何に何を使うか
どのやり方も、私の仕事から消えたわけではありません。
- 手で書く数個の
INSERTは、バグを再現するケースのように、中身がわかっている特定のデータが必要なときに。 - SQLで生成する連番は、量と、値の散らばり方の制御が必要なときに。たとえば、10万人の顧客のうち、登録の80%が直近の1年に集中しているようなデータです。
- Calíopeは、画面が動くところを見るために、テーブルに何か入っていればいいときに。