<?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: Arya Hegiste</title>
    <description>The latest articles on DEV Community by Arya Hegiste (@arya_hegiste_8528edf8cd29).</description>
    <link>https://dev.to/arya_hegiste_8528edf8cd29</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4166464%2F6ebbf156-6cac-423e-9ce7-78aec5216d12.png</url>
      <title>DEV Community: Arya Hegiste</title>
      <link>https://dev.to/arya_hegiste_8528edf8cd29</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arya_hegiste_8528edf8cd29"/>
    <language>en</language>
    <item>
      <title>Visualizing a Mixed-Entity DynamoDB Table: What Schema Inference Reveals—and What It Misses</title>
      <dc:creator>Arya Hegiste</dc:creator>
      <pubDate>Sat, 10 Oct 2026 18:05:27 +0000</pubDate>
      <link>https://dev.to/arya_hegiste_8528edf8cd29/visualizing-a-mixed-entity-dynamodb-table-what-schema-inference-reveals-and-what-it-misses-69c</link>
      <guid>https://dev.to/arya_hegiste_8528edf8cd29/visualizing-a-mixed-entity-dynamodb-table-what-schema-inference-reveals-and-what-it-misses-69c</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;One of the most interesting aspects of Amazon DynamoDB is that a table does not need to represent a single business entity.&lt;/p&gt;

&lt;p&gt;In a relational database, we might create separate tables for users, orders, and products, with foreign keys connecting related records. DynamoDB supports a different approach: multiple entity types can coexist in a single table, using carefully designed partition keys, sort keys, and secondary indexes to support application access patterns.&lt;/p&gt;

&lt;p&gt;This is commonly called &lt;strong&gt;single-table design&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, single-table design introduces an interesting challenge: how do we understand the logical structure of a table when the physical database does not enforce a complete schema for every item?&lt;/p&gt;

&lt;p&gt;To explore this, I used the &lt;strong&gt;Schema Visualizer in Tables by Serverless Creed&lt;/strong&gt; to examine two controlled DynamoDB datasets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 70-item dataset containing User, Order, and Product records.&lt;/li&gt;
&lt;li&gt;A smaller three-item dataset with explicit &lt;code&gt;EntityType&lt;/code&gt; attributes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The results revealed an important distinction between &lt;strong&gt;physical key patterns, logical entity boundaries, and inferred schemas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, I'll examine the underlying DynamoDB design, compare the visualizer's findings with the known datasets, and discuss what engineers should verify before trusting an inferred data model.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understanding the challenge of mixed-entity tables
&lt;/h2&gt;

&lt;p&gt;DynamoDB is a NoSQL database that supports flexible item attributes. Within a table, different items can contain different non-key attributes.&lt;/p&gt;

&lt;p&gt;For example, consider an application containing three logical entities:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity&lt;/th&gt;
&lt;th&gt;Example attributes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;Name, Email, Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order&lt;/td&gt;
&lt;td&gt;OrderId, Amount, Status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;td&gt;ProductName, Price, Stock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These entities can be represented in one DynamoDB table using a composite primary key.&lt;/p&gt;

&lt;p&gt;A partition key determines the logical partition-key value associated with an item, while the sort key distinguishes items within that partition-key value and supports ordered access.&lt;/p&gt;

&lt;p&gt;A possible design is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PK&lt;/th&gt;
&lt;th&gt;SK&lt;/th&gt;
&lt;th&gt;Logical entity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER#001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROFILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER#001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ORDER#001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRODUCT#001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;META&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that the User and Order records share the same partition key.&lt;/p&gt;

&lt;p&gt;This is intentional.&lt;/p&gt;

&lt;p&gt;If an application frequently needs a user's profile and orders, storing related items under the same partition key can support efficient access through DynamoDB's &lt;code&gt;Query&lt;/code&gt; operation.&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 python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TC-OPT-002-Blog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;KeyConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PK = :pk AND begins_with(SK, :prefix)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USER#001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORDER#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This illustrative Boto3 low-level client query retrieves order items under a specific user partition-key value.&lt;/p&gt;

&lt;p&gt;The important design decision is that &lt;strong&gt;the partition key represents an access grouping, not necessarily an entity type&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A single partition-key pattern can contain several logical entities.&lt;/p&gt;

&lt;p&gt;That distinction becomes particularly important when software attempts to infer the schema automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Experiment One: Analyzing a 70-item mixed-entity table
&lt;/h2&gt;

&lt;p&gt;My first experiment used the DynamoDB table:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TC-OPT-002-Test&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The dataset contained 70 items representing User profiles, Orders, and Products.&lt;/p&gt;

&lt;p&gt;The table used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composite primary key: &lt;code&gt;PK&lt;/code&gt; and &lt;code&gt;SK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On-demand capacity mode&lt;/li&gt;
&lt;li&gt;One Global Secondary Index, &lt;code&gt;GSI1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AWS Region: &lt;code&gt;ap-south-1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The known fixture contained:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record type&lt;/th&gt;
&lt;th&gt;Items&lt;/th&gt;
&lt;th&gt;Key structure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User profiles&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;USER#&amp;lt;userId&amp;gt;&lt;/code&gt; / &lt;code&gt;PROFILE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;USER#&amp;lt;userId&amp;gt;&lt;/code&gt; / &lt;code&gt;ORDER#&amp;lt;orderId&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Products&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PRODUCT#&amp;lt;productId&amp;gt;&lt;/code&gt; / &lt;code&gt;META&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Verifying an application access pattern
&lt;/h3&gt;

&lt;p&gt;Before examining the inferred schema, I verified that the table supported a practical query.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;USER#001&lt;/code&gt; as the partition-key value and &lt;code&gt;ORDER#&lt;/code&gt; as the sort-key prefix, the query returned two matching order items.&lt;/p&gt;

&lt;p&gt;This demonstrated an important advantage of the design: related orders could be retrieved using a key condition rather than scanning the entire table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 1 — Querying orders under a user partition key&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmkaqsro62ct5y8vu8wgd.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmkaqsro62ct5y8vu8wgd.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A DynamoDB &lt;code&gt;Query&lt;/code&gt; with a partition-key equality condition and sort-key prefix can target a specific access pattern. By contrast, a table-wide &lt;code&gt;Scan&lt;/code&gt; reads through the table and is generally less suitable for frequently executed, selective application requests.&lt;/p&gt;

&lt;p&gt;This is why DynamoDB data modeling begins with access patterns rather than simply copying a relational entity diagram.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. What the Schema Visualizer inferred
&lt;/h2&gt;

&lt;p&gt;After verifying the dataset, I opened the Schema Visualizer.&lt;/p&gt;

&lt;p&gt;The results were interesting.&lt;/p&gt;

&lt;p&gt;Although the known fixture contained three logical entity types, the visualizer reported:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Observed result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inferred entities&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationships&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global Secondary Indexes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attributes&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Displayed confidence&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The inferred entity groups were:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt; and &lt;strong&gt;Product&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Order was not represented as a separate inferred entity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 2 — Schema Visualizer showing two inferred entities&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdu5enpbwouzjd42a9c7x.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdu5enpbwouzjd42a9c7x.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why were Orders grouped with Users?
&lt;/h3&gt;

&lt;p&gt;The Key Structure view provided a useful explanation.&lt;/p&gt;

&lt;p&gt;The visualizer identified 60 items associated with the &lt;code&gt;USER#&amp;lt;userId&amp;gt;&lt;/code&gt; partition-key pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;40 items with &lt;code&gt;ORDER#&amp;lt;orderId&amp;gt;&lt;/code&gt; sort keys&lt;/li&gt;
&lt;li&gt;20 items with &lt;code&gt;PROFILE&lt;/code&gt; sort keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The remaining 10 items followed the &lt;code&gt;PRODUCT#&amp;lt;productId&amp;gt;&lt;/code&gt; partition-key pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 3 — Key structure showing 60 User-group items and 10 Product items&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1ytsnmyh46xzuvsrq66.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1ytsnmyh46xzuvsrq66.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This illustrates the central modeling problem.&lt;/p&gt;

&lt;p&gt;From the application's perspective, User and Order are different business entities.&lt;/p&gt;

&lt;p&gt;From the physical key perspective, both are stored under the same &lt;code&gt;USER#&lt;/code&gt; partition-key pattern.&lt;/p&gt;

&lt;p&gt;The visualizer's output was consistent with grouping those items by their shared partition-key structure.&lt;/p&gt;

&lt;p&gt;However, the screenshot does not establish the exact internal inference algorithm. We cannot conclude that partition-key prefixes were the only factor responsible for the grouping.&lt;/p&gt;

&lt;p&gt;The engineering lesson is broader:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A schema inferred from observed keys is a hypothesis about the logical model—not a definitive description of the application's business entities.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Examining the Global Secondary Index
&lt;/h2&gt;

&lt;p&gt;The original table also contained one active GSI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index: GSI1
Partition key: GSI1PK
Sort key: GSI1SK
Projection: ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Schema Visualizer's Access Paths view showed that 16 items in the inferred User group participated in this index, representing approximately 27% of that 60-item group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 4 — GSI access path and observed index coverage&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmlzsvgybiq1elcld44k.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmlzsvgybiq1elcld44k.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a useful example of a &lt;strong&gt;sparse index&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In DynamoDB, an item appears in a GSI only when the required index key attributes are present. A table can therefore contain many items while only a subset participates in a particular GSI.&lt;/p&gt;

&lt;p&gt;Sparse indexes are useful when an application needs an alternative access pattern for selected records without indexing every item.&lt;/p&gt;

&lt;p&gt;For example, an application might populate GSI keys only for orders requiring processing or records associated with a particular lookup pattern.&lt;/p&gt;

&lt;p&gt;The screenshot establishes that only a subset of the observed items participated in &lt;code&gt;GSI1&lt;/code&gt;; it does not establish why those particular records were indexed.&lt;/p&gt;

&lt;p&gt;Another important consideration is that a GSI introduces additional storage and write costs. Indexes should therefore be designed around demonstrated access requirements rather than created for every potentially useful attribute.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Experiment Two: Can explicit entity types clarify the model?
&lt;/h2&gt;

&lt;p&gt;The original experiment raised a useful question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when the logical entity type is explicitly recorded as an item attribute?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To investigate, I created a second disposable table:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TC-OPT-002-Blog&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This experiment was performed using &lt;strong&gt;Tables version 3.3.37&lt;/strong&gt; in &lt;code&gt;ap-south-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The table used the same &lt;code&gt;PK&lt;/code&gt; and &lt;code&gt;SK&lt;/code&gt; key names, but contained only three items and no GSI.&lt;/p&gt;
&lt;h3&gt;
  
  
  User record
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER#001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROFILE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EntityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Test User 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user1@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER#001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORDER#001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EntityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"OrderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORDER#001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1499&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PLACED"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Product record
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PRODUCT#001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"META"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EntityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PRODUCT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ProductName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wireless Keyboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1299&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Stock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important property is that User and Order still share the same partition-key value, &lt;code&gt;USER#001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The distinction is expressed through both the sort-key pattern and the explicit &lt;code&gt;EntityType&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 5 — Three-item fixture showing PK, SK, and EntityType&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhtwe2hgcm7tiezn38oa.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhtwe2hgcm7tiezn38oa.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Comparing the second inference result
&lt;/h2&gt;

&lt;p&gt;When I opened the Schema Visualizer for the smaller fixture, the output was different.&lt;/p&gt;

&lt;p&gt;This time, Tables inferred all three logical entities:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order, Product, and User.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The results were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Observed result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inferred entities&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationships&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSIs&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attributes&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Displayed confidence&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Figure 6 — Three inferred entities in the smaller fixture&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5a3ejebqxrvzdvrfo4f.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5a3ejebqxrvzdvrfo4f.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Key Structure view provided additional detail.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Inferred entity&lt;/th&gt;
&lt;th&gt;PK pattern&lt;/th&gt;
&lt;th&gt;SK pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Order&lt;/td&gt;
&lt;td&gt;&lt;code&gt;USER#&amp;lt;userId&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ORDER#&amp;lt;orderId&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;&lt;code&gt;USER#&amp;lt;userId&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROFILE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PRODUCT#&amp;lt;productId&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;META&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Figure 7 — Distinct User and Order groups sharing a PK pattern&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhiwkq8g3xypm1cqj63f.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhiwkq8g3xypm1cqj63f.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This result is especially interesting because the partition-key pattern alone still cannot distinguish User from Order.&lt;/p&gt;

&lt;p&gt;The two records share the same &lt;code&gt;USER#&lt;/code&gt; pattern, but differ in their sort keys and explicit entity types.&lt;/p&gt;

&lt;p&gt;The second fixture therefore demonstrates that the visualizer can represent distinct logical entities sharing a partition-key pattern.&lt;/p&gt;
&lt;h3&gt;
  
  
  Did &lt;code&gt;EntityType&lt;/code&gt; cause the difference?
&lt;/h3&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;The experiments differed in multiple ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dataset size: 70 items versus 3&lt;/li&gt;
&lt;li&gt;Attribute distribution&lt;/li&gt;
&lt;li&gt;Number of GSIs&lt;/li&gt;
&lt;li&gt;Distribution of sort-key patterns&lt;/li&gt;
&lt;li&gt;Explicit entity-type values in the smaller fixture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because these variables were not controlled independently, the results do not prove that adding &lt;code&gt;EntityType&lt;/code&gt; alone changed the inference.&lt;/p&gt;

&lt;p&gt;A stronger causal experiment would compare identical fixtures with and without the &lt;code&gt;EntityType&lt;/code&gt; attribute while holding the other variables constant.&lt;/p&gt;

&lt;p&gt;For this article, the supported conclusion is narrower: &lt;strong&gt;the smaller fixture, which included explicit entity types, produced three inferred groups, while the original larger fixture produced two.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  7. Verifying entity types independently of schema inference
&lt;/h2&gt;

&lt;p&gt;A visualizer is useful for understanding the model, but an engineer should also verify the underlying records.&lt;/p&gt;

&lt;p&gt;I applied two filters in the table's data workspace.&lt;/p&gt;

&lt;p&gt;First:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EntityType = ORDER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was the order item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK: USER#001
SK: ORDER#001
EntityType: ORDER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Figure 8 — Filtering for Order items&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkzqi50kxz1ifh3rpqs9.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkzqi50kxz1ifh3rpqs9.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EntityType = USER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was the profile item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK: USER#001
SK: PROFILE
EntityType: USER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Figure 9 — Filtering for User items&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjridzwvwq3ird4zs9n5e.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjridzwvwq3ird4zs9n5e.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both filters scanned three items and returned one matching item.&lt;/p&gt;

&lt;p&gt;This confirms that the logical entity types can be distinguished directly from their stored attributes, even when they share a partition-key value.&lt;/p&gt;
&lt;h3&gt;
  
  
  An important performance consideration
&lt;/h3&gt;

&lt;p&gt;The verification used a &lt;code&gt;Scan&lt;/code&gt; with an attribute filter.&lt;/p&gt;

&lt;p&gt;This is acceptable for inspecting a three-item test fixture, but it should not automatically become the production access strategy.&lt;/p&gt;

&lt;p&gt;DynamoDB filter expressions are applied after items have been read. They do not reduce the read capacity consumed by the underlying scan or query.&lt;/p&gt;

&lt;p&gt;For application workloads, a better approach is often to use the primary key and sort-key conditions directly.&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;PK = USER#001
SK begins_with ORDER#
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This retrieves orders associated with a specific user without scanning unrelated partition-key values.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;EntityType&lt;/code&gt; attribute remains valuable for semantic clarity, application validation, and analytical interpretation, but it is not automatically an efficient query key.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Why schema inference requires engineering judgment
&lt;/h2&gt;

&lt;p&gt;Comparing the two experiments reveals several limitations engineers should understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inference depends on observed data
&lt;/h3&gt;

&lt;p&gt;A DynamoDB table may contain optional attributes, rare entity types, and records that appear only in certain application states.&lt;/p&gt;

&lt;p&gt;If an inference process examines only a subset of the data, uncommon patterns may be absent from the sample.&lt;/p&gt;

&lt;p&gt;Therefore, the inferred model should not be treated as a guaranteed complete representation of every item in the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confidence is not a completeness guarantee
&lt;/h3&gt;

&lt;p&gt;The original visualizer displayed 95% confidence while identifying two entities.&lt;/p&gt;

&lt;p&gt;The smaller fixture displayed 91% confidence while identifying three.&lt;/p&gt;

&lt;p&gt;These percentages should not be interpreted as a verified probability that the application model is correct or complete. Without the internal confidence calculation, their precise statistical meaning is unknown.&lt;/p&gt;

&lt;p&gt;A higher displayed confidence does not necessarily mean more business entities have been identified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relationships are not always explicit
&lt;/h3&gt;

&lt;p&gt;Both experiments reported zero inferred relationships.&lt;/p&gt;

&lt;p&gt;That does not prove that the application has no logical relationships.&lt;/p&gt;

&lt;p&gt;In single-table design, relationships can be represented through shared partition keys, sort-key conventions, and application-level access patterns rather than foreign-key constraints.&lt;/p&gt;

&lt;p&gt;The User–Order association in this experiment is an example.&lt;/p&gt;

&lt;h3&gt;
  
  
  An inferred model is not the same as a deployed schema
&lt;/h3&gt;

&lt;p&gt;In the original test, I also opened the inferred design in the Single-Table Modeler, where four access patterns were reported as valid.&lt;/p&gt;

&lt;p&gt;This was useful for reviewing whether the inferred design could represent the configured access patterns.&lt;/p&gt;

&lt;p&gt;However, model validation should not be confused with independent proof that every production query will perform efficiently or that every possible item shape has been discovered.&lt;/p&gt;

&lt;p&gt;The visualizer and modeler are engineering aids. They do not replace workload testing and independent verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Practical recommendations for DynamoDB engineers
&lt;/h2&gt;

&lt;p&gt;Based on these experiments, I would apply the following principles when working with mixed-entity DynamoDB tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design around access patterns.&lt;/strong&gt; Decide which queries the application needs before choosing partition keys, sort keys, and GSIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make logical entity types explicit where useful.&lt;/strong&gt; An attribute such as &lt;code&gt;EntityType&lt;/code&gt; improves clarity, validation, and troubleshooting, especially when different entities share key prefixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use sort-key conventions consistently.&lt;/strong&gt; Patterns such as &lt;code&gt;PROFILE&lt;/code&gt;, &lt;code&gt;ORDER#&amp;lt;orderId&amp;gt;&lt;/code&gt;, and &lt;code&gt;META&lt;/code&gt; help express the intended item structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat inferred schemas as working models.&lt;/strong&gt; Compare them with known fixtures, application code, and real query requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inspect sparse GSI participation.&lt;/strong&gt; A GSI may intentionally contain only selected records. Verify that its coverage matches the intended access pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate semantic filtering from efficient retrieval.&lt;/strong&gt; An attribute filter may distinguish entity types, but a key condition is generally preferable for selective access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify the data independently.&lt;/strong&gt; A diagram can reveal patterns, but direct item inspection and DynamoDB queries establish what is actually stored.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Conclusion
&lt;/h2&gt;

&lt;p&gt;DynamoDB single-table design allows multiple business entities to share a physical table and even the same partition-key pattern.&lt;/p&gt;

&lt;p&gt;That flexibility is powerful, but it also makes schema discovery more complicated than simply reading a table definition.&lt;/p&gt;

&lt;p&gt;In my first experiment, the Schema Visualizer examined a 70-item mixed-entity fixture and inferred two groups, even though the known application dataset contained User, Order, and Product records.&lt;/p&gt;

&lt;p&gt;In the second experiment, a three-item fixture with explicit &lt;code&gt;EntityType&lt;/code&gt; attributes produced three inferred entities, including separate User and Order groups sharing the same partition-key pattern.&lt;/p&gt;

&lt;p&gt;The comparison highlights an important principle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A DynamoDB key structure tells us how data is organized for access. It does not always tell us where one business entity ends and another begins.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tools such as the Schema Visualizer can make key patterns, inferred entities, and secondary-index access paths easier to understand. But their results become most valuable when engineers compare those inferences with the known data model and the application's actual access requirements.&lt;/p&gt;

&lt;p&gt;The goal is not merely to generate a diagram.&lt;/p&gt;

&lt;p&gt;It is to understand whether the inferred structure accurately represents the system we intend to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;Tables by Serverless Creed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmed application version&lt;/td&gt;
&lt;td&gt;3.3.37 (second experiment)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Amazon DynamoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Region&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ap-south-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original dataset&lt;/td&gt;
&lt;td&gt;70 items, 1 GSI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow-up dataset&lt;/td&gt;
&lt;td&gt;3 items, 0 GSIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary key&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PK&lt;/code&gt; (String), &lt;code&gt;SK&lt;/code&gt; (String)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;Tables queries, scans, filters, and Schema Visualizer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: The original 70-item experiment's application version was not independently recorded.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Want to give it a try: &lt;a href="https://tables.serverlesscreed.com/" rel="noopener noreferrer"&gt;https://tables.serverlesscreed.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>database</category>
    </item>
    <item>
      <title>Import CSV Data into DynamoDB with Tables: Validate, Fix, and Retry Invalid Rows</title>
      <dc:creator>Arya Hegiste</dc:creator>
      <pubDate>Wed, 07 Oct 2026 02:16:06 +0000</pubDate>
      <link>https://dev.to/arya_hegiste_8528edf8cd29/import-csv-data-into-dynamodb-with-tables-validate-fix-and-retry-invalid-rows-2534</link>
      <guid>https://dev.to/arya_hegiste_8528edf8cd29/import-csv-data-into-dynamodb-with-tables-validate-fix-and-retry-invalid-rows-2534</guid>
      <description>&lt;p&gt;Importing CSV data into DynamoDB involves more than moving rows from a file into a table. Before writing anything, it is important to understand how source columns map to DynamoDB attributes, how types are inferred, and what happens when a record is missing a required key.&lt;/p&gt;

&lt;p&gt;In this walkthrough, I use Tables by Serverless Creed to import CSV data into DynamoDB and examine how the workflow handles valid records, type inference, and rows with missing partition or sort keys.&lt;/p&gt;

&lt;p&gt;We’ll specifically look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Importing a large CSV fixture&lt;/li&gt;
&lt;li&gt;Reviewing type inference&lt;/li&gt;
&lt;li&gt;Testing records with missing DynamoDB keys&lt;/li&gt;
&lt;li&gt;Confirming invalid input is blocked before import&lt;/li&gt;
&lt;li&gt;Correcting the input and retrying&lt;/li&gt;
&lt;li&gt;Verifying the recovered records in DynamoDB
The goal is not simply to see an “import completed” message. We want to verify what actually reached the table and understand how to recover when validation fails.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;1. Prepare the import fixture&lt;/strong&gt;&lt;br&gt;
For this exercise, I used the disposable DynamoDB table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TC-IMP-002-Test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table uses a composite key consisting of a partition key and sort key.&lt;/p&gt;

&lt;p&gt;The main CSV fixture contained 1,000 source rows designed to exercise several import behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Valid records&lt;/li&gt;
&lt;li&gt;Duplicate-key records&lt;/li&gt;
&lt;li&gt;Records missing required keys&lt;/li&gt;
&lt;li&gt;A value designed to inspect type inference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the initial import, the DynamoDB table contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;961 items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stored-item count should not automatically be interpreted as “961 accepted and 39 rejected.”&lt;/p&gt;

&lt;p&gt;Duplicate DynamoDB keys can overwrite an existing item rather than increase the number of unique items in the table. Also, because the original import completion dialog was not retained, I do not have direct evidence for the exact accepted, rejected, and duplicate counters from that run.&lt;/p&gt;

&lt;p&gt;For that reason, I use the resulting table state as an observation rather than reconstructing import-summary numbers that were not captured.&lt;/p&gt;

&lt;p&gt;Figure 1 — DynamoDB table after the initial CSV import&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8edeoh8378lm0ar58kmt.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8edeoh8378lm0ar58kmt.png" alt=" " width="799" height="423"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;2. Inspect type inference instead of assuming a validation error&lt;/strong&gt;&lt;br&gt;
One record in the fixture was intentionally given a value that looked incompatible with a numeric field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK: INVALID#001
Value: not-a-number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reasonable assumption might be that the importer would reject the record because not-a-number is not numeric.&lt;/p&gt;

&lt;p&gt;That is not what happened in this test.&lt;/p&gt;

&lt;p&gt;After the import, I inspected INVALID#001 in Tables. The value had been imported as a DynamoDB String.&lt;/p&gt;

&lt;p&gt;In other words, the importer did not treat this value as a malformed number. Its type inference allowed the value to be represented as text.&lt;/p&gt;

&lt;p&gt;Figure 2 — &lt;code&gt;not-a-number&lt;/code&gt; imported as a String through type inference&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmd75kebonhumr5mj9nq.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmd75kebonhumr5mj9nq.png" alt=" " width="799" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an important distinction when investigating an import.&lt;/p&gt;

&lt;p&gt;A value that appears invalid according to an expected application schema is not necessarily invalid according to DynamoDB's data model or the importer's inferred type.&lt;/p&gt;

&lt;p&gt;Required key violations, however, are different.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;3. Create a controlled missing-key test&lt;/strong&gt;&lt;br&gt;
To test a clear validation failure, I used a smaller CSV containing three rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Value&lt;/span&gt;
&lt;span class="k"&gt;TEST&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mf"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;PROFILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Valid&lt;/span&gt; &lt;span class="k"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;PROFILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Missing&lt;/span&gt; &lt;span class="k"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;TEST&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mf"&gt;003&lt;/span&gt;&lt;span class="p"&gt;,,&lt;/span&gt;&lt;span class="k"&gt;Missing&lt;/span&gt; &lt;span class="k"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first record contains both required keys.&lt;/p&gt;

&lt;p&gt;The second record is missing the partition key (PK).&lt;/p&gt;

&lt;p&gt;The third record is missing the sort key (SK).&lt;/p&gt;

&lt;p&gt;This gives us one valid control record and two deliberately invalid records.&lt;/p&gt;

&lt;p&gt;When this file was loaded into the import workflow, the preview made the missing key values visible before the import was allowed to proceed.&lt;/p&gt;

&lt;p&gt;Figure 3 — Import preview containing missing PK and SK values&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jalvdacqjj09rc3edqx.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jalvdacqjj09rc3edqx.png" alt=" " width="799" height="423"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;4. Block invalid rows before writing&lt;/strong&gt;&lt;br&gt;
Tables identified the key problem during preview validation.&lt;/p&gt;

&lt;p&gt;The interface reported:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;2 preview rows are missing a required key value. Fix the source data or mapping before importing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Start import action was disabled.&lt;/p&gt;

&lt;p&gt;This is an important safety behavior because DynamoDB requires the complete primary key for an item. Rather than beginning the import and discovering the key problem after writes had started, the workflow blocked this controlled import during validation.&lt;/p&gt;

&lt;p&gt;The screen also showed the target table and import configuration, making it possible to confirm where the operation would write before proceeding.&lt;/p&gt;

&lt;p&gt;Figure 4 — Import blocked because two rows are missing required key values&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3rz41xr6izcqxkd8ephw.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3rz41xr6izcqxkd8ephw.png" alt=" " width="799" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, the correct recovery was not to bypass the validation. The source data or mapping needed to be corrected.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;5. Correct the source and retry&lt;/strong&gt;&lt;br&gt;
I corrected the three-row CSV so that every record had both required key values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Value&lt;/span&gt;
&lt;span class="k"&gt;TEST&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mf"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;PROFILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Valid&lt;/span&gt; &lt;span class="k"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;TEST&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mf"&gt;002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;PROFILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Fixed&lt;/span&gt; &lt;span class="k"&gt;Missing&lt;/span&gt; &lt;span class="k"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;TEST&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mf"&gt;003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;PROFILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Fixed&lt;/span&gt; &lt;span class="k"&gt;Missing&lt;/span&gt; &lt;span class="k"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After loading the corrected input, the preview changed to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Preview validation passed. The streaming import is ready to run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The previously blocked import could now proceed.&lt;/p&gt;

&lt;p&gt;Figure 5 — Corrected input passes validation and is ready for retry&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbvvif19uzdytxukcd6f.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbvvif19uzdytxukcd6f.png" alt=" " width="799" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This creates a clear recovery sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Missing required keys
        ↓
Preview validation fails
        ↓
Import blocked
        ↓
Correct source/mapping
        ↓
Preview validation passes
        ↓
Retry import
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No special workaround was needed. The recovery consisted of correcting the data so it satisfied the target table's key requirements and then rerunning the normal import workflow.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;6. Verify the recovered data in DynamoDB&lt;/strong&gt;&lt;br&gt;
An import completion message is useful, but I also wanted to verify that the corrected data actually existed in DynamoDB.&lt;/p&gt;

&lt;p&gt;After the retry, I read back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK: TEST#002
SK: PROFILE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting item contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Fixed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Missing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;PK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Value:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table count had also increased from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;961
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;964
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which is consistent with the three corrected test records being added as unique items.&lt;/p&gt;




&lt;p&gt;Figure 6 — Corrected &lt;code&gt;TEST#002&lt;/code&gt; item read back after the successful retry&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2dmcxgmgcz4yht0jni37.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2dmcxgmgcz4yht0jni37.png" alt=" " width="799" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This final read-back is useful because it verifies the resulting table state rather than relying solely on the import UI.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Type inference and key validation solve different problems&lt;/strong&gt;&lt;br&gt;
One of the more useful lessons from this exercise is the difference between type inference and required-key validation.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Value = not-a-number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The importer could represent that value as a string, so it was not necessarily invalid for DynamoDB.&lt;/p&gt;

&lt;p&gt;But this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK = &amp;lt;missing&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cannot produce a valid item for a table whose primary key requires &lt;code&gt;PK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is why the second case produced a blocking validation error while the first did not.&lt;/p&gt;

&lt;p&gt;When investigating rejected or unexpected CSV imports, it helps to separate these questions:&lt;br&gt;
&lt;strong&gt;1. Can the source value be represented as a DynamoDB type?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;2. Does the resulting item satisfy the target table's required key structure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They are related to import validation, but they are not the same check.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Why read-back verification matters&lt;/strong&gt;&lt;br&gt;
Import tools can tell us whether an operation started, completed, or encountered validation errors.&lt;/p&gt;

&lt;p&gt;But when correctness matters, it is useful to verify the resulting DynamoDB state independently.&lt;/p&gt;

&lt;p&gt;For this exercise, the strongest recovery evidence was not simply that the corrected preview said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Preview validation passed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was that the corrected item could subsequently be read from the table with the expected key and values.&lt;/p&gt;

&lt;p&gt;A useful validation workflow is therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CSV source
   ↓
Preview and mapping
   ↓
Validation
   ↓
Import
   ↓
DynamoDB read-back
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If validation fails, correct the source or mapping and repeat the same path rather than assuming the failed records were written.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
CSV imports into DynamoDB become easier to troubleshoot when validation is treated as part of the workflow rather than just a gate before the import.&lt;/p&gt;

&lt;p&gt;Using Tables, I was able to inspect inferred values, reproduce a controlled missing-key failure, see the import blocked before proceeding, correct the affected records, retry the operation, and verify the recovered data in DynamoDB.&lt;/p&gt;

&lt;p&gt;The test also demonstrated why apparent data-type problems and required-key problems should be investigated separately. A value such as &lt;code&gt;not-a-number&lt;/code&gt; may still be valid as a DynamoDB string, while a missing required partition or sort key prevents the item from satisfying the table's key schema.&lt;/p&gt;

&lt;p&gt;Most importantly, don't stop verification at “Import completed.”&lt;/p&gt;

&lt;p&gt;For repeatable CSV-to-DynamoDB workflows, a stronger process is:&lt;br&gt;
&lt;strong&gt;preview → validate → import → read back.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That final read-back provides evidence of what actually reached DynamoDB.&lt;/p&gt;




&lt;p&gt;Give it a try on &lt;a href="https://tables.serverlesscreed.com/" rel="noopener noreferrer"&gt;https://tables.serverlesscreed.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Exporting DynamoDB Data Safely to CSV with Tables</title>
      <dc:creator>Arya Hegiste</dc:creator>
      <pubDate>Tue, 06 Oct 2026 18:14:53 +0000</pubDate>
      <link>https://dev.to/arya_hegiste_8528edf8cd29/exporting-dynamodb-data-safely-to-csv-with-tables-ddj</link>
      <guid>https://dev.to/arya_hegiste_8528edf8cd29/exporting-dynamodb-data-safely-to-csv-with-tables-ddj</guid>
      <description>&lt;p&gt;Exporting DynamoDB data to CSV sounds straightforward until the dataset contains values that spreadsheets interpret differently from plain text.&lt;/p&gt;

&lt;p&gt;Formula-like strings such as =2+3, Unicode text, commas, quotes, embedded newlines, nested DynamoDB values, binary data, booleans, and nulls can all make a seemingly simple export more interesting.&lt;/p&gt;

&lt;p&gt;In this walkthrough, I used Tables by Serverless Creed to export a controlled DynamoDB dataset to CSV and verified both the raw file and its behavior when opened in Microsoft Excel.&lt;/p&gt;

&lt;p&gt;The goal was to answer a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a DynamoDB result be exported to CSV without losing its structure or allowing formula-like strings to execute unexpectedly in a spreadsheet?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Test setup&lt;/strong&gt;&lt;br&gt;
I created a disposable DynamoDB table named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TC-EXP-002-Blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table used a composite primary key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK  — String
SK  — String
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was created in &lt;code&gt;ap-south-1&lt;/code&gt; using on-demand billing.&lt;/p&gt;

&lt;p&gt;I inserted eight controlled items covering several CSV edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Formula-like strings such as =2+3, +1+2, @formula-test, and -10+5&lt;/li&gt;
&lt;li&gt;Unicode text: नमस्ते 日本語 🚀&lt;/li&gt;
&lt;li&gt;Commas inside values&lt;/li&gt;
&lt;li&gt;Double quotes&lt;/li&gt;
&lt;li&gt;An embedded newline&lt;/li&gt;
&lt;li&gt;Nested maps and lists&lt;/li&gt;
&lt;li&gt;A null value&lt;/li&gt;
&lt;li&gt;Binary data&lt;/li&gt;
&lt;li&gt;Numbers and booleans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS CLI verification confirmed that the table contained exactly eight items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Count: 8
ScannedCount: 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsshv9wei3m93xup9rq5.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsshv9wei3m93xup9rq5.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Configuring the CSV export&lt;/strong&gt;&lt;br&gt;
From the table's data workspace, I opened Export and selected CSV as the output format.&lt;/p&gt;

&lt;p&gt;The export review screen showed several useful details before any file was written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Export mode: Streaming
Scope: All matching rows across all pages
Format: CSV
Privacy: Original values
File safety: Atomic replacement
Destination: Local file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interface also displayed an important spreadsheet-safety message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Formula-like strings and column names receive a leading apostrophe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This matters because applications such as Excel can interpret values beginning with characters such as &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, or &lt;code&gt;@&lt;/code&gt; as formulas rather than ordinary data.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8k8vr7l5i99tpehjv88i.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8k8vr7l5i99tpehjv88i.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Inspecting the raw CSV first&lt;/strong&gt;&lt;br&gt;
After exporting the eight rows, I deliberately inspected the CSV as a raw text file before opening it in Excel.&lt;/p&gt;

&lt;p&gt;This is an important verification step. A spreadsheet application can transform or interpret CSV values while opening the file, so looking only at Excel does not necessarily tell us what the exporter actually wrote.&lt;/p&gt;

&lt;p&gt;The raw file contained entries such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'=2+3
'+1+2
'@formula-test
'-10+5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leading apostrophe was present in the exported data.&lt;/p&gt;

&lt;p&gt;That means the current export successfully neutralized the formula-like strings before they reached the spreadsheet.&lt;/p&gt;

&lt;p&gt;The raw CSV also preserved the Unicode value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;नमस्ते 日本語 🚀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other edge cases were visible as well.&lt;/p&gt;

&lt;p&gt;A value containing quotes was CSV-escaped appropriately, while comma-containing values were quoted. The embedded newline remained part of its quoted CSV field.&lt;/p&gt;

&lt;p&gt;Nested DynamoDB data was serialized into string representations suitable for the flattened CSV format.&lt;/p&gt;

&lt;p&gt;Binary data containing &lt;code&gt;Hello&lt;/code&gt; appeared as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;`SGVsbG8=`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The null attribute was represented as an empty CSV field.&lt;/p&gt;

&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foifaiqdjci5ahjli4teq.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foifaiqdjci5ahjli4teq.png" alt=" " width="800" height="136"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Opening the export in Excel&lt;/strong&gt;&lt;br&gt;
I then opened the same CSV file directly in Microsoft Excel.&lt;/p&gt;

&lt;p&gt;The most important result was that the formula-like values were no longer executed.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=2+3 → 5
+1+2 → 3
-10+5 → -5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the exported values remained text.&lt;/p&gt;

&lt;p&gt;This confirms that the spreadsheet-safety transformation visible in the raw CSV prevented these values from being evaluated as formulas in this test.&lt;/p&gt;

&lt;p&gt;Numbers and boolean values were also retained, and the CSV structure remained readable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One important Unicode observation&lt;/strong&gt;&lt;br&gt;
There was, however, a difference between the raw file and Excel.&lt;/p&gt;

&lt;p&gt;The raw CSV contained the expected Unicode text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;नमस्ते 日本語 🚀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I opened the CSV directly in my Excel environment, that text displayed as garbled characters.&lt;/p&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;The raw-file inspection showed that the Unicode data existed correctly in the exported file. Therefore, the evidence from this test does not show that Tables destroyed the Unicode value during export.&lt;/p&gt;

&lt;p&gt;Instead, the issue appeared during the way this Excel environment interpreted the CSV when opening it directly.&lt;/p&gt;

&lt;p&gt;This is exactly why checking both the raw output and the spreadsheet representation is useful.&lt;/p&gt;

&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5oz7r2ms07ls9zhkxik.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5oz7r2ms07ls9zhkxik.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Testing export failure and recovery&lt;/strong&gt;&lt;br&gt;
I also wanted to verify what happened when the destination itself was invalid.&lt;/p&gt;

&lt;p&gt;During another export, I deliberately entered the following Windows filename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TC-EXP-002:invalid.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;:&lt;/code&gt; is not permitted in a normal Windows filename, the operating system rejected it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The file name is not valid.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The invalid destination therefore did not produce a final CSV presented as a successful export.&lt;/p&gt;

&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F74vsty22av29jhiypiis.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F74vsty22av29jhiypiis.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I then corrected the destination to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;TC&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;EXP&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;002&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;Blog&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;Recovery&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;csv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and retried the export.&lt;/p&gt;

&lt;p&gt;The corrected export downloaded successfully.&lt;/p&gt;

&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xaea08jjlvoy18ji0cl.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xaea08jjlvoy18ji0cl.png" alt=" " width="606" height="55"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This provided a simple failure-and-recovery check without modifying the DynamoDB source data.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A useful lesson: verify the file at two levels&lt;/strong&gt;&lt;br&gt;
The most useful lesson from this test was not simply that the export completed successfully.&lt;/p&gt;

&lt;p&gt;It was that CSV verification should happen at two different levels.&lt;/p&gt;

&lt;p&gt;First, inspect the raw file to determine what the exporter actually produced.&lt;/p&gt;

&lt;p&gt;Then open the file in the intended consumer, such as Excel, to determine how that application interprets it.&lt;/p&gt;

&lt;p&gt;Those are not always the same thing.&lt;/p&gt;

&lt;p&gt;In this test, the raw CSV proved that formula-like strings were neutralized and Unicode text was present correctly. Excel confirmed the formula-safety behavior, but its direct CSV-opening path displayed the Unicode value incorrectly.&lt;/p&gt;

&lt;p&gt;Without inspecting the raw file first, it would have been easy to incorrectly attribute that display behavior to the exporter.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Tables provided a straightforward streaming workflow for exporting DynamoDB results to CSV, while also exposing important behaviors before the export was started.&lt;/p&gt;

&lt;p&gt;For this controlled eight-item dataset, the export:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserved CSV structure for commas, quotes, and newlines;&lt;/li&gt;
&lt;li&gt;serialized complex DynamoDB values;&lt;/li&gt;
&lt;li&gt;preserved numbers, booleans, nulls, and binary data in appropriate CSV representations;&lt;/li&gt;
&lt;li&gt;neutralized formula-like strings before spreadsheet use;&lt;/li&gt;
&lt;li&gt;preserved the tested Unicode value in the raw file; and&lt;/li&gt;
&lt;li&gt;recovered cleanly after an invalid destination filename was rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Excel test also highlighted an important practical point: a correct CSV file and a correct spreadsheet rendering are two separate things worth verifying.&lt;/p&gt;

&lt;p&gt;For DynamoDB exports that may eventually be opened in spreadsheet software, checking both makes the export much easier to trust.&lt;/p&gt;

&lt;p&gt;Give it a try on &lt;a href="https://tables.serverlesscreed.com/" rel="noopener noreferrer"&gt;https://tables.serverlesscreed.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
