hpqtypes-1.15.0.0: bench-db/Main.hs
module Main (main) where
import Control.DeepSeq
import Control.Exception
import Control.Monad
import Data.Int
import Data.Pool (defaultPoolConfig)
import Data.Text qualified as T
import Data.Time
import Database.PostgreSQL.PQTypes
import System.Environment
import Test.Tasty.Bench
-- | Number of records inserted into each of the two tables.
numRecords :: Int
numRecords = 50000
-- | Number of children per parent. Only the first @numRecords \`div\`
-- childrenPerParent@ parents get children, so the selection phase decodes
-- both non-trivial and empty arrays.
childrenPerParent :: Int
childrenPerParent = 10
-- | Number of scalars fetched by 'selectBigArray' and 'selectManyRows'.
-- They deliver the same amount of data, as one array and as that many rows
-- respectively, so that the array decoder and the row decoder can be
-- compared against each other.
bigArraySize :: Int32
bigArraySize = 100000
----------------------------------------
data Child = Child Int32 T.Text Double UTCTime Integer
data Parent = Parent Int32 T.Text Double UTCTime Integer [Child]
instance NFData Child where
rnf (Child a b c d e) =
rnf a `seq` rnf b `seq` rnf c `seq` rnf d `seq` rnf e
instance NFData Parent where
rnf (Parent a b c d e f) =
rnf a `seq` rnf b `seq` rnf c `seq` rnf d `seq` rnf e `seq` rnf f
type instance CompositeRow Child = (Int32, T.Text, Double, UTCTime, Integer)
instance PQFormat Child where
pqFormat = "%bench_child_"
instance CompositeFromSQL Child where
toComposite (cid, t, d, ts, n) = Child cid t d ts n
-- | Deterministic record with the given id.
recordData :: UTCTime -> Int -> (Int32, T.Text, Double, UTCTime, Integer)
recordData base i =
( fromIntegral i
, T.pack $ "record " <> show i <> " with some textual payload"
, fromIntegral i * 1.5
, addUTCTime (fromIntegral i) base
, 2 ^ (70 :: Int) + fromIntegral i
)
----------------------------------------
createTables :: DBT IO ()
createTables = do
runSQL_ $
mconcat
[ "CREATE TABLE bench_parents_ ("
, " id INTEGER NOT NULL"
, ", t TEXT NOT NULL"
, ", d DOUBLE PRECISION NOT NULL"
, ", ts TIMESTAMPTZ NOT NULL"
, ", n NUMERIC NOT NULL"
, ", PRIMARY KEY (id)"
, ")"
]
runSQL_ $
mconcat
[ "CREATE TABLE bench_children_ ("
, " id INTEGER NOT NULL"
, ", parent_id INTEGER NOT NULL"
, ", t TEXT NOT NULL"
, ", d DOUBLE PRECISION NOT NULL"
, ", ts TIMESTAMPTZ NOT NULL"
, ", n NUMERIC NOT NULL"
, ", PRIMARY KEY (id)"
, ", FOREIGN KEY (parent_id) REFERENCES bench_parents_ (id)"
, ")"
]
runSQL_ "CREATE INDEX bench_children_parent_id_idx_ ON bench_children_ (parent_id)"
runSQL_ $
mconcat
[ "CREATE TYPE bench_child_ AS ("
, " id INTEGER"
, ", t TEXT"
, ", d DOUBLE PRECISION"
, ", ts TIMESTAMPTZ"
, ", n NUMERIC"
, ")"
]
dropTables :: DBT IO ()
dropTables = do
runSQL_ "DROP TYPE IF EXISTS bench_child_"
runSQL_ "DROP TABLE IF EXISTS bench_children_"
runSQL_ "DROP TABLE IF EXISTS bench_parents_"
-- | Empty the tables so that 'insertData' can be run again.
truncateTables :: DBT IO ()
truncateTables = runSQL_ "TRUNCATE bench_children_, bench_parents_"
insertData :: UTCTime -> DBT IO ()
insertData base = do
forM_ [0 .. numRecords - 1] $ \i -> do
runQuery_ $
rawSQL
"INSERT INTO bench_parents_ (id, t, d, ts, n) VALUES ($1, $2, $3, $4, $5)"
(recordData base i)
forM_ [0 .. numRecords - 1] $ \i -> do
let (cid, t, d, ts, n) = recordData base i
pid = fromIntegral $ i `div` childrenPerParent :: Int32
runQuery_ $
rawSQL
"INSERT INTO bench_children_ (id, parent_id, t, d, ts, n) VALUES ($1, $2, $3, $4, $5, $6)"
(cid, pid, t, d, ts, n)
selectParents :: DBT IO [Parent]
selectParents = do
runSQL_ "SELECT p.id, p.t, p.d, p.ts, p.n FROM bench_parents_ p ORDER BY p.id"
fetchMany $ \(pid, t, d, ts, n) -> Parent pid t d ts n []
selectData :: DBT IO [Parent]
selectData = do
runSQL_ $
mconcat
[ "SELECT p.id, p.t, p.d, p.ts, p.n"
, ", ARRAY(SELECT (c.id, c.t, c.d, c.ts, c.n)::bench_child_"
, " FROM bench_children_ c WHERE c.parent_id = p.id ORDER BY c.id)"
, " FROM bench_parents_ p ORDER BY p.id"
]
fetchMany $ \(pid, t, d, ts, n, CompositeArray1 children) ->
Parent pid t d ts n children
selectBigArray :: DBT IO [Int32]
selectBigArray = do
runQuery_ $
rawSQL "SELECT ARRAY(SELECT generate_series(1, $1))::int4[]" (Identity bigArraySize)
fetchOne $ \(Identity (Array1 elems)) -> elems
-- | The counterpart of 'selectBigArray': the same scalars, delivered as one
-- column of that many rows instead of as one array.
selectManyRows :: DBT IO [Int32]
selectManyRows = do
runQuery_ $ rawSQL "SELECT generate_series(1, $1)" (Identity bigArraySize)
fetchMany runIdentity
----------------------------------------
-- | The connection info string is taken from the @CONNINFO@ environment
-- variable, as the command line belongs to @tasty-bench@. If it's not set,
-- the choice is left to @libpq@, i.e. to the @PG*@ variables.
main :: IO ()
main = do
connInfo <- maybe T.empty T.pack <$> lookupEnv "CONNINFO"
let settings = defaultConnectionSettings {csConnInfo = connInfo}
ConnectionSource cs <- pooled settings
let runDB :: DBT IO a -> IO a
runDB = runDBT cs defaultTransactionSettings
base <- getCurrentTime
runDB $ do
-- Keep the NOTICEs of the DROPs below out of the benchmark report.
runSQL_ "SET client_min_messages TO WARNING"
dropTables
createTables
insertData base
-- Registration of composites happens at connection time, so bench_child_
-- needs to exist before this source is used.
ConnectionSource csComposite <- pooled settings {csComposites = ["bench_child_"]}
let runDBComposite :: DBT IO a -> IO a
runDBComposite = runDBT csComposite defaultTransactionSettings
(`finally` runDB dropTables) . defaultMain $
[ -- Insertion refills the tables it empties, so the selection benchmarks
-- below see the same data regardless of whether this one ran.
bench "insert" . nfIO . runDB $ truncateTables >> insertData base
, bench "select parents" . nfIO $ runDB selectParents
, bench "select parents with children" . nfIO $ runDBComposite selectData
, bench "select big array" . nfIO $ runDB selectBigArray
, bench "select many rows" . nfIO $ runDB selectManyRows
]
where
-- A pool holding a single connection is used rather than 'simpleSource'
-- so that establishing one isn't measured by every iteration of a
-- benchmark.
pooled settings = poolSource settings $ \connect disconnect ->
defaultPoolConfig connect disconnect 60 1