diff --git a/include/caosdb/transaction.h b/include/caosdb/transaction.h
index e73840e4f2aa355961f653b24817784a2773313f..002a9d3eaca381732b0c0104cb4f076d6eab2fe6 100644
--- a/include/caosdb/transaction.h
+++ b/include/caosdb/transaction.h
@@ -185,6 +185,54 @@ using TransactionResponseCase = caosdb::entity::v1::TransactionResponse::Transac
 using caosdb::utility::get_arena;
 using google::protobuf::Arena;
 
+class ResultTableImpl;
+
+class ResultTableColumn {
+public:
+  /**
+   * Get the name of the column.
+   */
+  [[nodiscard]] auto GetName() const noexcept -> const std::string &;
+};
+
+class ResultTable {
+  class HeaderIterator;
+
+public:
+  /**
+   * Number of rows.
+   *
+   * The header is not counted as a row.
+   */
+  [[nodiscard]] auto size() const noexcept -> int;
+  /**
+   * Get the header of this table, i.e. the list of columns.
+   */
+  [[nodiscard]] auto GetHeader() const noexcept -> const HeaderIterator;
+
+  friend class ResultTableImpl;
+
+private:
+  class HeaderIterator : std::iterator<std::output_iterator_tag, ResultTableColumn> {
+  public:
+    explicit HeaderIterator(const ResultTable *result_table, int index = 0);
+    auto operator*() const -> const ResultTableColumn &;
+    auto operator++() -> HeaderIterator &;
+    auto operator++(int) -> HeaderIterator;
+    auto operator!=(const HeaderIterator &rhs) const -> bool;
+    auto size() const noexcept -> int;
+    auto begin() const -> HeaderIterator;
+    auto end() const -> HeaderIterator;
+
+  private:
+    int current_index = 0;
+    const ResultTable *result_table;
+  };
+
+  explicit ResultTable(std::unique_ptr<ResultTableImpl> delegate);
+  std::unique_ptr<ResultTableImpl> delegate;
+};
+
 class Transaction;
 
 /**
@@ -422,6 +470,28 @@ public:
     return *(this->result_set.get());
   }
 
+  /**
+   * Return the ResultTable of a SELECT query.
+   */
+  [[nodiscard]] inline auto GetResultTable() const noexcept -> const ResultTable & {
+    if (this->GetStatus().GetCode() < 0) {
+      CAOSDB_LOG_ERROR(logger_name)
+        << "GetResultTable was called before the transaction has terminated. This is a programming "
+           "error of the code which uses the transaction.";
+      // TODO(tf) This is a really bad SegFault factory. When the transaction
+      // terminates and the result_set is being overriden, the unique_ptr
+      // created here will be deleted and any client of the return ResultTable
+      // will have a SegFault.
+    } else if (this->GetStatus().GetCode() == StatusCode::SPOILED) {
+      CAOSDB_LOG_ERROR(logger_name)
+        << "GetResultTable was called on a \"spoiled\" transaction. That means "
+           "that the result table has already been released via "
+           "ReleaseResultTable(). This is a programming error of the code which "
+           "uses the transaction.";
+    }
+    return *(this->result_table.get());
+  }
+
   /**
    * Return the ResultSet of this transaction.
    *
@@ -547,6 +617,7 @@ private:
   std::string error_message;
   mutable long query_count;
   mutable std::unique_ptr<ResultSet> result_set;
+  mutable std::unique_ptr<ResultTable> result_table;
 };
 
 template <class InputIterator>
diff --git a/src/caosdb/transaction.cpp b/src/caosdb/transaction.cpp
index ca8338dce4e4b721202e72c3fdecf58339746ee0..59c94387558f29813c28ad17c487af4513d22d56 100644
--- a/src/caosdb/transaction.cpp
+++ b/src/caosdb/transaction.cpp
@@ -25,6 +25,7 @@
 #include "caosdb/file_transmission/register_file_upload_handler.h" // for RegisterFileUploadHandler
 #include "caosdb/file_transmission/upload_request_handler.h"       // Upload...
 #include "caosdb/logging.h"                                        // for CAOSDB_LOG_FATAL
+#include "caosdb/protobuf_helper.h"      // for ProtoMessageWrapper
 #include "caosdb/status_code.h"                                    // for StatusCode
 #include "caosdb/transaction_handler.h"                            // for EntityTransactionHandler
 #include <algorithm>                                               // for max
@@ -51,12 +52,47 @@ using TransactionResponseCase = caosdb::entity::v1::TransactionResponse::Transac
 using RetrieveResponseCase = caosdb::entity::v1::RetrieveResponse::RetrieveResponseCase;
 using RetrieveResponse = caosdb::entity::v1::RetrieveResponse;
 using ProtoEntity = caosdb::entity::v1::Entity;
+using ProtoSelectQueryResult = caosdb::entity::v1::SelectQueryResult;
 using caosdb::entity::v1::EntityRequest;
+using caosdb::utility::ScalarProtoMessageWrapper;
 
 using google::protobuf::Arena;
 using NextStatus = grpc::CompletionQueue::NextStatus;
 using RegistrationStatus = caosdb::entity::v1::RegistrationStatus;
 
+
+class ResultTableImpl : public ScalarProtoMessageWrapper<ProtoSelectQueryResult> {
+  static auto create(ProtoSelectQueryResult *select_result) -> std::unique_ptr<ResultTable>;
+  ResultTableImpl();
+  explicit ResultTableImpl(ProtoSelectQueryResult *result_table);
+
+  friend class ResultTable;
+  friend auto ProcessSelectResponse(ProtoSelectQueryResult *select_result) -> std::unique_ptr<ResultTable>;
+};
+
+auto ResultTableImpl::create(ProtoSelectQueryResult *select_result) -> std::unique_ptr<ResultTable> {
+  return std::unique_ptr<ResultTable>(new ResultTable(std::unique_ptr<ResultTableImpl>(new ResultTableImpl(select_result))));
+}
+
+ResultTableImpl::ResultTableImpl(ProtoSelectQueryResult *result_table) : ScalarProtoMessageWrapper<ProtoSelectQueryResult>(result_table) {}
+
+ResultTable::ResultTable(std::unique_ptr<ResultTableImpl> delegate) : delegate(std::move(delegate)) {}
+
+auto ResultTable::size() const noexcept -> int {
+  // TODO
+  return 0;
+}
+
+ResultTable::HeaderIterator::HeaderIterator(const ResultTable *result_table_param, int index) : current_index(index), result_table(result_table_param) {}
+
+auto ResultTable::GetHeader() const noexcept -> const HeaderIterator {
+  return HeaderIterator(this, 0);
+}
+
+auto ResultTable::HeaderIterator::size() const noexcept -> int {
+  return this->result_table->delegate->wrapped->header().columns_size();
+}
+
 ResultSet::iterator::iterator(const ResultSet *result_set_param, int index)
   : current_index(index), result_set(result_set_param) {}
 
@@ -318,6 +354,10 @@ auto Transaction::ExecuteAsynchronously() noexcept -> StatusCode {
   return StatusCode::EXECUTING;
 }
 
+auto ProcessSelectResponse(ProtoSelectQueryResult *select_result) -> std::unique_ptr<ResultTable> {
+  return ResultTableImpl::create(select_result);
+}
+
 auto Transaction::ProcessRetrieveResponse(RetrieveResponse *retrieve_response,
                                           std::vector<std::unique_ptr<Entity>> *entities,
                                           bool *set_error) const noexcept
@@ -329,9 +369,7 @@ auto Transaction::ProcessRetrieveResponse(RetrieveResponse *retrieve_response,
     result = std::make_unique<Entity>(retrieve_entity_response);
   } break;
   case RetrieveResponseCase::kSelectResult: {
-    CAOSDB_LOG_ERROR(logger_name) << "Results of a SELECT query cannot be "
-                                     "processed by this client yet.";
-    // TODO(tf) Select queries
+    this->result_table = ProcessSelectResponse(retrieve_response->mutable_select_result());
   } break;
   case RetrieveResponseCase::kCountResult: {
     this->query_count = retrieve_response->count_result();