[{"data":1,"prerenderedAt":595},["ShallowReactive",2],{"kc-/knowledge/why-relational-data":3,"kc-clusters-/knowledge/why-relational-data":365,"kc-related-/knowledge/why-relational-data":366},{"id":4,"title":5,"author":6,"body":7,"date":323,"description":324,"draft":325,"extension":326,"faqs":327,"image":337,"isPillar":325,"meta":338,"navigation":219,"path":339,"pillar":340,"pillarName":341,"seo":342,"sources":343,"stem":355,"tags":356,"takeaways":359,"updated":363,"__hash__":364},"knowledge/knowledge/why-relational-data.md","Why Your Form Data Deserves a Real Database","RoundPushPin Team",{"type":8,"value":9,"toc":317},"minimark",[10,14,21,26,33,36,154,157,185,189,195,198,255,258,262,265,294,298,310,313],[11,12,13],"p",{},"Most form builders store your responses as JSON documents. It works — until you need to actually do something with that data. Here's why we chose a relational model instead.",[11,15,16],{},[17,18],"img",{"alt":19,"src":20},"Diagram comparing a JSON blob and relational tables for storing the same form response","/images/knowledge/diagrams/relational-vs-json.png",[22,23,25],"h2",{"id":24},"why-are-json-blobs-a-problem-for-form-data","Why are JSON blobs a problem for form data?",[11,27,28,32],{},[29,30,31],"strong",{},"JSON blobs are easy to store but hard to use: they offer no referential integrity, no cross-form queries, no type safety, and no efficient indexing."," As soon as you need to analyze, join, or validate responses, the unstructured format works against you.",[11,34,35],{},"When a user submits a form on most platforms, the response gets serialized into something like this:",[37,38,43],"pre",{"className":39,"code":40,"language":41,"meta":42,"style":42},"language-json shiki shiki-themes github-light github-dark","{\n  \"form_id\": \"abc123\",\n  \"responses\": {\n    \"name\": \"Jane Doe\",\n    \"email\": \"jane@example.com\",\n    \"role\": \"Engineer\",\n    \"experience\": \"5-10 years\"\n  },\n  \"submitted_at\": \"2026-01-28T10:00:00Z\"\n}\n","json","",[44,45,46,55,72,81,94,107,120,131,137,148],"code",{"__ignoreMap":42},[47,48,51],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sVt8B","{\n",[47,56,58,62,65,69],{"class":49,"line":57},2,[47,59,61],{"class":60},"sj4cs","  \"form_id\"",[47,63,64],{"class":53},": ",[47,66,68],{"class":67},"sZZnC","\"abc123\"",[47,70,71],{"class":53},",\n",[47,73,75,78],{"class":49,"line":74},3,[47,76,77],{"class":60},"  \"responses\"",[47,79,80],{"class":53},": {\n",[47,82,84,87,89,92],{"class":49,"line":83},4,[47,85,86],{"class":60},"    \"name\"",[47,88,64],{"class":53},[47,90,91],{"class":67},"\"Jane Doe\"",[47,93,71],{"class":53},[47,95,97,100,102,105],{"class":49,"line":96},5,[47,98,99],{"class":60},"    \"email\"",[47,101,64],{"class":53},[47,103,104],{"class":67},"\"jane@example.com\"",[47,106,71],{"class":53},[47,108,110,113,115,118],{"class":49,"line":109},6,[47,111,112],{"class":60},"    \"role\"",[47,114,64],{"class":53},[47,116,117],{"class":67},"\"Engineer\"",[47,119,71],{"class":53},[47,121,123,126,128],{"class":49,"line":122},7,[47,124,125],{"class":60},"    \"experience\"",[47,127,64],{"class":53},[47,129,130],{"class":67},"\"5-10 years\"\n",[47,132,134],{"class":49,"line":133},8,[47,135,136],{"class":53},"  },\n",[47,138,140,143,145],{"class":49,"line":139},9,[47,141,142],{"class":60},"  \"submitted_at\"",[47,144,64],{"class":53},[47,146,147],{"class":67},"\"2026-01-28T10:00:00Z\"\n",[47,149,151],{"class":49,"line":150},10,[47,152,153],{"class":53},"}\n",[11,155,156],{},"This is easy to store. It's flexible. But it has serious limitations:",[158,159,160,167,173,179],"ol",{},[161,162,163,166],"li",{},[29,164,165],{},"No referential integrity."," Delete a question and orphaned data floats around forever.",[161,168,169,172],{},[29,170,171],{},"No cross-form queries."," Want the average completion time grouped by question type across all forms? You need to deserialize every response.",[161,174,175,178],{},[29,176,177],{},"No type safety."," The \"experience\" field could contain anything — a number, a string, an array.",[161,180,181,184],{},[29,182,183],{},"No indexing."," You can't efficiently query \"all responses where role = Engineer\" without scanning every document.",[22,186,188],{"id":187},"how-does-a-relational-model-store-form-data","How does a relational model store form data?",[11,190,191,194],{},[29,192,193],{},"In a relational model, every form maps to a structured PostgreSQL schema: each question becomes a typed column and each response becomes a row."," Foreign keys keep everything connected, so nothing gets orphaned and every field has a known type.",[11,196,197],{},"In RoundPushPin, the same data looks like this:",[37,199,203],{"className":200,"code":201,"language":202,"meta":42,"style":42},"language-sql shiki shiki-themes github-light github-dark","-- Forms table\nSELECT * FROM forms WHERE id = 'abc123';\n\n-- Blocks (questions) with enforced types\nSELECT * FROM blocks WHERE form_id = 'abc123' ORDER BY position;\n\n-- Responses with proper columns\nSELECT name, email, role, experience\nFROM responses_abc123\nWHERE submitted_at > '2026-01-01';\n","sql",[44,204,205,210,215,221,226,231,235,240,245,250],{"__ignoreMap":42},[47,206,207],{"class":49,"line":50},[47,208,209],{},"-- Forms table\n",[47,211,212],{"class":49,"line":57},[47,213,214],{},"SELECT * FROM forms WHERE id = 'abc123';\n",[47,216,217],{"class":49,"line":74},[47,218,220],{"emptyLinePlaceholder":219},true,"\n",[47,222,223],{"class":49,"line":83},[47,224,225],{},"-- Blocks (questions) with enforced types\n",[47,227,228],{"class":49,"line":96},[47,229,230],{},"SELECT * FROM blocks WHERE form_id = 'abc123' ORDER BY position;\n",[47,232,233],{"class":49,"line":109},[47,234,220],{"emptyLinePlaceholder":219},[47,236,237],{"class":49,"line":122},[47,238,239],{},"-- Responses with proper columns\n",[47,241,242],{"class":49,"line":133},[47,243,244],{},"SELECT name, email, role, experience\n",[47,246,247],{"class":49,"line":139},[47,248,249],{},"FROM responses_abc123\n",[47,251,252],{"class":49,"line":150},[47,253,254],{},"WHERE submitted_at > '2026-01-01';\n",[11,256,257],{},"Each form gets a structured schema. Each question maps to a typed column. Foreign keys ensure nothing gets orphaned.",[22,259,261],{"id":260},"what-does-relational-form-storage-enable","What does relational form storage enable?",[11,263,264],{},"With relational storage, you can:",[266,267,268,276,282,288],"ul",{},[161,269,270,64,273],{},[29,271,272],{},"Run analytics directly",[44,274,275],{},"SELECT role, COUNT(*) FROM responses GROUP BY role",[161,277,278,281],{},[29,279,280],{},"Join with other data",": Connect form responses to your user table, CRM, or any other system",[161,283,284,287],{},[29,285,286],{},"Enforce constraints",": Required fields, unique values, valid email formats — at the database level",[161,289,290,293],{},[29,291,292],{},"Version your schema",": Track changes to form structure over time with migrations",[22,295,297],{"id":296},"whats-the-trade-off-of-a-relational-schema","What's the trade-off of a relational schema?",[11,299,300,303,304,309],{},[29,301,302],{},"Relational schemas are less flexible than JSON."," Adding a question means adding a column. But we think this trade-off is worth it. The discipline of a structured schema pays dividends in data quality, queryability, and long-term maintainability — a principle at the heart of ",[305,306,308],"a",{"href":307},"/knowledge/building-better-forms","our product vision",".",[11,311,312],{},"Your form data isn't throwaway. It deserves a real database.",[314,315,316],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":42,"searchDepth":57,"depth":57,"links":318},[319,320,321,322],{"id":24,"depth":57,"text":25},{"id":187,"depth":57,"text":188},{"id":260,"depth":57,"text":261},{"id":296,"depth":57,"text":297},"2026-01-28","JSON blobs vs relational tables: why we chose PostgreSQL as the backbone of RoundPushPin and what it means for your data.",false,"md",[328,331,334],{"q":329,"a":330},"Why use a relational database for form responses instead of JSON?","Relational tables enforce types and relationships and let you query and join responses with SQL, while JSON blobs store everything in one document you must deserialize to analyze. For data you'll actually use, relational wins on integrity and queryability.",{"q":332,"a":333},"Can you store form data in PostgreSQL?","Yes. PostgreSQL suits form data well: each question becomes a typed column, constraints enforce required and format rules, and you query responses with standard SQL. RoundPushPin maps every form to a PostgreSQL schema automatically.",{"q":335,"a":336},"What's wrong with storing form responses as JSON blobs?","JSON blobs lack referential integrity, type safety, and efficient indexing, so you can't easily validate, join, or query responses without deserializing every document. They're flexible but become a liability once you need to analyze the data.","/images/knowledge/why-relational-data.png",{},"/knowledge/why-relational-data","form-data-architecture","Form data architecture",{"title":5,"description":324},[344,348,351],{"title":345,"url":346,"publisher":347},"JSON Types","https://www.postgresql.org/docs/current/datatype-json.html","PostgreSQL Global Development Group",{"title":349,"url":350,"publisher":347},"Constraints","https://www.postgresql.org/docs/current/ddl-constraints.html",{"title":352,"url":353,"publisher":354},"Drizzle ORM — Overview","https://orm.drizzle.team/docs/overview","Drizzle","knowledge/why-relational-data",[357,358],"technical","architecture",[360,361,362],"JSON blobs are easy to write but offer no type safety, no joins, and no referential integrity.","A relational model maps each question to a typed column and each response to a row you can query with SQL.","RoundPushPin stores responses in PostgreSQL automatically, so your data is queryable and joinable from day one.","2026-02-02","lOhUA5IBPXmTygdlUyB9c6jupBhbfDak7GF2SOtSymk",[],[367,453],{"id":368,"title":369,"author":6,"body":370,"date":423,"description":424,"draft":325,"extension":326,"faqs":425,"image":435,"isPillar":325,"meta":436,"navigation":219,"path":437,"pillar":340,"pillarName":341,"seo":438,"sources":439,"stem":446,"tags":447,"takeaways":451,"updated":423,"__hash__":452},"knowledge/knowledge/export-form-responses-to-csv.md","How to Export Form Responses to CSV",{"type":8,"value":371,"toc":418},[372,375,379,385,389,399,403],[11,373,374],{},"Exporting form responses to CSV produces a comma-separated file with one row per submission and one column per question — the most portable way to move responses into a spreadsheet, BI tool, or another system. A clean export depends entirely on how the responses were stored.",[22,376,378],{"id":377},"what-makes-a-clean-csv-export","What makes a \"clean\" CSV export?",[11,380,381,384],{},[29,382,383],{},"A clean CSV has stable headers, one column per question, consistent types down each column, and proper escaping of commas, quotes, and line breaks."," The CSV format is specified in RFC 4180, and the details — quoting fields that contain delimiters, consistent line endings — are exactly what trips up hand-rolled exports. When data already lives in typed columns, the export is mechanical; when it lives in JSON blobs, every export risks shifting or missing columns.",[22,386,388],{"id":387},"why-are-spreadsheet-based-form-tools-messy-to-export","Why are spreadsheet-based form tools messy to export?",[11,390,391,394,395,398],{},[29,392,393],{},"Because the column layout drifts."," If responses are stored as documents or in an ever-widening sheet, adding or removing a question changes the shape of every future export, and types are unenforced — a \"number\" column may contain text. Relational storage avoids this: the schema fixes the columns, so the CSV is the same shape every time. Databases like PostgreSQL even expose a dedicated ",[44,396,397],{},"COPY"," command to stream a table straight to CSV.",[22,400,402],{"id":401},"how-roundpushpin-exports-to-csv","How RoundPushPin exports to CSV",[11,404,405,408,409,413,414,309],{},[29,406,407],{},"Because RoundPushPin stores each response as typed relational rows, a CSV export is one click and always well-formed: stable headers, consistent types, proper escaping."," When you outgrow files, the same clean source ",[305,410,412],{"href":411},"/knowledge/export-form-responses-to-bigquery","exports directly to BigQuery"," or is ",[305,415,417],{"href":416},"/knowledge/query-form-data-with-sql","queryable in place with SQL",{"title":42,"searchDepth":57,"depth":57,"links":419},[420,421,422],{"id":377,"depth":57,"text":378},{"id":387,"depth":57,"text":388},{"id":401,"depth":57,"text":402},"2026-02-10","CSV is the universal format for moving form responses into spreadsheets and other tools. This guide covers what a clean CSV export looks like, the pitfalls of messy exports, and how RoundPushPin exports in one click.",[426,429,432],{"q":427,"a":428},"How do I export form responses to CSV?","Most form tools offer a CSV export; the quality depends on how the data is stored. Relational storage yields stable headers, one column per question, consistent types, and proper escaping — RoundPushPin exports a clean CSV in one click.",{"q":430,"a":431},"Why is my form CSV export messy?","Usually because the columns drift: if responses are stored as documents or an ever-widening sheet, adding or removing a question changes every export and types aren't enforced. Relational storage fixes the column layout.",{"q":433,"a":434},"What is the standard CSV format?","RFC 4180 defines CSV: one record per line, fields separated by commas, and fields containing commas, quotes, or line breaks wrapped in double quotes. Hand-rolled exports often get the escaping wrong.","/images/knowledge/export-form-responses-to-csv.png",{},"/knowledge/export-form-responses-to-csv",{"title":369,"description":424},[440,444],{"title":441,"url":442,"publisher":443},"Common Format and MIME Type for CSV Files (RFC 4180)","https://www.rfc-editor.org/rfc/rfc4180","IETF",{"title":397,"url":445,"publisher":347},"https://www.postgresql.org/docs/current/sql-copy.html","knowledge/export-form-responses-to-csv",[448,449,450],"csv","export","guide",[],"Yr1qEbZAaKKsvAjU32SFRvpGh9Mjbh9-9tnKT98Budw",{"id":454,"title":455,"author":6,"body":456,"date":565,"description":566,"draft":325,"extension":326,"faqs":567,"image":577,"isPillar":325,"meta":578,"navigation":219,"path":416,"pillar":340,"pillarName":341,"seo":579,"sources":580,"stem":590,"tags":591,"takeaways":593,"updated":565,"__hash__":594},"knowledge/knowledge/query-form-data-with-sql.md","How to Query Form Data With SQL",{"type":8,"value":457,"toc":559},[458,461,465,476,480,483,531,535,545,549],[11,459,460],{},"Querying form data with SQL means asking questions of your responses directly in the database — filtering, grouping, and joining them — instead of exporting a file and pivoting in a spreadsheet. It only works when responses are stored relationally, with each question as a typed column.",[22,462,464],{"id":463},"can-you-query-form-responses-with-sql","Can you query form responses with SQL?",[11,466,467,470,471,475],{},[29,468,469],{},"You can when responses live in a relational database."," If a form tool stores answers as JSON blobs you have to deserialize every document first; if each question is a typed column, you query it like any other table. The difference is the storage model — see ",[305,472,474],{"href":473},"/knowledge/form-data-architecture","form data architecture"," for why it matters.",[22,477,479],{"id":478},"what-can-you-actually-ask","What can you actually ask?",[11,481,482],{},"The three workhorses are filtering, aggregating, and joining:",[266,484,485,494,517],{},[161,486,487,490,491],{},[29,488,489],{},"Filter"," to a segment: ",[44,492,493],{},"SELECT * FROM responses WHERE role = 'Engineer' AND submitted_at > '2026-01-01'",[161,495,496,499,500,502,503,506,507,506,510,506,513,516],{},[29,497,498],{},"Aggregate"," to a metric: ",[44,501,275],{}," — PostgreSQL's aggregate functions (",[44,504,505],{},"COUNT",", ",[44,508,509],{},"AVG",[44,511,512],{},"MIN",[44,514,515],{},"MAX",") turn raw rows into answers.",[161,518,519,522,523,526,527,530],{},[29,520,521],{},"Join"," to context: connect responses to your ",[44,524,525],{},"users"," or ",[44,528,529],{},"orders"," table on a shared key to analyze answers alongside the rest of your data.",[22,532,534],{"id":533},"how-do-you-analyze-completion-or-drop-off-with-sql","How do you analyze completion or drop-off with SQL?",[11,536,537,540,541,544],{},[29,538,539],{},"Group by question and count non-null answers."," Because each question is its own column, a query like ",[44,542,543],{},"SELECT COUNT(question_3) / COUNT(*)::float FROM responses"," gives the completion ratio for that question — the kind of drop-off analysis that is painful when the whole response is one opaque blob.",[22,546,548],{"id":547},"how-roundpushpin-makes-responses-queryable","How RoundPushPin makes responses queryable",[11,550,551,554,555,558],{},[29,552,553],{},"RoundPushPin stores every response relationally, so your data is queryable from day one — no export step, no reshaping."," Connect your own SQL client or ",[305,556,557],{"href":411},"export to BigQuery"," when you want to analyze form data next to everything else.",{"title":42,"searchDepth":57,"depth":57,"links":560},[561,562,563,564],{"id":463,"depth":57,"text":464},{"id":478,"depth":57,"text":479},{"id":533,"depth":57,"text":534},{"id":547,"depth":57,"text":548},"2026-02-08","When form responses live in a relational database, you can answer questions with SQL instead of exporting spreadsheets. This guide shows the queries that matter — filtering, aggregating, and joining responses.",[568,571,574],{"q":569,"a":570},"Can you run SQL on form responses?","Yes, when responses are stored relationally — each question as a typed column. You can filter, aggregate, and join them like any table. If a tool stores answers as JSON blobs, you must deserialize them first.",{"q":572,"a":573},"How do you analyze form drop-off?","Group by question and count non-null answers: comparing answered versus total per question reveals where people stop. This is simple when each question is its own column and painful when the response is one opaque blob.",{"q":575,"a":576},"How do you join form data with other tables?","Connect responses to another table on a shared key, such as a user ID, with a SQL JOIN. Relational storage makes this direct; RoundPushPin stores responses this way so they sit alongside your other data.","/images/knowledge/query-form-data-with-sql.png",{},{"title":455,"description":566},[581,584,587],{"title":582,"url":583,"publisher":347},"SELECT","https://www.postgresql.org/docs/current/sql-select.html",{"title":585,"url":586,"publisher":347},"Aggregate Functions","https://www.postgresql.org/docs/current/functions-aggregate.html",{"title":588,"url":589,"publisher":347},"Joins Between Tables","https://www.postgresql.org/docs/current/tutorial-join.html","knowledge/query-form-data-with-sql",[202,592,450],"analytics",[],"8-YH_T1SbhmtDvDbowCzrSccBzS9qyn4zgXNLGRfKJk",1780692426756]