I often hear that batch insert can help to increase the throughput. Instead of insert row by row, we can combine many rows into one batch and insert once. But I want to understand two things:
why is batching help increase insert throughput if batching increase the throughput, why don’t i just use a very huge batch. Is there a upper limit for a batch size. Benchmark setup Environment Sysbench running on EC2 t3.micro 2 vCPU, 1GB RAM Postgres 18 RDS db.t4g.micro 2 vCPU, 1GB RAM, 20GB storage, 90MB shared buffer Both EC2 and RDS are in the same region I choose Sysbench over PgBench because it help me to build the batch data from client with Lua script easily Scripts Schema 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 -- unlogged table for faster seeding CREATE UNLOGGED TABLE transactions ( id BIGSERIAL PRIMARY KEY, account_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, amount NUMERIC(12,2) NOT NULL, currency CHAR(3) NOT NULL DEFAULT 'USD', status SMALLINT NOT NULL DEFAULT 0, type SMALLINT NOT NULL DEFAULT 0, reference_id UUID NOT NULL DEFAULT gen_random_uuid(), description VARCHAR(255) NOT NULL, ip_address INET NOT NULL, device_id VARCHAR(64) NOT NULL, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(), updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp() ); -- seeding INSERT INTO transactions ( account_id, merchant_id, amount, currency, status, type, reference_id, description, ip_address, device_id, metadata, created_at, updated_at ) SELECT (random() * {{NUM_ACCOUNTS}})::bigint, (random() * {{NUM_MERCHANTS}})::bigint, (random() * 10000)::numeric(12,2), (ARRAY['USD','EUR','GBP','AUD','SGD'])[(random()*4)::int + 1], (random() * 3)::smallint, (random() * 1)::smallint, gen_random_uuid(), 'Payment ref-' || g, ('10.' || (random()*255)::int || '.' || (random()*255)::int || '.' || (random()*255)::int)::inet, 'device-' || (random() * {{NUM_ACCOUNTS}})::bigint, jsonb_build_object('channel', (ARRAY['web','mobile','pos'])[(random()*2)::int + 1], 'attempt', (random()*3)::int + 1), NOW() - (random() * INTERVAL '90 days'), NOW() - (random() * INTERVAL '90 days') FROM generate_series(1, {{NUM_ROWS}}) g; ALTER TABLE transactions SET LOGGED; CREATE INDEX ON transactions (account_id); CREATE INDEX ON transactions (merchant_id); CREATE INDEX ON transactions (account_id, created_at DESC); CREATE INDEX ON transactions (status, created_at) WHERE status IN (0, 2); CREATE INDEX ON transactions (created_at); -- update table's statistic VACUUM ANALYZE transactions; CHECKPOINT; -- warm the table SELECT COUNT(*) FROM transactions; Sysbench Lua script 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 -- batch_insert.lua -- Sysbench Lua script for pg batch INSERT benchmark. -- Accepts --batch-size=N on the CLI (passed via sysbench_options in mybench). -- All row data is generated in the Lua VM (client side) before the query is sent. -- -- Usage (standalone): -- sysbench batch_insert.lua \ -- --pgsql-host=localhost --pgsql-port=5432 \ -- --pgsql-db=bench --pgsql-user=postgres \ -- --threads=8 --time=180 --batch-size=100 run -- --------------------------------------------------------------------------- -- Custom CLI options -- --------------------------------------------------------------------------- sysbench.cmdline.options = { batch_size = {"Number of rows to INSERT per transaction", 1} } -- --------------------------------------------------------------------------- -- Per-thread setup / teardown -- --------------------------------------------------------------------------- local CURRENCIES = {"USD", "EUR", "GBP", "AUD", "SGD"} local CHANNELS = {"web", "mobile", "pos"} function thread_init() drv = sysbench.sql.driver() con = drv:connect() end function thread_done() con:disconnect() end -- --------------------------------------------------------------------------- -- Main benchmark event -- Each call = one transaction inserting `batch_size` rows. -- The entire VALUES list is built in Lua (client side) before the query fires. -- --------------------------------------------------------------------------- function event() local batch_size = tonumber(sysbench.opt.batch_size) local values = {} for i = 1, batch_size do local account_id = sysbench.rand.uniform(1, 100000) local merchant_id = sysbench.rand.uniform(1, 10000) local amount = math.floor(sysbench.rand.uniform(1, 1000000)) / 100.0 -- 2 decimal places local currency = CURRENCIES[sysbench.rand.uniform(1, #CURRENCIES)] local status = sysbench.rand.uniform(0, 3) local txn_type = sysbench.rand.uniform(0, 1) local channel = CHANNELS[sysbench.rand.uniform(1, #CHANNELS)] local attempt = sysbench.rand.uniform(1, 4) local device_id = "device-" .. sysbench.rand.uniform(1, 100000) local ip = sysbench.rand.uniform(0, 255) .. "." .. sysbench.rand.uniform(0, 255) .. "." .. sysbench.rand.uniform(0, 255) .. "." .. sysbench.rand.uniform(1, 254) -- Escape single quotes in description just in case local description = "Payment ref-" .. sysbench.rand.uniform(1, 1000000) values[i] = string.format( -- account_id, merchant_id, amount, currency, status, type, -- reference_id, description, ip_address, device_id, metadata, -- created_at, updated_at "(%d, %d, %.2f, '%s', %d, %d, gen_random_uuid(), '%s', '%s'::inet, '%s', " .. "'{\"channel\":\"%s\",\"attempt\":%d}'::jsonb, clock_timestamp(), clock_timestamp())", account_id, merchant_id, amount, currency, status, txn_type, description, ip, device_id, channel, attempt ) end local sql = "INSERT INTO transactions " .. "(account_id, merchant_id, amount, currency, status, type, " .. "reference_id, description, ip_address, device_id, metadata, " .. "created_at, updated_at) VALUES " .. table.concat(values, ",") con:query("BEGIN") con:query(sql) con:query("COMMIT") end Run Parameters Number of threads: 2. Meaning two sysbench clients will concurrently send requests to RDS Duration: 180 seconds Batch size: 1, 10, 50, 100, 500, 1000, 2000, 5000, 10000 Parameter value was used in the seeding file: 1 2 3 NUM_ACCOUNTS 100000 NUM_MERCHANTS 10000 NUM_ROWS 10000 Sample sysbench command:
...