diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 739f99446c9a3169349c8bc17a614135f788a793..0e534d4e4d72985e54c4470e8a6189eafcde95bc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -191,6 +191,9 @@ trigger_octavelib:
   trigger:
     project: caosdb/src/caosdb-octavelib
     branch: $OCTAVELIB_REF
+  # It's ok if octave fails in case of features that might already be
+  # implemented in proto, server, and cpplib, but not yet in Octave.
+  allow_failure: true
 
 # ... and for Julia.
 trigger_julialib:
@@ -210,6 +213,9 @@ trigger_julialib:
   trigger:
     project: caosdb/src/caosdb-julialib
     branch: $JULIALIB_REF
+  # It's ok if julia fails in case of features that might already be
+  # implemented in proto, server, and cpplib, but not yet in Julia.
+  allow_failure: true
 
 # Build the sphinx documentation and make it ready for deployment by Gitlab Pages
 # Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages
diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md
index 7c73e5991c9369cca78349fa0d8d243971c75497..7dcd73afc2e172db348e63f31760d19973779f38 100644
--- a/doc/CHANGELOG.md
+++ b/doc/CHANGELOG.md
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+* Support for `SELECT` queries.
+
 ### Changed
 
 ### Deprecated
diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt
index 29c27eaf536434d2f870fc111ca00f5febec1327..31f0e81d6a8fc8e55a6005b86d7160c834a2dfd7 100644
--- a/include/CMakeLists.txt
+++ b/include/CMakeLists.txt
@@ -35,6 +35,8 @@ set(libcaosdb_INCL
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/logging.h
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/message_code.h
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/protobuf_helper.h
+    ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/result_table.h
+    ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/result_set.h
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/status_code.h
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/transaction.h
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/transaction_handler.h
diff --git a/include/caosdb/result_set.h b/include/caosdb/result_set.h
new file mode 100644
index 0000000000000000000000000000000000000000..2ce70e67e770a93e86512c564954ea57d10046eb
--- /dev/null
+++ b/include/caosdb/result_set.h
@@ -0,0 +1,125 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#ifndef CAOSDB_RESULT_SET_H
+#define CAOSDB_RESULT_SET_H
+
+#include "caosdb/entity.h" // for Entity, FileDe...
+#include <algorithm>       // for max
+#include <iterator>        // for iterator, next
+#include <memory>          // for unique_ptr
+#include <utility>         // for move
+#include <vector>          // for vector
+// IWYU pragma: no_include <ext/alloc_traits.h>
+
+namespace caosdb::transaction {
+using caosdb::entity::Entity;
+
+/**
+ * Abstract base class for the results of a Transaction.
+ */
+class ResultSet {
+  class iterator;
+
+public:
+  virtual ~ResultSet() = default;
+  [[nodiscard]] virtual auto size() const noexcept -> int = 0;
+  [[nodiscard]] virtual auto at(const int index) const -> const Entity & = 0;
+  [[nodiscard]] virtual auto mutable_at(int index) const -> Entity * = 0;
+  /**
+   * Return the Entity at the given index.
+   *
+   * This method releases the entity from the underlying collection and thus
+   * leaves the ResultSet in a corrupted state.
+   *
+   * This method can be called only once for each index.
+   */
+  [[nodiscard]] virtual auto release_at(int index) -> Entity * = 0;
+  auto begin() const -> iterator;
+  auto end() const -> iterator;
+
+private:
+  class iterator : public std::iterator<std::output_iterator_tag, Entity> {
+  public:
+    explicit iterator(const ResultSet *result_set, int index = 0);
+    auto operator*() const -> const Entity &;
+    auto operator++() -> iterator &;
+    auto operator++(int) -> iterator;
+    auto operator!=(const iterator &rhs) const -> bool;
+
+  private:
+    int current_index = 0;
+    const ResultSet *result_set;
+  };
+};
+
+class AbstractMultiResultSet : public ResultSet {
+public:
+  /**
+   * Copy Constructor.
+   *
+   * Copies the underlying collection of entities.
+   */
+  inline AbstractMultiResultSet(const AbstractMultiResultSet &original) {
+    for (const Entity &entity : original) {
+      this->items.push_back(std::make_unique<Entity>(entity));
+    }
+  }
+  virtual ~AbstractMultiResultSet() = default;
+  inline explicit AbstractMultiResultSet(std::vector<std::unique_ptr<Entity>> result_set)
+    : items(std::move(result_set)) {}
+  [[nodiscard]] inline auto size() const noexcept -> int override { return this->items.size(); }
+  [[nodiscard]] inline auto at(const int index) const -> const Entity & override {
+    return *(this->items.at(index));
+  }
+  [[nodiscard]] inline auto mutable_at(int index) const -> Entity * override {
+    return this->items.at(index).get();
+  }
+  /**
+   * Return the Entity at the given index.
+   *
+   * This method releases the entity from the underlying collection and thus
+   * leaves the ResultSet in a corrupted state.
+   *
+   * This method can be called only once for each index.
+   */
+  [[nodiscard]] inline auto release_at(int index) -> Entity * override {
+    return this->items.at(index).release();
+  }
+  /**
+   * Remove all entities from this result set.
+   */
+  inline auto clear() noexcept -> void { this->items.clear(); }
+
+protected:
+  std::vector<std::unique_ptr<Entity>> items;
+};
+
+/**
+ * Container with results of a transaction.
+ */
+class MultiResultSet : public AbstractMultiResultSet {
+public:
+  ~MultiResultSet() = default;
+  explicit MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set);
+};
+
+} // namespace caosdb::transaction
+#endif
diff --git a/include/caosdb/result_table.h b/include/caosdb/result_table.h
new file mode 100644
index 0000000000000000000000000000000000000000..6795e27e5b60a680c1700d9fa98bcdd50b059474
--- /dev/null
+++ b/include/caosdb/result_table.h
@@ -0,0 +1,124 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#ifndef CAOSDB_RESULT_TABLE_H
+#define CAOSDB_RESULT_TABLE_H
+
+#include "caosdb/value.h" // for Value
+#include <iterator>       // for iterator, next
+#include <memory>         // for unique_ptr
+#include <string>         // for string
+
+namespace caosdb::transaction {
+using caosdb::entity::Value;
+
+class ResultTableImpl;
+
+class ResultTableColumnImpl;
+
+class ResultTableRowImpl;
+
+class ResultTableRow {
+public:
+  [[nodiscard]] auto GetValue(const std::string &column) const noexcept -> Value;
+  friend class ResultTable;
+  explicit ResultTableRow(std::unique_ptr<ResultTableRowImpl> delegate);
+
+private:
+  std::unique_ptr<ResultTableRowImpl> delegate;
+};
+
+class ResultTableColumn {
+public:
+  /**
+   * Get the name of the column.
+   */
+  [[nodiscard]] auto GetName() const noexcept -> const std::string &;
+
+  friend class ResultTable;
+  explicit ResultTableColumn(std::unique_ptr<ResultTableColumnImpl> delegate);
+
+private:
+  std::unique_ptr<ResultTableColumnImpl> delegate;
+};
+
+class ResultTable {
+  class HeaderIterator;
+  class RowIterator;
+
+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 -> HeaderIterator;
+  /**
+   * Get the data rows, i.e. the actual data.
+   */
+  [[nodiscard]] auto GetRows() const noexcept -> RowIterator;
+
+  friend class ResultTableImpl;
+
+private:
+  class HeaderIterator : std::iterator<std::output_iterator_tag, ResultTableColumn> {
+  public:
+    explicit HeaderIterator(const ResultTable *result_table, int index = 0);
+    HeaderIterator(const HeaderIterator &other);
+    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;
+  };
+
+  class RowIterator : std::iterator<std::output_iterator_tag, ResultTableRow> {
+  public:
+    explicit RowIterator(const ResultTable *result_table, int index = 0);
+    RowIterator(const RowIterator &other);
+    auto operator*() const -> const ResultTableRow &;
+    auto operator++() -> RowIterator &;
+    auto operator++(int) -> RowIterator;
+    auto operator!=(const RowIterator &rhs) const -> bool;
+    auto size() const noexcept -> int;
+    auto begin() const -> RowIterator;
+    auto end() const -> RowIterator;
+
+  private:
+    int current_index = 0;
+    const ResultTable *result_table;
+  };
+
+  explicit ResultTable(std::unique_ptr<ResultTableImpl> delegate);
+  std::unique_ptr<ResultTableImpl> delegate;
+};
+
+} // namespace caosdb::transaction
+#endif
diff --git a/include/caosdb/transaction.h b/include/caosdb/transaction.h
index e73840e4f2aa355961f653b24817784a2773313f..b1d3bee8f740b1bcbb62c5f8fe924a937cbdf36c 100644
--- a/include/caosdb/transaction.h
+++ b/include/caosdb/transaction.h
@@ -30,20 +30,20 @@
 #include "caosdb/logging.h"                 // for CAOSDB_LOG_ERR...
 #include "caosdb/protobuf_helper.h"         // for get_arena
 #include "caosdb/status_code.h"             // for StatusCode
+#include "caosdb/result_set.h"              // for ResultSet
+#include "caosdb/result_table.h"            // for ResultTable
 #include "caosdb/transaction_status.h"      // for StatusCode
-#include <algorithm>                        // for max
+#include "caosdb/value.h"                   // for Value
 #include <future>                           // for async, future
 #include <google/protobuf/arena.h>          // for Arena
 #include <google/protobuf/util/json_util.h> // for MessageToJsonS...
 #include <grpcpp/completion_queue.h>        // for CompletionQueue
 #include <iterator>                         // for iterator, next
 #include <map>                              // for map
-// IWYU pragma: no_include <ext/alloc_traits.h>
-#include <memory>  // for unique_ptr
-#include <mutex>   // for mutex
-#include <string>  // for string
-#include <utility> // for move
-#include <vector>  // for vector
+#include <memory>                           // for unique_ptr
+#include <mutex>                            // for mutex
+#include <string>                           // for string
+#include <vector>                           // for vector
 
 /**
  * Do all necessary checks and assure that another retrieval (by id or by
@@ -166,6 +166,7 @@
 namespace caosdb::transaction {
 using caosdb::entity::Entity;
 using caosdb::entity::FileDescriptor;
+using caosdb::entity::Value;
 using caosdb::entity::v1::EntityResponse;
 using caosdb::entity::v1::EntityTransactionService;
 using caosdb::entity::v1::FileDownloadRequest;
@@ -187,95 +188,6 @@ using google::protobuf::Arena;
 
 class Transaction;
 
-/**
- * Abstract base class for the results of a Transaction.
- */
-class ResultSet {
-  class iterator;
-
-public:
-  virtual ~ResultSet() = default;
-  [[nodiscard]] virtual auto size() const noexcept -> int = 0;
-  [[nodiscard]] virtual auto at(const int index) const -> const Entity & = 0;
-  [[nodiscard]] virtual auto mutable_at(int index) const -> Entity * = 0;
-  /**
-   * Return the Entity at the given index.
-   *
-   * This method releases the entity from the underlying collection and thus
-   * leaves the ResultSet in a corrupted state.
-   *
-   * This method can be called only once for each index.
-   */
-  [[nodiscard]] virtual auto release_at(int index) -> Entity * = 0;
-  auto begin() const -> iterator;
-  auto end() const -> iterator;
-
-private:
-  class iterator : public std::iterator<std::output_iterator_tag, Entity> {
-  public:
-    explicit iterator(const ResultSet *result_set, int index = 0);
-    auto operator*() const -> const Entity &;
-    auto operator++() -> iterator &;
-    auto operator++(int) -> iterator;
-    auto operator!=(const iterator &rhs) const -> bool;
-
-  private:
-    int current_index = 0;
-    const ResultSet *result_set;
-  };
-};
-
-class AbstractMultiResultSet : public ResultSet {
-public:
-  /**
-   * Copy Constructor.
-   *
-   * Copies the underlying collection of entities.
-   */
-  inline AbstractMultiResultSet(const AbstractMultiResultSet &original) {
-    for (const Entity &entity : original) {
-      this->items.push_back(std::make_unique<Entity>(entity));
-    }
-  }
-  virtual ~AbstractMultiResultSet() = default;
-  inline explicit AbstractMultiResultSet(std::vector<std::unique_ptr<Entity>> result_set)
-    : items(std::move(result_set)) {}
-  [[nodiscard]] inline auto size() const noexcept -> int override { return this->items.size(); }
-  [[nodiscard]] inline auto at(const int index) const -> const Entity & override {
-    return *(this->items.at(index));
-  }
-  [[nodiscard]] inline auto mutable_at(int index) const -> Entity * override {
-    return this->items.at(index).get();
-  }
-  /**
-   * Return the Entity at the given index.
-   *
-   * This method releases the entity from the underlying collection and thus
-   * leaves the ResultSet in a corrupted state.
-   *
-   * This method can be called only once for each index.
-   */
-  [[nodiscard]] inline auto release_at(int index) -> Entity * override {
-    return this->items.at(index).release();
-  }
-  /**
-   * Remove all entities from this result set.
-   */
-  inline auto clear() noexcept -> void { this->items.clear(); }
-
-protected:
-  std::vector<std::unique_ptr<Entity>> items;
-};
-
-/**
- * Container with results of a transaction.
- */
-class MultiResultSet : public AbstractMultiResultSet {
-public:
-  ~MultiResultSet() = default;
-  explicit MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set);
-};
-
 /**
  * @brief Create a transaction via `CaosDBConnection.createTransaction()`
  */
@@ -422,6 +334,24 @@ 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.";
+    } 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 +477,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/include/caosdb/value.h b/include/caosdb/value.h
index a92847a82d3cd15ad2dda61e64c6843b99ac6d75..cffb11162b9ed8b83508316c155f1fca46be2714 100644
--- a/include/caosdb/value.h
+++ b/include/caosdb/value.h
@@ -52,14 +52,6 @@ using ScalarValueCase = caosdb::entity::v1::ScalarValue::ScalarValueCase;
 class ScalarValue;
 class Value;
 
-// Represents special values which are otherwise hard to tranfer via protobuf.
-enum SpecialValue {
-  // Represent the NULL value.
-  NULL_VALUE = ProtoSpecialValue::SPECIAL_VALUE_UNSPECIFIED,
-  // The empty string.
-  EMPTY_STRING = ProtoSpecialValue::SPECIAL_VALUE_EMPTY_STRING,
-};
-
 /**
  * Pure abstract base class for values.
  */
@@ -297,6 +289,9 @@ public:
     this->wrapped->CopyFrom(*value.GetProtoValue());
   }
   explicit inline Value(ProtoValue *wrapped) : ScalarProtoMessageWrapper<ProtoValue>(wrapped) {}
+  explicit inline Value(const ProtoValue &value) : ScalarProtoMessageWrapper<ProtoValue>() {
+    this->wrapped->CopyFrom(value);
+  }
   explicit inline Value(const std::string &value) : ScalarProtoMessageWrapper<ProtoValue>() {
     this->wrapped->mutable_scalar_value()->set_string_value(value);
   }
diff --git a/proto b/proto
index 0b301401cf28d7a3edeb2c55e418f072b83cf5a7..96e7a1fb667ed1bb3b2602af6c69724519bf5118 160000
--- a/proto
+++ b/proto
@@ -1 +1 @@
-Subproject commit 0b301401cf28d7a3edeb2c55e418f072b83cf5a7
+Subproject commit 96e7a1fb667ed1bb3b2602af6c69724519bf5118
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f64bc7671cbe142390ca5439916dfbddd3660294..939a85ba1f0492a6d985d4f873f2560adb22f039 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -27,6 +27,9 @@ set(libcaosdb_SRC
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/connection.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/configuration.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/protobuf_helper.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/result_set.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/result_table.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/result_table_impl.h
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/transaction.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/transaction_handler.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/utility.cpp
@@ -45,6 +48,5 @@ IF(BUILD_ACM)
          ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/user_impl.h
          )
 ENDIF()
-
 # pass variable to parent scope
 set(libcaosdb_SRC ${libcaosdb_SRC}  PARENT_SCOPE)
diff --git a/src/caosdb/result_set.cpp b/src/caosdb/result_set.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a4172a836d4e8bb9ed5e568439556ebace53c4e4
--- /dev/null
+++ b/src/caosdb/result_set.cpp
@@ -0,0 +1,55 @@
+/*
+ * This file is a part of the CaosDB Project.
+ * Copyright (C) 2021-2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2021-2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#include "caosdb/result_set.h" // for ResultSet
+#include <memory>              // for unique_ptr
+#include <utility>             // for move, pair
+
+namespace caosdb::transaction {
+
+ResultSet::iterator::iterator(const ResultSet *result_set_param, int index)
+  : current_index(index), result_set(result_set_param) {}
+
+auto ResultSet::iterator::operator*() const -> const Entity & {
+  return this->result_set->at(current_index);
+}
+
+auto ResultSet::iterator::operator++() -> ResultSet::iterator & {
+  current_index++;
+  return *this;
+}
+
+auto ResultSet::iterator::operator++(int) -> ResultSet::iterator {
+  iterator tmp(*this);
+  operator++();
+  return tmp;
+}
+
+auto ResultSet::iterator::operator!=(const iterator &rhs) const -> bool {
+  return this->current_index != rhs.current_index;
+}
+
+auto ResultSet::begin() const -> ResultSet::iterator { return ResultSet::iterator(this, 0); }
+
+auto ResultSet::end() const -> ResultSet::iterator { return ResultSet::iterator(this, size()); }
+
+MultiResultSet::MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set)
+  : AbstractMultiResultSet(std::move(result_set)) {}
+
+} // namespace caosdb::transaction
diff --git a/src/caosdb/result_table.cpp b/src/caosdb/result_table.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f7d5c3e3564fdad9b7f9755c77c6ddce37d3c25f
--- /dev/null
+++ b/src/caosdb/result_table.cpp
@@ -0,0 +1,182 @@
+
+/*
+ * This file is a part of the CaosDB Project.
+ * Copyright (C) 2021-2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2021-2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#include "caosdb/result_table.h"            // for ResultTable, ResultTab...
+#include "caosdb/entity/v1/main.pb.h"       // for SelectQueryResult, Sel...
+#include "caosdb/protobuf_helper.h"         // for ScalarProtoMessageWrapper
+#include "caosdb/result_table_impl.h"       // for ResultTableImpl, Resul...
+#include "caosdb/value.h"                   // for Value
+#include <algorithm>                        // for max
+#include <google/protobuf/repeated_field.h> // IWYU pragma: keep for RepeatedPtrField
+#include <memory>                           // for unique_ptr
+#include <string>                           // for string, operator==
+#include <utility>                          // for move
+#include <vector>                           // for vector
+// IWYU pragma: no_include "net/proto2/public/repeated_field.h"
+
+namespace caosdb::transaction {
+using caosdb::entity::Value;
+using ProtoSelectQueryResult = caosdb::entity::v1::SelectQueryResult;
+using ProtoSelectQueryHeader = caosdb::entity::v1::SelectQueryHeader;
+using ProtoSelectQueryColumn = caosdb::entity::v1::SelectQueryColumn;
+using ProtoSelectQueryRow = caosdb::entity::v1::SelectQueryRow;
+using caosdb::utility::ScalarProtoMessageWrapper;
+
+ResultTableRowImpl::ResultTableRowImpl(ProtoSelectQueryResult *table, int row)
+  : header(*table->mutable_header()), row(*table->mutable_data_rows(row)) {}
+
+auto ResultTableRowImpl::GetColumnIndex(const std::string &column_name) const noexcept -> int {
+  for (int i = 0; i < this->header.columns_size(); i++) {
+    if (this->header.columns(i).name() == column_name) {
+      return i;
+    }
+  }
+  return -1;
+}
+
+auto ResultTableRowImpl::GetValue(const std::string &column) const noexcept -> Value {
+  const auto column_index = GetColumnIndex(column);
+  if (column_index == -1) {
+    // NULL_VALUE
+    return {};
+  }
+  Value result(this->row.cells(column_index));
+  return result;
+}
+
+ResultTableRow::ResultTableRow(std::unique_ptr<ResultTableRowImpl> delegate)
+  : delegate(std::move(delegate)) {}
+
+auto ResultTableRow::GetValue(const std::string &column) const noexcept -> Value {
+  return this->delegate->GetValue(column);
+}
+
+ResultTableColumnImpl::ResultTableColumnImpl(ProtoSelectQueryColumn *column)
+  : ScalarProtoMessageWrapper<ProtoSelectQueryColumn>(column) {}
+
+ResultTableColumn::ResultTableColumn(std::unique_ptr<ResultTableColumnImpl> delegate)
+  : delegate(std::move(delegate)) {}
+
+auto ResultTableColumn::GetName() const noexcept -> const std::string & {
+  return this->delegate->wrapped->name();
+}
+
+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) {
+  for (auto &column : *this->wrapped->mutable_header()->mutable_columns()) {
+    this->columns.emplace_back(
+      std::unique_ptr<ResultTableColumnImpl>(new ResultTableColumnImpl(&column)));
+  }
+  for (int i = 0; i < this->wrapped->data_rows_size(); i++) {
+    this->rows.emplace_back(
+      std::unique_ptr<ResultTableRowImpl>(new ResultTableRowImpl(this->wrapped, i)));
+  }
+}
+
+ResultTable::ResultTable(std::unique_ptr<ResultTableImpl> delegate)
+  : delegate(std::move(delegate)) {}
+
+auto ResultTable::size() const noexcept -> int { return this->delegate->wrapped->data_rows_size(); }
+
+auto ResultTable::GetRows() const noexcept -> RowIterator { return RowIterator(this, 0); }
+
+ResultTable::RowIterator::RowIterator(const ResultTable *result_table_param, int index)
+  : current_index(index), result_table(result_table_param) {}
+
+ResultTable::RowIterator::RowIterator(const RowIterator &other)
+  : current_index(other.current_index), result_table(other.result_table) {}
+
+auto ResultTable::RowIterator::size() const noexcept -> int {
+  return this->result_table->delegate->wrapped->data_rows_size();
+}
+
+auto ResultTable::RowIterator::operator*() const -> const ResultTableRow & {
+  return this->result_table->delegate->rows.at(this->current_index);
+}
+
+auto ResultTable::RowIterator::operator++() -> RowIterator & {
+  current_index++;
+  return *this;
+}
+
+auto ResultTable::RowIterator::operator++(int) -> RowIterator {
+  RowIterator tmp(*this);
+  operator++();
+  return tmp;
+}
+
+auto ResultTable::RowIterator::operator!=(const RowIterator &rhs) const -> bool {
+  return this->current_index != rhs.current_index;
+}
+
+auto ResultTable::RowIterator::begin() const -> RowIterator {
+  return RowIterator(this->result_table);
+}
+
+auto ResultTable::RowIterator::end() const -> RowIterator {
+  return RowIterator(this->result_table, size());
+}
+
+auto ResultTable::GetHeader() const noexcept -> HeaderIterator { return HeaderIterator(this, 0); }
+
+ResultTable::HeaderIterator::HeaderIterator(const ResultTable *result_table_param, int index)
+  : current_index(index), result_table(result_table_param) {}
+
+ResultTable::HeaderIterator::HeaderIterator(const HeaderIterator &other)
+  : current_index(other.current_index), result_table(other.result_table) {}
+
+auto ResultTable::HeaderIterator::size() const noexcept -> int {
+  return this->result_table->delegate->wrapped->header().columns_size();
+}
+
+auto ResultTable::HeaderIterator::operator*() const -> const ResultTableColumn & {
+  return this->result_table->delegate->columns.at(this->current_index);
+}
+
+auto ResultTable::HeaderIterator::operator++() -> HeaderIterator & {
+  current_index++;
+  return *this;
+}
+
+auto ResultTable::HeaderIterator::operator++(int) -> HeaderIterator {
+  HeaderIterator tmp(*this);
+  operator++();
+  return tmp;
+}
+
+auto ResultTable::HeaderIterator::operator!=(const HeaderIterator &rhs) const -> bool {
+  return this->current_index != rhs.current_index;
+}
+
+auto ResultTable::HeaderIterator::begin() const -> HeaderIterator {
+  return HeaderIterator(this->result_table);
+}
+
+auto ResultTable::HeaderIterator::end() const -> HeaderIterator {
+  return HeaderIterator(this->result_table, size());
+}
+
+} // namespace caosdb::transaction
diff --git a/src/caosdb/result_table_impl.h b/src/caosdb/result_table_impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..3450a68cf7ae0638d42d3ea1c79fd9d181a3eaab
--- /dev/null
+++ b/src/caosdb/result_table_impl.h
@@ -0,0 +1,84 @@
+/*
+ * This file is a part of the CaosDB Project.
+ * Copyright (C) 2021-2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2021-2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#ifndef CAOSDB_RESULT_TABLE_IMPL_H
+#define CAOSDB_RESULT_TABLE_IMPL_H
+
+#include "caosdb/transaction.h"
+#include "caosdb/entity/v1/main.grpc.pb.h"                         // for EntityTransac...
+#include "caosdb/entity/v1/main.pb.h"                              // for TransactionRe...
+#include "caosdb/file_transmission/download_request_handler.h"     // Download...
+#include "caosdb/file_transmission/file_reader.h"                  // for path
+#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
+#include <filesystem>                                              // for operator<<, path
+#include <future>                                                  // for async, future
+#include <google/protobuf/arena.h>                                 // for Arena
+#include <grpc/grpc.h>                                             // for gpr_timespec
+#include <map>                                                     // for map, operator!=
+#include <memory>                                                  // for unique_ptr
+#include <random>                                                  // for mt19937, rand...
+#include <system_error>                                            // for std::system_error
+#include <utility>                                                 // for move, pair
+
+namespace caosdb::transaction {
+using caosdb::entity::Value;
+using ProtoSelectQueryResult = caosdb::entity::v1::SelectQueryResult;
+using ProtoSelectQueryHeader = caosdb::entity::v1::SelectQueryHeader;
+using ProtoSelectQueryColumn = caosdb::entity::v1::SelectQueryColumn;
+using ProtoSelectQueryRow = caosdb::entity::v1::SelectQueryRow;
+using caosdb::utility::ScalarProtoMessageWrapper;
+
+class ResultTableRowImpl {
+  explicit ResultTableRowImpl(ProtoSelectQueryResult *table, int row);
+  [[nodiscard]] auto GetValue(const std::string &column) const noexcept -> Value;
+  [[nodiscard]] auto GetColumnIndex(const std::string &column) const noexcept -> int;
+  friend class ResultTableRow;
+  friend class ResultTableImpl;
+  ProtoSelectQueryHeader &header;
+  ProtoSelectQueryRow &row;
+};
+
+class ResultTableColumnImpl : public ScalarProtoMessageWrapper<ProtoSelectQueryColumn> {
+  explicit ResultTableColumnImpl(ProtoSelectQueryColumn *column);
+  friend class ResultTableColumn;
+  friend class ResultTableImpl;
+};
+
+class ResultTableImpl : public ScalarProtoMessageWrapper<ProtoSelectQueryResult> {
+  static auto create(ProtoSelectQueryResult *select_result) -> std::unique_ptr<ResultTable>;
+  ResultTableImpl();
+  explicit ResultTableImpl(ProtoSelectQueryResult *result_table);
+  std::vector<ResultTableColumn> columns;
+  std::vector<ResultTableRow> rows;
+
+  friend class ResultTable;
+  friend class ResultTable::HeaderIterator;
+  friend class ResultTableColumn;
+  friend auto ProcessSelectResponse(ProtoSelectQueryResult *select_result)
+    -> std::unique_ptr<ResultTable>;
+};
+
+} // namespace caosdb::transaction
+#endif
diff --git a/src/caosdb/transaction.cpp b/src/caosdb/transaction.cpp
index ca8338dce4e4b721202e72c3fdecf58339746ee0..28f9e35fc1a70083d07c99e7f8a4fabfbe58bc32 100644
--- a/src/caosdb/transaction.cpp
+++ b/src/caosdb/transaction.cpp
@@ -25,6 +25,10 @@
 #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/result_set.h"                                     // for ResultSet
+#include "caosdb/result_table.h"                                   // for ResultTable
+#include "caosdb/result_table_impl.h"                              // for ResultTableImpl
 #include "caosdb/status_code.h"                                    // for StatusCode
 #include "caosdb/transaction_handler.h"                            // for EntityTransactionHandler
 #include <algorithm>                                               // for max
@@ -38,7 +42,6 @@
 #include <random>                                                  // for mt19937, rand...
 #include <system_error>                                            // for std::system_error
 #include <utility>                                                 // for move, pair
-// IWYU pragma: no_include <bits/exception.h>
 // IWYU pragma: no_include <cxxabi.h>
 // IWYU pragma: no_include "net/proto2/public/repeated_field.h"
 
@@ -51,41 +54,13 @@ 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 google::protobuf::Arena;
 using NextStatus = grpc::CompletionQueue::NextStatus;
 using RegistrationStatus = caosdb::entity::v1::RegistrationStatus;
 
-ResultSet::iterator::iterator(const ResultSet *result_set_param, int index)
-  : current_index(index), result_set(result_set_param) {}
-
-auto ResultSet::iterator::operator*() const -> const Entity & {
-  return this->result_set->at(current_index);
-}
-
-auto ResultSet::iterator::operator++() -> ResultSet::iterator & {
-  current_index++;
-  return *this;
-}
-
-auto ResultSet::iterator::operator++(int) -> ResultSet::iterator {
-  iterator tmp(*this);
-  operator++();
-  return tmp;
-}
-
-auto ResultSet::iterator::operator!=(const iterator &rhs) const -> bool {
-  return this->current_index != rhs.current_index;
-}
-
-auto ResultSet::begin() const -> ResultSet::iterator { return ResultSet::iterator(this, 0); }
-
-auto ResultSet::end() const -> ResultSet::iterator { return ResultSet::iterator(this, size()); }
-
-MultiResultSet::MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set)
-  : AbstractMultiResultSet(std::move(result_set)) {}
-
 Transaction::Transaction(std::shared_ptr<EntityTransactionService::Stub> entity_service,
                          std::shared_ptr<FileTransmissionService::Stub> file_service)
   : entity_service(std::move(entity_service)), file_service(std::move(file_service)),
@@ -318,6 +293,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 +308,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();
diff --git a/test/test_issues.cpp b/test/test_issues.cpp
index b7a6cc8fe7df2bbcf8b2db0902c57c7146b77faf..6422da5f4c86128be44286d853d768cd58db3c8c 100644
--- a/test/test_issues.cpp
+++ b/test/test_issues.cpp
@@ -19,6 +19,7 @@
  */
 #include "caosdb/configuration.h"      // for InsecureConnectionConfig...
 #include "caosdb/connection.h"         // for Connection
+#include "caosdb/result_set.h"         // for ResultSet
 #include "caosdb/status_code.h"        // for StatusCode, EXECUTING
 #include "caosdb/transaction.h"        // for Transaction
 #include "caosdb/transaction_status.h" // for StatusCode
diff --git a/test/test_transaction.cpp b/test/test_transaction.cpp
index 77d457c6eee2c5e9d95c53c95c1b2f0a713dcff4..ed4dcd2aa72e3e857bc871f401504147b330385b 100644
--- a/test/test_transaction.cpp
+++ b/test/test_transaction.cpp
@@ -22,6 +22,7 @@
 #include "caosdb/entity.h"              // for Entity
 #include "caosdb/entity/v1/main.pb.h"   // for Entity
 #include "caosdb/exceptions.h"          // for ConnectionError
+#include "caosdb/result_set.h"          // for MultiResultSet, Entity, Resu...
 #include "caosdb/status_code.h"         // for StatusCode
 #include "caosdb/transaction.h"         // for Transaction
 #include "caosdb/transaction_handler.h" // for MultiTransactionResponse