<?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: Neel Phadnis</title>
    <description>The latest articles on DEV Community by Neel Phadnis (@nphadnis).</description>
    <link>https://dev.to/nphadnis</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F393096%2Fea778238-db3c-430f-9290-2437d9032133.png</url>
      <title>DEV Community: Neel Phadnis</title>
      <link>https://dev.to/nphadnis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nphadnis"/>
    <language>en</language>
    <item>
      <title>Parallelism with Fine-Grained Streams (Part 2)</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Tue, 17 Jan 2023 16:37:01 +0000</pubDate>
      <link>https://dev.to/aerospike/parallelism-with-fine-grained-streams-part-2-3lgg</link>
      <guid>https://dev.to/aerospike/parallelism-with-fine-grained-streams-part-2-3lgg</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7ylI75eM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2AWwTTqEyllcpnzjLD" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7ylI75eM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2AWwTTqEyllcpnzjLD" alt="(Source: Photo by Clem Onojeghuo on Unsplash" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Clem Onojeghuo on &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While it is possible to process a data set using a large number of parallel streams, a higher degree of parallelism may not be necessarily optimal or even possible. This article explores how to think about parallelism, and discusses many bottlenecks that limit the level of parallelism. It also highlights the need to perform measurements in the target setup due to many factors that cannot be easily quantified.&lt;/p&gt;

&lt;p&gt;This article is a sequel to the the blog post &lt;a href="https://developer.aerospike.com/blog/parallel-streams"&gt;Processing Large Data Sets in Fine-Grained Parallel Streams&lt;/a&gt; and tutorial &lt;a href="https://developer.aerospike.com/tutorials/java/query_splits"&gt;Splitting Large Data Sets for Parallel Processing of Queries&lt;/a&gt;, in which we discussed how large data sets can be efficiently divided into an arbitrary number of splits for processing across multiple workers in order to achieve high throughput through parallel processing of partition streams. &lt;/p&gt;

&lt;h2&gt;
  
  
  Query Computation Graph
&lt;/h2&gt;

&lt;p&gt;Let's look at a query in isolation, and how it is processed in parallel. For a given query, the platform’s query planner defines a plan, depicted as a computation graph in the following diagram, for how the query will be executed. In the query computation graph, nodes are workers and edges are data streams.&lt;/p&gt;

&lt;p&gt;Please refer to &lt;a href="https://developer.aerospike.com/blog/parallel-streams"&gt;the prior post&lt;/a&gt; for the context and terminology.&lt;/p&gt;

&lt;p&gt;&lt;a href="//./assets/neel-phadnis/query-graph.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DXE9OaIe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/v1/./assets/neel-phadnis/query-graph.png" alt="Query Graph" width="" height=""&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The plan can consist of multiple stages, each stage having a set of workers processing part of the data, and feeding into the next stage. The first stage typically is data access from the appropriate data source(s), and the last stage involves final processing of results such as sort order and size limit. &lt;/p&gt;

&lt;p&gt;For example, an aggregate computation on a data set may involve two stages of map-reduce, where the first-stage workers retrieve and process their respective data partitions, and the second-stage aggregates results from the first stage. In a join of data from two sources, the first stage may involve workers retrieving filtered and projected data from the two sources, bucketing data for the next stage based on the join predicate, and forwarding respective buckets to next stage workers to perform the join. &lt;/p&gt;

&lt;p&gt;The terms upstream and downstream refer to the direction of data flow through the query graph. &lt;/p&gt;

&lt;p&gt;For simplicity, we will focus on the first stage, which is the data access stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Access Stage
&lt;/h2&gt;

&lt;p&gt;In this stage, the Aerospike data is divided into multiple splits, each accessed and processed by a worker. The data access involves “pushing down” certain operations to the Aerospike cluster in order to minimize data transfer. Such operations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filtering: A subset of records are selected based on some condition. Appropriate indexes are used to evaluate the condition. &lt;/li&gt;
&lt;li&gt;Projection: A subset of bins (columns) of each record are selected. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each worker retrieves and processes records, and forwards results to the next stage. The processing complexity will determine the worker throughput: A simple aggregation can yield a high throughput rate of thousands of records per second whereas a complex transformation involving a database lookup can be much slower yielding just a few tens or hundreds of records per second. &lt;/p&gt;

&lt;h2&gt;
  
  
  Optimal Parallelism: Matched Stage Throughputs
&lt;/h2&gt;

&lt;p&gt;Parallelism in each stage is defined by the number of workers in that stage.&lt;/p&gt;

&lt;p&gt;The throughput of the overall computation is dictated by the slowest stage, or the bottleneck. For maximum efficiency, throughputs of all stages should match to avoid idle resources and/or excessive buffering. In other words, the output throughput of any stage should equal the processing capacity of the following stage. &lt;/p&gt;

&lt;p&gt;The throughput of a stage depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;worker throughput, which is determined by the computational complexity of processing in that stage, and&lt;/li&gt;
&lt;li&gt;number of workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The optimizer determines the optimal throughputs at each stage based on the resource requirements of processing in the stage, available resources, and other scheduling constraints. The number of workers at each stage are determined to deliver the matched throughput. &lt;/p&gt;

&lt;p&gt;To simplify the discussion, we will focus on the data access stage.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;The following methodology involves first understanding the resource limitations that will cap the throughput. These are typically disk and network bandwidth, and the number and capacity of the cluster nodes. We use a simple data access request, such as a single scan or a query, to make the discussion concrete. After you calculate the hardware bottleneck and the throughput limit, you may need to further adjustment to the device I/O, network bandwidth, and/or cluster resources. With a given hardware configuration, we can then focus on optimizing software config and request parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and Bottlenecks
&lt;/h2&gt;

&lt;p&gt;The overall throughput is constrained by the following factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device I/O bandwidth: Disk I/O is a common bottleneck in databases. Aerospike uses SSD devices as high-density fast storage.&lt;/li&gt;
&lt;li&gt;Database node resources for pushdown processing: Processing such as filtering, bin projection, and UDF execution cannot exceed the available node resources. &lt;/li&gt;
&lt;li&gt;Network bandwidth: The data transfer between the server and worker nodes can not exceed the network bandwidth.&lt;/li&gt;
&lt;li&gt;Worker resources: The number of workers as well as processing at worker nodes must be within the available capacity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let us assume the following parameters in the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of database nodes: S&lt;/li&gt;
&lt;li&gt;Number of workers in data access stage:: N&lt;/li&gt;
&lt;li&gt;Device I/O bandwidth at a database node: d&lt;/li&gt;
&lt;li&gt;Effective network bandwidth: B&lt;/li&gt;
&lt;li&gt;Record size: R&lt;/li&gt;
&lt;li&gt;Filter selectivity (fraction of the total records selected): F&lt;/li&gt;
&lt;li&gt;Projection factor (reduction in record size due to bin projection): P&lt;/li&gt;
&lt;li&gt;Workers per worker node: 100&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SSD Throughput
&lt;/h3&gt;

&lt;p&gt;The maximum record I/O throughput at an Aerospike node is d/R. The maximum cumulative record I/O in the cluster is Sxd/R.&lt;/p&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;An SSD on an AWS instance may have an I/O rating of several GBps (say, 4GBps).&lt;br&gt;
Assuming a record size of a few KB (say, 2KB),  this translates into several million records per second.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Node SSD I/O = 4x10^9 / (2x10^3) = 2x10^6 or 2 million records per second&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also, assuming a cluster size S=10, a cluster can provide device I/O of several tens of millions of records per second.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cluster SSD I/O = 10 x 2x10^6 = 20x10^6 or 20 million records per second&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If a worker on average can process ten thousand records per second (W=10x10^3), it will take several thousand workers in the data access stage to saturate the disk I/O in such a system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Max workers in data access stage = 20x10^6 / (10x10^3) = 2x10^3 or 2000  workers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A lower processing throughput per worker would require a larger number of workers before SSD I/O becomes the bottleneck. Another resource may impose a lower limit on the number of workers.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Server Node Throughput
&lt;/h3&gt;

&lt;p&gt;Each Aerospike node reads records from the disk and outputs processed records for the data access workers to consume. Depending on the type of processing, the number and size of the output records will be different. For example, filtering will reduce the number of records, and bin (column) projections will reduce the size of records. &lt;/p&gt;

&lt;p&gt;A query involving a scan with a filter needs to read all records from the device, whereas a secondary-index query needs to read only the records filtered by the secondary index. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The max record throughput at an Aerospike node is d/R. This corresponds to the record throughput for a scan without a filter.&lt;/li&gt;
&lt;li&gt;The max node record throughput for a scan with filter: d/(RxF)&lt;/li&gt;
&lt;li&gt;The max node record throughput for a secondary-index query: d/R&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;Assuming d, R, and S are the same as above, we have a cluster throughput of several tens of millions of records for a scan query without filtering. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cluster unfiltered scan throughput = Sxd/R = 10 x 4x10^9 / 2X10^3 = 20X10^6 or 20 million records per second&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Assuming selectivity factor F=10, a cluster can provide several millions of records throughput for filtered scans and several tens of millions of records for a secondary-index query. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cluster filtered scan throughput = Sxd/(RxF) = 10 x 4x10^9 / (2x10^3 x 10) = 2x10^6 or 2 million records per second&lt;/p&gt;

&lt;p&gt;Cluster secondary-index query throughput = Sxd/R = 10 x 4x10^9 / (2x10^3 ) = 20x10^6 or 20 million records per second&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To saturate the Aerospike cluster throughput in this setup, assuming a processing rate of ten thousand records per second at each worker (W=10^4), it will take thousands of workers for a scan query without filtering as well as a secondary-index query, and hundreds of workers for a scan with filtering.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Max workers for an unfiltered scan = 20x10^6 / (10x10^3) = 2x10^3 or 2000 workers.&lt;/p&gt;

&lt;p&gt;Max workers for a filtered scan = 2x10^6 / (10x10^3) = 2x10^2 or 200 workers.&lt;/p&gt;

&lt;p&gt;Max workers for a secondary-index query = 20x10^6 / (10x10^3) = 2x10^3 or 2000 workers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Again, a lower processing throughput per worker would require a larger number of workers before Aerospike cluster throughput becomes the bottleneck. Also, another resource may impose a lower limit on the number of workers.   &lt;/p&gt;

&lt;h4&gt;
  
  
  Complex Pushdown Processing
&lt;/h4&gt;

&lt;p&gt;An aggregation performed on a server node can dramatically reduce the number of result records, typically to just one, and therefore aggregation processing on a server node is not pertinent to the number of workers discussion. Only one or a very few downstream workers would suffice, and we will ignore this case for this discussion. In order to perform complex cluster operations such as aggregations, the number of nodes in the Aerospike cluster, as well as each node’s  memory and CPU resources, should be sized appropriately. &lt;/p&gt;

&lt;p&gt;We will assume Aerospike node CPU and memory are not a bottleneck for this discussion. &lt;/p&gt;

&lt;h3&gt;
  
  
  Network Bandwidth
&lt;/h3&gt;

&lt;p&gt;The network I/O at a worker or a server node has an upper limit. For a large worker cluster, the cumulative worker I/O can exceed the subnet bandwidth. Additionally, the traffic to the database may need to traverse routers and gateways which will also impose their own limits. The most stringent of these limits can become the bottleneck.&lt;/p&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;In AWS, the network I/O limit for an instance may range from a few Gbps to 100s of Gbps per instance. A cluster on a subnet may have a total network capacity of 100’s of Gbps for all nodes. If the Aerospike cluster placement requires access to another VPC in AWS, the VPC Gateway I/O limit is in the range of 100 Gbps. &lt;/p&gt;

&lt;p&gt;We can reasonably work with the effective network bandwidth limit (B) of 100 Gbps.&lt;/p&gt;

&lt;p&gt;Compare this to the Aerospike cluster SSD throughput of several tens of GBps or hundreds of Gbps, and note that the network bandwidth is smaller of the two, potentially an order of magnitude smaller. In such a system, the network bandwidth can become the bottleneck. &lt;/p&gt;

&lt;p&gt;Assuming a maximum bandwidth of 100 Gbps and using the prior values for R and W, we will need several hundred workers to saturate the network.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Max workers to saturate network I/O = B / (WxR) = 100/8 x 10^9 / (W*R)&lt;br&gt;
= 12.5x10^9 / (10^4 x 2x10^3) = 6.25 x 10^2 = 625&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Interestingly, for a scan with filtering and projection, the network bandwidth requirement at data access stage reduces by a factor or FxP because now fewer records of smaller size need to traverse the network. Assuming F=10 and P=10, the network bandwidth needed just for the data access portion goes down by a hundred fold, which can shift the bottleneck to the SSD I/O. Removing the SSD I/O bottleneck may entail adding additional SSD drives to each Aerospike node.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worker Nodes
&lt;/h3&gt;

&lt;p&gt;The number of available worker nodes can itself be the limit. A worker node can run a large number of worker processes (or workers), typically configured at 1-2 times the number of CPU cores. &lt;/p&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;If we assume 100 workers per worker node, the number of data access worker nodes needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To saturate the cluster record throughput the number of worker nodes is up to a few tens.
&amp;gt;Worker nodes = Range of workers / workers per worker node 
= Hundreds to thousands / 100  = a few to tens of nodes&lt;/li&gt;
&lt;li&gt;To saturate the effective network bandwidth the number of worker nodes is in single digits.
&amp;gt;Worker nodes = Range of workers in hundreds  / 100 = a few nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A processing throughput per worker lower than ten thousand records per second as assumed above would mean a larger number of worker nodes to hit the bottleneck (which is the network bandwidth in this case). Note, the above gives the nodes needed in the data access stage only, and not in the entire worker cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrent Jobs
&lt;/h3&gt;

&lt;p&gt;Concurrent computations on the worker cluster share the resources and throughput. &lt;/p&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;If a shared platform requires fair scheduling of, say, 3 similar computations at a time, the resource limit for each computation will be 1/3 of the total available limits. Each query in our scenario, thus, can only need a few worker nodes or a few hundred workers before reaching the bottleneck, which is the network bandwidth in our example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another Setup
&lt;/h2&gt;

&lt;p&gt;Let's run through a lower tier hardware setup for a similar workload of scans and queries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of database nodes: S = 3&lt;/li&gt;
&lt;li&gt;Device I/O bandwidth at a database node: d = 2 GBps&lt;/li&gt;
&lt;li&gt;Effective network bandwidth: B = 50 Gbps&lt;/li&gt;
&lt;li&gt;Workers per worker node: 50&lt;/li&gt;
&lt;li&gt;Record size: R = 2KB&lt;/li&gt;
&lt;li&gt;Filter selectivity : F = 10&lt;/li&gt;
&lt;li&gt;Projection factor : P = 10&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Device I/O limit
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;Cluster SSD I/O = Sxd/R = 3 x 2x10^9/2x10^3 = 3 x 10^6 or 3 million records per second&lt;/p&gt;

&lt;p&gt;Max workers in data access stage = 3x10^6 / (5x10^2) = 6 x 10^3 or 6000 workers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Database throughput limit
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;Cluster unfiltered scan throughput = Sxd/R = 3 x 2x10^9 / 2x10^3 = 3x10^6 or 3 million records per second&lt;/p&gt;

&lt;p&gt;Max workers for unfiltered scan = 3x10^6 / (10^3) = 3x10^3 or 3000 workers.&lt;/p&gt;

&lt;p&gt;Cluster filtered scan throughput = Sxd/(RxF) = 3 x 2x10^9 / (2x10^3 x 10) = 3x10^5 or 0.3 million records per second&lt;/p&gt;

&lt;p&gt;Max workers for filtered scan = 0.3x10^6 / (10^3) = 0.3x10^3 or 300 workers.&lt;/p&gt;

&lt;p&gt;Cluster secondary-index query throughput = Sxd/R = 3 x 2x10^9 / (2x10^3 ) = 3x10^6 or 3 million records per second&lt;/p&gt;

&lt;p&gt;Max workers for secondary-index query = 3x10^6 / (10^3) = 3x10^3 or 3000 workers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To saturate the cluster record throughput the number of worker nodes is in low tens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Range of workers / workers per worker node &lt;br&gt;
= 300-3000  / 50  -&amp;gt; 6 - 60 nodes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Network bandwidth limit
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;Max workers to saturate network I/O = B/(WxR) = 50/8 x 10^9 / (WxR) &lt;br&gt;
= 6.25x10^9 / (10^3 x 2x10^3) = 3.125 x 10^3 &lt;br&gt;
= 3125 workers&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To saturate the effective network bandwidth the number of worker nodes is in low tens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Workers / workers per worker node&lt;br&gt;
= 3125  / 50 -&amp;gt; 62.5 nodes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Concurrency limit
&lt;/h4&gt;

&lt;p&gt;With 3 similar computations at a time, the resource limit for each computation will be 1/3 of the total available limits, or up to 21 worker nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing Data Access
&lt;/h2&gt;

&lt;p&gt;The problem of optimizing data access is to achieve performance and throughput as close to the hardware limits as possible. &lt;/p&gt;

&lt;p&gt;In general, streamlined processing with fewest context switches provides superior efficiency.  So data access is likely to be more efficient and provide better throughput where: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data is spread across fewer server nodes as it requires less split-merge overhead,&lt;/li&gt;
&lt;li&gt;access chunk (or page) size is larger as it allows longer uninterrupted runs, and &lt;/li&gt;
&lt;li&gt;asynchronous mode is used as resources are not held up while waiting on response. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the same time, there are many factors that cannot be easily predicted. Concurrency conflicts, context switching, and flow control delays can lead to potential bottlenecks. Suboptimal system configuration such as missing indexes, and suboptimal choice of the query plan due to incorrect heuristics or metadata will also lead to througput surprises. Therefore, the best way to find out the optimal parameters is to experiment in the target environment with the target workload.&lt;/p&gt;




&lt;p&gt;The hardware capacities of a given environment will limit the level of parallelism and throughput. Different workloads place different types of resource burden and can expose bottlenecks in different areas. The system should be balanced with the desired workload in mind so that a bottleneck in one area does not waste unused capacity in other areas. While a good understanding of such limits is important, there are many factors that are dynamic and cannot be predicted easily. Therefore, experimenting in the target environment is essential to discover the system model for optimal performance.&lt;/p&gt;




</description>
      <category>aerospike</category>
      <category>bigdata</category>
      <category>paralllelprocessing</category>
      <category>queryprocessing</category>
    </item>
    <item>
      <title>Processing Large Data Sets in Fine-Grained Parallel Streams</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Tue, 17 Jan 2023 16:36:37 +0000</pubDate>
      <link>https://dev.to/aerospike/processing-large-data-sets-in-fine-grained-parallel-streams-3o1l</link>
      <guid>https://dev.to/aerospike/processing-large-data-sets-in-fine-grained-parallel-streams-3o1l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F1600%2F0%2ArZiI9aVOg6cvtu7W" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F1600%2F0%2ArZiI9aVOg6cvtu7W" alt="(Source: Photo by Dan Gold on Unsplash" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Dan Gold on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Aerospike provides several mechanisms for accessing large data sets over parallel streams to match worker throughput in parallel computations. This article explains the key mechanisms, and describes specific schemes for defining data splits and a framework for testing them.&lt;/p&gt;

&lt;p&gt;Follow along in the interactive notebook &lt;a href="https://developer.aerospike.com/tutorials/java/query_splits" rel="noopener noreferrer"&gt;Splitting Large Data Sets for Parallel Processing of Queries&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Processing of Large Data Sets
&lt;/h2&gt;

&lt;p&gt;In order to process large data sets, a common scheme is to split the data into partitions and assign a worker task to process each partition. The partitioning scheme must have the following properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The partitions are collectively exhaustive, meaning they cover the entire data set, and mutually exclusive, meaning they do not overlap.&lt;/li&gt;
&lt;li&gt;They are deterministically and efficiently computed.&lt;/li&gt;
&lt;li&gt;They are accessible in an efficient and flexible manner as required by worker tasks, for example, in smaller chunks at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also critical for the application or platform to have an efficient mechanism to coordinate with workers and aggregate results to benefit from such a partitioning scheme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Partitions in Aerospike
&lt;/h2&gt;

&lt;p&gt;Aerospike organizes records in a namespace in 4096 partitions. The partitions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uniformly balanced, meaning they hold approximately the same number of records that are hashed to partitions using the RIPEMD160 hash function, and &lt;/li&gt;
&lt;li&gt;uniformly distributed across cluster nodes, meaning each node has about the same number of partitions. To be precise, each node has about the same number of partitions if the cluster size is a power of two, and more in some nodes otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three types of Aerospike indexes - primary, set, and secondary - are partition oriented. That is, they are split by partitions at each node (in releases 5.7+), and queries are processed at each node over individual partitions. A client can request a query to be processed over specific partitions so that multiple client workers can work in parallel. It is easy to see how parallel streams up to the total number of partitions (4096) can be set up for parallel processing data streams.&lt;/p&gt;

&lt;p&gt;Pagination is supported with Aerospike queries where the client can process a chunk of records at a time by repeatedly asking for a certain number of records until all records are retrieved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting Data Sets Beyond 4096
&lt;/h2&gt;

&lt;p&gt;Many data processing platforms allow more worker tasks than 4096. For example, Spark allows up to 32K worker tasks to run in parallel. Trino allows theoretical concurrency of greater than 4K. &lt;/p&gt;

&lt;p&gt;Aerospike allows for data splits larger than 4096 by allowing a partition to be divided into sub-partitions efficiently. The scheme is based on the &lt;code&gt;digest-modulo&lt;/code&gt; function that can divide a partition into an arbitrary number of non-overlapping and collectively complete sub-partitions. It involves adding the  filter expression &lt;code&gt;digest % N == i for 0 &amp;lt;= i &amp;lt; N&lt;/code&gt;, where the &lt;code&gt;digest&lt;/code&gt; is the hashed key of the record.&lt;/p&gt;

&lt;p&gt;The advantage of the digest-modulo function is that it can be evaluated without reading individual records from the storage device (such as SSDs). Digests of all records are held in the primary index, which resides in memory. Therefore, determining the membership of a digest, and equivalently of the corresponding record, in a sub-partition is fast. Each sub-partition stream needs to read only its records from the potentially slower storage device, although it needs to perform the in-memory digest-modulo evaluation, which is much faster, for all records.&lt;/p&gt;

&lt;p&gt;This scheme works for primary-index and set-index queries because they hold digests of records. The secondary index holds the primary index  location of the record, and a lookup provides the digest information. &lt;/p&gt;

&lt;h2&gt;
  
  
  Defining and Assigning Splits
&lt;/h2&gt;

&lt;p&gt;The problem can be stated as: How are the splits over a data set defined and assigned to N worker tasks, where N can vary from 1 to any arbitrarily large number. In reality, there would be an upper bound on N on a given platform because of either a platform-defined absolute limit, or the overhead of processing a large number of parallel streams and coordinating across them can negate the benefits.&lt;/p&gt;

&lt;p&gt;An Aerospike partition ID varies from 0 to 4095. If a partition is divided into S sub-partitions, each sub-partition is identified by a tuple of partition-id &lt;code&gt;p&lt;/code&gt; and sub-partition-id &lt;code&gt;s&lt;/code&gt;, and modulo factor &lt;code&gt;m&lt;/code&gt;: &lt;code&gt;(p, s, m)&lt;/code&gt;. Each of the splits need an assignment of a list of partitions and/or sub-partitions. For example, a split &lt;code&gt;i&lt;/code&gt; can have:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Split i -&amp;gt; [pi1, pi2, pi3, …, (psi1, si1, m), (psi2, si2, m), …]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It is important to understand what partitions or sub-partitions can be requested in a single Aerospike API call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Full partitions and sub-partitions cannot be mixed in a call.&lt;/li&gt;
&lt;li&gt;Full partitions must be consecutive in order, or &lt;code&gt;(pstart-id, pcount)&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Sub-partitions must be consecutive, belong to consecutive partitions, and use the same modulo factor, or &lt;code&gt;(pi, pcount, sstart-id, scount, m)&lt;/code&gt;. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is to achieve best efficiency with the operations available in the APIs. &lt;/p&gt;

&lt;p&gt;We will adhere to these constraints in the following discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split Assignment Schemes
&lt;/h2&gt;

&lt;p&gt;We will examine three variations of split assignment. &lt;/p&gt;

&lt;p&gt;If N is the requested number of splits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At-most N splits (can be fewer), same sized, one API call per split.&lt;/li&gt;
&lt;li&gt;At-least N splits (can be more), same sized, one API call per split.&lt;/li&gt;
&lt;li&gt;Exactly N splits, same sized, up to three API calls per split.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two allow specific discrete values of splits to allocate the same amount of data (as partitions or a sub-partition), and choose the closest allowed number of splits that is a factor or multiple of 4096. Each split is processed with one API call. &lt;/p&gt;

&lt;p&gt;The third one allows any number of splits with the same sized data assignment of partitions and/or sub-partitions. Each split however may require up to three API calls. &lt;/p&gt;

&lt;p&gt;These schemes are described in detaill below.&lt;/p&gt;

&lt;h3&gt;
  
  
  At-Most N Splits
&lt;/h3&gt;

&lt;p&gt;In this case, the returned splits can be fewer in number, matching the closest lower factor or multiple of 4096.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case 1: N &amp;lt; 8192: Full partition assignments

&lt;ul&gt;
&lt;li&gt;Returned number of splits F is the closest &lt;em&gt;factor&lt;/em&gt; of 4096 that is &amp;lt;= N.&lt;/li&gt;
&lt;li&gt;Number of partitions in each split, n: 4096/F&lt;/li&gt;
&lt;li&gt;Partitions in split i: &lt;code&gt;(start = i*n, count = n)&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Case 2: N &amp;gt;= 8192: Sub-partition assignment

&lt;ul&gt;
&lt;li&gt;Returned number of splits M is the closest &lt;em&gt;multiple&lt;/em&gt; of 4096 that is &amp;lt;=  N.&lt;/li&gt;
&lt;li&gt;Number of sub-partitions or modulo-factor, m: M/4096&lt;/li&gt;
&lt;li&gt;Sub-partition in split i: &lt;code&gt;(floor(i/m), i%m, m)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  At-Least N Splits
&lt;/h3&gt;

&lt;p&gt;In this case, the returned splits can be more in number, matching the closest higher factor or multiple of 4096.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case 1: N &amp;lt;= 4096: Full partition assignments

&lt;ul&gt;
&lt;li&gt;Returned number of splits F is the closest &lt;em&gt;factor&lt;/em&gt; of 4096 that is &amp;gt;= N.&lt;/li&gt;
&lt;li&gt;Number of partitions in each split, n: 4096/F&lt;/li&gt;
&lt;li&gt;Partitions in split i: &lt;code&gt;(start = i*n, count = n)&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Case 2: N &amp;gt;= 8192: Sub-partition assignment

&lt;ul&gt;
&lt;li&gt;Returned number of splits M is the closest &lt;em&gt;multiple&lt;/em&gt; of 4096 that is &amp;gt;= N.&lt;/li&gt;
&lt;li&gt;Number of sub-partitions or modulo-factor, m: M/4096&lt;/li&gt;
&lt;li&gt;Sub-partition in split i: &lt;code&gt;(floor(i/m), i%m, m)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exactly N Splits
&lt;/h3&gt;

&lt;p&gt;In this case, the exact number of splits of equal sizes are created.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each of the 4096 partitions is divided into N sub-partitions, resulting in total 4096 * N sub-partitions, &lt;/li&gt;
&lt;li&gt;Each split is assigned 4096 sub-partitions in the following manner:

&lt;ul&gt;
&lt;li&gt;Sub-partitions are enumerated vertically from sub-partition 0 to N-1 in each partition, starting at partition 0 and ending at partition 4095. &lt;/li&gt;
&lt;li&gt;After assigning 4096 consecutive sub-partitions to a split,  the next split gets the following 4096 sub-partitions, and so on.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Thus, sub-partitions in a split fall in one or more of the following three groups, each of which can be retrieved using one API call:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Up to 4095 (including none) consecutive sub-partitions in the starting partition&lt;/li&gt;
&lt;li&gt;Up to 4096 (including none)  consecutive full partitions&lt;/li&gt;
&lt;li&gt;Up to 4095 ((including none) consecutive  sub-partitions in the ending partition&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, if 3 splits are desired, each split can have 4096/3 or 1365 1/3 partitions. In this scheme, the first split would consist of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;0 sub-partitions in partition 0&lt;/li&gt;
&lt;li&gt;1365 full partitions: 0-1364&lt;/li&gt;
&lt;li&gt;1 (0th) of 3 sub-partitions in partition 1365&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the next (second) split will consist of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;2 (0-1) of 3 sub-partitions in partition 1365&lt;/li&gt;
&lt;li&gt;1364 full partitions: 1366-2729&lt;/li&gt;
&lt;li&gt;2 (0-1) of 3 sub-partitions in partition 2730&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The algorithm details with code and examples are available in &lt;a href="https://developer.aerospike.com/tutorials/java/query_splits" rel="noopener noreferrer"&gt;the notebook tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternative Ways of Splitting
&lt;/h3&gt;

&lt;p&gt;Splits can be assigned in many other ways. For example, the notebook shows two additional examples of the Exactly N Splits scheme, however you can experiment with a different scheme in the notebook or your target environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Query Framework
&lt;/h2&gt;

&lt;p&gt;The parallel stream processing from the above split assignments can be tested with the following simple framework that is implemented in &lt;a href="https://developer.aerospike.com/tutorials/java/query_splits" rel="noopener noreferrer"&gt;the  notebook tutorial&lt;/a&gt;. It can be tweaked to suit the needs of the intended workload and environment.&lt;/p&gt;

&lt;p&gt;The test data consists of 100K records (can be changed) of ~1KB size, with a secondary index defined on an integer bin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Processing Flow
&lt;/h3&gt;

&lt;p&gt;The processing takes place as follows (tunable parameters are &lt;em&gt;italicized&lt;/em&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Splits assignments are made for the requested &lt;em&gt;number of splits&lt;/em&gt; and the desired &lt;em&gt;split type&lt;/em&gt;. &lt;/li&gt;
&lt;li&gt;The desired &lt;em&gt;number of workers&lt;/em&gt; (threads) are created. All workers start at the same time to process the splits. Each worker thread does the following in a loop until there are no unprocessed splits available:

&lt;ul&gt;
&lt;li&gt;- Obtain the &lt;em&gt;next scheduled&lt;/em&gt; split.&lt;/li&gt;
&lt;li&gt;- Create one or more query requests over the split’s partitions and sub-partitions and process them sequentially.&lt;/li&gt;
&lt;li&gt;- Assign the &lt;em&gt;secondary-index query predicate&lt;/em&gt; depending on the requested query type. &lt;/li&gt;
&lt;li&gt;- Create the requested &lt;em&gt;filter expression&lt;/em&gt;. Append it (with AND) to the sub-partition filter expression if one is being used, otherwise use it separately.&lt;/li&gt;
&lt;li&gt;- Process the query with the filter in the requested &lt;em&gt;mode&lt;/em&gt; (sync or async).

&lt;ul&gt;
&lt;li&gt;- Get &lt;em&gt;chunk-size&lt;/em&gt; records at a time until all records are retrieved.&lt;/li&gt;
&lt;li&gt;- Process the records using the &lt;em&gt;stream processing implementation&lt;/em&gt;. The &lt;a href="https://developer.aerospike.com/tutorials/java/query_splits" rel="noopener noreferrer"&gt;notebook&lt;/a&gt; example has CountAndSum processing that:

&lt;ul&gt;
&lt;li&gt;- Aggregates the number of records in a count by the worker.&lt;/li&gt;
&lt;li&gt;- Aggregates an integer bin value in a sum by the worker.&lt;/li&gt;
&lt;li&gt;- Aggregates count and sum across all workers at the end.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Wait for all workers to finish, and output the aggregated results from stream processing.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In the CountAndSum example, the total number of processed records and the sum of the integer bin across all records must be the same for a given query predicate and filter irrespective of the number of splits, split type, number of workers, and processing mode.&lt;/p&gt;

&lt;p&gt;A summary of split assignments and worker statistics can be optionally printed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters and Variations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Number of splits&lt;/strong&gt;: Any number of splits over the data set may be requested. Example range 1-10K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split type&lt;/strong&gt;: One of the three variations discussed above can be requested: At-Most N, At-Least N, and Exactly N.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Number of workers&lt;/strong&gt;: The desired parallelism in processing, example values range between 1-10K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query index type&lt;/strong&gt;: Either primary- or secondary-index query can be specified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary-index predicate&lt;/strong&gt;: In case of a secondary-index query, a secondary-index predicate is specified. The default secondary-index predicate  is &lt;code&gt;50001 &amp;lt;= bin1 &amp;lt;= 100000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter expression&lt;/strong&gt;: An optional filter expression can also be specified. The default filter expression is &lt;code&gt;bin1 % 2 == 0&lt;/code&gt;, that is, only even-valued records will be retrieved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk size&lt;/strong&gt;: Or page size for iterative retrieval of records in a split. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing mode&lt;/strong&gt;: Either sync or async processing mode to process the query results may be selected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream processing&lt;/strong&gt;: How records are aggregated or otherwise processed; can be customized by overriding the abstract class StreamProcessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work scheduling&lt;/strong&gt;: How splits are assigned to workers; can be customized by overriding the abstract class WorkScheduling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://developer.aerospike.com/tutorials/java/query_splits" rel="noopener noreferrer"&gt;notebook&lt;/a&gt; illustrates many interesting variations, and you can play with additional ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases for Fine-Grained Parallelism
&lt;/h2&gt;

&lt;p&gt;Processing speed can benefit from a very high degree of parallelism for a very large data set processed with transforms, aggregations, and updates. &lt;/p&gt;

&lt;p&gt;Multiple data sets that need to be joined, and require shuffling subsets across a large number of worker nodes, may not benefit from a very high degree of parallelism. In such cases, the cost of transfer of data in subsequent steps across a large number of worker nodes can limit the benefit of fine-grained retrieval streams. A Cost Based Optimizer (CBO) on the processing platform should be able to determine the best level of parallelism for data access from Aerospike for a given query.&lt;/p&gt;

&lt;p&gt;It would be useful to examine simple heuristics for the level of parallelism in complex computations over a data set. In a future post, we will explore the optimal level of parallelism given the potential conflicting goals of throughput, response time, resource cost, and utilization. &lt;/p&gt;

</description>
      <category>watercooler</category>
    </item>
    <item>
      <title>Of Queries and Indexes</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Thu, 13 Oct 2022 21:33:05 +0000</pubDate>
      <link>https://dev.to/aerospike/of-queries-and-indexes-1cl3</link>
      <guid>https://dev.to/aerospike/of-queries-and-indexes-1cl3</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pfyiXTAj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2A3EjjAPKeG-tmetcs" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pfyiXTAj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2A3EjjAPKeG-tmetcs" alt="(Source: Photo by Jan Antonin Kolar on Unsplash" width="800" height="555"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Jan Antonin Kolar on &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Queries, scans, indexes, pagination, and parallelism are common concepts in databases, but each database differs in specifics. It is vital to understand the specifics in order to get the most out of a database. In Aerospike, queries and indexes play a key role in realizing its speed-at-scale objective. The goal of this post is to help developers better understand the Aerospike capabilities in these areas.&lt;/p&gt;

&lt;p&gt;A query is a request for data that meets specific criteria. The criteria or conditions that the result must meet are called the query predicate. &lt;/p&gt;

&lt;p&gt;In Aerospike, a query is processed using one of these indexes: the primary index, a set index, or a secondary index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary and Set Indexes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Primary Index
&lt;/h3&gt;

&lt;p&gt;In Aerospike, there is only one system-defined primary index for each namespace, built on the &lt;code&gt;digest&lt;/code&gt; of records. The digest is a &lt;a href="https://en.wikipedia.org/wiki/RIPEMD"&gt;RIPEMD160&lt;/a&gt; hash of the tuple &lt;code&gt;(set, user-key)&lt;/code&gt;, where a set, which is equivalent to a table, is an application defined grouping of records in a namespace, which is equivalent to a database or schema, and user-key is an application-provided id that is unique within the set.  The primary index is not optional, is created automatically, and cannot be removed, nor can the field on which it is defined be changed. While another index may be created on a bin, which is equivalent to a column, holding a primary key, it is considered a secondary index. For example, for a set having records with employee-number as the user-key and an "ssn" bin for social security number, an index created on the ssn bin is a secondary index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scan or Primary-Index Query
&lt;/h3&gt;

&lt;p&gt;In Aerospike, the general way of processing data requests is a scan with a &lt;code&gt;filter expression&lt;/code&gt; that captures the query predicate. For example, for a request "get records from employees set where employee-number is in range 100-200", a scan is performed with a filter expression to capture the query predicate "employee-number is in range 100-200". The primary index is used to scan the namespace, and therefore a scan is also called a &lt;code&gt;primary-index query&lt;/code&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Set Indexes
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://docs.aerospike.com/server/architecture/set-indexes"&gt;set index&lt;/a&gt; can optionally be created for potential performance improvements when querying a set. In the previous example, the request will execute faster by having a set index on the employees set. If a set index has been created, it will be used for a set query instead of the primary index. &lt;/p&gt;

&lt;h3&gt;
  
  
  Order
&lt;/h3&gt;

&lt;p&gt;A query using the primary index, or a set index, follows the internal, deterministic digest ordering. Given the digest is a hashed value, this order will not be significant to the application. For example, while the employee numbers have a recognizable order, records with employee-number user-key will have a random scan order. In general, any ordering must be implemented in the application as query results are typically gathered from multiple partitions on multiple nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Processing
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://docs.aerospike.com/server/guide/expressions#filter-expressions"&gt;filter expression&lt;/a&gt; is a boolean computation of the record metadata and/or data, using supported operators and methods. The expression is specified as part of the operation policy, and is evaluated for every record. Only records that pass the filter are further processed. If no filter expression is specified, all records in the set or namespace are processed.&lt;/p&gt;

&lt;p&gt;A primary index or set index query can be performed in sync or async mode. In the sync mode, the application thread is blocked until the results are returned, whereas in the async mode, the application thread is not blocked, and the results are returned in a callback.&lt;/p&gt;

&lt;p&gt;Code examples of queries using a filter expression can be found &lt;a href="https://developer.aerospike.com/client/usage/queries/basic/primary"&gt;here&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/tutorials/java/sql_select#scan-based-on-expression-filter"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secondary Indexes
&lt;/h2&gt;

&lt;p&gt;A secondary index can be optionally defined to speed up processing of queries. &lt;/p&gt;

&lt;h3&gt;
  
  
  Mapping Bin or CDT Values to Records
&lt;/h3&gt;

&lt;p&gt;A secondary index is defined on a bin (column) or an element at any level of a Collection Data Type (CDT, which is a List or a Map), over its integer, string, or geospatial values. A secondary index keys (maps) a value to one or more records, but not within a record (such as a bin or a CDT element).  Thus, a seoondary index is a mapping from a value to one or more records. When a secondary index is defined on a CDT, all CDT values of the indexed type map to the record. So a secondary index on List [1,2,3] in record R will have mappings 1-&amp;gt;R, 2-&amp;gt;R, and 3-&amp;gt;R.&lt;/p&gt;

&lt;p&gt;A secondary index is created on a set (table) of records. In Aerospike Database 6.1+, a secondary index created with a null set (or no set parameter) encompasses all records in the namespace. In earlier versions, it would span only the records that were created with a null set parameter. In 6.1+, a secondary index cannot be created on records that are not in any set, and the best practice recommendation is to always create a record in a (non-null) set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indexed Value Types
&lt;/h3&gt;

&lt;p&gt;It is important to note that an index is strongly typed, meaning it holds only values of a specific type: integer, string, or geospatial. A bin or a CDT element in Aerospike however is not strongly typed, and can hold a value of any type. An index maps only values of the index type in the bin or CDT element; other values are ignored. For example, an integer index on a List [1, 2, 3, "a", "b", "c"] will index only 1, 2, and 3, and ignore the string elements. A bin or CDT element can have multiple indexes defined to allow queries on different types of values. In this example, a string index must be created on the same List if records need to be retrieved that, say, have a value "c" in the list using a secondary index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indexing on Custom Values
&lt;/h3&gt;

&lt;p&gt;In some cases, the values may not be available in one place or even stored in a record or CDT element for indexing. For example, a specific object field "a" in an array of objects: [{"a": 1, "b": 11}, {"a": 2, "b":22}, ...]. In such cases, these values can be copied, or computed and stored, to a bin or a CDT that can then be indexed. In this example, create and index List a-values: [1, 2, ...]. The indexed bin or CDT must be kept in sync with the changes in the values. In the example, if the field "a" is updated in any object, that must be reflected in the a-values list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uniqueness and Order
&lt;/h3&gt;

&lt;p&gt;A secondary index cannot be defined as unique or sorted. That is, the secondary index does not support the uniqueness constraint on the field, although it can be defined on a bin that holds unique values, such as the ssn bin in the earlier example. As explained above, it is up to the application to order query results. Also, composite indexes over multiple bins are currently not directly supported, but can be implemented as described earlier in the section Indexing on Custom Values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secondary-Index Query
&lt;/h3&gt;

&lt;p&gt;A query using a secondary index is called a &lt;code&gt;secondary-index query&lt;/code&gt;, to be distinguished from a primary-index query. A secondary-index query will fail if the supporting secondary index does not exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Processing
&lt;/h3&gt;

&lt;p&gt;The secondary index lookup identifies the records to process. A secondary-index query may also specify a filter expression, in which case the secondary-index predicate is processed first, the filter expression is evaluated for the resulting records, and the matching records then are processed further. For efficient processing, the most selective available index should be used for the secondary-index predicate and the remaining condition as the filter expression. For example, to find all black Skoda cars in California, a secondary index on manufacturer and not on color should be used, along with a filter expression for black color.&lt;/p&gt;

&lt;p&gt;Code examples of a scan using a filter expression can be found &lt;a href="https://developer.aerospike.com/client/usage/queries/basic//secondary"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Find additional details on CDT indexing in the blog post &lt;a href="https://developer.aerospike.com/blog/query-json-documents-faster"&gt;Query JSON Document Faster (and More) with CDT Indexing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination
&lt;/h2&gt;

&lt;p&gt;The application can get the results in a stream of smaller chunks by using pagination. Pagination is supported with all types of queries and indexes.&lt;/p&gt;

&lt;p&gt;The chunk size limit is specified in the max-records policy parameter. Note, a smaller number of records may be returned because the chunk size limit is divided evenly across all server nodes, but the data may be unevenly distributed with respect to the query predicate.&lt;/p&gt;

&lt;p&gt;The same query handle is used to get subsequent chunks until all records are retrieved. &lt;/p&gt;

&lt;p&gt;Check out a concrete pagination example and code &lt;a href="https://developer.aerospike.com/client/usage/queries/basic/primary#pagination"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Query Processing
&lt;/h2&gt;

&lt;p&gt;Aerospike distributes namespace records in 4096 uniform partitions, and allows separate queries over them for parallelism. Queries can be split into independent parallel sub-queries over one or more partitions, for the needed parallelism to match the required throughput. Further, each partition can be subdivided into N sub-partitions by adding the modulo filter expression &lt;code&gt;digest % N == i&lt;/code&gt; for 0 &amp;lt;= i &amp;lt; N. Note, the filter expression evaluation for the sub-partitions is purely metadata based, digest being record metadata. Since record metadata is held in memory, the evaluation requires no access to data on the SSD. A sub-partition only reads its own records, minimizing the necessary SSD reads across the multiple sub-partitions, resulting in maximum parallel throughput. &lt;/p&gt;

&lt;p&gt;Using this scheme, a large number of workers on a platform such as Spark (which supports up to 32K workers) can uniformly spread the data among workers for processing via an equal number of mutually exclusive and collectively exhaustive sub-streams using partition queries in combination with the modulo filter expression as described above. The appropriate data scale, throughput, and latency can be achieved by adjusting the cluster size as well as the number of attached SSD devices per node. &lt;/p&gt;

&lt;h2&gt;
  
  
  Processing Using Indexes
&lt;/h2&gt;

&lt;p&gt;In addition to retrieving records, one can perform additional operations on the selected records:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Project (retrieve) specific bins and computed expressions. &lt;/li&gt;
&lt;li&gt;Further processing of selected records with read or write operations. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;1 is not conceptually different from retrieving entire records and hence we will not discuss it further. 2 is discussed below.&lt;/p&gt;

&lt;p&gt;It is worth mentioning that processing multiple records using indexes is different from batch processing where records are specified by their keys and not by a predicate. Please refer to the blog post to learn more about &lt;a href="https://developer.aerospike.com/blog/batch-operations-in-aerospike"&gt;Batch Operations in Aerospike&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Read and Write Operations
&lt;/h3&gt;

&lt;p&gt;In processing operations using indexes, read and write operations cannot be mixed; either only read or only update operations can be specified for  processing. &lt;/p&gt;

&lt;p&gt;A record may match multiple times for a given condition when using a collection index type. There is no guarantee that a record will be de-duplicated for the same value. In such cases, &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The application must be prepared to handle duplication within results.&lt;/li&gt;
&lt;li&gt;Write operations must deal with any duplication appropriately (for example, make them idempotent or include logic to apply the operations only once).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Read operations
&lt;/h3&gt;

&lt;p&gt;Read operations are specified using: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/java-intro_to_transactions#operating-on-complex-data-types"&gt;bin (transaction) operations&lt;/a&gt; for efficient access to complex data types such as HyperLogLog, GeoJSON, Blob, List, and Map, or&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/sql_aggregates_1"&gt;a stream UDF&lt;/a&gt; for aggregate processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Background Updates
&lt;/h3&gt;

&lt;p&gt;Records can also be updated in a “background mode” in conjunction with a query. Such background updates work differently from read operations: the entire operation is processed in the background. The application can only check the status of a background operation, but cannot obtain granular results from it. Any record specific status must be ascertained, and corrected if necessary, separately. Background updates are an efficient way to update a large number of records. &lt;/p&gt;

&lt;p&gt;Note that updates using indexes are not supported in “foreground” sync and async modes like read operations where the application receives record-specific results. &lt;/p&gt;

&lt;p&gt;Update operations are specified using &lt;a href="https://developer.aerospike.com/tutorials/java/sql_update#list-of-bin-updates"&gt;bin (transaction) operations&lt;/a&gt; or &lt;a href="https://developer.aerospike.com/tutorials/java/sql_update#using-udf"&gt;a record UDF&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Find additional code examples &lt;a href="https://developer.aerospike.com/client/usage/queries/background"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Queries and indexes are important to realize speed at scale. This post describes key aspects of indexes and queries in Aerospike to help developers better understand these capabilities and utilize them effectively.&lt;/p&gt;




</description>
      <category>aerospike</category>
      <category>query</category>
      <category>index</category>
      <category>secondaryindex</category>
    </item>
    <item>
      <title>Building Large-Scale Real-Time JSON Applications</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Tue, 13 Sep 2022 14:58:17 +0000</pubDate>
      <link>https://dev.to/aerospike/building-large-scale-real-time-json-applications-4npo</link>
      <guid>https://dev.to/aerospike/building-large-scale-real-time-json-applications-4npo</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gdk2QiaK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2AIGbSx8ooGN8w5rn_" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gdk2QiaK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2AIGbSx8ooGN8w5rn_" alt="(Source: Photo by Wilhelm Gunkel on [Unsplash](https://unsplash.com/) )" width="800" height="503"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Wilhelm Gunkel on &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;“Real-time describes various operations or processes that respond to inputs reliably within a specified time interval (&lt;a href="https://en.wikipedia.org/wiki/Real-time"&gt;Wikipedia&lt;/a&gt;).” &lt;/p&gt;

&lt;p&gt;Real-time data must be processed soon after it is generated otherwise its value is diminished, and real-time applications must respond within a tight timeframe otherwise the user experience and business results are impaired. It is critical for real-time applications to have reliably fast access to all data, real-time or otherwise. &lt;/p&gt;

&lt;p&gt;The number of real-time interactions between people and devices continues to grow. Leveraging real-time data is still a competitive edge in some areas but its use is expected in others. Up-to-the-moment relevant information is expected to be applied in delivering the best possible customer experience or business decisions.&lt;/p&gt;

&lt;p&gt;Much of the data today is generated, transferred, stored, and consumed in the JSON format, including real-time data such as feeds from IOT sensors and  social networks, and prior data such as user profiles and product catalogs. Therefore, JSON data is ubiquitous and growing in use. The best possible real-time decisions, increasingly based on AI/ML algorithms, will be arrived at using continually updated massive data sets. &lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;This article discusses the database perspective on building large-scale real-time JSON applications and touches upon the following key topics: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What to look for in a real-time data platform&lt;/li&gt;
&lt;li&gt;How to organize JSON documents for speed at scale&lt;/li&gt;
&lt;li&gt;The core JSON functionality required for ease of development&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Database for Large-Scale JSON Applications
&lt;/h2&gt;

&lt;p&gt;The key requirements in a database to build such applications are described below, along with how the Aerospike Database delivers them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reliably fast random access at scale
&lt;/h3&gt;

&lt;p&gt;Reliably fast response time for read and write operations at any scale and any read-write workload mix is required to meet the real-time contract. Aerospike delivers it through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast and uniform hash-based data distribution to all nodes for optimal resource utilization&lt;/li&gt;
&lt;li&gt;Hybrid Memory Architecture (HMA) to store indexes and data in DRAM, SSD, and other devices to provide cost-effective fast storage capacity&lt;/li&gt;
&lt;li&gt;Optimized processing of writes and garbage collection for predictable response&lt;/li&gt;
&lt;li&gt;One-hop access to all data from the application&lt;/li&gt;
&lt;li&gt;Smart Client that handles cluster transitions and data movements transparently&lt;/li&gt;
&lt;li&gt;Primary and secondary indexes for fast access&lt;/li&gt;
&lt;li&gt;Async and background processing modes for greater efficiency&lt;/li&gt;
&lt;li&gt;Multi-op requests to perform many single-record operations atomically in one request&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fast ingest rate
&lt;/h3&gt;

&lt;p&gt;The database must support fast ingestion speeds so that surges in real-time data feeds do not overwhelm the system or result in data loss.&lt;/p&gt;

&lt;p&gt;In Aerospike Database 6.0+, &lt;a href="https://developer.aerospike.com/blog/batch-operations-in-aerospike_"&gt;batch operations&lt;/a&gt; for read, write, delete, and UDF operations are supported so that ingest can achieve the necessary high throughput.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fast queries
&lt;/h3&gt;

&lt;p&gt;The database must handle concurrent queries over large data efficiently. To this end, Aerospike provides various indexes and granular control over parallel processing of queries. &lt;/p&gt;

&lt;h3&gt;
  
  
  Convenient JSONPath based access
&lt;/h3&gt;

&lt;p&gt;JSONPath based Document API offers a convenient way to access and modify specific elements within a document. Aerospike support for JSON documents in 6.0+ is discussed below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rich Document Functionality
&lt;/h3&gt;

&lt;p&gt;JSON documents are stored in the database as a &lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;Collection Data Type (CDT)&lt;/a&gt;. CDTs are essentially Map and List data types that offer rich functionality to JSON applications.&lt;/p&gt;

&lt;h4&gt;
  
  
  Efficient storage and transfer
&lt;/h4&gt;

&lt;p&gt;CDTs are stored and transferred efficiently in the &lt;code&gt;MessagePack&lt;/code&gt; format.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rich API
&lt;/h4&gt;

&lt;p&gt;The API supports many common List and Map usages that involve complex processing. They are processed entirely on the server side to eliminate retrieval of data to the client side.&lt;/p&gt;

&lt;h4&gt;
  
  
  Well integrated into other performance features
&lt;/h4&gt;

&lt;p&gt;CDTs are well integrated into various performance features including &lt;a href="https://developer.aerospike.com/tutorials/java/expressions"&gt;Expressions&lt;/a&gt;, batch requests, multi-op requests, and secondary indexes. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDT operations can be used in Expressions that offer efficient server side execution. &lt;/li&gt;
&lt;li&gt;Batch requests allow operations on multiple documents in one request. &lt;/li&gt;
&lt;li&gt;Multi-op request allows many operations on one document to be performed in one request. For instance, in the same request, you can add items to a JSON array, sort it, get its new size, and top N items in it. &lt;/li&gt;
&lt;li&gt;CDT elements at any nested level can be indexed for fast and convenient access, described further below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Synchronizing data with other systems
&lt;/h3&gt;

&lt;p&gt;Aerospike offers control over replicating all or a subset of the data efficiently to other Aerospike clusters through &lt;a href="https://aerospike.com/resources/tech-videos/xdr/"&gt;Cross-Data-Center Replication (XDR)&lt;/a&gt;. Edge-core synchronization is often necessary for collecting real-time data as well as delivering  real-time user experience at the edge. Various connectors facilitate convenient and fast synchronization with other systems as described below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Easy integration with real-time data streams
&lt;/h3&gt;

&lt;p&gt;Aerospike provides &lt;a href="https://docs.aerospike.com/connect"&gt;streaming connectors&lt;/a&gt; to integrate with the standard streaming platforms like Kafka, Pulsar and JMS, and also allow CDC streams to be delivered to any HTTP end-point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fast access from data processing and analytics platforms
&lt;/h3&gt;

&lt;p&gt;The Aerospike &lt;a href="https://docs.aerospike.com/connect/data-processing"&gt;Spark&lt;/a&gt; and &lt;a href="https://docs.aerospike.com/connect/data-access"&gt;Presto(Trino)&lt;/a&gt; connectors enable analytics, AI/ML, and other processing on the respective platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organizing for Scale and Speed
&lt;/h2&gt;

&lt;p&gt;A critical part of building large-scale JSON applications is to ensure the JSON objects are organized efficiently in the database for optimal storage and access. &lt;/p&gt;

&lt;p&gt;Documents may be organized in Aerospike in one or more dedicated sets, over one or more namespaces to reflect ingest, access, and removal patterns. Multiple documents may be grouped and stored in one record either in separate bins (columns) or as sub-documents in a container group document. Record keys are constructed as a combination of the collection-id and the group-id to provide fast logical access as well as group-oriented enumeration of documents. For example, the ticker data for a stock can be organized in multiple records that have keys consisting of the stock symbol (collection-id) + date (group-id). Multiple documents can be accessed using either a scan with a filter expression, a query on a secondary index, or both. A filter expression consists of values and properties of the elements in JSON, for example, an array larger than a certain size or a certain value being present in a sub-tree. A secondary index defined on a basic or collection type provides fast value-based queries as described below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Real-Time Events Data
&lt;/h2&gt;

&lt;p&gt;Real-Time event streams can be ingested and stored in Aerospike as JSON documents. To allow access by event-id as well as timestamp, they can be organized as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Record key:(namespace, set, &amp;lt;event_id&amp;gt;)
JSON bin:
{ 
    id: &amp;lt;event-id&amp;gt;,
    timestamp: &amp;lt;ts&amp;gt;,
    … 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event-id based document access is a simple record access by incorporating the event-id in the record key. The exact match or range query on timestamp is possible by defining an integer index on it. &lt;/p&gt;

&lt;p&gt;For greater scalability, multiple event objects can be grouped in a single document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Record key:(namespace, set, &amp;lt;group-id&amp;gt;)
JSON bin:
{
    events: [ 
        {
            id: &amp;lt;group-id, event-num&amp;gt;,
            timestamp: &amp;lt;ts&amp;gt;,
            … 
        }, {
        …
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event-id &lt;code&gt;id&lt;/code&gt; contains the group-id and event-num which is unique within the group. The group-id, which identifies the record, can be a time period identifier such as the day, week, or month in the year covering all events in the record, or another logical identifier for all record events such as the sensor-id. To access an event directly by its event-id, the group-id is extracted from the event-id, the record is accessed by group-id, and then a JSONPath query is issued on the matching &lt;code&gt;id&lt;/code&gt; field. The exact match or range query on timestamp can be performed by creating an integer index on the respective fields in the record.&lt;/p&gt;

&lt;p&gt;Review the blog posts &lt;a href="https://developer.aerospike.com/blog/aerospike-time-series-api"&gt;Aerospike Time Series API&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale-part-2"&gt;Data Modeling for Speed-At-Scale (Part 2)&lt;/a&gt; for further discussion on organizing JSON documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON Support in Aerospike
&lt;/h2&gt;

&lt;p&gt;Aerospike announced support for JSON documents in Database 6.0. The Aerospike Document API provides CRUD operations on a JSON document at points indicated by JSONPath.  Below are some snippets of document APIs. &lt;/p&gt;

&lt;p&gt;More details on the document API can be found in the &lt;a href="https://github.com/aerospike/aerospike-document-lib"&gt;github repo&lt;/a&gt;, &lt;a href="https://developer.aerospike.com/tutorials/java/doc_api"&gt;tutorial&lt;/a&gt; and &lt;a href="https://medium.com/aerospike-developer-blog/aerospike-document-api-jsonpath-queries-bd6260b2d076"&gt;blog post&lt;/a&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Store a JSON file to database
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Initialize the DocumentClient from AerospikeClient
AerospikeClient aerospikeClient = new AerospikeClient(cPolicy, seedHost, port);
AerospikeDocumentClient documentClient = new AerospikeDocumentClient(aerospikeClient);

// Read the json document into a string.
String jsonString = FileUtils.readFileToString(new File(JsonFilePath));

// Convert JSON string to a JsonNode
JsonNode jsonNode = JsonConverters.convertStringToJsonNode(jsonString);

// Add the document to database
documentClient.put(recordKey, documentBinName, jsonNode);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get document elements by JsonPATH
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Read an element by path
Object docObject = documentClient.get(recordKey, documentBinName, "$.path.to.the.element");
Object anotherDocObject = documentClient.get(recordKey, documentBinName, "$.path.to.array[index]");

// Get instances of a field from array elements 
Object docObject = documentClient.get(recordKey, documentBinName, "$.path.to.array[*].field");
// Get instances of a field in the document
Object anotherDocObject = documentClient.get(recordKey, documentBinName, "$...field");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Query JSON documents
&lt;/h3&gt;

&lt;p&gt;JSON documents can be indexed for fast queries. In &lt;a href="https://aerospike.com/blog/query-and-data-distribution/"&gt;Aerospike Database 6.1&lt;/a&gt;+, any JSON element may be indexed to support exact match or range queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client.createIndex(policy,namespace,set,indexName,documentBinName,
                indexType, collectionType, contextPath);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query can be issued using different filters depending on the index type - either a basic type (string or integer) or a collection type (List, MapKeys, MapValues):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Filter filter = Filter.range(documentBinName, fromValue, toValue, contextPath));
Filter filter2 = Filter.contains(documentBinName, collectionType,
                value, contextPath));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Aerospike Database 6.0+, parallel partition-grained secondary index queries are available to boost throughput in large-scale applications. &lt;/p&gt;

&lt;p&gt;Find more details on indexing JSON documents in the blog post &lt;a href="https://developer.aerospike.com/blog/query-json-documents-faster"&gt;Query JSON Documents Faster&lt;/a&gt; and code examples in the tutorial on &lt;a href="https://developer.aerospike.com/tutorials/java/cdt_indexing"&gt;CDT Indexing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Find, run, and modify working examples, and also  run your own code, in the &lt;a href="https://developer.aerospike.com/tutorials/sandbox"&gt;code sandbox&lt;/a&gt; from your browser. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Real-time large-scale JSON applications need reliably fast access to data, high ingest rates, powerful queries, rich document functionality, scalability with no practical limit, always-on operation, and integration with streaming and analytical platforms. They need all this at low cost. The &lt;a href="https://aerospike.com/"&gt;Aerospike Real-time Data Platform&lt;/a&gt; provides all this functionality, making it a good choice for building such applications. The Collection Data Types (CDTs) in Aerospike provide powerful support for modeling, organizing, and querying a large JSON document store. Visit the &lt;a href="https://developer.aerospike.com/tutorials"&gt;tutorials&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/tutorials/sandbox"&gt;code sandbox&lt;/a&gt; on the &lt;a href="https://developer.aerospike.com/"&gt;Developer Hub&lt;/a&gt; to explore the capabilities of the platform, and play with the Document API and query capabilities for JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related Links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/cdt_indexing"&gt;CDT Indexing&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; (documentation) &lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com/"&gt;Aerospike Sandbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com/tutorials/"&gt;Aerospike Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com"&gt;Aerospike Developer Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aerospike.com/"&gt;Aerospike Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aerospike</category>
      <category>json</category>
      <category>document</category>
      <category>realtime</category>
    </item>
    <item>
      <title>Query JSON Documents Faster (and More) with New CDT Indexing</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Tue, 13 Sep 2022 14:58:06 +0000</pubDate>
      <link>https://dev.to/aerospike/query-json-documents-faster-and-more-with-new-cdt-indexing-1kk2</link>
      <guid>https://dev.to/aerospike/query-json-documents-faster-and-more-with-new-cdt-indexing-1kk2</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1JYcNGCM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2Ac6r7hAWc7yVNPttX" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1JYcNGCM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2Ac6r7hAWc7yVNPttX" alt="(Source: Photo by Cameron Ballard on [Unsplash](https://unsplash.com/) )" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Cameron Ballard on &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The  &lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; in Aerospike are List and Map. They offer powerful capabilities to model and access your data for speed-at-scale. A major use of the CDTs is to store and process JSON documents efficiently. In the recent Aerospike Database 6.1 release, secondary index capabilities over the CDTs have been enhanced to make the CDTs even more useful and powerful for JSON documents in addition to other uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Context-Path and Path Specifiers
&lt;/h2&gt;

&lt;p&gt;A CDT element is identified by its &lt;code&gt;context-path&lt;/code&gt;. A CDT element’s context-path is defined as the path from the root to the element. &lt;/p&gt;

&lt;p&gt;A context-path is very similar to &lt;a href="https://goessner.net/articles/JsonPath/"&gt;JSONPath&lt;/a&gt; in a JSON document, but differs from JSONPath in some respects. Like JSONPath, a context-path describes the path from the root or the top level of the CDT to a nested element. &lt;/p&gt;

&lt;p&gt;While a node in a JSONPath is an index in an array or a field in a map (object), each node in a context-path, on the other hand, uniquely identifies an element at that level by one of the following specifiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index (physical position, 0 indexed)&lt;/li&gt;
&lt;li&gt;Key (applicable only to a Map)&lt;/li&gt;
&lt;li&gt;Rank (relative value position, with 0 being the lowest and -1 being the highest)&lt;/li&gt;
&lt;li&gt;Value (the first element with that value)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a context path is a concatenation of specifiers that identify path nodes. For example, consider a Map:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{“k1”: 1, “k2”: 2, “k3”: [11, 12, 13], “k4”: {“k11”: 11, “k22”: 22}}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The context path for the value &lt;code&gt;22&lt;/code&gt; is: &lt;code&gt;By-Key(“k4”), By-Key(“k22”)&lt;/code&gt; or &lt;code&gt;By-Key(“k4”), By-Value(22)&lt;/code&gt;. The JSONPath for it is: &lt;code&gt;$.k4.k22&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Consider another nested object represented as a Map at the top level (level 0): it has a List at level 1, and a Map at level 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object = {  “id1”: [ {“a”: 1, “b”: 2}, {“c”: 3, “d”: 4} ],
            “id2”: [ {“e”: 5, “f”: 6}, {“g”: 7, “h”: 8}] }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A context path to the nested element &lt;code&gt;“c”&lt;/code&gt;, can look like: &lt;code&gt;By-Key(“id1”), By-Index(1), By-Key(“c”)&lt;/code&gt;. &lt;br&gt;
The JSONPath of &lt;code&gt;“c”&lt;/code&gt; in the corresponding JSON document looks very similar: &lt;code&gt;$.id1[1].c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note, however, that &lt;code&gt;“c”&lt;/code&gt; can be reached using other context-paths, such as, &lt;code&gt;By-Index(0), By-Rank(1), By-Value(3)&lt;/code&gt;. There are no alternative contiguous JSONPaths to &lt;code&gt;“c”&lt;/code&gt;. Also, a JSONPath can skip a node and can point to more than one element in a JSON document. For example, &lt;code&gt;$.id1..c&lt;/code&gt; will point to all &lt;code&gt;“c”&lt;/code&gt; nodes below &lt;code&gt;“id1”&lt;/code&gt;. A context-path does not allow an interim node to be skipped and cannot point to more than one element in a CDT. So a similar construct &lt;code&gt;By-Key(“id1”)..By-Key(“c”)&lt;/code&gt; is not supported.&lt;/p&gt;
&lt;h2&gt;
  
  
  New CDT Indexing Capabilities
&lt;/h2&gt;

&lt;p&gt;In a nutshell, in Aerospike Database 6.1 and later, any CDT element can be indexed irrespective of their nesting level. Specifically, there are two main new capabilities to highlight:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Elements in a CDT can now be indexed based on their "index” (meaning physical position of the element in CDT), key, rank, or value. So it is now possible to create a secondary index, say, on the element at rank -1 (the highest value) of a List, so that the equality and range queries for the highest value in the List across records can be efficiently executed. For example, retrieve all users that have a personal best score greater than 100 from their lifetime scores List.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Embedded List or Map can now be indexed, So in a List bin with value  &lt;code&gt;[1, 2, 3, “s1”, “s2”, “s3”, [11, 12 13]]&lt;/code&gt;, the embedded List &lt;code&gt;[11, 12, 13]&lt;/code&gt; can now be indexed. Many complex objects, especially JSON documents, have deep hierarchies. Now, the elements below the top level can be indexed and efficiently queried on. So in a Map bin &lt;code&gt;{“k1”: 1, “k2”: 2, “k3”: [11, 12, 13], “k4”: {“k11”: 11, “k22”: 22}}&lt;/code&gt;, a secondary index can be created on the List &lt;code&gt;“k3”&lt;/code&gt;, the Map &lt;code&gt;“k4”&lt;/code&gt;, as well as all the elements within them. For example, this makes it possible to retrieve all records with a value 31 in the &lt;code&gt;“k3”&lt;/code&gt; list or records with the value of &lt;code&gt;“k11”&lt;/code&gt; in the range 10-20.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following types of secondary indexes can be created on a CDT element. Note, since a given CDT element can hold a value of any type, only values of the specified type are indexed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Non-collection type: Index an element for values of one of the following types. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integer&lt;/li&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;li&gt;Geospatial&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;List collection type: Index List values of an element. All values within a List of one of the following types are indexed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integer List values&lt;/li&gt;
&lt;li&gt;String List values&lt;/li&gt;
&lt;li&gt;Geospatial List values&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Map collection type: Index Map values of an element. All Map keys or Map values of one of the following types are indexed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integer Map keys&lt;/li&gt;
&lt;li&gt;String Map keys&lt;/li&gt;
&lt;li&gt;Integer Map values&lt;/li&gt;
&lt;li&gt;String Map values&lt;/li&gt;
&lt;li&gt;Geospatial Map values&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also new in 6.1 is the ability to index all &lt;code&gt;namespace&lt;/code&gt; records, in addition to the previously supported &lt;code&gt;set&lt;/code&gt; specific indexing. Thus, an index can be specified on a CDT element across the namespace for querying records from the entire namespace.&lt;/p&gt;
&lt;h2&gt;
  
  
  Many Indexes on Same Element
&lt;/h2&gt;

&lt;p&gt;In many cases, it is required to create multiple indexes on the same CDT element.&lt;/p&gt;
&lt;h3&gt;
  
  
  Multiple Types
&lt;/h3&gt;

&lt;p&gt;Multiple indexes of different types are allowed on the same context-path in order to index the respective data type values. &lt;/p&gt;

&lt;p&gt;As CDTs do not conform to a schema, an element can be of any type. A secondary index is defined for values of a specific type, and only considers values of that type. Other type values at that context path will be simply ignored. Thus, an integer index at a Map key or List rank will only index integer values at that path. So in a record with List &lt;code&gt;[1, 2, 3, “s1”, “s2”, “s3”]&lt;/code&gt;, in order to select the record on equality query on &lt;code&gt;“s3”&lt;/code&gt;, a string index must be present on all List string values or a specific string element using index, rank, or value in the List.&lt;/p&gt;

&lt;p&gt;JSON documents are saved as CDTs, but are simpler as they have single type values in a List (numeric or string) and Map keys (string), and therefore will need only one index value type on these collection types. Map values, however, can be mixed (string or numeric).&lt;/p&gt;
&lt;h3&gt;
  
  
  Multiple Paths
&lt;/h3&gt;

&lt;p&gt;The same element in a CDT can be arrived at in multiple ways via different context paths using different specifiers. For example, an element at Map index X may also be the rank Y and key Z. Multiple indexes of the same value type therefore are possible on the same static element. However, they are semantically different.&lt;/p&gt;

&lt;p&gt;Important: As values in a CDT change, the context paths that were pointing to the same element may no longer point to one or the same element. Queries based on indexes defined using different context paths pointing to the same static element in one CDT can yield different results. The application should define the indexes and queries carefully with data and application semantics in mind.&lt;/p&gt;
&lt;h2&gt;
  
  
  Querying Values Across Multiple Elements
&lt;/h2&gt;

&lt;p&gt;In some cases, a List or Map is not readily available for indexing, as the elements may be distributed in multiple places in the CDT. An example is a List of Maps, where we want to find out if a specific key in any Map has a specific value. Such an array (List) of objects (Maps) is a common occurrence in JSON documents, and querying a specific object field for a value may be necessary. For a concrete example, consider the JSON document of Nobel laureates in a year and category :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "year" : "2021",
  "category" : "chemistry",
  "laureates" : [ {
     "id" : "1002",
     "name" : "Benjamin List"
    }, {
     "id" : "1003",
     "name" : "David MacMillan"
  } ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to issue a query “find the Nobel prize(s) by the winner’s name”, the &lt;code&gt;name&lt;/code&gt; field in all objects in the &lt;code&gt;laureates&lt;/code&gt; List need to be indexed across such records. &lt;/p&gt;

&lt;p&gt;While such a collection type is not directly supported for indexing, the following solution can be implemented: Create a separate List of &lt;code&gt;name&lt;/code&gt;s of  &lt;code&gt;laureates&lt;/code&gt; in the record, and index that List. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;laureate_names_in_record: ["Benjamin List", "David MacMillan", ..]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It is then possible to find the Nobel prize(s) by the winner's name. &lt;/p&gt;

&lt;p&gt;For data that does not change, such as the names of Nobel laureates in the above example, this is relatively straightforward. In order to index values in a record that can change, the indexed List must be kept in sync with the changing values. Depending on where the values are in the CDT and how values are updated, it may be possible to update a value and its index List entry together atomically using the multi-op CDT operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Examples
&lt;/h2&gt;

&lt;p&gt;In order to make it clearer, we will describe with examples. You can follow along in the interactive notebook &lt;a href="https://developer.aerospike.com/tutorials/java/cdt_indexing"&gt;CDT Indexing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Examples are shown for two categories of CDT index below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Non-collection index&lt;/li&gt;
&lt;li&gt;Collection index

&lt;ul&gt;
&lt;li&gt;LIST (all values in the List) for a List, or&lt;/li&gt;
&lt;li&gt;Either MAP_KEYS (all keys) or MAP_VALUES(all values) for a Map&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The CDT model is a superset JSON, and therefore the examples described in CDT terminology below are also applicable to JSON documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-collection Index
&lt;/h3&gt;

&lt;p&gt;A non-collection index supports equality queries on integer and string values with &lt;code&gt;equal&lt;/code&gt; filter, and range queries on integer values with the &lt;code&gt;range&lt;/code&gt; filter.&lt;/p&gt;

&lt;h4&gt;
  
  
  Equality queries
&lt;/h4&gt;

&lt;p&gt;Get records with a specific integer or string value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At a specific index or rank position of a List or a Map

&lt;ul&gt;
&lt;li&gt;records with 100/“ABC” at index 0 of a list or a map&lt;/li&gt;
&lt;li&gt;records with 100/”ABC” at rank -1 (highest value) of a list or a map&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;At a specific key position of a Map 

&lt;ul&gt;
&lt;li&gt;records with 100/“ABC” at key XYZ of a map&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Range queries
&lt;/h4&gt;

&lt;p&gt;Range queries are supported on integer values only. &lt;/p&gt;

&lt;p&gt;Get records having an integer value within a range:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At a specific index or rank position of a List or a Map

&lt;ul&gt;
&lt;li&gt;records with value in range 1-100 at index 0 of a list or a map&lt;/li&gt;
&lt;li&gt;records with value in range 1-100 at rank -1 (highest value) of a list or a map&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;At a specific key position of a Map 

&lt;ul&gt;
&lt;li&gt;records with value in range 1-100  at key XYZ of a map&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Collection Index
&lt;/h3&gt;

&lt;p&gt;A collection index supports equality queries on integer and string values with the &lt;code&gt;contains&lt;/code&gt; filter, and range queries on integer values with the &lt;code&gt;range&lt;/code&gt; filter. The collection type is &lt;code&gt;LIST&lt;/code&gt;, &lt;code&gt;MAP_KEYS&lt;/code&gt;, or &lt;code&gt;MAP_VALUES&lt;/code&gt;  in the &lt;code&gt;createIndex&lt;/code&gt; Java API.&lt;/p&gt;

&lt;h4&gt;
  
  
  Equality queries
&lt;/h4&gt;

&lt;p&gt;Get records with a specific integer or string value. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a List

&lt;ul&gt;
&lt;li&gt;records with a list containing 100/“ABC” &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;In a Map’s keys

&lt;ul&gt;
&lt;li&gt;records with a map containing 100/“ABC” as a key&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;In a Map’s values

&lt;ul&gt;
&lt;li&gt;records with a map containing 100/“ABC” as a value&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Range queries
&lt;/h4&gt;

&lt;p&gt;Range queries are supported on integer values only. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a List

&lt;ul&gt;
&lt;li&gt;records with a list containing a value in range 1-100 &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;In a Map’s keys

&lt;ul&gt;
&lt;li&gt;records with a map containing a key in range 1-100&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;In a Map’s values

&lt;ul&gt;
&lt;li&gt;records with a map containing a value in range 1-100 &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;In Aerospike Database 6.1+, any CDT element can be indexed irrespective of their nesting level. Elements in a CDT can also be indexed based on their position, key, rank, or value. A CDT element can have multiple context-paths with different semantics, and therefore the application should carefully determine the correct context-path while defining an index, with data and application semantics in mind.  When values that need to be indexed are not available in one List or Map, consider replicating the values in a separate List for defining an index, and keep the indexed List in sync with the values. View and work on the examples in the notebook &lt;a href="https://developer.aerospike.com/tutorials/java/cdt_indexing"&gt;CDT Indexing&lt;/a&gt; and the &lt;a href="https://developer.aerospike.com/"&gt;Aerospike Sandbox&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related Links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/cdt_indexing"&gt;CDT Indexing&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; (documentation) &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; (interactive tutorials)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com/"&gt;Aerospike Sandbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com/tutorials/"&gt;Aerospike Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com"&gt;Aerospike Developer Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aerospike.com/"&gt;Aerospike Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aerospike</category>
      <category>json</category>
      <category>cdt</category>
      <category>secondaryindex</category>
    </item>
    <item>
      <title>Aerospike Through SQL</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Tue, 16 Aug 2022 15:57:30 +0000</pubDate>
      <link>https://dev.to/aerospike/aerospike-through-sql-1o4d</link>
      <guid>https://dev.to/aerospike/aerospike-through-sql-1o4d</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t3B6qFa8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2A8f9iXuwsmmzqrG_F" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t3B6qFa8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2A8f9iXuwsmmzqrG_F" alt="(Source: Photo by Alex wong on Unsplash [Unsplash](https://unsplash.com/) )" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Alex wong on Unsplash &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;SQL is broadly used as a data access language for analytics. Even if you are an application developer, chances are you have used it or at least are familiar with it. &lt;/p&gt;

&lt;p&gt;Aerospike has broad support for SQL, enabling you to use SQL to access Aerospike data in multiple ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trino
&lt;/h2&gt;

&lt;p&gt;For analytics, you can access Aerospike data on &lt;a href="https://trino.io/"&gt;Trino&lt;/a&gt; with the Aerospike Trino Connector.&lt;/p&gt;

&lt;p&gt;Through Trino, analytics use cases such as ad-hoc SQL queries, reports, and dashboard have access to data in one or more Aerospike clusters, and they can also merge Aerospike data with data from other sources.&lt;/p&gt;

&lt;p&gt;For more details of the Trino Connector, see the blog posts &lt;a href="https://developer.aerospike.com/blog/deploy-aerospike-and-trino-based-analytics-platform-using-docker"&gt;Deploy Aerospike and Trino based analytics platform using Docker&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/blog/aerospike-trino-connector-chapter-two"&gt;Aerospike Trino Connector - Chapter Two&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Starburst is a SQL-based MPP query engine based on Trino that enables you to run Trino on a single machine, a cluster of machines, on-prem or in the cloud. The blog post &lt;a href="https://developer.aerospike.com/blog/analyze-data-with-aerospike-and-starburst-anywhere"&gt;Analyze Data with Aerospike and Starburst Anywhere&lt;/a&gt; describes how to use &lt;a href="https://www.starburst.io"&gt;Starburst Enterprise&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The data browser described in the blog post &lt;a href="https://developer.aerospike.com/blog/aerospike-data-browser"&gt;Aerospike Data Browser&lt;/a&gt; uses Trino with the Trino Connector underneath. &lt;/p&gt;

&lt;h2&gt;
  
  
  Spark
&lt;/h2&gt;

&lt;p&gt;You can use Spark SQL to manipulate Aerospike data on the Spark platform. Aerospike Spark Connector provides parallel access to the Aerospike cluster from Spark.&lt;/p&gt;

&lt;p&gt;Spark SQL merges two abstractions: Replicated Distributed Datasets (RDDs) and relational tables. Find examples of importing and storing Aerospike data to and from RDDs in &lt;a href="https://developer.aerospike.com/tutorials/spark"&gt;these Aerospike Spark tutorials&lt;/a&gt;. You can use Spark SQL to manipulate and process data in RDDs.&lt;/p&gt;

&lt;p&gt;More details on the Spark Connector are available in the blog posts &lt;a href="https://medium.com/aerospike-developer-blog/aerospike-is-a-highly-scalable-key-value-database-offering-best-in-class-performance-5922450aaa78"&gt;Using Aerospike Connect for Spark&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/blog/accelerate-spark-queries-with-predicate-pushdown-using-aerospike"&gt;Accelerate Spark queries with Predicate Pushdown using Aerospike&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  JDBC
&lt;/h2&gt;

&lt;p&gt;Application developers can use simple SQL with JDBC with the community-contributed JDBC Connector. &lt;br&gt;
Please read more details in the blog post &lt;a href="https://developer.aerospike.com/blog/introducing-aerospike-jdbc-driver"&gt;Introducing Aerospike JDBC Driver&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Aerospike API
&lt;/h2&gt;

&lt;p&gt;While the various connectors allow broad SQL access for multiple purposes, the connectors may not be suitable for general applications as they do not provide the full Aerospike API functionality that a general application needs. For example, update capabilities are limited through SQL. &lt;/p&gt;

&lt;p&gt;We recommend that you use the Aerospike API  to access its full functionality and performance. Aerospike, a NoSQL database, does not directly support all SQL features. Inversely, Aerospike has many capabilities that cannot be expressed in SQL. This is to be expected because SQL is designed to provide physical data independence, which means the user need not worry about the physical details of the data such as the data distribution, size, selectivity, indexes, and so forth. The query optimizer deals with these details and selects the best execution plan. The goal of the Aerospike API is to  provide full control to developers for optimal performance of their applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article describes how a developer who is familiar with SQL can quickly implement specific SQL CRUD operations using the Aerospike API. The goal is not to discuss the many mechanisms to control optimal performance (although it points to some of them), but to provide a ramp for a developer who has some knowledge of SQL to map the basic CRUD queries into the Aerospike API. We encourage you to learn about the performance features using the pointers provided.&lt;/p&gt;

&lt;p&gt;While Aerospike supports many languages, we have used the Java client API in our examples as it is most widely used. The functionality is similar across all client libraries, and you can find equivalent functions in each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping SQL to Aerospike
&lt;/h2&gt;

&lt;p&gt;While there is no direct mapping of full SQL to Aerospike API, simple CRUD functionality can be easily mapped to Aerospike API as the underlying data models are similar: Aerospike’s set-record-bin organization matches the SQL’s table-record-column organization (see below). &lt;/p&gt;

&lt;p&gt;We point out differences and unsupported constructs below. They need to be handled through alternative means such as specific features, libraries, and application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Similarities
&lt;/h3&gt;

&lt;p&gt;Aerospike has a record-based data model. An Aerospike Database holds multiple namespaces, which are equivalent to databases in the relational model. A namespace holds records (rows), organized in sets (tables) that are accessed using a unique key that serves as the record ID. A record can contain one or more bins (columns), and a bin can hold a value of different data types. Sets and records do not conform to any schema. The primary index provides fast access to a record by key, which is a unique record identifier, while  secondary indexes defined on a bin are supported for content based access.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL concept&lt;/th&gt;
&lt;th&gt;Aerospike equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database or schema&lt;/td&gt;
&lt;td&gt;Namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table&lt;/td&gt;
&lt;td&gt;Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Record&lt;/td&gt;
&lt;td&gt;Record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column&lt;/td&gt;
&lt;td&gt;Bin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index&lt;/td&gt;
&lt;td&gt;Primary and Secondary indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored Procedures&lt;/td&gt;
&lt;td&gt;User Defined Functions (UDFs)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Differences
&lt;/h3&gt;

&lt;p&gt;Aerospike is a NoSQL database, and its API has many differences from SQL databases.  Following are some key differences: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set: A set is a tag on the record that gets created when the first record is created in the set. A set is schemaless and can hold records holding different bins. &lt;/li&gt;
&lt;li&gt;Record: A record is schemaless, and can hold any combination of bins.&lt;/li&gt;
&lt;li&gt;Bin: A bin is typeless, and can hold a value of any type.
&lt;/li&gt;
&lt;li&gt;Index: Integrity constraints, such as uniqueness, cannot be specified on an index. &lt;/li&gt;
&lt;li&gt;Transactions: All single record requests are transactional. The transaction boundary does not span multiple records. For a detailed discussion, see the blog post &lt;a href="https://aerospike.com/blog/developers-understanding-aerospike-transactions/"&gt;Developers: Understanding Aerospike Transactions&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Constructs Not Directly Supported
&lt;/h3&gt;

&lt;p&gt;Due to the differences in its data and execution models, Aerospike API does not directly support  the following SQL constructs, however, they can be implemented using data modeling, alternative features, and application code. We will discuss them later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Join&lt;/li&gt;
&lt;li&gt;Aggregations (max, min, top, average, sum, etc.)&lt;/li&gt;
&lt;li&gt;Order By, Distinct, Union&lt;/li&gt;
&lt;li&gt;Limit&lt;/li&gt;
&lt;li&gt;Constraints: NULL, Foreign Key, Default&lt;/li&gt;
&lt;li&gt;Built-in functions&lt;/li&gt;
&lt;li&gt;View&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;For the purpose of our discussion, SQL queries can be organized in these categories: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SELECT or read operations, &lt;/li&gt;
&lt;li&gt;CREATE, UPDATE, DELETE or write operations,&lt;/li&gt;
&lt;li&gt;Metadata operations, and&lt;/li&gt;
&lt;li&gt;Other functionality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use these &lt;a href="https://developer.aerospike.com/tutorials/java/sql-operations"&gt;interactive tutorials&lt;/a&gt; to work along with this text.  &lt;/p&gt;

&lt;h3&gt;
  
  
  A Word on Key, Metadata, Policy, and API Variants
&lt;/h3&gt;

&lt;p&gt;Before we dive in, it is useful to know record key, record metadata, operation policy, and API variants. &lt;/p&gt;

&lt;h4&gt;
  
  
  Record Key
&lt;/h4&gt;

&lt;p&gt;Each record is uniquely identified by a key or id, consisting of a triple: (namespace, set, user-key) where user-key is a user-specified id that is unique within the set. The key (also called the digest) is returned in all read APIs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Record Metadata
&lt;/h4&gt;

&lt;p&gt;Each record has metadata associated with it: generation (or version) and expiration time (or time-to-live in seconds). This metadata is returned in all read operations. It is possible to retrieve only the metadata without the record's bins through the "getHeader" operation explained below.&lt;/p&gt;

&lt;h4&gt;
  
  
  Policy
&lt;/h4&gt;

&lt;p&gt;Aerospike API calls take a policy parameter which includes many details of the how and what of the request. For example, timeout, retries, filter expression, and additional write semantics are specified in the policy object. We will specify  significant policy info that is relevant to the operation semantics in each operation below.&lt;/p&gt;

&lt;h4&gt;
  
  
  API Variants
&lt;/h4&gt;

&lt;p&gt;Aerospike API is designed for control and simplicity. As such, a read and write operation that has one form in SQL has multiple variations in Aerospike API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By number of records involved: Single record, batch, and query&lt;/li&gt;
&lt;li&gt;By the processing mode: Sync, async, and background&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the following examples, only the synchronous APIs are shown when available, but you can easily discover the asynchronous variants in the documentation. &lt;/p&gt;

&lt;h2&gt;
  
  
  SQL SELECT and Equivalent Read Operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Single-Record Read Operations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Get
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL Query&lt;/th&gt;
&lt;th&gt;Equivalent Aerospike API (Java)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM namespace.set WHERE id = key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record Client::get(Policy policy, Key key)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT bins FROM namespace.set WHERE id = key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record Client::get(Policy policy, Key key, String... binNames)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Existence​
&lt;/h4&gt;

&lt;p&gt;There is a variant of single record retrieval to check a record's existence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT EXISTS(SELECT * FROM namespace.set WHERE id = key)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean Client::exists(Policy policy, Key key)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Metadata​
&lt;/h4&gt;

&lt;p&gt;It is possible to only obtain a record's header info or metadata, consisting of generation (or version) and expiration (time-to-live in seconds).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT generation, expiration FROM namespace.set WHERE id = key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record Client::getHeader(Policy policy, Key key)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Batch Read Operations
&lt;/h3&gt;

&lt;p&gt;A batch request operates on a list of records identified by the keys provided. It  works similar to a single record retrieval, except multiple records are returned. &lt;/p&gt;

&lt;p&gt;Batch requests are critical for high performance applications as they eliminate multiple client-server round trips, one for each record.&lt;/p&gt;

&lt;h4&gt;
  
  
  Read
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM namespace.set WHERE id IN key-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record[] Client::get(BatchPolicy policy, Key[] keys)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT bins FROM namespace.set WHERE id in key-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record[] Client::get(BatchPolicy policy, Key[] keys, String... binNames)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Existence​
&lt;/h4&gt;

&lt;p&gt;There is a variant of batch retrieval to check record existence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT id, EXISTS(SELECT * FROM namespace.set WHERE id = key) WHERE key IN key-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean[] Client::exists(Policy policy, Key[] keys)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Metadata​
&lt;/h4&gt;

&lt;p&gt;It is possible to obtain header info or metadata consisting of generation (or version) and expiration time (time-to-live in seconds) for a specified set of records.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT generation, expiration FROM namespace.set WHERE id IN key-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record[] Client::getHeader(Policy policy, Key[] keys)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Composite Batch Read
&lt;/h4&gt;

&lt;p&gt;A more general form of batch reads is also available that provides a union of simple batch results with different namespace, set, and bin specification. The &lt;code&gt;records&lt;/code&gt; argument takes the input record keys and populates record details on return.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(SELECT bins1 FROM namespace1.set1 WHERE id IN key-list1)&lt;/code&gt; &lt;br&gt; &lt;code&gt;UNION&lt;/code&gt; &lt;br&gt; &lt;code&gt;(SELECT bins2  FROM namespace2.set2  WHERE id IN key-list2)&lt;/code&gt; &lt;br&gt; &lt;code&gt;UNION ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::get(BatchPolicy policy, List&amp;lt;BatchRead&amp;gt; records)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Predicate-Based Read Operations
&lt;/h2&gt;

&lt;p&gt;In predicate-based read operations (aka queries), records matching a general predicate or condition are retrieved. In SQL, the predicate is specified in the WHERE clause.&lt;br&gt;
Aerospike provides two ways of performing an SQL query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using a secondary index based predicate, which can optionally be ANDed with an expression filter&lt;/li&gt;
&lt;li&gt;Using a scan (which uses the primary “key” index), which can optionally be ANDed with an expression filter&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Secondary Index Query​
&lt;/h3&gt;

&lt;p&gt;While a query in SQL doesn’t require an index to exist, the query API in Aerospike requires that the corresponding secondary index exists. &lt;/p&gt;

&lt;p&gt;The namespace, set, and secondary index based predicate is specified in the &lt;code&gt;statement&lt;/code&gt; argument. The expression filter is optionally specified in the &lt;code&gt;policy&lt;/code&gt; argument for additional conditions to be ANDed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT bins FROM namespace.set WHERE condition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record[] Client::query(QueryPolicy policy, Statement statement)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Scan
&lt;/h3&gt;

&lt;p&gt;The scan operation takes a &lt;code&gt;callback&lt;/code&gt; object which is called for every record in the result (within the scope of the call which remains blocked until the operation completes).&lt;br&gt;
The expression filter is optionally specified in the &lt;code&gt;policy&lt;/code&gt; argument.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT bins FROM namespace.set WHERE condition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::scanAll(ScanPolicy policy, String namespace, String setName, ScanCallback callback, String... binNames)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  SQL CREATE, UPDATE, DELETE and Equivalent Write Operations
&lt;/h2&gt;

&lt;p&gt;Aerospike combines Create and Update in a single write operation. The following &lt;code&gt;record-exists-action&lt;/code&gt; options specified in the write-policy define the operation semantics if the record already exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create-only: Create if record doesn't exist, fail otherwise.&lt;/li&gt;
&lt;li&gt;update: Create if record doesn't exist, update otherwise.&lt;/li&gt;
&lt;li&gt;update-only: Update if record exists, fail otherwise.&lt;/li&gt;
&lt;li&gt;replace: Create if record doesn't exist, replace otherwise.&lt;/li&gt;
&lt;li&gt;replace-only: Replace if record exists, fail otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SQL INSERT maps to &lt;code&gt;create-only&lt;/code&gt; and SQL UPDATE maps to &lt;code&gt;update-only&lt;/code&gt; options. SQL does not have a way to specify other options, such as replace, which removes an existing record.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-Record Write Operations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  INSERT and UPDATE
&lt;/h4&gt;

&lt;p&gt;The put operation handles Create (Insert) and Update. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;INSERT INTO namespace.set VALUES (id=key, bin=value, ...)&lt;/code&gt; &lt;br&gt; + &lt;br&gt; &lt;code&gt;UPDATE namespace.set SET (bin=value, ...) WHERE id=key&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::put(WritePolicy policy, Key key, Bin... bins)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Type-Specific Write Operations
&lt;/h4&gt;

&lt;p&gt;Aerospike allows type-specific update operations. For integer and string types, they include the following. The &lt;code&gt;bins&lt;/code&gt; argument holds multiple bin objects, each with the bin name and the operand value.  &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UPDATE namespace.set SET (bin = bin + intval) WHERE id=key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::add(WritePolicy policy, Key key, Bin... bins)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UPDATE namespace.set SET (bin = bin + strval) WHERE id=key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::append(WritePolicy policy, Key key, Bin... bins)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UPDATE namespace.set SET (bin = strval + bin) WHERE id=key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::prepend( WritePolicy policy, Key key, Bin... bins)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other type specific operations including on Collection Data Types (CDTs), are described in the &lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;documentation&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/tutorials/java/cdt"&gt;tutorials&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  DELETE
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE FROM namespace.set WHERE id=key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::delete(WritePolicy policy, Key key)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Batch Write Operations
&lt;/h3&gt;

&lt;p&gt;A batch write operates on multiple records specifically identified with a list of keys. There is a batch API for insertion, update, and deletion of multiple records.&lt;br&gt;
Two forms of batch writes are shown below. Other forms including one with a UDF (described below) and key-specific operations are described in the blog post &lt;a href="https://developer.aerospike.com/blog/batch-operations-in-aerospike"&gt;Batch Operations&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The argument ops is a list of operations to be performed in the specified sequence on each record, and can include read as well as write operations. The argument &lt;code&gt;batchPolicy&lt;/code&gt; contains the specifics of  how the batch is processed, whereas the arguments &lt;code&gt;writePolicy&lt;/code&gt; and &lt;code&gt;deletePolicy&lt;/code&gt; have the specifics of how the respective individual record operation is performed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;UPDATE namespace.set SET (bin1=fn_1(bin_1), ...) WHERE id in key-list&lt;/code&gt; &lt;br&gt; + &lt;br&gt; &lt;code&gt;SELECT fn_n(bin_n), ,,, FROM namespace.set WHERE id in key-list&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BatchResults operate(BatchPolicy batchPolicy, BatchWritePolicy writePolicy Key[] keys, Operation... ops)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE FROM namespace.set WHERE id in key-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BatchResults delete BatchPolicy batchPolicy, BatchDeletePolicy deletePolicy, Key[] keys)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Predicate-Based Write Operations
&lt;/h3&gt;

&lt;p&gt;Predicate-based updates and deletes are possible by specifying the WHERE condition using the secondary index predicate (specified in a statement object) and expression filter (specified in the write policy) as explained earlier.&lt;/p&gt;

&lt;p&gt;Predicate-based updates and deletes can involve a large number of records, and therefore are processed in background execution mode with the execute API. Sync and callback async modes are not available. Two forms of execute are possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using a list of bin updates and deletes: A multi-op request provides a list of bin operations. Multi-op requests are further described below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since &lt;code&gt;execute&lt;/code&gt; performs in a background mode with no returned results, the operation list in the &lt;code&gt;statement&lt;/code&gt; object cannot have a read operation, only updates.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;UPDATE namespace.set SET (bin=value, ...) WHERE condition&lt;/code&gt; &lt;br&gt; + &lt;br&gt; &lt;code&gt;DELETE FROM namespace.set WHERE condition&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Client::execute(WritePolicy policy, Statement statement)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;User Defined Functions (UDFs): UDFs are equivalent to stored procedures, and are described further below. Record-oriented UDFs implement arbitrary logic in a Lua function that is registered with the server and invoked through an API call. &lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;UPDATE namespace.set SET (bin1=fn1(args), ...) WHERE condition&lt;/code&gt; &lt;br&gt; + &lt;br&gt; &lt;code&gt;DELETE FROM namespace.set WHERE condition&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ExecuteTask Client::execute(WritePolicy policy, Statement statement, String packageName, String functionName, Value... functionArgs)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  SQL Stored Procedures and Aerospike User Defined Functions (UDFs)
&lt;/h2&gt;

&lt;p&gt;User Defined Functions (UDFs) are equivalent to stored procedures in SQL systems. A custom User Defined Function (UDF) is written in Lua, registered on the server, and invoked for a specified record(s). You can find further details in the documentation on &lt;a href="https://docs.aerospike.com/server/architecture/udf"&gt;User Defined Functions (UDFs)&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;In the following example, the UDF is specified using the arguments &lt;code&gt;packageName&lt;/code&gt; and &lt;code&gt;functionName&lt;/code&gt;, and supplied a list of arguments it expects in &lt;code&gt;functionArgs&lt;/code&gt;. The API returns a generic &lt;code&gt;Object&lt;/code&gt; which can be anything like a single value or a map of key-value pairs. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXEC StoredProcedure @arg1 = val1, @arg2 = val2, …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object Client::execute(WritePolicy policy, Key key, String packageName, String functionName, Value... functionArgs)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A UDF can have arbitrary logic combining CRUD operations. &lt;/p&gt;

&lt;p&gt;In Aerospike, aggregation functions such as MIN, MAX, AVERAGE, SUM, etc, over multiple records are implemented with Stream UDFs. This article does not cover the specifics of Stream UDFs; please refer to the tutorials on &lt;a href="https://developer.aerospike.com/tutorials/java/sql_aggregates_1"&gt;SQL: Aggregates&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Op Requests
&lt;/h2&gt;

&lt;p&gt;Multiple single bin read and write operations are possible through the &lt;code&gt;operate&lt;/code&gt; API. It differs from the dedicated “single-op” requests, which allow just one operation. The operations in the argument &lt;code&gt;operations&lt;/code&gt; are executed atomically and in the order specified, &lt;/p&gt;

&lt;p&gt;Unlike in SQL, read and write operations can be combined in the same request (for single-record and batch requests) as illustrated below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SELECT fn1(bin1), …FROM namespace.set WHERE id=key&lt;/code&gt; &lt;br&gt; + &lt;br&gt; &lt;code&gt;UPDATE namespace.set SET (bin1=fn_n(bin_n), ...) WHERE id=key&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Record Client::operate( WritePolicy policy, Key key, Operation... operations)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Multi-op operate APIs are available for a single record, batch, and query operations. See &lt;a href="https://developer.aerospike.com/tutorials/java/java-intro_to_transactions"&gt;this tutorial&lt;/a&gt; that illustrates multi-ops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata Operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Namespace Operations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  CREATE Namespace​
&lt;/h4&gt;

&lt;p&gt;There is no API to create a namespace. A namespace is added through the config and requires a server restart.&lt;/p&gt;

&lt;h4&gt;
  
  
  TRUNCATE Namespace​
&lt;/h4&gt;

&lt;p&gt;The truncate API removes all records in a set or the entire namespace. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TRUNCATE namespace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::truncate(policy, namespace, set=null, beforeLastUpdate=null)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  DELETE Namespace​
&lt;/h4&gt;

&lt;p&gt;There is no API to delete a namespace. A namespace has one or more dedicated storage devices, and they must be wiped clean to delete the namespace. &lt;/p&gt;

&lt;h3&gt;
  
  
  Set Operations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  CREATE Set​
&lt;/h4&gt;

&lt;p&gt;There is no explicit operation to create a set. A set is created when the first record is inserted in the set.&lt;/p&gt;

&lt;h4&gt;
  
  
  ALTER Set​
&lt;/h4&gt;

&lt;p&gt;A set is schemaless, and can hold records that have different schemas or bins. A bin has no type associated with it, and can hold values of any type. Therefore ALTER operation on a set to modify its schema is not needed.&lt;/p&gt;

&lt;h4&gt;
  
  
  TRUNCATE Set​
&lt;/h4&gt;

&lt;p&gt;All records in a set can be truncated using the truncate API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TRUNCATE namespace.set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void Client::truncate(policy, namespace, set, beforeLastUpdate=null)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  DROP Set​
&lt;/h4&gt;

&lt;p&gt;There is no notion of deleting a set as a set is just a name that a record is tagged with. The namespace must be deleted to remove the set name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Index Operations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  CREATE Index​
&lt;/h4&gt;

&lt;p&gt;An index is created on a bin for a specific value type. Integer, string, and GeoJSON types are currently supported for indexing. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CREATE Index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;createIndex(Policy policy, String namespace, String setName, String indexName, String binName,IndexType indexType)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  DROP Index​
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DROP Index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dropIndex(Policy policy, String namespace, String setName, String indexName)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  UDF Operations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  CREATE UDF
&lt;/h4&gt;

&lt;p&gt;The arguments &lt;code&gt;clientPath&lt;/code&gt; and &lt;code&gt;serverPath&lt;/code&gt; below define the path to UDFfile  on the client and server respectively.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Aerospike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CREATE StoredProcedure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Client::register(Policy policy, String clientPath, String serverPath, Language.LUA)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Other SQL Capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Join
&lt;/h3&gt;

&lt;p&gt;Most NoSQL databases do not have the Join operation as it is slow and complex.  You can avoid Joins by storing the joined objects in aggregate form. Alternatively, the join can be performed in the application by retrieving the referenced object.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limit
&lt;/h3&gt;

&lt;p&gt;The policy parameter &lt;code&gt;max-records&lt;/code&gt; can be specified as a hint. Fewer objects may be returned as the limit gets divided among participating nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Order By, Top, Union, Distinct
&lt;/h3&gt;

&lt;p&gt;List and Expressions can be used to implement these operations. Alternatively, they can be performed in the application. &lt;/p&gt;

&lt;h3&gt;
  
  
  Aggregations
&lt;/h3&gt;

&lt;p&gt;Aggregations involving Group-By, Having, and Aggregate Functions (such as Max, Min, Top, Average, Sum) can be implemented using Stream UDFs as shown in the tutorials &lt;a href="https://developer.aerospike.com/tutorials/java/sql_aggregates_1"&gt;SQL Aggregates - Part 1&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/tutorials/java/sql_aggregates_2"&gt;Part 2&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Constraints
&lt;/h3&gt;

&lt;p&gt;Integrity constraints such as NULL, Foreign Key, Default should be handled in the application logic. The uniqueness constraint can be enforced in a List or Map.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-In Functions
&lt;/h3&gt;

&lt;p&gt;Many built-in functions like UPPER, TRIM, can be implemented with Expressions or UDFs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Beyond SQL with Aerospike
&lt;/h2&gt;

&lt;p&gt;​​In order to get the most out of Aerospike for speed-at-scale, thinking beyond SQL is necessary. &lt;br&gt;
The process starts with modeling your data for performance, scale, and other needs of the application. Please review the series &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Data Modeling for Speed At Scale&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Learn about and use the various performance features that the Aerospike API provides through the &lt;a href="https://docs.aerospike.com/"&gt;documentation&lt;/a&gt; and  &lt;a href="https://developer.aerospike.com/tutorials/"&gt;tutorials&lt;/a&gt;. Examples of such features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collection Data Types (CDTs) &lt;/li&gt;
&lt;li&gt;Multi-op requests&lt;/li&gt;
&lt;li&gt;Batch requests&lt;/li&gt;
&lt;li&gt;Expressions&lt;/li&gt;
&lt;li&gt;Secondary indexes&lt;/li&gt;
&lt;li&gt;Set indexes&lt;/li&gt;
&lt;li&gt;Complex Data Types - Binary, HLL, GeoJSON&lt;/li&gt;
&lt;li&gt;User Defined Functions (UDFs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You can use SQL to access Aerospike data through the Trino, Spark, and JDBC Connectors. While the connectors work quite well for the environment and intent they are built for, they do not provide the full Aerospike API functionality that the application may need. Therefore, use of the Aerospike API is recommended for full functionality and performance. The Aerospike API is designed with the goal of enabling developers of high performance applications who need to control performance specific details to make better decisions. &lt;/p&gt;

&lt;p&gt;The article describes how a developer who is familiar with SQL can quickly implement specific SQL CRUD operations using the Aerospike API. Coming from a SQL background, it is important to remember that through NoSQL data modeling one should be able to avoid certain SQL features entirely such as the Join to maximize the benefit of using Aerospike for performance and scale. With the introduction provided in this article, you should be able to take the next step to learn the mechanisms in Aerospike API to optimize your application’s performance and scale. &lt;/p&gt;

&lt;h3&gt;
  
  
  Related Links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/sql-operations"&gt;SQL Operations&lt;/a&gt; (interactive tutorials)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/"&gt;Aerospike Documentation&lt;/a&gt; (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/"&gt;Aerospike Tutorials&lt;/a&gt; (interactive tutorials)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com"&gt;Aerospike Developer Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aerospike.com/blog/developers-understanding-aerospike-transactions/"&gt;Developers: Understanding Aerospike Transactions&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; (documentation) &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; (interactive tutorials)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/batch-operations-in-aerospike"&gt;Batch Operations&lt;/a&gt;  (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/java-intro_to_transactions"&gt;Introduction to Transactions&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/architecture/udf"&gt;User Defined Functions (UDFs)&lt;/a&gt; (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Data Modeling for Speed At Scale&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/deploy-aerospike-and-trino-based-analytics-platform-using-docker"&gt;Deploy Aerospike and Trino based analytics platform using Docker&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/aerospike-trino-connector-chapter-two"&gt;Aerospike Trino Connector - Chapter Two&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/analyze-data-with-aerospike-and-starburst-anywhere"&gt;Analyze Data with Aerospike and Starburst Anywhere&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/aerospike-data-browser"&gt;Aerospike Data Browser&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/spark"&gt;Aerospike Spark Tutorials&lt;/a&gt; (interactive tutorials)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/aerospike-developer-blog/aerospike-is-a-highly-scalable-key-value-database-offering-best-in-class-performance-5922450aaa78"&gt;Using Aerospike Connect for Spark&lt;/a&gt; (blog post) &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/accelerate-spark-queries-with-predicate-pushdown-using-aerospike"&gt;Accelerate Spark queries with Predicate Pushdown using Aerospike&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/introducing-aerospike-jdbc-driver"&gt;Introducing Aerospike JDBC Driver&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aerospike</category>
      <category>sql</category>
      <category>api</category>
      <category>crud</category>
    </item>
    <item>
      <title>A Quick Orientation to Aerospike API</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Wed, 20 Jul 2022 16:58:09 +0000</pubDate>
      <link>https://dev.to/aerospike/a-quick-orientation-to-aerospike-api-5g4p</link>
      <guid>https://dev.to/aerospike/a-quick-orientation-to-aerospike-api-5g4p</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--y_ynL0pk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2ArQSAKcQxLfgY2G6-" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--y_ynL0pk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2ArQSAKcQxLfgY2G6-" alt="(Source: Photo by Jametlene Reskp on [Unsplash](https://unsplash.com/) )" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Jametlene Reskp on &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Aerospike Database and the client API provide a rich set of capabilities that have evolved over more than a decade through an increasing number of mission critical deployments. This post provides a high level view of the Aerospike architecture and API to give developers a broader understanding of its architecture and capabilities, and help them become more productive and effective. This post also points to resources for further exploration of specific areas.&lt;/p&gt;

&lt;p&gt;The post is organized in the following sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core Concepts: Describes the core architecture and data distribution concepts.&lt;/li&gt;
&lt;li&gt;Functional Elements: Describes major elements of functionality in the API.&lt;/li&gt;
&lt;li&gt;Key Specifics: Describes a few things that are useful to know up front.&lt;/li&gt;
&lt;li&gt;Performance Features: Summarizes the common performance enhancing features.&lt;/li&gt;
&lt;li&gt;Useful Libraries: Mentions libraries you should be aware of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A caveat: Aerospike has client libraries for many languages. Not every aspect described here may apply to all client libraries precisely. While the discussion is broadly applicable to all client libraries, some details may be specific to the Java client library as it is most widely used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Concepts
&lt;/h2&gt;

&lt;p&gt;The key architecture concepts to understand include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data organization in a cluster, &lt;/li&gt;
&lt;li&gt;workings of the client library, &lt;/li&gt;
&lt;li&gt;transaction support and replica consistency, &lt;/li&gt;
&lt;li&gt;server-side execution of complex data type operations, &lt;/li&gt;
&lt;li&gt;various processing modes, and &lt;/li&gt;
&lt;li&gt;primary and secondary index queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data Organization in Cluster
&lt;/h3&gt;

&lt;p&gt;Aerospike is a distributed record-oriented database with support for the document-oriented data model. An Aerospike Database holds multiple &lt;code&gt;namespaces&lt;/code&gt;, which are equivalent to databases in the relational model. A namespace holds &lt;code&gt;records&lt;/code&gt; (rows), organized in &lt;code&gt;sets&lt;/code&gt; (tables) and are accessed using a unique &lt;code&gt;key&lt;/code&gt; that serves as the record id. A record can contain one or more &lt;code&gt;bins&lt;/code&gt; (columns), and a bin can hold a value of different data types. Aerospike supports type-specific operations on Integer, String, List, Map, Geospatial, HyperLogLog, and Blob types.  Sets and records do not conform to any schema. The &lt;code&gt;primary index&lt;/code&gt; provides fast access to a record by key, and &lt;code&gt;secondary indexes&lt;/code&gt; are supported for predicate based access.&lt;/p&gt;

&lt;p&gt;Records are hashed by key across 4096 data &lt;code&gt;partitions&lt;/code&gt; which are uniformly distributed across &lt;code&gt;cluster&lt;/code&gt; nodes. Each data partition is replicated with a &lt;code&gt;Replication Factor&lt;/code&gt; (RF) number of copies for fault tolerance.&lt;/p&gt;

&lt;p&gt;Please find more details in the &lt;a href="https://docs.aerospike.com/server/architecture/overview"&gt;architecture overview section of the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smart Client
&lt;/h3&gt;

&lt;p&gt;Aerospike has client libraries or &lt;code&gt;clients&lt;/code&gt; that implement the API in multiple languages including Java, C#, Python, Go, REST, Node.js, C, Ruby, and more. A client library simplifies application development by taking care of many complex aspects. It has the smarts to actively track and adapt to the latest cluster state and data distribution, almost working as an extension of the cluster. As such, it is referred to as the Smart Client. The Smart Client implements a common wire protocol for server interactions, directly connects to all nodes in the cluster, determines the specific server nodes for a data request, sends the request to them, and coordinates a response back to the application. It also handles timeouts, automatic retries, connection pooling, request throttling, replica selection, among other things.   &lt;/p&gt;

&lt;p&gt;Please refer to the &lt;a href="https://docs.aerospike.com/server/architecture/clients"&gt;Smart Client section&lt;/a&gt; and the &lt;a href="https://developer.aerospike.com/client"&gt;supported clients&lt;/a&gt; in the documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data-Type Server
&lt;/h3&gt;

&lt;p&gt;The API supports many complex data types including List, Map, Blob (or Binary), GeoJSON, and HyperLogLog (HLL). Since many complex data elements can get large in size, Aerospike eliminates expensive client-server data transfer by executing the operations entirely on the server. &lt;/p&gt;

&lt;p&gt;Developers can leverage the following features to minimize client-server data transfers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex operations supported in the API. To minimize data transfer, the API provides server-side execution of operations, finer control of the returned data, as well as the ability to customize multi-element processing logic for Collection Data Type (CDT) operations. Please refer to the &lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;CDT documentation&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/tutorials/java/cdt"&gt;tutorials&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Expressions allow flexible logic to be computed on the server for filtering, retrieval, and updates. Please see the tutorial on &lt;a href="https://developer.aerospike.com/tutorials/java/expressions"&gt;Expressions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Lua UDFs: Allow general logic to be computed on the server for retrieval and updates (described further below).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Transactions and Consistency
&lt;/h3&gt;

&lt;p&gt;Aerospike guarantees all single-record requests to be atomic, that is, they either succeed or fail. Multi-op requests (described below) on a single record are transactional. However, multi-record batch and query operations (also described below) are not transactional and there is no rollback available for partially successful requests. The transactional boundary assured is for individual record operations within a multi-record request. &lt;/p&gt;

&lt;p&gt;Aerospike replicates data for resiliency and performance in multiple replicas. Replicas are kept in sync by applying a synchronous write operation to all replicas. Read replicas are automatically selected based on the consistency requirements that are specified in the policy. &lt;/p&gt;

&lt;p&gt;Please view the blog post &lt;a href="https://aerospike.com/blog/developers-understanding-aerospike-transactions/"&gt;Developers: Understanding Aerospike Transactions&lt;/a&gt; for details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Processing Modes
&lt;/h3&gt;

&lt;p&gt;Aerospike supports multiple modes to process a request, each with its trade-offs, and the application should choose the appropriate mode. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synchronous: In the synchronous mode, the client waits for all responses to arrive from the server nodes before handing the cumulative response to the application. The application can spawn multiple threads and process multiple synchronous requests in parallel, one per thread at a time. From the application’s standpoint, synchronous requests can be easier to implement, but may not offer best resource utilization.&lt;/li&gt;
&lt;li&gt;Asynchronous: In the asynchronous mode, the application can submit a request without waiting for the results. The results are processed when they are available in a different &lt;code&gt;callback&lt;/code&gt; thread. Depending on the application’s choice, the client library can call back once for each record response, or just once with all responses. The asynchronous mode has superior efficiency and performance, but may be more complex to implement. Check out the tutorial on &lt;a href="https://developer.aerospike.com/tutorials/java/async_ops"&gt;Asynchronous Operations&lt;/a&gt; for details. &lt;/li&gt;
&lt;li&gt;Background: Common updates to a large number of records that are selected by a query can be performed in a background mode, where no results are returned from the server. The application can query whether a background request is in progress, completed successfully, or failed; and if necessary must determine the details of any failure separately. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Primary and Secondary Index Queries
&lt;/h3&gt;

&lt;p&gt;Queries use either the Primary Index or a Secondary Index. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A primary-index query is simply a scan, performed on either a set or the entire namespace. A set index can optionally be created to boost performance, and is automatically used in the scan of that set. If a set index is not available, the entire namespace is scanned to determine the set records. A namespace scan uses the primary index.&lt;/li&gt;
&lt;li&gt;A secondary-index query returns records meeting a predicate or condition supported by the corresponding secondary index: equality and range for Integer, equality for String, contains and is-contained-by for GeoJSON data type. An appropriate secondary index must have been created in order to execute a secondary-index query. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;code&gt;filter expressions&lt;/code&gt; (described below) provide a powerful mechanism to select records for operations, and are broadly used with queries.&lt;/p&gt;

&lt;p&gt;You can view query examples in the tutorial on &lt;a href="https://developer.aerospike.com/tutorials/java/sql_select"&gt;Implementing SQL: Select&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functional Elements
&lt;/h2&gt;

&lt;p&gt;This section describes major functional elements including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single- and multi-record function variants, &lt;/li&gt;
&lt;li&gt;multi-op requests, &lt;/li&gt;
&lt;li&gt;Collection Data Types (CDTs), &lt;/li&gt;
&lt;li&gt;expressions, and &lt;/li&gt;
&lt;li&gt;User Defined Functions (UDFs). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note, these categories are not exclusive. For example, a multi-op request can involve CDTs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single and Multi-Record Function Variants
&lt;/h3&gt;

&lt;p&gt;In addition to the execution modes, there are function variants based on the number of records involved.&lt;/p&gt;

&lt;p&gt;Aerospike defines distinct API functions for single-record, batch, and query operations for simplicity instead of having a common API function with a generic operand that can take a variable number of records or a query predicate. Thus, there are separate functions for single-record and batch variations of operations like exists, get, put, append, and operate as well as their sync and async invocations. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single-record request operates on a specified key or record. &lt;/li&gt;
&lt;li&gt;Batch operations operate over multiple keys or records, where each key is specified. Please see the blog post on &lt;a href="https://developer.aerospike.com/blog/batch-operations-in-aerospike"&gt;Batch Operations&lt;/a&gt; for details.&lt;/li&gt;
&lt;li&gt;A query operates on multiple records that are identified by: 

&lt;ul&gt;
&lt;li&gt;a primary-index query (a scan of a set or the entire namespace) or a secondary-index query (a condition or predicate that uses an existing secondary index), &lt;/li&gt;
&lt;li&gt;a filter expression (which does not use or require a secondary index but is calculated on each record), &lt;/li&gt;
&lt;li&gt;both, or &lt;/li&gt;
&lt;li&gt;neither (in which case all records in the specified namespace and set are selected for the operation). &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Query requests can be used for retrieval or update. The latter are executed in the background mode as mentioned earlier. &lt;/p&gt;

&lt;p&gt;You can find examples of the function variants in the tutorials on &lt;a href="https://developer.aerospike.com/tutorials/java/sql-operations"&gt;SQL Operations&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Op Requests
&lt;/h3&gt;

&lt;p&gt;Aerospike allows multiple bin operations to be performed on a record with a single multi-op &lt;code&gt;Operate&lt;/code&gt; request that takes a list of &lt;code&gt;Operation&lt;/code&gt; objects. The operations are performed in the sequence specified. The results are returned by the bins involved - either their final state or the individual operation results in the specified order. CRUD functions including CDT operations can be specified as Operations in the Operate request. Read/Write Expressions that allow server-side bin computations can also be used. Operate can be used for single-record, batch, and query requests in sync, async, and background (query updates only) modes. &lt;/p&gt;

&lt;p&gt;You can find examples of multi-op requests in the documentation and tutorials, such as this tutorial on &lt;a href="https://developer.aerospike.com/tutorials/java/java-intro_to_transactions"&gt;Introduction to Transactions&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collection Data Types (CDTs)
&lt;/h3&gt;

&lt;p&gt;Applications commonly use Collection Data Types (CDTs), namely List and Map, to store objects. CDTs are useful to model data for efficient storage, access, and transactional updates. Please see the blog post on &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale-part-2"&gt;Data Modeling for Speed At Scale (Part 2)&lt;/a&gt; for details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expressions
&lt;/h3&gt;

&lt;p&gt;Aerospike Expressions are defined using bins and metadata, API functions, and various operators, and are evaluated on the server to filter records (&lt;code&gt;filter expressions&lt;/code&gt;), return computed values (&lt;code&gt;read expressions&lt;/code&gt;), and write to bins (&lt;code&gt;write expressions&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A filter expression is a general mechanism usable in most operations. An operation is applied to a record only if the filter expression evaluates to true. A filter expression is specified in the &lt;code&gt;policy&lt;/code&gt; parameter of the operation.  &lt;/p&gt;

&lt;p&gt;As a selection mechanism, a filter expression allows general conditions, whereas a secondary-index query offers superior performance. For best performance, use the most selective condition in a secondary-index query when possible.&lt;/p&gt;

&lt;p&gt;Please find details in the workshop on &lt;a href="https://www.youtube.com/watch?v=ebRLnXvpWaI&amp;amp;list=PLGo1-Ya-AEQCdHtFeRpMEg6-1CLO-GI3G&amp;amp;index=9s"&gt;Unleashing the Power of Expressions&lt;/a&gt; and the tutorial on &lt;a href="https://developer.aerospike.com/tutorials/java/expressions"&gt;Expressions in Aerospike&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  User Defined Functions (UDFs)
&lt;/h3&gt;

&lt;p&gt;User Defined Functions (UDFs) allow custom code to be executed on the server. A UDF is written in Lua, registered with the server, and invoked through a request from the client. &lt;br&gt;
There are two distinct types of UDF: Record and Stream. A Record UDF performs a read and/or write operation on a single record, and can be invoked in sync, async, or background mode. A Stream UDF performs an aggregate computation over multiple records selected by a query. A Stream UDF typically also has a client execution phase that is handled by the client library. &lt;/p&gt;

&lt;p&gt;Please be aware that UDFs may not be appropriate for performance sensitive use cases. For record-oriented functions, expressions should be the first choice whenever possible for best performance.&lt;/p&gt;

&lt;p&gt;Please find details in &lt;a href="https://docs.aerospike.com/server/architecture/udf"&gt;UDF documentation&lt;/a&gt; and the tutorials on &lt;a href="https://developer.aerospike.com/tutorials/java/sql-operations"&gt;SQL Operations&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Specifics
&lt;/h2&gt;

&lt;p&gt;Some API specifics are useful to know to avoid potential confusion: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the policy parameter, &lt;/li&gt;
&lt;li&gt;data persistence and expiration, &lt;/li&gt;
&lt;li&gt;write semantics, and &lt;/li&gt;
&lt;li&gt;metadata operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Policy
&lt;/h3&gt;

&lt;p&gt;Most API calls take a &lt;code&gt;policy&lt;/code&gt; parameter which includes significant info that affects both the how and what of the request. Many specifics that define how an operation is performed (such as the timeout and retries) and what data is involved (such as the filter expression) are specified in the policy object. Many operation semantics details may not be obvious by just looking at the call parameters because they are specified in the policy. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;filter-expression&lt;/code&gt;: condition to select records for the operation, &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send-key&lt;/code&gt;: store the user-key at record creation, &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;record-exists-action&lt;/code&gt;: different behavior of writes depending on whether the record exists or not, &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;generation-policy&lt;/code&gt;: used to isolate concurrent read-write transactions, &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expiration&lt;/code&gt;: assign time-to-live duration to a record, and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;durable-delete&lt;/code&gt;: used to prevent deleted records from reappearing after node failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read more about policies in the &lt;a href="https://docs.aerospike.com/server/guide/policies"&gt;Policies&lt;/a&gt; section of the documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Persistence and Expiration
&lt;/h3&gt;

&lt;p&gt;The server applies updates by default in memory, to be flushed to persistent storage at regular intervals. Updates can also be made immediately durable or persistent by using the &lt;code&gt;commit-to-device&lt;/code&gt; option in the namespace configuration.&lt;/p&gt;

&lt;p&gt;A record by default is created never to expire, but a different &lt;code&gt;time-to-live (ttl)&lt;/code&gt; can be specified. An expired record is automatically removed and its space reclaimed, thus relieving the lifecycle management burden in applications for temporary objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write Semantics
&lt;/h3&gt;

&lt;p&gt;In Aerospike, a &lt;code&gt;put&lt;/code&gt; or write combines Create (Insert), Update, and sometimes Delete. The default &lt;code&gt;create-or-replace&lt;/code&gt; semantics of a write can be modified for alternative behaviors.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;record-exists-action&lt;/code&gt; specified within the &lt;code&gt;write-policy&lt;/code&gt; defines the operation semantics when the record already exists, with create/update/update-only/replace/replace-only variants. &lt;/li&gt;
&lt;li&gt;Map updates have a &lt;code&gt;write-mode&lt;/code&gt; of “update”, “create-only”, and “update-only”` to control insertion behavior based on the existence of a key.&lt;/li&gt;
&lt;li&gt;A bin is removed from a record by writing.a NULL value to it. When the last bin is removed, the record is automatically deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Metadata Operations
&lt;/h3&gt;

&lt;p&gt;Not all metadata operations are available through the API in the client libraries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Namespace: A namespace is added through the config and requires a server restart. The truncate API removes all records in a set or the entire namespace. &lt;/li&gt;
&lt;li&gt;Set: A set is automatically created when the first record is inserted in the set. Records in a set can be truncated using the truncate API.&lt;/li&gt;
&lt;li&gt;Index: The API supports creation and deletion of a set index and secondary index. A secondary index is defined on a bin or an element in List or Map for a specific value type.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please view the tutorial &lt;a href="https://developer.aerospike.com/tutorials/java/sql_update"&gt;SQL: Updates&lt;/a&gt; for examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Features
&lt;/h2&gt;

&lt;p&gt;Aerospike is purpose-built to deliver high performance for large data with small cluster size. All features are designed  and tradeoffs are made with this overarching goal. Data modeling is key to achieving best performance. Please see the blog post &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Data Modeling for Speed At Scale&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each application is different, and Aerospike provides flexibility to accommodate custom performance considerations. At the same time, a developer should be aware of the following performance features (described earlier) that are commonly used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collection Data Types (CDTs)&lt;/li&gt;
&lt;li&gt;Multi-op requests&lt;/li&gt;
&lt;li&gt;Batch requests&lt;/li&gt;
&lt;li&gt;Expressions&lt;/li&gt;
&lt;li&gt;Secondary indexes&lt;/li&gt;
&lt;li&gt;Set indexes&lt;/li&gt;
&lt;li&gt;Complex Data Types - Binary, HLL, GeoJSON&lt;/li&gt;
&lt;li&gt;User Defined Functions (UDFs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Useful Libraries
&lt;/h2&gt;

&lt;p&gt;Here are some useful libraries. &lt;/p&gt;

&lt;h3&gt;
  
  
  Document API
&lt;/h3&gt;

&lt;p&gt;The Aerospike Document API provides CRUD operations at arbitrary points within a JSON document. It allows a JSONPath argument to specify parts of the document simply and expressively to apply these methods. Check out the tutorial for the &lt;a href="https://developer.aerospike.com/tutorials/java/doc_api"&gt;Document API&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Java Object Mapper
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/aerospike/java-object-mapper"&gt;object mapper library&lt;/a&gt; uses Java annotations to define the Aerospike semantics for the saving and loading behavior. Annotations are specified next to the definitions of a class, methods, and fields. The object mapper makes managing persistent data easier to implement, easier to understand, and less error prone. Check out the Java Object Mapper &lt;a href="https://www.youtube.com/watch?v=QQhdX661raM&amp;amp;list=PLGo1-Ya-AEQCdHtFeRpMEg6-1CLO-GI3G&amp;amp;index=6"&gt;workshop&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/tutorials/java/object_mapper"&gt;tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Aerospike client API provides a rich set of capabilities. This post provides a high level view of the Aerospike architecture and API to give developers a broader understanding of its architecture and capabilities, and help them become more productive and effective. It provides an orientation of the client API by describing the core concepts, functional elements, key specifics, common performance features, and useful libraries; and points out resources for further exploration of specific topics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com"&gt;Aerospike Developer Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/architecture/overview"&gt;Architecture Overview&lt;/a&gt; (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/architecture/clients"&gt;Smart Client&lt;/a&gt; (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/client"&gt;Clients&lt;/a&gt;  (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aerospike.com/blog/developers-understanding-aerospike-transactions/"&gt;Developers: Understanding Aerospike Transactions&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/sql-operations"&gt;SQLOperations&lt;/a&gt; (interactive tutorials)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/async_ops"&gt;Asynchronous Operations&lt;/a&gt;(interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/java-intro_to_transactions"&gt;Introduction to Transactions&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/batch-operations-in-aerospike"&gt;Batch Operations&lt;/a&gt;  (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; (interactive tutorials)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale-part-2"&gt;Data Modeling for Speed At Scale (Part 2)&lt;/a&gt;  (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;Collection Data Types (CDTs)&lt;/a&gt; (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=ebRLnXvpWaI&amp;amp;list=PLGo1-Ya-AEQCdHtFeRpMEg6-1CLO-GI3G&amp;amp;index=9s"&gt;Unleashing the Power of Expressions&lt;/a&gt; (workshop)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/expressions"&gt;Expressions in Aerospike&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/architecture/udf"&gt;User Defined Functions (UDFs)&lt;/a&gt; (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aerospike.com/server/guide/policies"&gt;Policies&lt;/a&gt; (documentation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/doc_api"&gt;Document API&lt;/a&gt;  (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=QQhdX661raM&amp;amp;list=PLGo1-Ya-AEQCdHtFeRpMEg6-1CLO-GI3G&amp;amp;index=6"&gt;Java Object Mapper&lt;/a&gt; (workshop)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/aerospike/java-object-mapper"&gt;Java Object Mapper&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aerospike</category>
      <category>api</category>
      <category>orientation</category>
      <category>smartclient</category>
    </item>
    <item>
      <title>Data Modeling for Speed At Scale (Part 2)</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Wed, 29 Jun 2022 00:10:57 +0000</pubDate>
      <link>https://dev.to/aerospike/data-modeling-for-speed-at-scale-part-2-2i45</link>
      <guid>https://dev.to/aerospike/data-modeling-for-speed-at-scale-part-2-2i45</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BlPiLNBb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2APhgUE2UT719S0iBB" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BlPiLNBb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2APhgUE2UT719S0iBB" alt="(Source: Photo by Pietro Jeng on [Unsplash](https://unsplash.com/) )" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by Pietro Jeng on &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post focuses on the use of Collection Data Types (CDTs) for data modeling in Aerospike with a large number of objects. This is Part 2 in the two part series on Data Modeling. You can find the first post &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Data Modeling is the exercise of mapping application objects onto the model and mechanisms provided by the database for persistence, performance, consistency, and ease of access.&lt;/p&gt;

&lt;p&gt;Aerospike Database is purpose built for applications that require predictable sub-millisecond access to billions and trillions of objects and need to store many terabytes and petabytes of data, while keeping the cluster size - and therefore the operational costs - small. The goals of large data size and small cluster size mean the capacity of high-speed data storage on each node must be high.&lt;/p&gt;

&lt;p&gt;Aerospike pioneered the database technology to effectively use SSDs to provide high-capacity high-speed persistent storage per node. Among its key innovations are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access SSDs like direct addressable memory which results in superior performance,&lt;/li&gt;
&lt;li&gt;Support a hybrid memory architecture for index and data in DRAM, PMEM, or SSD,&lt;/li&gt;
&lt;li&gt;Implement proprietary algorithms for consistent, resilient, and scalable storage across cluster nodes, and&lt;/li&gt;
&lt;li&gt;Provide Smart Client for a single-hop access to data while adapting to the changes in the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, choosing Aerospike Database as the data store is a significant step toward enabling your application for speed at scale.  By choosing the Aerospike Database, a company of any size can leverage large amounts of data to solve real-time business problems and  continue to scale in the future while keeping the operational costs low.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Part 1&lt;/a&gt; described many capabilities that Aerospike provides toward speed-at-scale such as indexes, data compression, server-side operations, namespace and cluster configuration, multi-op requests, batch requests, and more. &lt;/p&gt;

&lt;p&gt;This post focuses on Collection Data Types (CDTs), specifically List and Map data types, and discusses how applications can optimize speed as well as storage density by leveraging them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Collection Data Types (CDTs)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;List&lt;/code&gt; and &lt;code&gt;Map&lt;/code&gt; are the Collection Data Types (&lt;code&gt;CDTs&lt;/code&gt;) in Aerospike. A List is a tuple or array of values, and a Map is a dictionary of key-value pairs. The element value can be of any supported type, including List and Map, and CDTs can be nested at an arbitrary level. &lt;/p&gt;

&lt;p&gt;CDTs are essential to model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aggregation of related objects in one record, allowing transactional semantics across multi-object updates,&lt;/li&gt;
&lt;li&gt;containers for collection of objects to effectively store large number of objects, and&lt;/li&gt;
&lt;li&gt;complex objects such as JSON documents. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will briefly describe key CDT concepts like nesting and ordering before diving into specific modeling patterns and techniques. &lt;/p&gt;

&lt;h3&gt;
  
  
  Nested Elements and Context Path
&lt;/h3&gt;

&lt;p&gt;A nested element in a CDT can be accessed directly by using its &lt;code&gt;context-path&lt;/code&gt;. A context-path describes the path from the root or the top level of the CDT to a nested element, where each node in context-path uniquely identifies an element at that level by key, index, value, or rank (value order).  The context-path points to only one element which can be of any data type, and an operation on the element identified by a context-path must be a valid operation on the element’s data type.  &lt;/p&gt;

&lt;p&gt;Consider a nested object represented as a Map (level 0): it has a List at level 1, and a Map at level 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object = {  “id1”: [ {“a”: 1, “b”: 2}, {“c”: 3, “d”: 4} ], 
            “id2”: [ {“e”: 5, “f”: 6}, {“g”: 7, “h”: 8}] }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A context path to the nested element “c”, can look like: &lt;code&gt;[By-Key(“id1”), By-Index(1), By-Key(“c”)]&lt;/code&gt;.  &lt;/p&gt;

&lt;p&gt;Note, in the Aerospike API, the top CDT level (level 0) is implied by the bin and only lower level elements require a non-null context-path. &lt;/p&gt;

&lt;p&gt;As a performance and convenience feature, CDTs allow creation of missing interim levels as part of the create operation of a nested element, as checking all path nodes prior to creation of a nested element in the application can be inconvenient and slow. For example, adding an element to a Map in a List, none of which exist, will first create the List, add the Map to the List, and then add the element to the Map. &lt;/p&gt;

&lt;h3&gt;
  
  
  Global Ordering of Values
&lt;/h3&gt;

&lt;p&gt;Applications commonly use an Ordered List for use cases such as the Leaderboard. In Aerospike, List elements can be any type, and therefore Aerospike defines a deterministic ordering within and across supported types. &lt;/p&gt;

&lt;p&gt;A CDT is frequently used as a container for objects that are represented as either List or Map. To support retrieval by rank (value-order) as well as by specific value-range, Aerospike defines how List values and Map values are compared. For example, two lists are compared by comparing their respective elements in stored order until the first inequality results or the end of either or both lists is reached. As well, there is a defined order across the data types. For example, any integer value is ranked lower than any string value. You can view the ordering rules &lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt-ordering"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This deterministic comparison of values provides the basis for content or value based selection of elements such as By-Value and By-Value-Range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling Objects with CDTs
&lt;/h2&gt;

&lt;p&gt;CDTs make it possible to aggregate related objects in a single record, and therefore transactional updates across them. &lt;/p&gt;

&lt;p&gt;Application objects can be stored in multiple ways in Aerospike: As a record, as a List, or as a Map.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storing Objects As a Record
&lt;/h3&gt;

&lt;p&gt;Object fields are stored in record bins. Flat objects can be stored in simple bins, that is, without use of CDTs, but objects with array and map fields require CDT bins. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object:  id = 4, name = “Adam Smith”, start-date = 1/1/2015, department = finance, salary = 100000
Record bins: id:4, name: “Adam Smith”, start-date: 1/1/2015, department: finance, salary: 100000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modeling objects as records have the following advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mapping of object fields to record bins is simple to understand.&lt;/li&gt;
&lt;li&gt;Secondary indexes can only be defined at a bin level for field-value based access.&lt;/li&gt;
&lt;li&gt;Certain data types like HyperLogLog (HLL) and BLOB can only be stored as a bin.&lt;/li&gt;
&lt;li&gt;XDR sync granularity can be specified at a field (bin) level, allowing  greater control and efficiency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Storing Objects As a List
&lt;/h3&gt;

&lt;p&gt;As a List, an object is stored as a tuple of its field values, where each field is placed at a specific position in the List. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object: id = 4, name = “Adam Smith”, start-date = 1/1/2015, department = finance, salary = 100000
List bin: [4, “Adam Smith”, “20150101”, “finance”, 100000]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lists offer these unique advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The tuple form eliminates redundant storage of keys as in Map. Objects stored as tuples must use Unordered (or more precisely, insertion- or application-ordered) List, which preserves the tuple order irrespective of the field values. Application must manage the object schema, that is, how the tuple order matches the object fields, and also schema evolution. The &lt;a href="https://github.com/aerospike/java-object-mapper"&gt;Object Mapper library&lt;/a&gt; can be used to manage these aspects transparently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lists also allow convenient value-based selection of objects. The value of a List object is based on the initial field in the tuple. For example, objects represented as [type, size, color] can be retrieved by matching type values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A wildcard based value match can be conveniently specified. For example, get-by-value(collection, [“type x”, *]) will match all List tuples in the collection with “type x” as their first element.&lt;/li&gt;
&lt;li&gt;A value range based selection can be conveniently specified using the value delimiters NIL and INF, NIL denoting the absolute lowest and INF denoting the absolute highest value. For example, get-by-value-range(collection, [“value1”, NIL],[“value2”,INF]) selects all List objects with the first element between “value1” and “value2” (both values inclusive).
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Storing Objects As a Map
&lt;/h3&gt;

&lt;p&gt;As a Map, an object is stored as field specific key-value pairs. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object: id = 4, name = “Adam Smith”, start-date = 1/1/2015, department = finance, salary = 100000
Map bin: {“id”: 4, “name”: “Adam Smith”, “start-date”: “20150101”, “department”: “finance”, “salary”: 100000}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advantages of using a Map are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maps provide a natural way for storing JSON documents. &lt;/li&gt;
&lt;li&gt;The object schema is self defining, thus removing the burden on the application of managing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Map values are not convenient to compare for value based access as List values. Wildcard cannot be used to denote a range of Map values. For example, {a:*} or {a:1, *} cannot be used for value comparison. Exact value or value-range requires specifying all keys in the map, which is not convenient for large maps. For these reasons, objects should be modeled as List tuples if value based access is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed-At-Scale with CDTs as Containers
&lt;/h2&gt;

&lt;p&gt;CDTs unlock many advantages toward speed-at-scale when they are used as a container for a collection of objects. &lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;Related objects stored in a CDT container can be written or retrieved together in a single operation, which can provide a significant throughput improvement.&lt;/p&gt;

&lt;p&gt;Aerospike CDTs support the normal list and map operations, but more significant are the many common patterns requiring more complex processing. They are performed fully on the server side to eliminate retrieval of data to the client side, and therefore provide superior performance.&lt;/p&gt;

&lt;p&gt;Additional performance aspects include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multi-element updates are akin to batch operations on a CDT. Applications can get the right semantics using the constructs provided for individual element failure handling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Many selection criteria are available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access based on value and “rank” (value order) in addition to the normal index or key based access.
&lt;/li&gt;
&lt;li&gt;Single, multi, and range selectors by index, key, value, and rank.&lt;/li&gt;
&lt;li&gt;Vicinity or relative selection, for example, using “relative rank” with respect to a value.&lt;/li&gt;
&lt;li&gt;Negate the selection criterion with a convenient INVERT flag.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple return types allow just the required data to be requested. For example, COUNT for the number of elements selected, and NONE when no values need to be returned such as when adding elements, among others.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CDT operations can be used in &lt;a href="https://developer.aerospike.com/tutorials/java/expressions"&gt;Expressions&lt;/a&gt;, which is another mechanism for efficient server side execution and minimizing the data transfer cost. Expressions allow server side record selection (filter expression), reading (read expression) or writing (write expression) a computed value.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can review List performance analysis &lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt-list-performance"&gt;here&lt;/a&gt; and Map performance analysis &lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt-map-performance"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale
&lt;/h3&gt;

&lt;p&gt;Each record in Aerospike incurs a 64 byte storage overhead in the primary index, which is typically stored in DRAM. For a large number of objects, the DRAM size and cost can be significant if each object is stored in a record. CDTs when used as containers can store multiple objects in a single record, providing greater density and scale.&lt;/p&gt;

&lt;p&gt;Further, List provides a compact tuple form for storing objects which eliminates the bin overhead. &lt;/p&gt;

&lt;h3&gt;
  
  
  Ordering
&lt;/h3&gt;

&lt;p&gt;Objects stored as records cannot be retrieved from the server in any specific order. Sorting must be performed on the client side. &lt;/p&gt;

&lt;p&gt;On the other hand, a CDT supports identifying elements by different orders: index or key as well as value and rank order. For instance, Ordered List maintains a value-based sort order allowing easy modeling of common patterns like Leaderboard, time-ordered events, and ordered groups. Unordered List maintains insertion-order and can be used to store, for example, a Queue and objects in tuple form as described earlier. Note, the object should be stored as a tuple with the first field that is significant for value ordering. For example, type, id, and timestamp.&lt;/p&gt;

&lt;p&gt;CDTs maintain internal indexes for fast access by key, index, or value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforcing Unique Values
&lt;/h3&gt;

&lt;p&gt;Ordered Lists provide a way to check and enforce uniqueness of values at the time of insertion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bin Limits
&lt;/h3&gt;

&lt;p&gt;In Aerospike, bin names can be up to 14 characters long. Distinct bin names in a namespace are limited to 32K. Storing objects as Maps or Lists has no limit on the length or the number of distinct object field names. &lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling Object Collections
&lt;/h2&gt;

&lt;p&gt;When an object is stored as a record, storing a collection of objects is a matter of mapping it on one or more sets and one or more namespaces. &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Part 1&lt;/a&gt; talks about some of the considerations in organizing records in sets and namespaces. &lt;/p&gt;

&lt;p&gt;Multiple related objects can be stored in a single CDT container for access and storage efficiencies as discussed above. If the number of objects in the collection exceeds the record size limit, the collection must be split by some criteria across multiple records. The  considerations in organizing records in sets and namespaces from &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Part 1&lt;/a&gt; apply in this case. CDT containers work especially well for objects that will be stored or retrieved together. &lt;/p&gt;

&lt;p&gt;When using the CDTs for collections, key design considerations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Direct access: When direct access to the object in a collection is needed, it can be achieved by proper object id design.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Query access: Multiple objects can be accessed by some criteria with a scan using filter expression, and/or a query using a secondary index.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How objects in the collection are stored - as a tuple in a List or as key-value pairs in a Map - affects direct access and query. Tuple or List object representation offers better support for both as described earlier. &lt;/p&gt;

&lt;h3&gt;
  
  
  Object ID Design for Direct Access
&lt;/h3&gt;

&lt;p&gt;Objects stored in CDTs may need to be independently accessible. In this case, the object identifier must be designed for direct access.&lt;/p&gt;

&lt;p&gt;To enable direct access using the object id, object ids should contain record id (key), say as a prefix. With this, the record key extracted from the object id can be used to navigate first to the record, and then to the specific object within the CDT. All objects stored in a record will contain the common record key. For example, if a CDT holds all store objects in a region, the record key can be the region id, which is also embedded in all store ids of the region. &lt;/p&gt;

&lt;p&gt;Note, it is not necessary to aggregate objects in a record by a real world relationship. A subset of the hashed object id bits may be used as a record id. See &lt;a href="https://medium.com/aerospike-developer-blog/record-aggregation-in-aerospike-for-performance-and-economy-222673ee5b83"&gt;this blog post&lt;/a&gt; for the details of this scheme.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct Access with List and Map Object Representation
&lt;/h3&gt;

&lt;p&gt;A List object can be accessed within the container (List or Map) using the object id by value if the id is stored as the first field. A Map object is not easy to access in a List container because a Map does not allow wildcard based value comparison, for example, access by id like get-by-value(list, {“id”:”id1”, *}) is not possible.&lt;/p&gt;

&lt;p&gt;Consider a List of Lists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[   ["id1", 10, 11,…], ["id2", 20, …], 
    ["id3", 30, …], ["id4", 10, 101…]  ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct access using id: &lt;code&gt;get-by-value(outerList, ["id1", *])&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or a Map of Lists&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{   "id1": ["id1", 10, 11,…], "id2": ["id2", 20, …], 
    "id3": ["id3", 30, …], "id4": ["id4", 10, 101…]  }`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct access using id: &lt;code&gt;get-by-key(map, "id1")&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;Now consider a List of Maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[   {“id”:"id1", “a”: 10, “b”: 11,…}, {‘“id”: "id2", “a”: 20, …}, 
    {“id”: "id3", “a”: 30, …}, {‘“id”: "id4", “a': 10, “b”: 101…}  ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here it is not possible to directly access the object with id "id1" in the List container.&lt;/p&gt;

&lt;p&gt;Or a Map of Maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{   “id1”: {“id”:"id1", “a”: 10, “b”: 11,…}, “id2”: {“id”: id2’, “a”: 20, …},   
    “id3”: {“id”: "id3", “a”: 30, …}, “id4”: {“id”: "id4", “a': 10, “b”: 101…}  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct access using id: &lt;code&gt;get-by-key(outerMap, "id1")&lt;/code&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  Organizing Collections in Records
&lt;/h3&gt;

&lt;p&gt;Due to the limit on the size of a record, a large object collection may need to be split across multiple CDTs, each stored in a separate record. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Single record collection: The record key represents the collection id.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-record collection: Record keys are generated by appending the collection id with record-specific group ids. For example, the ticker for a stock can be organized in multiple records that have keys stock symbol (collection id) + date (group id).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Organizing Collections in Sets and Namespaces
&lt;/h3&gt;

&lt;p&gt;All record organization concepts apply to records holding collections in CDTs as described in &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Part 1&lt;/a&gt;. For instance, a multi-record collection can be further organized if necessary in one or more dedicated sets, over one or more namespaces. &lt;/p&gt;

&lt;h2&gt;
  
  
  Querying Collections
&lt;/h2&gt;

&lt;p&gt;A query or predicate based access to multiple objects is provided with these mechanisms: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In a single CDT:&lt;br&gt;
Internal indexes are maintained within a CDT that allow fast access to elements by specific keys (Map only), indexes, values, and ranks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Across multiple records:&lt;br&gt;
A secondary index can be defined on List values, Map keys, and Map values, to allow queries across List or Map bins in multiple records. The secondary index efficiently identifies all matching records, and with appropriate CDT operation, specific CDT elements from each record can be retrieved. &lt;br&gt;
(Note indexing at any CDT level is planned for Aerospike Database 6.1. Prior to 6.1, CDT elements below the top level of the CDT can be replicated for indexing in a separate record bin or at the top level of the CDT.)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A filter expression can be specified in an operation so that the operation is applied only to matching records. A filter expression can be defined using the CDTs in the record. For example, a filter expression can select a record if a CDT bin has a nested List larger than a certain size or a certain value exists in a nested Map element.&lt;/p&gt;

&lt;p&gt;As discussed earlier, it is easy to define value based predicates on List tuples using the wildcard, NIL, and INF values. Therefore, to be able to use filter expressions with value based predicates, use tuple representation. Note, the value based predicates cannot use arbitrary fields in the object tuple, only the first field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Temporary Objects
&lt;/h2&gt;

&lt;p&gt;The record-level metadata such as the expiration time and update time are not applicable to individual objects within the CDTs, if they can be updated or expired independently. In such cases, these mechanisms must be implemented on a per-object basis. It is possible to piggyback such housekeeping expiration of  objects on other regular application operations using a multi-op request and CDT delete operations based on the rank or value range (“delete all elements with expiration-time field less than current time”). &lt;/p&gt;

&lt;p&gt;Index and rank based selection also allows one to keep the size of a CDT capped to a specified maximum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration with Performance Features
&lt;/h2&gt;

&lt;p&gt;CDTs are integrated into and benefit from the other Aerospike performance features discussed in &lt;a href="https://developer.aerospike.com/blog/data-modeling-for-speed-at-scale"&gt;Part 1&lt;/a&gt; including:&lt;/p&gt;

&lt;h3&gt;
  
  
  Sets and set indexes
&lt;/h3&gt;

&lt;p&gt;You can organize a collection efficiently in one or more sets, and define set indexes for scan performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-op requests
&lt;/h3&gt;

&lt;p&gt;You can perform multiple operations on a record in a single request. For instance, in the same request, you can add items to a list, sort it, get its new size, and top N items.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch requests
&lt;/h3&gt;

&lt;p&gt;You can conveniently issue one request for operations on multiple records hosting a collection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions and Consistency
&lt;/h2&gt;

&lt;p&gt;Data stored in a CDT can be updated in a single transaction because all single record operations support transactional semantics in Aerospike. Different parts within the CDT can be updated or retrieved atomically and efficiently using a single multi-op request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maximum Record Size
&lt;/h2&gt;

&lt;p&gt;A namespace is configured for a maximum record size, with the upper limit of 8MB, and represents the unit of transfer in a device IO operation. Record data cannot exceed the configured maximum record size, and is an important consideration for large object as well as multi-object record design. The application design may consider workarounds such as &lt;a href="https://github.com/aerospike-examples/adaptive-map"&gt;a multi-record map&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Examples
&lt;/h2&gt;

&lt;p&gt;The following examples illustrate the use of CDTs. &lt;/p&gt;

&lt;h3&gt;
  
  
  Events Data for Real-Time Applications
&lt;/h3&gt;

&lt;p&gt;Event objects can be stored for access by event-id as well as timestamps by storing them in a Map container with each event object stored as a tuple in a List with timestamp as the first field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{ event-id: [timestamp, other event attributes], … }&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Event-id based access is a simple key access in the Map container. You can retrieve the event that is at or closest to a timestamp with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get-by-relative-rank(map, value=timestamp, relative-rank=-1, count=2)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The above operation returns two elements in the events container Map with values starting just prior to the timestamp indicated by relative-rank of -1, and the one after that, which could be the exact timestamp or the one immediately after that. This scheme takes care of the timestamp not present because it is beyond the range of existing timestamps as well as there is no event at that exact timestamp.&lt;/p&gt;

&lt;p&gt;The size of the container can be efficiently managed with remove-by-index-range (trim to specific size) or remove-by-value-range (remove old events) operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rank Ordered Lists
&lt;/h3&gt;

&lt;p&gt;Lists that need to be retrieved by rank such as players with highest game scores, blogs with highest views, and videos with most likes can be conveniently modeled with an Ordered List container with each object stored as a tuple in an Unordered (application ordered) List.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[ [score, other attributes], … ]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To obtain N objects with the highest scores:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get-by-rank-range(list, start-rank=-N)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In the above operation, the start-rank indicates the element with the Nth highest score. All elements after that are returned. The operation effectively returns the top N ranked elements. &lt;/p&gt;

&lt;h3&gt;
  
  
  Efficient Container Computations
&lt;/h3&gt;

&lt;p&gt;Expressions allow operations involving one or more large containers in a record to be performed efficiently  on the server side. For example, the top N elements common to two Lists A and B in a record can be computed on the server side with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get-by-rank-range( list=remove-by-value-list( list=A,  value-list=get-list(B), op-flag=INVERT ), start-rank=-N )&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The first server computation is A-(A- B) which yields common elements in A and B. Note, op-flag=INVERT has the effect of inverting the selection, so in this case instead of A-B, it would return the elements in A that are not in A-B, or A-(A-B). The second computation returns the top N elements from the common list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Object Mapping Libraries
&lt;/h2&gt;

&lt;p&gt;The following libraries are available for applications to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/aerospike/aerospike-document-lib"&gt;Document API&lt;/a&gt; library to store a JSON document to, and retrieve it from, a Map bin. The library also supports JSONPath queries on a stored document. Check out the &lt;a href="https://developer.aerospike.com/tutorials/java/doc_api"&gt;interactive tutorial&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/aerospike/java-object-mapper"&gt;Java Object Mapper&lt;/a&gt; library for convenient and efficient mapping of objects into Aerospike database with simple annotation of Java classes. Check out the &lt;a href="https://www.youtube.com/watch?v=QQhdX661raM&amp;amp;list=PLGo1-Ya-AEQCdHtFeRpMEg6-1CLO-GI3G&amp;amp;index=6"&gt;workshop&lt;/a&gt; and &lt;a href="https://developer.aerospike.com/tutorials/java/object_mapper"&gt;interactive tutorial&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Aerospike Database is built for speed at scale, and provides a path to companies of any size to leverage large data for real-time decisions without incurring huge operational cost, and to scale in the future.  The blog post describes the Collection Data Types (CDTs), how they can be used to model objects for speed at scale, and their capabilities like ordering and server-side execution to improve performance and ease of implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aerospike.com/server/guide/data-types/cdt"&gt;Aerospike Documentation: CDTs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/aerospike-developer-blog/record-aggregation-in-aerospike-for-performance-and-economy-222673ee5b83"&gt;Record Aggregation in Aerospike For Performance and Economy&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/aerospike-developer-blog/aerospike-modeling-iot-sensors-c74e1411d493?source=collection_home---4------20-----------------------"&gt;Aerospike Modeling: IoT Sensors&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=7rVk0WCRJtQ&amp;amp;list=PLGo1-Ya-AEQD7g9hmXy4eYKsG5PtZPkIT&amp;amp;index=6"&gt;Document Oriented Data Modeling&lt;/a&gt; (workshop)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/java-modeling_using_lists"&gt;Modeling Using Lists&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/java-modeling_using_maps"&gt;Modeling Using Maps&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/java-advanced_collection_data_types"&gt;Advanced Collection Data Types&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/doc_api"&gt;Document API for JSON Documents&lt;/a&gt;  (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/aerospike/aerospike-document-lib"&gt;Aerospike Document API Library&lt;/a&gt; (github repo)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/aerospike-developer-blog/aerospike-document-api-fd8870b4106c"&gt;Aerospike Document API&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/aerospike-developer-blog/aerospike-document-api-jsonpath-queries-bd6260b2d076"&gt;Aerospike Document API: JSONPath Queries&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=QQhdX661raM&amp;amp;list=PLGo1-Ya-AEQCdHtFeRpMEg6-1CLO-GI3G&amp;amp;index=6"&gt;Java Object Mapper&lt;/a&gt; (workshop)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.aerospike.com/tutorials/java/object_mapper"&gt;Java Object Mapper&lt;/a&gt; (interactive tutorial)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/aerospike/java-object-mapper"&gt;Java Object Mapper&lt;/a&gt; (github repo)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://developer.aerospike.com/"&gt;Aerospike Developer Hub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datamodeling</category>
      <category>speedatscale</category>
      <category>cdt</category>
      <category>nosql</category>
    </item>
    <item>
      <title>Data Modeling for Speed At Scale</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Thu, 02 Jun 2022 22:04:35 +0000</pubDate>
      <link>https://dev.to/aerospike/data-modeling-for-speed-at-scale-m60</link>
      <guid>https://dev.to/aerospike/data-modeling-for-speed-at-scale-m60</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2EJvtWq3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2A_ZyySJkVOTNuAE0D" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2EJvtWq3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/1600/0%2A_ZyySJkVOTNuAE0D" alt="(Source: Photo by NASA on [Unsplash](https://unsplash.com/) )" width="800" height="385"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Photo by NASA on &lt;a href="https://unsplash.com/"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Data Modeling is the exercise of mapping application objects onto the model and mechanisms provided by the database for persistence, performance, consistency, and ease of access. &lt;/p&gt;

&lt;p&gt;Aerospike Database is purpose built for applications that require predictable sub-millisecond access to billions and trillions of objects and need to store many terabytes and petabytes of data, while keeping the cluster size - and therefore the operational costs - small. The goals of large data size and small cluster size mean the capacity of high-speed data storage on each node must be high. &lt;/p&gt;

&lt;p&gt;Aerospike pioneered the database technology to effectively use SSDs to provide high-capacity high-speed persistent storage per node. Among its key innovations are that Aerospike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accesses SSDs like direct addressable memory which results in superior performance,&lt;/li&gt;
&lt;li&gt;Supports a hybrid memory architecture for index and data in DRAM, PMEM, or SSD,&lt;/li&gt;
&lt;li&gt;Implements proprietary algorithms for consistent, resilient, and scalable storage across cluster nodes,  and &lt;/li&gt;
&lt;li&gt;Provides Smart Client for a single-hop access to data while adapting to the changes in the cluster.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, choosing Aerospike Database as the data store is a significant step toward enabling your application for speed at scale.  By choosing the Aerospike Database today, it is possible for a company of any size to leverage large amounts of data to solve real-time business problems and  continue to scale in the future while keeping the operational costs low.&lt;/p&gt;

&lt;p&gt;Data design should take into account many capabilities that Aerospike provides toward speed-at-scale such as data compression, Collection Data Types (CDTs), secondary indexes, multi-op requests, batch requests, server-side operations, cluster organization, and more. We discuss them later in this post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  NoSQL Data Modeling Principles
&lt;/h2&gt;

&lt;p&gt;Aerospike is a NoSQL database, and does not have rigid schema as required by relational databases, To enable web-scale applications, Aerospike has a distributed architecture, and allows applications to choose availability or consistency during a network partition per &lt;a href="https://en.wikipedia.org/wiki/CAP_theorem"&gt;the CAP theorem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Typically, NoSQL data modeling starts with identifying the patterns of access in the application, that is, how the application reads and updates the data. The goal is to organize data for the required performance, efficiency, and consistency. In some NoSQL databases, design of keys, which serve as handles for access, is an important consideration for collocating them using a common property value. More on this later.&lt;/p&gt;

&lt;p&gt;In Aerospike, many key data modeling principles are applicable that are prevalent in NoSQL databases including the use of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Denormalization: Allowing duplication of data, by storing it in multiple places, to simplify and optimize access. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Aggregates: Storing nested entities together in embedded form to simplify and optimize access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application joins: Performing joins in the application in rare cases when they are required, for example, to follow the stored references in many-to-many relationships.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Single record transactions: Storing data that must be updated atomically together in one record.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Modeling Object Relationships
&lt;/h3&gt;

&lt;p&gt;Related objects can be modeled either by holding a reference to the objects or by embedding the objects. The choice involves the trade-offs in ease, performance, and consistency; and is governed by two key factors: 1) the cardinality - 1:1, 1:N, M:N - of relationships, and 2) access patterns, as described below. Data modeling requires striking the right balance of conflicting goals such as, for example, while related objects should be embedded for ease and performance of reads, embedding across multiple objects can adversely affect update performance and consistency. &lt;/p&gt;

&lt;p&gt;The following factors will dictate whether to embed or to reference an object:&lt;/p&gt;

&lt;h4&gt;
  
  
  Shared or exclusive relationship
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Exclusive relationships 1:1 or 1:N should be embedded. For example, these 1:1 relationships should be stored together: owner and car, citizen and passport, family and residence; and so should these 1:N relationships: account and transactions, person and properties, and company and brands.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shared objects with M:N relationships should be stored independently. For example, students and courses, tourists and destinations, and donors and charities.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Being accessed together
&lt;/h4&gt;

&lt;p&gt;If 1:1 or 1:N embedded objects and the parent object are accessed and updated independent of each other, they are candidates for storing separately.  For example, owner and car, person and accounts can have different operations and access patterns.  Aggregates are often not optimal when embedded objects would be frequently and independently updated. For example, user and sent- or received- message folder have very different update patterns.&lt;/p&gt;

&lt;h4&gt;
  
  
  Immutability 
&lt;/h4&gt;

&lt;p&gt;If an M:N shared object does not change and also is accessed together with the referring object, it should be embedded with the referring object. For example, travelers and favorite destinations, students and completed courses.&lt;/p&gt;

&lt;h4&gt;
  
  
  Consistency requirements
&lt;/h4&gt;

&lt;p&gt;The application may be able to tolerate temporary inconsistency in a shared object. If an M:N shared object is accessed together with the referring object, updated infrequently, and may remain slightly out-of-date while all its embedded copies are being updated, it is a candidate for embedding. For example, students and current course instructors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beyond the standard NoSQL modeling techniques and guidelines, data modeling in Aerospike involves additional considerations as discussed below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Model Database
&lt;/h2&gt;

&lt;p&gt;Aerospike is a &lt;code&gt;record&lt;/code&gt;-oriented store.  It's easy to view a key-value store as a special case of the record-oriented store, where a record holds just one (nameless) field. &lt;/p&gt;

&lt;p&gt;In the Aerospike data model, a record is a schema-less list of &lt;code&gt;bins&lt;/code&gt; (fields), which means a record can hold a variable number of arbitrary bins. A bin is type-less, which means it can hold a value of any supported type. Aerospike supports scalar types like Integer, String, Boolean, and Double; Collection Data Types (&lt;code&gt;CDTs&lt;/code&gt;) like &lt;code&gt;List&lt;/code&gt; and &lt;code&gt;Map&lt;/code&gt;; and special data types like Blob (bytes), GeoJSON, and HyperLogLog (HLL).&lt;/p&gt;

&lt;p&gt;Records are created in a &lt;code&gt;namespace&lt;/code&gt;. A record is typically assigned to a set (similar to a table) within the namespace. A database can have multiple namespaces, and each namespace has dedicated storage devices and policy for how indexes and data are stored, for example, hybrid DRAM and flash, all flash, and so on.&lt;/p&gt;

&lt;p&gt;Aerospike Collection Data Types provide an efficient way to store hierarchical objects, including JSON documents. Application objects can be stored in multiple ways in Aerospike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;As a record: Object fields are stored in record bins&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As a Map: Object fields are stored as key-value pairs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As a List: Object field values are stored in the List in a specific order.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will defer the discussion of CDTs to a future post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design of Record Keys
&lt;/h2&gt;

&lt;p&gt;Records are accessed with a unique &lt;code&gt;key&lt;/code&gt;. Aerospike record key (or digest) is a hash of the tuple (set, user-key) and is unique within a namespace, where user-key is an application provided id. &lt;/p&gt;

&lt;p&gt;Aerospike does not provide a way for records to be placed on the same node for locality of access through complex key design schemes as some other databases. Aerospike uniformly distributes records across nodes for load balancing, optimal resource utilization, and performance, and so no effort need be spent in designing keys for collocation. &lt;/p&gt;

&lt;p&gt;At the same time, it is possible for the application to compose the key to quickly access related objects as described below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling Related Objects
&lt;/h2&gt;

&lt;p&gt;There are multiple ways in which related object can be organized:&lt;/p&gt;

&lt;p&gt;Sets provide a mechanism to keep records organized by some criterion, such as type of objects, metadata vs data, a logical mapping, and so on.  &lt;/p&gt;

&lt;p&gt;Related objects can be held in CDTs either in one record that has the group-id as its key, or multiple records whose keys are generated by appending sub-group ids to the group-id. For example, ticker for a stock can be organized in records by stock (group id) + date (sub-group id).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A List can be used to store a group of related objects as a List of Lists or a List of Maps. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Map can also be used as a Map of Lists or a Map of Maps.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CDTs provide many advantages such as greater density of objects per node by reducing the per-record system overhead, powerful server-side operations, as well as element ordering. We will cover use of CDTs in a future post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Transactions and Consistency
&lt;/h2&gt;

&lt;p&gt;It is important to &lt;a href="https://aerospike.com/blog/developers-understanding-aerospike-transactions/"&gt;understand transactions in Aerospike&lt;/a&gt; to ensure data consistency and correctness. Aerospike allows a namespace to be configured for Availability or Strong Consistency (SC). Multiple read consistency levels are possible in the SC mode for the application to strike the right balance of performance and level of consistency.&lt;/p&gt;

&lt;p&gt;In the SC mode, Aerospike provides transactional guarantees for single record operations. This includes multiple operations on a single record that can be performed in a single request. Therefore, data that needs to be updated atomically must be stored in one record. CDTs provide an easy way to store such objects in one record.&lt;/p&gt;

&lt;p&gt;While transactional updates are currently not available across multiple records, delayed consistency across multiple records can be achieved through &lt;a href="https://aerospike.com/blog/microservices-with-aerospike/"&gt;known schemes&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Temporary Objects
&lt;/h2&gt;

&lt;p&gt;Aerospike has useful mechanisms that should be leveraged to manage objects with a defined lifespan. Such records can be marked with an expiration time (or time-to-live, TTL; the default is no expiration). Expired objects are automatically removed and their space recovered through garbage collection. This mechanism provides a convenient and efficient way for the application to manage its temporary objects that have a specific lifetime and must be removed after that. &lt;/p&gt;

&lt;p&gt;If data needs to be archived based on some age criterion to another location, sets and secondary indexes can be used to efficiently identify the records to archive. &lt;/p&gt;

&lt;h2&gt;
  
  
  Organizing Namespaces
&lt;/h2&gt;

&lt;p&gt;A namespace’s index and data can be placed in different storage types with different speed, size, and cost characteristics such as DRAM. PMEM, and SSD. Applications can allocate data to different namespaces depending on the speed and size needs for different objects. &lt;/p&gt;

&lt;p&gt;The “data in index” option is available for high speed counters: It stores a single numerical value, typically a counter, that is updated at high frequency and for which access speed as well as consistency are critical. For example, it is important to accurately read and update the number of seats available for a popular event when the tickets go on sale to avoid under-booking or over-booking. Similarly, fast objects can be stored in a PMEM or fast SSD namespace, and large low-cost “all-flash” namespace can store objects with less stringent access latency. &lt;/p&gt;

&lt;p&gt;It is also possible to split an object across multiple namespaces with the same set and user-key and therefore, the digest of records, serving as an implicit reference. For example, one namespace may hold  archived versions, and another the latest version.&lt;/p&gt;

&lt;p&gt;Other namespace configuration options significant for data modeling decisions include maximum record size and choice of Availability versus Strong Consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maximum Record Size
&lt;/h2&gt;

&lt;p&gt;A namespace is configured for a maximum record size, with the upper limit of 8MB, and represents the unit of transfer in a device IO operation. Record data cannot exceed the configured maximum record size, and is an important consideration for large object as well as multi-object record design. The application design may consider workarounds such as &lt;a href="https://github.com/aerospike-examples/adaptive-map"&gt;a multi-record map&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compressing Data
&lt;/h2&gt;

&lt;p&gt;Using compression can significantly compact data and reduce the data storage requirements, thus increasing the data density per node, reducing the cluster size, and lowering the cost.  &lt;/p&gt;

&lt;p&gt;In addition to improving storage density, compression can also improve wire transfer speed for large objects. Compression can be enabled for efficient client-server data transfer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing Speed
&lt;/h2&gt;

&lt;p&gt;To achieve optimal performance, many mechanisms are available in Aerospike including the following.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secondary Indexes 
&lt;/h3&gt;

&lt;p&gt;Scan operations use primary indexes on namespace and sets, whereas query operations use secondary indexes. Secondary indexes can be created on a bin’s Integer, String, and GeoJSON values. Secondary indexes improve query performance, but have a cost of keeping the index in sync when the underlying data is updated. Typically, a secondary index on a field works best for high query/update ratio and high selectivity of the index field.&lt;/p&gt;

&lt;p&gt;In Aerospike Database 6.0+, the application can boost access throughput with hyper-parallel “partition-grained” secondary index queries, in addition to primary index queries from prior releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch Operations
&lt;/h3&gt;

&lt;p&gt;Prior to Aerospike Database 6.0,  “read' or “exists” operations on multiple records could be batched in a single request for efficiency and speed. In 6.0, batch operations for write, delete, and UDF operations are also supported. Fast ingests, for example, for IOT streams, can get better throughput with batch writes. &lt;/p&gt;

&lt;h3&gt;
  
  
  Server-Side Operations
&lt;/h3&gt;

&lt;p&gt;Expressions and UDFs allow complex operations to be performed on the server, without having to retrieve data to the client first. &lt;/p&gt;

&lt;p&gt;Expressions: Expressions offer a powerful way to define complex logic for server-side evaluation - either to filter records, or to retrieve data and store results. &lt;/p&gt;

&lt;p&gt;UDF: User Defined Functions (UDFs) are defined in Lua for record- and stream-oriented computations. They are invoked through a client request, and executed on server. &lt;/p&gt;

&lt;h3&gt;
  
  
  Sets and Set Indexes
&lt;/h3&gt;

&lt;p&gt;Related records can be organized in sets. To enable fast scans on a set, a set index can be defined. A set index can provide a big performance boost to small set scans as compared to the alternative of having to scan the entire namespace in the absence of a set index.  &lt;/p&gt;

&lt;p&gt;It is also a lot more efficient to truncate a set as opposed to deleting individual records when the data is no longer needed, and therefore such deletion cohorts may be organized in sets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Data Design Considerations 
&lt;/h2&gt;

&lt;p&gt;In addition to the data modeling aspects described above, there are Aerospike cluster design aspects that overlap with data design, and affect application performance, reliability, and ease of development. They are briefly described below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replication for Reliability and Performance
&lt;/h3&gt;

&lt;p&gt;An Aerospike cluster holds a Replication Factor (RF) number of copies of data for reliability and performance. A RF of 2 is typical, and for higher resilience can be 3, but a larger RF adversely impacts both speed and scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synchronous and Asynchronous Replication
&lt;/h3&gt;

&lt;p&gt;An important design decision is whether the data is held in one tightly synchronized cluster across multiple sites or racks, or multiple loosely XDR synchronized clusters. The decision depends on the application's need for consistency, site autonomy, and data regulation requirements. &lt;/p&gt;

&lt;h3&gt;
  
  
  Rack Awareness
&lt;/h3&gt;

&lt;p&gt;For fast local reads and availability, data is replicated in a rack aware fashion where all sites are similar and each site holds its own copy of the entire data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-Side Processing
&lt;/h3&gt;

&lt;p&gt;The client directly connects to all server nodes, and there is no coordinator node to coordinate processing, and as such, operations like sorting and aggregation involve client side processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;. . . .&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Data modeling is the exercise of mapping application objects and access patterns onto the database’s native data model and mechanisms for optimal performance, efficiency, and consistency. Aerospike Database is purpose built for speed at scale, and provides a path to companies of any size to leverage large data for real-time decisions without incurring huge operational cost, and also scale in the future. The blog post described data modeling considerations when designing for speed-at-scale applications with the Aerospike Database.  In a future post, we will describe how CDTs can be used for data modeling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://medium.com/aerospike-developer-blog/developers-understanding-aerospike-transactions-1c0ad5cfc357"&gt;Developers: Understanding Aerospike Transactions&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/aerospike-developer-blog/microservices-with-aerospike-e6dac788b0e6"&gt;Microservices with Aerospike&lt;/a&gt; (blog post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/aerospike-examples/adaptive-map"&gt;Adaptive Map&lt;/a&gt; (code repo)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.aerospike.com"&gt;Aerospike Developer Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aerospike.com/"&gt;Aerospike Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datamodeling</category>
      <category>speedatscale</category>
      <category>nosql</category>
      <category>documentmodel</category>
    </item>
    <item>
      <title>Resiliency in Aerospike Multi-Site Clusters</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Mon, 15 Jun 2020 16:29:20 +0000</pubDate>
      <link>https://dev.to/aerospike/resiliency-in-aerospike-multi-site-clusters-32n1</link>
      <guid>https://dev.to/aerospike/resiliency-in-aerospike-multi-site-clusters-32n1</guid>
      <description>&lt;p&gt;As part of the recently announced &lt;a href="https://www.aerospike.com/products/database-platform/"&gt;Aerospike Database 5&lt;/a&gt;, multi-site clusters are now a supported configuration. Aerospike database has supported a cluster architecture for almost a decade. This post describes what is different in multi-site clusters. Specifically it describes in greater detail how multi-site clusters provide strong resiliency against a variety of failures&lt;/p&gt;

&lt;h4&gt;
  
  
  Accelerating Global Business Transactions
&lt;/h4&gt;

&lt;p&gt;The changes in the user behavior and expectations brought about by mobile devices and digital transformation are accelerating the trend of global business transactions. Today, users are connected 24x7, across the globe, and expect immediate results whether they are making payments to their friends, ordering a product on a site, or tracking a package. To respond to these changes, businesses must be always-on and respond quickly to their customers and partners that can be anywhere. The database is a key enabler of the technology platform driving these capabilities, and must have the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-site: has global footprint to reflect business presence&lt;/li&gt;
&lt;li&gt;always on: is able to automatically recover from a variety of failures&lt;/li&gt;
&lt;li&gt;strongly consistent: provides guarantees against staleness and loss of data&lt;/li&gt;
&lt;li&gt;cost effective: inexpensive to buy and efficient to operate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we will see below, a multi-site cluster is a good fit for these needs of global business transactions.&lt;/p&gt;

&lt;h1&gt;
  
  
  What are Multi-Site Clusters?
&lt;/h1&gt;

&lt;p&gt;A multi-site cluster is essentially a regular cluster: it has multiple nodes, and the data is sharded in many partitions, each with a number (replication factor) of replicas, and the replicas are evenly distributed across the nodes. But there are several important differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Geographically Separated
&lt;/h2&gt;

&lt;p&gt;As the name suggests, a multi-site cluster is a cluster that spans multiple sites. Sites can be located anywhere geographically, across the same city or on multiple continents, and involve hybrid and heterogeneous environments consisting of VMs, containers, and bare-metal machines in on-premise data centers, as well as private and public clouds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2db4kfhp5h9479wcbn0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2db4kfhp5h9479wcbn0m.png" alt="Alt Text" width="800" height="600"&gt;&lt;/a&gt; &lt;em&gt;A 3-site cluster&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Typically, nodes in a multi-site cluster are identically sized and evenly distributed across all sites to enable symmetry in performance and recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strongly Consistent
&lt;/h2&gt;

&lt;p&gt;The key use case for a multi-site cluster is business transactions, and as such consistency of data across all sites is critical. Therefore, a multi-site cluster is configured in the Strong Consistency (SC) mode. In the SC mode, all replicas are synchronously updated, thus maintaining one version of the data so that the replicas are immediately and always consistent. Eventually consistent systems allow multiple versions that must be merged, requiring applications to be willing to work with stale versions and lost writes.&lt;br&gt;
The other, Available during Partitions (AP), mode in Aerospike does not provide guarantees against lost or stale data in the event of a cluster split, for instance, and is unsuitable for business transactions that cannot tolerate inconsistent data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rack Aware
&lt;/h2&gt;

&lt;p&gt;An Aerospike cluster can have nodes across multiple physical racks, and rack-aware data distribution ensures balanced distribution of data across all racks. In the context of a multi-site cluster, a rack is equivalent to a site, and rack-aware data distribution ensures no duplicate data at any site when the replication factor (RF, the number of replicas for all data partitions) is less than or equal to the number of racks (N). It ensures at most one replica when RF &amp;lt; N, and exactly one replica when RF = N.&lt;/p&gt;

&lt;p&gt;Typically, a multi-site cluster is configured with RF=N because it provides the best performance and resiliency characteristics. With RF=N, each site has a full copy of the entire data, and therefore all reads can be very fast as they are performed on the local replica. Also, in case of a site failure, the remaining site(s) has all data to serve all requests.&lt;/p&gt;

&lt;p&gt;While a multi-site cluster with 2 or 3 sites with the replication factor of 2 or 3 respectively are common, different configurations with more sites and replication factors are also supported.&lt;/p&gt;

&lt;h1&gt;
  
  
  "Always On" Resiliency
&lt;/h1&gt;

&lt;p&gt;Aerospike clusters support &lt;a href="https://www.aerospike.com/blog/zero-downtime-upgrades-in-aerospike-made-even-simpler/"&gt;zero downtime upgrades&lt;/a&gt; with full data availability. Also in multi-site clusters, all planned events including upgrades, patches, and hardware maintenance can be performed with no interruption to service.&lt;/p&gt;

&lt;p&gt;Multi-site clusters also support automatic recovery from most node, site, and link failures. Automatic recovery and continuity in the event of a site failure is a key reason for businesses to choose multi-site clusters.&lt;/p&gt;

&lt;p&gt;How Aerospike recovers from various failures is explained further below.&lt;/p&gt;

&lt;h4&gt;
  
  
  Fast Failure Detection and Recovery
&lt;/h4&gt;

&lt;p&gt;All nodes maintain the healthy (or original) state of the cluster called "the roster" which includes the nodes and replica-to-node map for all data partitions. All nodes exchange heartbeat messages with every other node in the cluster at a regular (configurable, typically sub-second) interval. The heartbeat messages include all nodes that a node can communicate with. Using a specific number (configurable) of recent heartbeats, a primary node is able to determine quickly any failures in the cluster and the new membership of the cluster (i.e., the nodes that can all see one another), and it disseminates the new cluster definition to all connected nodes. With this information, each node can make independent decisions about any of its roster replicas that must be promoted to become the new master to replace failed masters, as well as any new replicas that it must create to replace failed replicas. This process of failure detection and recovery from it to form a new cluster takes just a few seconds.&lt;/p&gt;

&lt;h4&gt;
  
  
  Availability During Migration
&lt;/h4&gt;

&lt;p&gt;New replicas are populated by migrating data from the master. While migrations typically take longer than a few seconds, especially when they involve inter-site data transfer, the partition would remain available for all operations while the migration is in progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery from Failures
&lt;/h2&gt;

&lt;p&gt;Next we will look into the details of recovery from various failures. It is important to note the two invariants of the SC cluster and partition availability rules during a cluster split that ensure strong consistency.&lt;/p&gt;

&lt;h4&gt;
  
  
  SC Cluster Invariants
&lt;/h4&gt;

&lt;p&gt;The following two invariants are preserved in an operational SC cluster at all times. Recovery from a failure ensures these invariants are met before new requests are serviced.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A partition has exactly one master. At no time can there be more than one master in order to preserve a single version of data. Potential race conditions involving an old and a new master are resolved using the higher "regime" number of the newer master. The master is the first available roster replica in the partition's succession list of nodes. A partition's succession list is deterministic and is derived solely from the partition id and node ids.&lt;/li&gt;
&lt;li&gt;A partition has exactly RF replicas. These are the first RF cluster nodes in the partition's succession list of nodes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note the second invariant does not mean that all replicas must have all data for continued operation; partitions remain available while background migrations bring replicas to an up-to-date state.&lt;/p&gt;

&lt;h4&gt;
  
  
  Partition Availability Rules
&lt;/h4&gt;

&lt;p&gt;The following three rules dictate whether a data partition can be active in a sub-cluster resulting from a failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A sub-cluster has the majority of nodes and at least one replica for the partition.&lt;/li&gt;
&lt;li&gt;A sub-cluster has exactly half nodes and has the master replica for the partition.&lt;/li&gt;
&lt;li&gt;A sub-cluster has all replicas for the partition..&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Node Failures
&lt;/h3&gt;

&lt;p&gt;In the following diagram, the cluster consists of 9 nodes across 3 sites with a replication factor of 3. When a node fails, such as Node 1 as shown in the diagram, all partition replicas on the node become unavailable. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fohlp19nwg9clpookkbde.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fohlp19nwg9clpookkbde.png" alt="Alt Text" width="800" height="600"&gt;&lt;/a&gt; &lt;em&gt;Node Failure: Master roles are transferred and new replicas are created&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The cluster detects the node failure and acts to reinstate the two invariants mentioned above.&lt;/p&gt;

&lt;p&gt;For every master replica on Node 1, the next roster replica in the partition's succession list becomes the new master. As illustrated in the diagram, P111-R1 on Node 6 becomes the new master.&lt;/p&gt;

&lt;p&gt;For every replica on Node 1, a new replica is created to preserve the replication factor. Also, per rack-aware distribution rules, the new replica must reside on the Site 1 to preserve one replica per site. As illustrated in the diagram, new replicas P111-R3 on Node 3 and P222-R3 on Node 2 are created.&lt;/p&gt;

&lt;p&gt;With this, the cluster is ready to process new requests while the new replicas continue to be populated from their respective master.&lt;/p&gt;

&lt;h3&gt;
  
  
  Site and Link Failures
&lt;/h3&gt;

&lt;p&gt;The following diagram shows a site failure (Site 1) and link failures (Site1-Site2 and Site1-Site3). Both result in the same recovery and end state, and so let's consider the latter for our discussion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc8lya1j09nurp1mtsgz4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc8lya1j09nurp1mtsgz4.png" alt="Alt Text" width="800" height="600"&gt;&lt;/a&gt; &lt;em&gt;Site or Link Failures: Sites 2 and 3 form the new cluster&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When the links fail, the cluster is split into two sub-clusters: one with nodes 1–3 and the other with nodes 4–9. Each sub-cluster acts to first determine which partitions are active by applying the three rules mentioned above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The sub-cluster with nodes 4–9 has the majority of the nodes and therefore is the majority sub-cluster. Since it has at least one (actually exactly two, one each on Site 2 and Site 3) replica for every partition, it can serve all requests for all data. No data is available within the other (minority) sub-cluster.&lt;/li&gt;
&lt;li&gt;No sub-cluster has exactly half the nodes, and therefore this rule is not applicable. (This rule would apply for a 2-site cluster with even nodes.)&lt;/li&gt;
&lt;li&gt;No sub-cluster has all replicas for any partition, and therefore this rule is also not applicable. (With Rack Aware distribution, this rule is never applicable in multi-site clusters.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The majority sub-cluster then proceeds to  reinstate the two invariants and promotes appropriate roster replicas to replace master replicas in the other sub-cluster and also creates new replicas for all replicas in the other sub-cluster.&lt;/p&gt;

&lt;p&gt;With this, the cluster is ready to process new requests while the new replicas continue to be populated from their respective master.&lt;/p&gt;

&lt;h3&gt;
  
  
  Return to Healthy State
&lt;/h3&gt;

&lt;p&gt;When failures are fixed, the cluster returns to a healthy state through a similar process of detection and recovery described earlier. The roster (i.e., original) masters regain the master role, roster replicas are brought up-to-date, and any new replicas are dropped. Requests received by a replica while it is receiving updates are proxied to appropriate replica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing Resiliency in 2-Site and 3-Site Clusters
&lt;/h2&gt;

&lt;p&gt;The following table demonstrates how a 3-site (RF=3) cluster provides superior resiliency as compared to a 2-site (RF=2) cluster.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;em&gt;A 3-site cluster is more resilient than a 2-site cluster&lt;/em&gt;

&lt;p&gt;Essentially, a 3-site cluster automatically recovers with full availability of data from these failure that a 2-site cluster cannot: any single site failure and node failures spanning any two sites. It can also recover from a two site failure with manual intervention that requires re-rostering with surviving nodes.&lt;/p&gt;

&lt;p&gt;The scenarios are described in more detail below.&lt;/p&gt;

&lt;h4&gt;
  
  
  2-Site Cluster (RF=2)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Node failures&lt;/strong&gt;: The cluster can automatically recover from up to a minority node failures on the same site. For node failures across sites, the cluster will be partially available as some partitions would lose both replicas and thus would become unavailable. For a majority node failures, the cluster will neither recover automatically nor will be fully available.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Site failure&lt;/strong&gt;: The cluster can automatically recover from a minority site (ie, the site with fewer nodes) failure in the case of an odd nodes cluster. When the site with equal nodes (in an even nodes cluster) or majority nodes fails, a manual intervention to re-roster the remaining nodes is needed to make the cluster fully operational.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Link failure&lt;/strong&gt;: In an odd nodes cluster, the majority site will remain operational, and so will the applications that can connect to the majority site. In an even nodes cluster, both sites will remain operational for exactly half the partitions for which they have the master replica. For an application to work, it must be able to connect to all nodes on the two sites.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3-Site Cluster (RF=3)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Node failures&lt;/strong&gt;: Just like the 2-site cluster case, a 3-site cluster can automatically recover from up to a minority node failures across any two nodes. For node failures involving all three sites, the cluster will be partially available as some partitions would lose all 3 replicas and would become unavailable. For a majority node failures, the cluster will neither recover automatically nor will be fully available.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Site failures&lt;/strong&gt;: The cluster can automatically recover from a single site failure. When two sites fail, a manual intervention to re-roster the nodes on the third site is needed to make the cluster fully operational.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Link failures&lt;/strong&gt;: In one or two link failures that allow two sites to form a majority sub-cluster, an automatic recovery is possible. If all three inter-site links fail, the operator can decide to re-roster the nodes at any one of the three sites to create an operational cluster. For an application to work, it must be able to connect to all nodes of the operational cluster.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Other Considerations
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Write Latency&lt;/strong&gt;&lt;br&gt;
Write transactions update all replicas synchronously. Therefore write latency is dictated by the maximum separation between any two sites. This can range from a few milliseconds in a cluster spanning multiple zones in the same region to hundreds of milliseconds for a cluster across multiple continents. Application requirements for strong consistency, disaster recovery, and write latency must be balanced to come up with the optimal cluster design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node Sizing&lt;/strong&gt;&lt;br&gt;
Node sizing should take into account maximum node failures on a site, as the content and load from the failed nodes will be distributed to the remaining nodes on the same site in Rack Aware distribution. In the extreme case, it is possible to have a single node hold all the site's replicas and server all its requests, however the cluster may not function optimally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration Time&lt;/strong&gt;&lt;br&gt;
With high bandwidth connectivity, a replica can be migrated quickly. Migrations are typically throttled so that the cluster can provide adequate response to the normal workload. Applications are unlikely to experience higher latency during this period if the workload is not bandwidth intensive. Multiple simultaneous node failures may extend the migration duration and latency depending on the configuration and network bandwidth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global Data Infrastructure&lt;/strong&gt;&lt;br&gt;
For a different class of applications that requires fast write performance, selective replication across sites to meet regulatory requirements, autonomy of site operations, but that can live with less stringent consistency guarantees, Aerospike provides &lt;a href="http://pages.aerospike.com/rs/229-XUE-318/images/Aerospike_Solution_Brief_XDR.pdf"&gt;Cross Data-center Replication (XDR)&lt;/a&gt;. XDR can be combined with multi-site clusters to architect an optimal global data infrastructure to satisfy multiple applications.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Aerospike multi-site clusters span geographically distant sites and provide strong consistency and always-on availability at a low cost, making them a good fit for global business transactions. Multi-site clusters can quickly detect and recover from node, site, and link failures to provide "always-on" availability. Today, many deployments are successfully running mission critical transactions at scale with multi-site clusters. Multi-site clusters and Cross Data-center Replication(XDR) together provide the capabilities and flexibility in creating the optimal global data infrastructure within an enterprise.&lt;/p&gt;

</description>
      <category>distributeddatabases</category>
      <category>faulttolerance</category>
      <category>globaltransactions</category>
      <category>strongconsistency</category>
    </item>
    <item>
      <title>Twelve Do's of Consistency in Aerospike</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Thu, 28 May 2020 00:29:26 +0000</pubDate>
      <link>https://dev.to/aerospike/twelve-do-s-of-consistency-in-aerospike-30ac</link>
      <guid>https://dev.to/aerospike/twelve-do-s-of-consistency-in-aerospike-30ac</guid>
      <description>&lt;p&gt;For applications that demand absolute correctness of data, Aerospike offers the Strong Consistency (SC) mode that guarantees no stale or dirty data is read and no committed data is lost. Aerospike's &lt;a href="https://www.aerospike.com/lp/exploring-data-consistency-aerospike-enterprise-edition/"&gt;strong consistency support&lt;/a&gt; has been independently confirmed through &lt;a href="http://jepsen.io/analyses/aerospike-3-99-0-3"&gt;Jepsen testing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Developers building such applications should follow the following Twelve Do's of Consistency.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Model your data for single record atomicity. 
&lt;/h3&gt;

&lt;p&gt;The scope of a transaction in Aerospike is a single request and a single record. In other words, an atomic update can only be performed on a single record. Therefore model your data such that data that must be updated in a transaction (atomically) is kept in a single record. &lt;a href="https://www.aerospike.com/blog/modeling-data-aerospike/"&gt;Data modeling techniques&lt;/a&gt; like embedding, linking, and denormalization can be used to achieve this goal.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Configure the namespace in SC mode by setting strong-consistency to true.
&lt;/h3&gt;

&lt;p&gt;Per the CAP theorem, the system must make a choice between Availability and Consistency if it continues to function during a network partition. Aerospike offers both choices. A namespace (equivalent to a database or schema) in a cluster can be configured in AP (choosing Availability over Consistency) or SC (Strong Consistency, choosing Consistency over Availability) mode. All writes in SC mode are serialized and synchronously replicated to all replicas. ensuring one version and immediate consistency.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Use the Read-Modify-Write pattern for read-write transactions.
&lt;/h3&gt;

&lt;p&gt;In this pattern, the generation comparison check is included in the write policy. A record's generation is its version, and this check preserves validity of a write that is dependent on a previous read. The "Check-And-Set" (CAS) equality check with read generation would fail raising generation-error if another write has incremented the generation in the meanwhile. In which case, the entire Read-Modify-Write pattern must be retried.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;em&gt;Read-Modify-Write pattern for read-write transactions&lt;/em&gt;




&lt;h3&gt;
  
  
  4. Tag a write with a unique id to confirm if a transaction succeeded or failed.
&lt;/h3&gt;

&lt;p&gt;Uncertainty about a transaction's outcome can arise due to client, connection, and server failures. System load can lead to incomplete replication sequence before the request times out with "in-doubt" status. There is no transaction handle for the application to use to probe the status in this case. It must therefore tag a record with a unique id as part of the transaction, which it can use later to check if the transaction succeeded or failed.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;em&gt;Tagging a write with a unique id&lt;/em&gt;




&lt;h3&gt;
  
  
  5. Achieve multi-operation atomicity and only-once effect through Operate, predicate expressions, and various policies.
&lt;/h3&gt;

&lt;p&gt;The Aerospike operation &lt;a href="https://www.aerospike.com/docs/client/python/usage/kvs/write.html#multi-ops"&gt;Operate&lt;/a&gt; allows multiple operations to be performed atomically on a single record. It can be combined with various policies that enable conditional execution to achieve only-once effect. Examples include &lt;a href="https://www.aerospike.com/apidocs/python/predexp.html#module-aerospike.predexp"&gt;predicate expressions&lt;/a&gt; in &lt;a href="https://www.aerospike.com/apidocs/python/client.html#operate-policies"&gt;operate policy&lt;/a&gt;, insertion in map with &lt;a href="https://www.aerospike.com/apidocs/python/client.html#map-policies"&gt;create-only write mode&lt;/a&gt;, insertion in list with &lt;a href="https://www.aerospike.com/apidocs/python/aerospike.html#list-write-flags"&gt;add-unique write flag&lt;/a&gt;, and so on.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Simplify write transactions by making write only-once (idempotent).
&lt;/h3&gt;

&lt;p&gt;An only-once write (enabled by the mechanisms described in 5 above) becomes safe to just retry on failure. A prior success will result in an "already exists" failure which indicates prior successful execution of the transaction.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;em&gt;Safe retries with only-once write transactions&lt;/em&gt;




&lt;h3&gt;
  
  
  7. Record the details for subsequent handling in a batch or manual process if a write's outcome cannot be resolved.
&lt;/h3&gt;

&lt;p&gt;During a long duration cluster split event, the client may be unable to resolve a transaction's outcome. The client can timeout after retries but should record the details needed for external resolution such as the record key, transaction id, and write details.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;em&gt;Record transaction details for external resolution&lt;/em&gt;




&lt;h3&gt;
  
  
  8. Choose the optimal read mode. 
&lt;/h3&gt;

&lt;p&gt;There are four SC read modes to choose from: Linearizable, Session, Allow-replica, and Allow-unavailable. They all guarantee no data loss and no dirty reads, but differ in "no stale" guarantees as well as performance. A Linearizable read ensures the latest version across all clients, but it involves checking with all replicas and therefore is most expensive. Also, without additional external synchronization mechanism among clients, the version is not guaranteed to be the latest when it reaches the client. A Session read is faster as it directly reads from the master replica, and therefore recommended. In a multi-site cluster, local reads are much faster than remote reads. Since the master replica may reside at another site, the Allow-replica mode offers much better performance with no-stale guarantee practically equivalent to the Session mode, and therefore is recommended in multi-site clusters. There are no staleness guarantees with Allow-unavailable mode, but the application may judiciously leverage it when it is aware of stale data but can still derive positive value from it.&lt;/p&gt;




&lt;h3&gt;
  
  
  9. Use the default value for max-retries (zero) in write-policy.
&lt;/h3&gt;

&lt;p&gt;The max-retries value indicates the number of retries that the client library will perform automatically in case of a failure. Because the transaction logic is sensitive to the type of failure, a transaction failure must be handled in the application, not automatically by the client library. Therefore use the default value to turn off the automatic retries in the client library.&lt;/p&gt;




&lt;h3&gt;
  
  
  10. For maximum durability, commit each write to the disk on a per-transaction basis using commit-to-device setting.
&lt;/h3&gt;

&lt;p&gt;With this setting, a replica flushes the write buffer to disk before acknowledging back to the master. The application on a successful write operation is certain that the update is secure on the disk at each replica, thus achieving maximum possible durability. Be aware of the performance implications of flushing each write to disk (unless using data in PMEM), and balance it with the desired durability.&lt;/p&gt;




&lt;h3&gt;
  
  
  11. For exactly-once multi-record (non-atomic) updates use the pattern: record atomically - post at-least-once - process only-once.
&lt;/h3&gt;

&lt;p&gt;Aerospike does not support multi-record transactions. To implement exactly-once semantics for multi-record updates, record the event atomically in the first record as part of the update. Implement a process to collect the recorded event and post it for processing in the second record. At-least-once semantics can be achieved by removing the event only after successful hand-off to or execution of the subsequent step which would update another record with only-once semantics. This sequence achieves exactly-once execution of multi-record updates. The pattern is explored further in this &lt;a href="https://www.aerospike.com/blog/microservices-with-aerospike/"&gt;post&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  12. Resolve in-flight transactions during crash recovery by recording the transaction intent. 
&lt;/h3&gt;

&lt;p&gt;Before a write request is sent to the server, record the intent so that it can be read and retried if necessary during crash recovery. The intent is removed on successful execution as part of normal processing. During recovery, the intent list is read and retried.&lt;/p&gt;




</description>
      <category>consistency</category>
      <category>aerospike</category>
      <category>nosql</category>
      <category>transactions</category>
    </item>
    <item>
      <title>Resolving Uncertain Transactions in Aerospike</title>
      <dc:creator>Neel Phadnis</dc:creator>
      <pubDate>Thu, 28 May 2020 00:13:04 +0000</pubDate>
      <link>https://dev.to/aerospike/resolving-uncertain-transactions-in-aerospike-14ld</link>
      <guid>https://dev.to/aerospike/resolving-uncertain-transactions-in-aerospike-14ld</guid>
      <description>&lt;p&gt;For applications that desire strong consistency, it is important to not only have all replicas in the cluster be always in sync, but also to have application's knowledge of a transaction's outcome consistent with the actual outcome in the database. If the application is uncertain about a transaction's outcome, it must first resolve it so that it can either do nothing, retry the transaction, or arrange for external resolution.&lt;/p&gt;

&lt;p&gt;There are many situations that leave the client uncertain of a transaction's outcome. &lt;/p&gt;

&lt;p&gt;Let us first look at how a write transaction is processed in Aerospike. Executing a write request involves many interactions between the client, master, and replicas as follows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YLBQZ2AX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://docs.google.com/drawings/d/e/2PACX-1vQW-poyU6SCPmUPJ1ObUbgjNOW-vPBFP2GexFNnvrFx63F9PWK10PFLrXvMm9FU9SibuO0YWllZUyHu/pub%3Fw%3D944%26h%3D495" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YLBQZ2AX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://docs.google.com/drawings/d/e/2PACX-1vQW-poyU6SCPmUPJ1ObUbgjNOW-vPBFP2GexFNnvrFx63F9PWK10PFLrXvMm9FU9SibuO0YWllZUyHu/pub%3Fw%3D944%26h%3D495" alt="A write transaction sequence" width="800" height="420"&gt;&lt;/a&gt; &lt;em&gt;A write transaction sequence&lt;/em&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client sends the request to the master.&lt;/li&gt;
&lt;li&gt;Master processes the write locally.&lt;/li&gt;
&lt;li&gt;Master sends updates to all replicas.&lt;/li&gt;
&lt;li&gt;Replicas acknowledge to the master after applying the update locally.&lt;/li&gt;
&lt;li&gt;After receiving acknowledgements from all replicas, master returns success to client.&lt;/li&gt;
&lt;li&gt;Master sends an advisory "replicated" message to all replicas. Replicas are then free to serve this version without having to consult with the master.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Sources of uncertainty
&lt;/h1&gt;

&lt;p&gt;There are failures that prevent a write from successfully completing and/or client from receiving a clear success or failure status. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Client, connection, and master failures&lt;/em&gt;&lt;br&gt;
Failures like a client crash, connection error, node failure, or network partition can happen at any moment in the above sequence. Any of them can leave the client uncertain about a write's outcome. The client must resolve the uncertainty so that it can achieve the intended state.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In-Doubt status&lt;/em&gt;&lt;br&gt;
The client can also receive the "in-doubt" flag in a timeout error. The flag signifies that the master has still not finished the replication sequence with all replicas. Clearly, the client, connection, and master all must be healthy for a timeout error to be returned with the in-doubt flag set.&lt;/p&gt;

&lt;p&gt;Why wouldn't the master finish the replication sequence? The causes can be many including slow master, network, and/or replica; node and/or network failure, storage failure, and potentially other failures. In such a case, the time for recovery to be completed, either automated or manual, and the write to be resolved can be unpredictable. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How common are such uncertainties?&lt;/strong&gt;&lt;br&gt;
The frequency of the events will be determined by factors like the size of the cluster and load. The following table shows these events are common enough to ignore if the application desires strong consistency.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;em&gt;Events causing write uncertainty are common&lt;/em&gt; 
&lt;h1&gt;
  
  
  Aerospike model
&lt;/h1&gt;

&lt;p&gt;It is important to understand the transaction model in Aerospike before we describe a solution. Aerospike was designed for speed@scale, with the goal of keeping most common operations simple and predictably fast, and deferring complexity to applications for less common scenarios.&lt;/p&gt;

&lt;p&gt;Transactions in Aerospike span a single request and a single record. The API does not support a notion of a transaction id or a way to query a transaction status. Therefore, the application must devise its way to query a transaction status.&lt;/p&gt;

&lt;p&gt;The application links with the client library (Smart Client) that directly and transparently connects to all nodes in the cluster and dynamically adapts to cluster changes. Therefore, simply retrying a transaction that failed due to a recoverable cluster failure can result in success.&lt;/p&gt;
&lt;h1&gt;
  
  
  Resolving uncertainty: Potential solutions
&lt;/h1&gt;

&lt;p&gt;Some intuitive potential solutions unfortunately don't always work.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Potential solution 1: A polling read back in a loop until the record generation reflects an update.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Note the read must look for the generation (ie, version) of the record that reflects the write. On a timeout after several read attempts, the application may attempt to ascertain a failed write by "touching" the record that increments its generation without changing any data.&lt;/p&gt;

&lt;p&gt;Case 1. The write sequence has completed. &lt;/p&gt;

&lt;p&gt;Assuming no subsequent updates, the read will return the prior version if the write failed. Otherwise it will return the new version. &lt;br&gt;
However, if there are subsequent updates, the latest version will be returned and there is no way of knowing the outcome of the write in question.&lt;/p&gt;

&lt;p&gt;Case 2. The write sequence is still in progress.&lt;/p&gt;

&lt;p&gt;If the read request goes to the same master, the read will return the prior version if the write is not completed yet. If the client attempts a "touch"in this case, it will be queued for the original write to finish (and may time out). The write's outcome remains unknown.&lt;/p&gt;

&lt;p&gt;If the read request is directed to a new master because of a cluster transition, the new master will return the new version (ie, updated by the write in question, assuming no subsequent updates) if the write was replicated to a replica in the newly formed cluster prior to the cluster transition. Otherwise the previous version will be returned. If the client attempts a "touch"in this case, the original write will lose out (ie, fail when the cluster reforms) to the version in the new cluster regime. Just as in Case 1, if there are subsequent updates in the new cluster, the latest version will be returned and there is no way of knowing the outcome of the write in question.&lt;/p&gt;

&lt;p&gt;Thus, this potential solution cannot be used to resolve a write's outcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Potential solution 2: Retry the write&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When a retry can work: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when the write is idempotent: Great, as the application knows which writes are idempotent, &lt;/li&gt;
&lt;li&gt;when the original request has failed: Good if the application knows, e.g., when there is a timeout with no in-doubt flag, and &lt;/li&gt;
&lt;li&gt;when there is a newly formed cluster and the write was not replicated to it: Yes, but the application does  not know if this is the case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a retry will not work: &lt;br&gt;
A retry is not safe because it will duplicate the write when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the original write sequence has since completed successfully, or &lt;/li&gt;
&lt;li&gt;the new cluster has the write replicated to it prior to the partition. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, this potential solution doesn't work in a general case.&lt;/p&gt;
&lt;h1&gt;
  
  
  A general solution
&lt;/h1&gt;

&lt;p&gt;In order to query the status of a write, the application must tag the write with a unique id that it can use as a transaction handle. This must be done atomically with the write using Aerospike's multi-operation "operate" API.&lt;/p&gt;

&lt;p&gt;It also is very useful to implement a write with"only-once" semantics, and safely retry when there is uncertainty about the outcome. This can be accomplished by storing the txn-id as a unique item in a map or list bin in the record. Aerospike has the create-only map write flag to ensure that the entire multi-operation "operate" succeeds only-once. (Other mechanisms such as predicate expressions may be used instead.) Subsequent attempts would result in an "element exists" error and point to a prior successful execution of the write.&lt;/p&gt;

&lt;p&gt;Adding a key-value ordered map, txns: txn_id =&amp;gt; timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;key: { &lt;br&gt;
 … // record data&lt;br&gt;
 txns: map (txn_id =&amp;gt; timestamp) // transaction tag&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Below is the pseudo code for the general solution. &lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;em&gt;Resolving uncertainty by tagging a write with a unique id&lt;/em&gt; 

&lt;h1&gt;
  
  
  A simpler solution?
&lt;/h1&gt;

&lt;p&gt;Requiring a txns map or list in each record and tagging and checking txn-id with each write, and also trimming txns can be a significant space and time overhead. Can it be avoided or simplified?&lt;/p&gt;

&lt;p&gt;If consistency is absolutely needed, this (or equivalent variations) is the recommended solution. Without an in-built support in the API or server, currently this is a general way to resolve uncertainties around a transaction's outcome and ensuring "only/exactly once" semantics.&lt;/p&gt;

&lt;p&gt;However, simplifications are possible. Here are some things to consider to devise a simpler solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frequency of updates: If writes are rare (e.g., daily update to usage stats), it may be possible to read back and resolve a write's outcome.&lt;/li&gt;
&lt;li&gt;Uniqueness of update: Can a client identify its update in another way (i.e., without a txn-id) in a multi-client writes scenario?&lt;/li&gt;
&lt;li&gt;A handful of write clients: If there are a small number of write clients, a more efficient scheme can be devised such as client-specific versions in their own bins (assuming a client can serialize its writes).&lt;/li&gt;
&lt;li&gt;Likelihood of client, connection, node, or network partition failures: If such failures are rare, an application may decide to live with lost or duplicate writes for less critical data.&lt;/li&gt;
&lt;li&gt;Ability to serialize all writes through external synchronization: A simpler solution can be devised in this case.&lt;/li&gt;
&lt;li&gt;Ability to record uncertain writes and resolve them out of band: Log the details for external resolution and make appropriate data adjustments if necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Strong consistency requires the application to resolve uncertain transaction outcomes and implement safe retries. In order to query and resolve uncertain outcomes, the application needs to tag a transaction with a unique id. To achieve exactly-once write semantics, the application can use mechanisms available in Aerospike Operator, Map/List, and Predicate Expressions. In some cases, knowledge of data, operation, and architecture may be used to simplify the solution.&lt;/p&gt;

</description>
      <category>transactions</category>
      <category>consistency</category>
      <category>aerospike</category>
      <category>nosql</category>
    </item>
  </channel>
</rss>
