diff --git a/squeal-postgresql.cabal b/squeal-postgresql.cabal
--- a/squeal-postgresql.cabal
+++ b/squeal-postgresql.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.2
 name: squeal-postgresql
-version: 0.9.0.0
+version: 0.9.1.0
 synopsis: Squeal PostgreSQL Library
 description: Squeal is a type-safe embedding of PostgreSQL in Haskell
 homepage: https://github.com/morphismtech/squeal
diff --git a/src/Squeal/PostgreSQL/Session/Decode.hs b/src/Squeal/PostgreSQL/Session/Decode.hs
--- a/src/Squeal/PostgreSQL/Session/Decode.hs
+++ b/src/Squeal/PostgreSQL/Session/Decode.hs
@@ -40,11 +40,13 @@
   , decodeRow
   , runDecodeRow
   , GenericRow (..)
+  , genericProductRow
   , appendRows
   , consRow
     -- * Decoding Classes
   , FromValue (..)
   , FromField (..)
+  , FromAliasedValue (..)
   , FromArray (..)
   , StateT (..)
   , ExceptT (..)
@@ -564,13 +566,62 @@
     . ReaderT
     $ fmap SOP.fromRecord
     . SOP.hsequence'
-    . SOP.htrans (SOP.Proxy @FromField) (SOP.Comp . runField)
+    . SOP.htrans (SOP.Proxy @FromField) runField
     where
       runField
         :: forall ty z. FromField ty z
         => SOP.K (Maybe Strict.ByteString) ty
-        -> Except Strict.Text (SOP.P z)
-      runField = liftEither . fromField @ty . SOP.unK
+        -> (Except Strict.Text SOP.:.: SOP.P) z
+      runField
+        = SOP.Comp
+        . liftEither
+        . fromField @ty
+        . SOP.unK
+
+{- | Assistant class for `genericProductRow`,
+this class forgets the name of a field while decoding it.
+-}
+class FromAliasedValue (field :: (Symbol, NullType)) (y :: Type) where
+  fromAliasedValue :: Maybe Strict.ByteString -> Either Strict.Text y
+instance FromValue ty y => FromAliasedValue (fld ::: ty) y where
+  fromAliasedValue = fromValue @ty
+
+{- | Positionally `DecodeRow`. More general than `genericRow`,
+which matches records both positionally and by field name,
+`genericProductRow` matches records _or_ tuples purely positionally.
+
+>>> import qualified GHC.Generics as GHC
+>>> import qualified Generics.SOP as SOP
+>>> :{
+let
+  decode :: DecodeRow '[ "foo" ::: 'NotNull 'PGint2, "bar" ::: 'NotNull 'PGtext] (Int16, String)
+  decode = genericProductRow
+in runDecodeRow decode (SOP.K (Just "\NUL\STX") :* SOP.K (Just "two") :* Nil)
+:}
+Right (2,"two")
+-}
+genericProductRow
+  :: ( SOP.IsProductType y ys
+     , SOP.AllZip FromAliasedValue row ys
+     )
+  => DecodeRow row y
+genericProductRow
+  = DecodeRow
+  . ReaderT
+  $ fmap SOP.productTypeTo
+  . SOP.hsequence'
+  . SOP.htrans (SOP.Proxy @FromAliasedValue) runField
+  where
+    runField
+      :: forall ty z. FromAliasedValue ty z
+      => SOP.K (Maybe Strict.ByteString) ty
+      -> (Except Strict.Text SOP.:.: SOP.I) z
+    runField
+      = SOP.Comp
+      . fmap SOP.I
+      . liftEither
+      . fromAliasedValue @ty
+      . SOP.unK
 
 {- |
 >>> :{
diff --git a/src/Squeal/PostgreSQL/Session/Encode.hs b/src/Squeal/PostgreSQL/Session/Encode.hs
--- a/src/Squeal/PostgreSQL/Session/Encode.hs
+++ b/src/Squeal/PostgreSQL/Session/Encode.hs
@@ -70,7 +70,7 @@
 import Foreign.C.Types (CUInt(CUInt))
 import GHC.TypeLits
 import Network.IP.Addr (NetAddr, IP)
-import PostgreSQL.Binary.Encoding
+import PostgreSQL.Binary.Encoding hiding (Composite, field)
 
 import qualified Data.Aeson as Aeson
 import qualified Data.ByteString.Lazy as Lazy.ByteString
