diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+# hpqtypes-extras-1.16.4.0 (2023-04-20)
+* Add support for the `UNION ALL` clause via `sqlUnionAll`.
+
 # hpqtypes-extras-1.16.3.1 (2023-04-13)
 * Add support for GHC 9.6.
 * Fix checkAndRememberMaterializationSupport's query.
diff --git a/hpqtypes-extras.cabal b/hpqtypes-extras.cabal
--- a/hpqtypes-extras.cabal
+++ b/hpqtypes-extras.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                hpqtypes-extras
-version:             1.16.3.1
+version:             1.16.4.0
 synopsis:            Extra utilities for hpqtypes library
 description:         The following extras for hpqtypes library:
                      .
diff --git a/src/Database/PostgreSQL/PQTypes/SQL/Builder.hs b/src/Database/PostgreSQL/PQTypes/SQL/Builder.hs
--- a/src/Database/PostgreSQL/PQTypes/SQL/Builder.hs
+++ b/src/Database/PostgreSQL/PQTypes/SQL/Builder.hs
@@ -129,6 +129,7 @@
   , sqlWith
   , sqlWithMaterialized
   , sqlUnion
+  , sqlUnionAll
   , checkAndRememberMaterializationSupport
 
   , sqlSelect
@@ -231,6 +232,7 @@
 data SqlSelect = SqlSelect
   { sqlSelectFrom     :: SQL
   , sqlSelectUnion    :: [SQL]
+  , sqlSelectUnionAll :: [SQL]
   , sqlSelectDistinct :: Bool
   , sqlSelectResult   :: [SQL]
   , sqlSelectWhere    :: [SqlCondition]
@@ -340,8 +342,8 @@
   toSQLCommand cmd = smconcat
     [ emitClausesSepComma "WITH" $
       map (\(name,command,mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlSelectWith cmd)
-    , if hasUnion
-      then emitClausesSep "" "UNION" (mainSelectClause : sqlSelectUnion cmd)
+    , if hasUnion || hasUnionAll
+      then emitClausesSep "" unionKeyword (mainSelectClause : unionCmd)
       else mainSelectClause
     , emitClausesSepComma "GROUP BY" (sqlSelectGroupBy cmd)
     , emitClausesSep "HAVING" "AND" (sqlSelectHaving cmd)
@@ -368,6 +370,19 @@
         ]
 
       hasUnion      = not . null $ sqlSelectUnion cmd
+      hasUnionAll   = not . null $ sqlSelectUnionAll cmd
+      unionKeyword = case (hasUnion, hasUnionAll) of
+                        (False, True) -> "UNION ALL"
+                        (True, False) -> "UNION"
+                        -- False, False is caught by the (hasUnion || hasUnionAll) above.
+                        -- Hence, the catch-all is implicitly for (True, True).
+                        _ -> error "Having both `sqlSelectUnion` and `sqlSelectUnionAll` is not supported at the moment."
+      unionCmd = case (hasUnion, hasUnionAll) of
+                        (False, True) -> sqlSelectUnionAll cmd
+                        (True, False) -> sqlSelectUnion cmd
+                        -- False, False is caught by the (hasUnion || hasUnionAll) above.
+                        -- Hence, the catch-all is implicitly for (True, True).
+                        _ -> error "Having both `sqlSelectUnion` and `sqlSelectUnionAll` is not supported at the moment."
       orderByClause = emitClausesSepComma "ORDER BY" $ sqlSelectOrderBy cmd
       limitClause   = unsafeSQL $ "LIMIT" <+> show (sqlSelectLimit cmd)
 
@@ -403,6 +418,7 @@
     , parenthesize . sqlConcatComma . map fst $ sqlInsertSelectSet cmd
     , parenthesize . toSQLCommand $ SqlSelect { sqlSelectFrom    = sqlInsertSelectFrom cmd
                                               , sqlSelectUnion   = []
+                                              , sqlSelectUnionAll = []
                                               , sqlSelectDistinct = sqlInsertSelectDistinct cmd
                                               , sqlSelectResult  = fmap snd $ sqlInsertSelectSet cmd
                                               , sqlSelectWhere   = sqlInsertSelectWhere cmd
@@ -460,14 +476,13 @@
   toSQLCommand cmd =
     "(" <+> smintercalate "AND" (map (parenthesize . toSQLCommand) (sqlAllWhere cmd)) <+> ")"
 
-
 sqlSelect :: SQL -> State SqlSelect () -> SqlSelect
 sqlSelect table refine =
-  execState refine (SqlSelect table [] False [] [] [] [] [] 0 (-1) [])
+  execState refine (SqlSelect table [] [] False [] [] [] [] [] 0 (-1) [])
 
 sqlSelect2 :: SQL -> State SqlSelect () -> SqlSelect
 sqlSelect2 from refine =
-  execState refine (SqlSelect from [] False [] [] [] [] [] 0 (-1) [])
+  execState refine (SqlSelect from [] [] False [] [] [] [] [] 0 (-1) [])
 
 sqlInsert :: SQL -> State SqlInsert () -> SqlInsert
 sqlInsert table refine =
@@ -533,6 +548,13 @@
 -- applies to the main SELECT, not the whole union.
 sqlUnion :: (MonadState SqlSelect m, Sqlable sql) => [sql] -> m ()
 sqlUnion sqls = modify (\cmd -> cmd { sqlSelectUnion = map toSQLCommand sqls })
+
+-- | Note: WHERE clause of the main SELECT is treated specially, i.e. it only
+-- applies to the main SELECT, not the whole union.
+--
+-- @since 1.16.4.0
+sqlUnionAll :: (MonadState SqlSelect m, Sqlable sql) => [sql] -> m ()
+sqlUnionAll sqls = modify (\cmd -> cmd { sqlSelectUnionAll = map toSQLCommand sqls })
 
 class SqlWhere a where
   sqlWhere1 :: a -> SqlCondition -> a
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1205,7 +1205,7 @@
   testPass
   runSQL_ "DELETE FROM bank"
   step "Checking for WITH MATERIALIZED support"
-  checkAndRememberMaterializationSupport 
+  checkAndRememberMaterializationSupport
   step "Running sql WITH tests again with WITH MATERIALIZED support flag set"
   testPass
   where
@@ -1257,6 +1257,45 @@
       (results :: [T.Text]) <- fetchMany runIdentity
       liftIO $ assertEqual "Wrong number of banks left" 2 (length results)
 
+testUnion :: HasCallStack => (String -> TestM ()) -> TestM ()
+testUnion step = do
+  step "Running SQL UNION tests"
+  testPass
+  where
+    testPass = do
+      runQuery_ . sqlSelect "" $ do
+        sqlResult "true"
+        sqlUnion
+          [ sqlSelect "" $ do
+              sqlResult "false"
+          , sqlSelect "" $ do
+              sqlResult "true"
+          ]
+      result <- fetchMany runIdentity
+      liftIO $ assertEqual "UNION of booleans"
+        [False, True]
+        result
+
+
+testUnionAll :: HasCallStack => (String -> TestM ()) -> TestM ()
+testUnionAll step = do
+  step "Running SQL UNION ALL tests"
+  testPass
+  where
+    testPass = do
+      runQuery_ . sqlSelect "" $ do
+        sqlResult "true"
+        sqlUnionAll
+          [ sqlSelect "" $ do
+              sqlResult "false"
+          , sqlSelect "" $ do
+              sqlResult "true"
+          ]
+      result <- fetchMany runIdentity
+      liftIO $ assertEqual "UNION ALL of booleans"
+        [True, False, True]
+        result
+
 migrationTest1 :: ConnectionSourceM (LogT IO) -> TestTree
 migrationTest1 connSource =
   testCaseSteps' "Migration test 1" connSource $ \step -> do
@@ -1398,6 +1437,18 @@
     freshTestDB step
     testSqlWith step
 
+unionTests :: ConnectionSourceM (LogT IO) -> TestTree
+unionTests connSource =
+  testCaseSteps' "SQL UNION Tests" connSource $ \step -> do
+    freshTestDB step
+    testUnion step
+
+unionAllTests :: ConnectionSourceM (LogT IO) -> TestTree
+unionAllTests connSource =
+  testCaseSteps' "SQL UNION ALL Tests" connSource $ \step -> do
+    freshTestDB step
+    testUnionAll step
+
 eitherExc :: MonadCatch m => (SomeException -> m ()) -> (a -> m ()) -> m a -> m ()
 eitherExc left right c = try c >>= either left right
 
@@ -1581,6 +1632,8 @@
                          , migrationTest5 connSource
                          , triggerTests connSource
                          , sqlWithTests connSource
+                         , unionTests connSource
+                         , unionAllTests connSource
                          ]
   where
     ings =
