<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: resurfacelabs</title>
    <description>The latest articles on DEV Community by resurfacelabs (@resurfacelabs).</description>
    <link>https://dev.to/resurfacelabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F646754%2F2386bab5-3221-416e-9796-7cca4030d755.png</url>
      <title>DEV Community: resurfacelabs</title>
      <link>https://dev.to/resurfacelabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/resurfacelabs"/>
    <language>en</language>
    <item>
      <title>Building a lightweight Trino distribution</title>
      <dc:creator>resurfacelabs</dc:creator>
      <pubDate>Tue, 15 Jun 2021 20:37:29 +0000</pubDate>
      <link>https://dev.to/resurfacelabs/building-a-lightweight-trino-distribution-54h7</link>
      <guid>https://dev.to/resurfacelabs/building-a-lightweight-trino-distribution-54h7</guid>
      <description>&lt;h4&gt;
  
  
  Too many data frameworks built for large scale have unacceptable complexity at small scale. But with a few tweaks, &lt;a href="//trino.io"&gt;Trino&lt;/a&gt; scales down to run nicely on small single-container configurations.
&lt;/h4&gt;

&lt;p&gt;(Trino is the new brand for PrestoSQL, an open source distributed query engine.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Official docker image is large
&lt;/h2&gt;

&lt;p&gt;The docker image provided by the Trino team (&lt;code&gt;trinodb/trino&lt;/code&gt;) is &lt;strong&gt;1.32 GB&lt;/strong&gt; when extracted. This includes a full CentOS distribution, which is a safe and comfortable choice. But this is pretty large for cases where Trino is embedded into another application, like we're doing with Resurface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking a smaller base image
&lt;/h2&gt;

&lt;p&gt;Much of the weight from the official Trino container is from the base CentOS image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM azul/zulu-openjdk-centos:11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching to an Alpine-based distribution like &lt;strong&gt;adoptopenjdk&lt;/strong&gt; cuts the download size dramatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM adoptopenjdk/openjdk11:jdk-11.0.10_9-alpine-slim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️Pick your Alpine distribution carefully! We've seen significant performance degradations for Java applications when using Alpine distributions that don't include glibc. The adoptopenjdk containers have good performance while still being relatively small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing the number of connectors
&lt;/h2&gt;

&lt;p&gt;The next step is optional, but has a big impact on container size. Trino ships with many pre-installed connectors, each of which requires supporting libraries.&lt;/p&gt;

&lt;p&gt;However, these connectors aren't all strictly required. For our single-container distributions, we strip out all the optional connectors except for our own Resurface connector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rm -rf /opt/trino/plugin/accumulo &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/atop &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/bigquery &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/blackhole &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/cassandra &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/clickhouse &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/druid &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/elasticsearch &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/example-http &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/geospatial &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/google-sheets &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/hive-hadoop2 &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/iceberg &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/jmx &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/kafka &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/kinesis &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/kudu &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/local-file &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/memsql &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/ml &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/mongodb &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/mysql &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/oracle &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/phoenix &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/phoenix5 &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/pinot &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/postgresql &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/prometheus &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/raptor-legacy &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/redis &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/redshift &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/sqlserver &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/teradata-functions &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/thrift &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/tpcds &amp;amp;&amp;amp;\
rm -rf /opt/trino/plugin/tpch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tuning memory parameters
&lt;/h2&gt;

&lt;p&gt;Trino is very tunable when it comes to memory usage. But beyond that, the Trino team doesn't discourage small configurations. When I had the chance to ask Martin Traverso about this, his reaction was that they expect Trino to pass all tests when running on a small laptop-sized configuration, just the same as on a large configuration. The fact that Martin reacted this way gave us renewed confidence to experiment with smaller configurations.&lt;/p&gt;

&lt;p&gt;For our smallest containers, we limit Trino to 1GB of memory using these standard parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query.max-length=1000000
query.max-memory=1000MB
query.max-memory-per-node=1000MB
query.max-total-memory=1000MB
query.max-total-memory-per-node=1000MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're still seeing out-of-memory conditions, you may also want to reduce the memory used by the query cache. This is especially important if your SQL statements are large, or if your transaction rates are relatively high so that a lot of query history data is being cached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query.max-history=20
query.min-expire-age=1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final results
&lt;/h2&gt;

&lt;p&gt;Following these steps yields a stable and high-performing Trino configuration that is &lt;strong&gt;391 MB&lt;/strong&gt;. That's just &lt;strong&gt;30%&lt;/strong&gt; of the download size of the standard Trino container! This doesn't come without tradeoffs, but is great to have this range in flexibility.&lt;/p&gt;

&lt;p&gt;If you're looking for a minimal Trino container image, you can use ours as a base. (The version tag corresponds to the Trino version)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM resurfaceio/trino-minimal:358
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can inspect this Dockerfile for ideas on how to build your own lightweight Trino image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/resurfaceio/containers/blob/master/trino/trino-minimal.dockerfile"&gt;https://github.com/resurfaceio/containers/blob/master/trino/trino-minimal.dockerfile&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>docker</category>
      <category>trino</category>
    </item>
    <item>
      <title>The dark side of Docker: Avoid the “latest” tag</title>
      <dc:creator>resurfacelabs</dc:creator>
      <pubDate>Wed, 09 Jun 2021 21:13:51 +0000</pubDate>
      <link>https://dev.to/resurfacelabs/the-dark-side-of-docker-avoid-the-latest-tag-2i0p</link>
      <guid>https://dev.to/resurfacelabs/the-dark-side-of-docker-avoid-the-latest-tag-2i0p</guid>
      <description>&lt;p&gt;We rely on Docker, and it’s one of our favorite technologies. But using Docker for commercial software shows some rough edges that we found the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Latest tag is broken at best, evil at worst
&lt;/h2&gt;

&lt;p&gt;It seems like the “latest” tag should work, but it really doesn’t, except in the simplest cases. We didn’t realize how crazy broken this could be until one night we were on a Zoom call and watched a new customer install software that was three months old, when there were 10 newer builds available. Not a good feeling.&lt;/p&gt;

&lt;p&gt;We’re not the first to recognize there were &lt;a href="https://vsupalov.com/docker-latest-tag/"&gt;major things wrong with the latest tag&lt;/a&gt;, but we really didn’t see these problems at first. We’ve now gone the other way and removed our “latest” tags from our Docker images. There were a couple of things that put us over the edge.&lt;/p&gt;

&lt;p&gt;The “latest” pattern bends or breaks the kind of caching that every CDN wants to do, because over time this leads to multiple artifacts with the same name but different content. What CDNs really like is immutable artifacts with unique version numbers. What’s really awful is that if the latest tag fails due to this kind of caching, you’ll never know. The latest tag will always resolve to something, even if that something is months old because of some lame caching issue.&lt;/p&gt;

&lt;p&gt;The other crazy thing about “latest” is that the actual version number is not easy for users to find. Let’s assume you publish an image under a specific version name and with the “latest” tag. After install, the output of docker images will show “latest” and not the version number, even if a newer version of the container is installed. So how are users supposed to figure out what versions are actually installed? By falling back to comparing the image id hashes, which is an insane use of people’s time.&lt;/p&gt;

&lt;p&gt;We get it — it feels like “latest” as documented wants to be an alias for the most recent container…but because this is actually implemented as a named tag, this leaves the door open for race conditions, CDN misfires, and confusion. Just don’t do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image caching is silent but deadly
&lt;/h2&gt;

&lt;p&gt;Docker images are built in layers, and Docker aggressively caches these layers to avoid having to download bits that haven’t changed. The problem is that this caching is based the script text that’s in the Dockerfile, versus the results of those scripts.&lt;/p&gt;

&lt;p&gt;Where you’re pretty much guaranteed to see this issue is when applying security and package updates. You’ll be wondering why “apt upgrade” worked at first but then fails to pick up any more updates. The reason is that the “apt upgrade” script didn’t change, so Docker will happily and silently use results from an earlier build, while you spend hours tearing your hair out.&lt;/p&gt;

&lt;p&gt;This is easy to work around with a --no-cache directive, but once you start doing this everywhere, you’ll be left wondering why the default behavior for Docker is to be silently vulnerable by relying on cached layers that are missing security patches.&lt;br&gt;
Perhaps the worst observation here is that using the --no-cache directive hasn’t really affected us. Our images are relatively small and so we weren’t getting a lot of benefit from that aggressive caching anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing for multiple processes is tricky
&lt;/h2&gt;

&lt;p&gt;Docker espouses the idea of each container having a single command that it executes. This is great for simple cases, but let’s say that you have three lightweight services that run together. Some folks would argue that this should always be broken down into three separate containers, but that doesn’t come without cost. Now you have 3X as many components to build/manage/version, and are more limited in how these processes can interact with each other, because they are completely separate. Refactoring these services into a different configuration means heavyweight changes to the build &amp;amp; packaging. So there’s no one-size-fits-all answer here.&lt;/p&gt;

&lt;p&gt;Our experience is that the ugliest bit around running multiple lightweight services is the scripting that’s involved. We started down the path of doing custom scripting for this, and found issues around startup order and race conditions that quickly became a distraction. This lead to a pretty spirited debate about whether we needed to throw out everything and start with a single-process set of containers, which would have themselves been subject to the same ordering and race condition pitfalls.&lt;/p&gt;

&lt;p&gt;Instead we kept our services together and standardized on &lt;strong&gt;supervisord&lt;/strong&gt; as the main control point for the container. This allows us to run multiple services in a single-container package with confidence, without writing any custom scripts. Easy peasy.&lt;/p&gt;

&lt;p&gt;P.S. Why not docker-compose or Kubernetes? Because those are heavy and complex. You don’t need a chainsaw to open a can of tuna.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller containers aren’t always faster
&lt;/h2&gt;

&lt;p&gt;When Docker started gaining a lot of steam, a lot of the justification was that Docker containers would be much smaller and have less overhead than the traditional virtual machines that they replace. You won’t have to run an extra copy of Ubuntu, you’ll have a teeny container instead! Unfortunately this is far from guaranteed.&lt;/p&gt;

&lt;p&gt;There’s also a myth that Alpine, as the most obvious teeny distribution, is universally better than other distros. It’s tiny, so it must be faster, right? Not necessarily! We saw significant performance regressions on Alpine that we didn’t see on RedHat or Ubuntu, because Alpine isn’t based on glibc. Ultimately, we decided that the extra time and energy to tune our configuration on Alpine was worth it to get the smallest download size possible. But this was a significant time investment, and it wasn’t obvious how to do at first.&lt;/p&gt;

&lt;p&gt;Docker makes it possible to build very small and performant containers, but most of the burden to streamline performance and download size still falls on the container developers. It’s really easy to publish a multi-GB image based on the same distro that you used for development. Getting to a featherweight image still requires skill and care.&lt;/p&gt;

&lt;h2&gt;
  
  
  We still ❤️ Docker
&lt;/h2&gt;

&lt;p&gt;Do any of these issues keep us from using and recommending Docker? Nope, most of these are easy to avoid, if you know what pitfalls to watch out for in advance. Hopefully this post saves you some time!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Selecting the right open source DB for your workload</title>
      <dc:creator>resurfacelabs</dc:creator>
      <pubDate>Wed, 09 Jun 2021 19:38:57 +0000</pubDate>
      <link>https://dev.to/resurfacelabs/selecting-the-right-open-source-db-for-your-workload-1jim</link>
      <guid>https://dev.to/resurfacelabs/selecting-the-right-open-source-db-for-your-workload-1jim</guid>
      <description>&lt;p&gt;&lt;strong&gt;So. Many. Databases.&lt;/strong&gt;&lt;br&gt;
The latest DB-Engines ranking lists 370 databases according to popularity. And the sheer number may lead some to go back to what they know, or rely on other developers’ recommendations. The more intrepid may even spend some time researching.&lt;/p&gt;

&lt;p&gt;It takes some navigation to understand the landscape across all the database flavors. And understanding doesn’t necessarily lead to the right choice. Martin Kleppman literally maps this out in his book, &lt;a href="https://dataintensive.net/"&gt;Designing Data-Intensive Applications&lt;/a&gt;, O’Reilly. (Highly recommended read)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ye olde capture-first&lt;/strong&gt;&lt;br&gt;
Early days of databases had fewer choices, there was Oracle, SQL Server, MySQL and PostgreSQL. You just had to choose between them, as they all had one thing in common, they were built for capture-first. Get the data. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a database&lt;/li&gt;
&lt;li&gt;Define your schema&lt;/li&gt;
&lt;li&gt;Start loading and integrating&lt;/li&gt;
&lt;li&gt;Tune normalization and queries&lt;/li&gt;
&lt;li&gt;Add materialized view and query caching&lt;/li&gt;
&lt;li&gt;Get ready to switch if this doesn’t work as intended&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this workflow, queries arrive too late to influence the database choice. Finding out later on that certain queries can’t be supported is problematic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tackling the paradox of choice&lt;/strong&gt;&lt;br&gt;
Through this process we quickly find out the chosen database and associated effort doesn’t meet the requirements. Here are a few options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stick with what you know&lt;/li&gt;
&lt;li&gt;Bribe a trusted DBA or architect&lt;/li&gt;
&lt;li&gt;Look at relevant TPC benchmarks&lt;/li&gt;
&lt;li&gt;See what database experience is missing from your résumé&lt;/li&gt;
&lt;li&gt;Consult a soothsayer, Magic 8 ball, dartboard&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Mind-shift to query-first&lt;/strong&gt;&lt;br&gt;
Start with the end in mind. Choose your database by working backwards from your target read workloads. Think of this as TDD for database architecture.&lt;/p&gt;

&lt;p&gt;I’m not saying you can ignore write performance (you can’t) or that write benchmarks don’t matter (they do). But don’t use write capabilities as the most important criteria for which database to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read workloads&lt;/strong&gt;&lt;br&gt;
Getting to the data is where the value lives in a database. Applying a Zen koan:&lt;/p&gt;

&lt;p&gt;“What is the value of a write that can’t be read?”&lt;/p&gt;

&lt;p&gt;Writes are simply the cost of expected reads. From this view, re-consider the database options in terms of reads, as each database will have different tricks for managing and accessing the reads. Indexes are extra writes to accelerate reads, and replication are extra writes to ensure reads. &lt;/p&gt;

&lt;p&gt;Think query-first. What kinds of reads can you expect and how to support them most efficiently and effectively? &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick the right database for query-first&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define seed data that approximates a working system&lt;/li&gt;
&lt;li&gt;Run read workloads for seed data on multiple DBs&lt;/li&gt;
&lt;li&gt;Select the database with best workload fit (ops/sec)&lt;/li&gt;
&lt;li&gt;Optimize for loading/maintaining data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If no single database platform is a match, use a distributed query engine like Trino, then replicate data through queues like Kafka or consider other tricks (share your tricks!)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Read Workload&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Category&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fetch value for single key&lt;/td&gt;
&lt;td&gt;Key/Value&lt;/td&gt;
&lt;td&gt;Returns unstructured value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fetch values for related keys&lt;/td&gt;
&lt;td&gt;Key/Value&lt;/td&gt;
&lt;td&gt;Returns collection of values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find single row with criteria&lt;/td&gt;
&lt;td&gt;OLTP&lt;/td&gt;
&lt;td&gt;Returns tuple (row of named columns) using column indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find group of rows with criteria&lt;/td&gt;
&lt;td&gt;OLTP&lt;/td&gt;
&lt;td&gt;Returns collection of tuples using column indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read rows within transaction&lt;/td&gt;
&lt;td&gt;OLTP&lt;/td&gt;
&lt;td&gt;Returns value based on transaction isolation level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join subset of rows &amp;amp; related rows&lt;/td&gt;
&lt;td&gt;OLAP&lt;/td&gt;
&lt;td&gt;Returns collection of tuples joined across multiple tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join/summarize for few columns&lt;/td&gt;
&lt;td&gt;OLAP&lt;/td&gt;
&lt;td&gt;Returns count/histogram on a limited set of columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find/join/summarize for all columns&lt;/td&gt;
&lt;td&gt;DSS&lt;/td&gt;
&lt;td&gt;Returns data transformation computed against all available columns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cut to the chase&lt;/strong&gt;&lt;br&gt;
For our own use, here’s how we evaluated the options for our read workloads:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;READ WORKLOAD&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;CACHE&lt;/strong&gt;&lt;br&gt;&lt;br&gt;Redis, Memcached&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;LSM&lt;/strong&gt;&lt;br&gt; ****&lt;br&gt;Cassandra, HBase, RocksDB, LevelDB&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;BTREE&lt;/strong&gt;&lt;br&gt; ****&lt;br&gt;MySQL, Postgresql, SQLite, SQL Server&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;COLUMNAR&lt;/strong&gt;&lt;br&gt; ****&lt;br&gt;Druid, Iceberg,&lt;br&gt;Parquet, Orc&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;M/R&lt;/strong&gt;&lt;br&gt; ****&lt;br&gt;Hadoop,&lt;br&gt;Resurface&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fetch value for single key&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;🤔&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fetch values for related keys&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find single row with criteria&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find group of rows with criteria&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read row within transaction&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🤔&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join subset of rows &amp;amp; related rows&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join/summarize for few columns&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find/join/summarize for all columns&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;😱&lt;/td&gt;
&lt;td&gt;😱&lt;/td&gt;
&lt;td&gt;🥳&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Bonus! A few advanced read optimizations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🧐&lt;br&gt;
Move data in-memory to eliminate device I/O&lt;br&gt;
Use local/embedded store to eliminate network&lt;/p&gt;

&lt;p&gt;😍&lt;br&gt;
Use computed (virtual) columns&lt;br&gt;
Use optimized storage when table scanning&lt;/p&gt;

&lt;p&gt;🤩&lt;br&gt;
Immutable writes as safe transactions&lt;br&gt;
In-memory storage via page cache&lt;br&gt;
Push queries closer to data&lt;/p&gt;

&lt;p&gt;🤓&lt;br&gt;
I could nerd out on this all day. I often do. Reach out if you want to talk database, query-first architectures, APIs or anything else.&lt;/p&gt;

</description>
      <category>database</category>
    </item>
  </channel>
</rss>
