From b52677d911f65aa00c76158859a3fb756c2aaedc Mon Sep 17 00:00:00 2001 From: David Li Date: Tue, 4 Feb 2025 14:38:04 +0900 Subject: [PATCH 01/14] [Website] Add "Data wants to be free" --- _posts/2025-02-04-data-wants-to-be-free.md | 306 +++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 _posts/2025-02-04-data-wants-to-be-free.md diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md new file mode 100644 index 000000000000..271074e3c34d --- /dev/null +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -0,0 +1,306 @@ +--- +layout: post +title: "Data wants to be free: fast data exchange with Apache Arrow" +description: "" +date: "2025-02-04 00:00:00" +author: David Li, Ian Cook, Matt Topol +categories: [application] +image: + path: /img/arrow-result-transfer/part-1-share-image.png + height: 1200 + width: 705 +--- + + + + + +_This is the second in a series of posts that aims to demystify the use of +Arrow as a data interchange format for databases and query engines._ + +As data practitioners, we often find our data “held hostage”. Instead of being +able to use data as soon as we get it, we have to spend time—time to parse and +clean up inefficient CSV files, time to wait for an outdated query engine to +struggle with a few gigabytes of data, and time to wait for the data to make +it across a socket. It’s that last point we’ll focus on today. In an age of +multi-gigabit networks, why is it even a problem in the first place? And it is +a problem—research by Mark Raasveldt and Hannes Mühleisen in their [2017 +paper](https://ir.cwi.nl/pub/26415/p852-muehleisen.pdf) demonstrated that some +systems take over **ten minutes** to transfer a dataset that should only take +ten *seconds*. + +Why are we waiting 60 times as long as we need to? [As we've argued before, +serialization overheads plague our +tools](https://arrow.apache.org/blog/2025/01/10/arrow-result-transfer/)—and +Arrow can help us here. So let’s make that more concrete: we’ll compare how +PostgreSQL and Arrow encode the same data to illustrate the impact of the data +serialization format. Then we’ll tour various ways to build protocols with +Arrow, like Arrow HTTP and Arrow Flight, and how you might use each of them. + +# PostgreSQL vs Arrow: Data Serialization + +Let’s compare the [PostgreSQL binary +format](https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.4) +and [Arrow +IPC](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc) +on the same dataset, and show how Arrow (with all the benefit of hindsight) +makes better trade-offs than its predecessors. + +First, we’ll create a table and fill it with data: + +``` +postgres=# CREATE TABLE demo (id BIGINT, val TEXT, val2 BIGINT); +CREATE TABLE +postgres=# INSERT INTO demo VALUES (1, 'foo', 64), (2, 'a longer string', 128), (3, 'yet another string', 10); +INSERT 0 3 +``` + +We can then use the COPY command to dump the raw binary data from PostgreSQL into a file: + +``` +postgres=# COPY demo TO '/tmp/demo.bin' WITH BINARY; +COPY 3 +``` + +Then we can look at the actual bytes of the data and annotate them, based on the [documentation](https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.4) for parsing the PostgreSQL binary format: + +
00000000: 50 47 43 4f 50 59 0a ff  PGCOPY..  COPY signature, flags,
+00000008: 0d 0a 00 00 00 00 00 00  ........  and extension
+00000010: 00 00 00 00 03 00 00 00  ........  Values in row
+00000018: 08 00 00 00 00 00 00 00  ........  Length of value
+00000020: 01 00 00 00 03 66 6f 6f  .....foo  Data
+00000028: 00 00 00 08 00 00 00 00  ........
+00000030: 00 00 00 40 00 03 00 00  ...@....
+00000038: 00 08 00 00 00 00 00 00  ........
+00000040: 00 02 00 00 00 0f 61 20  ......a
+00000048: 6c 6f 6e 67 65 72 20 73  longer s
+00000050: 74 72 69 6e 67 00 00 00  tring...
+00000058: 08 00 00 00 00 00 00 00  ........
+00000060: 80 00 03 00 00 00 08 00  ........
+00000068: 00 00 00 00 00 00 03 00  ........
+00000070: 00 00 12 79 65 74 20 61  ...yet a
+00000078: 6e 6f 74 68 65 72 20 73  nother s
+00000080: 74 72 69 6e 67 00 00 00  tring...
+00000088: 08 00 00 00 00 00 00 00  ........
+00000090: 0a ff ff                 ...
+ +Honestly, PostgreSQL’s binary format is quite understandable, and pretty compact at first glance. But a closer look isn’t so favorable. **PostgreSQL has overheads proportional to the number of rows and columns**: + +* Every row has a 2 byte prefix for the number of values in the row. *But the data is tabular—we already know this info, and it doesn’t change\!* +* Every value of every row has a 4 byte prefix for the length of the following data, or \-1 if the value is NULL. *But we know the data types, and those don’t change—most values have a fixed, known length\!* +* All values are big-endian. *But most of our devices are little-endian, so the data has to be converted.* + +Sure, we need to store if a value is NULL or not, but 4 bytes is a *bit* much for a boolean. String data and other non-fixed-length types need per-value lengths, but PostgreSQL adds the length for *every* type of value. And converting big-endian to little-endian is pretty trivial…but it’s still work that stands in between you and your data. To PostgreSQL’s credit, its format is at least cheap and easy to parse—[other formats](https://protobuf.dev/programming-guides/encoding/) get fancy with tricks like “varint” encoding which are quite expensive. + +For example, a single column of int32 values would have 4 bytes of data and 6 bytes of overhead per row—**60% is “wasted\!”**[^1] The ratio gets a little better with more columns (but not with more rows); in the limit we approach “only” 50% overhead. + +How does Arrow compare? We can use [ADBC](https://arrow.apache.org/adbc/current/driver/postgresql.html) to pull the PostgreSQL table into an Arrow table, then annotate it like before: + +```console +>>> import adbc_driver_postgresql.dbapi +>>> import pyarrow.feather +>>> conn = adbc_driver_postgresql.dbapi.connect("...") +>>> cur = conn.cursor() +>>> cur.execute("SELECT * FROM demo") +>>> data = cur.fetchallarrow() +>>> pyarrow.feather.write_feather(data, "demo.arrow") +``` + +(Aside: look how easy that is!) + +
00000000: 41 52 52 4f 57 31 00 00  ARROW1..
+00000008: ff ff ff ff d8 00 00 00  ........
+00000010: 10 00 00 00 00 00 0a 00  ........
+00000018: 0c 00 06 00 05 00 08 00  ........
+00000020: 0a 00 00 00 00 01 04 00  ........
+00000028: 0c 00 00 00 08 00 08 00  ........
+00000030: 00 00 04 00 08 00 00 00  ........
+00000038: 04 00 00 00 03 00 00 00  ........
+00000040: 74 00 00 00 38 00 00 00  t...8...
+00000048: 04 00 00 00 a8 ff ff ff  ........
+00000050: 00 00 01 02 10 00 00 00  ........
+00000058: 18 00 00 00 04 00 00 00  ........
+00000060: 00 00 00 00 04 00 00 00  ........
+00000068: 76 61 6c 32 00 00 00 00  val2....
+00000070: 9c ff ff ff 00 00 00 01  ........
+00000078: 40 00 00 00 d8 ff ff ff  @.......
+00000080: 00 00 01 05 10 00 00 00  ........
+00000088: 18 00 00 00 04 00 00 00  ........
+00000090: 00 00 00 00 03 00 00 00  ........
+00000098: 76 61 6c 00 04 00 04 00  val.....
+000000a0: 04 00 00 00 10 00 14 00  ........
+000000a8: 08 00 06 00 07 00 0c 00  ........
+000000b0: 00 00 10 00 10 00 00 00  ........
+000000b8: 00 00 01 02 10 00 00 00  ........
+000000c0: 1c 00 00 00 04 00 00 00  ........
+000000c8: 00 00 00 00 02 00 00 00  ........
+000000d0: 69 64 00 00 08 00 0c 00  id......
+000000d8: 08 00 07 00 08 00 00 00  ........
+000000e0: 00 00 00 01 40 00 00 00  ....@...
+000000e8: ff ff ff ff 08 01 00 00  ........
+000000f0: 14 00 00 00 00 00 00 00  ........
+000000f8: 0c 00 18 00 06 00 05 00  ........
+00000100: 08 00 0c 00 0c 00 00 00  ........
+00000108: 00 03 04 00 1c 00 00 00  ........
+00000110: c8 00 00 00 00 00 00 00  ........
+00000118: 00 00 00 00 0c 00 1c 00  ........
+00000120: 10 00 04 00 08 00 0c 00  ........
+00000128: 0c 00 00 00 98 00 00 00  ........
+00000130: 1c 00 00 00 14 00 00 00  ........
+00000138: 03 00 00 00 00 00 00 00  ........
+00000140: 00 00 00 00 04 00 04 00  ........
+00000148: 04 00 00 00 07 00 00 00  ........
+00000150: 00 00 00 00 00 00 00 00  ........
+00000158: 00 00 00 00 00 00 00 00  ........
+00000160: 00 00 00 00 00 00 00 00  ........
+00000168: 2a 00 00 00 00 00 00 00  *.......
+00000170: 30 00 00 00 00 00 00 00  0.......
+00000178: 00 00 00 00 00 00 00 00  ........
+00000180: 30 00 00 00 00 00 00 00  0.......
+00000188: 27 00 00 00 00 00 00 00  .......
+00000190: 58 00 00 00 00 00 00 00  X.......
+00000198: 3b 00 00 00 00 00 00 00  ;.......
+000001a0: 98 00 00 00 00 00 00 00  ........
+000001a8: 00 00 00 00 00 00 00 00  ........
+000001b0: 98 00 00 00 00 00 00 00  ........
+000001b8: 2a 00 00 00 00 00 00 00  *.......
+000001c0: 00 00 00 00 03 00 00 00  ........
+000001c8: 03 00 00 00 00 00 00 00  ........
+000001d0: 00 00 00 00 00 00 00 00  ........
+000001d8: 03 00 00 00 00 00 00 00  ........
+000001e0: 00 00 00 00 00 00 00 00  ........
+000001e8: 03 00 00 00 00 00 00 00  ........
+000001f0: 00 00 00 00 00 00 00 00  ........
+000001f8: 18 00 00 00 00 00 00 00  ........
+00000200: 04 22 4d 18 60 40 82 13  ."M.@..
+00000208: 00 00 00 22 01 00 01 00  ..."....
+00000210: 12 02 07 00 90 00 03 00  ........
+00000218: 00 00 00 00 00 00 00 00  ........
+00000220: 00 00 00 00 00 00 00 00  ........
+00000228: 10 00 00 00 00 00 00 00  ........
+00000230: 04 22 4d 18 60 40 82 10  ."M.@..
+00000238: 00 00 80 00 00 00 00 03  ........
+00000240: 00 00 00 12 00 00 00 24  .......$
+00000248: 00 00 00 00 00 00 00 00  ........
+00000250: 24 00 00 00 00 00 00 00  $.......
+00000258: 04 22 4d 18 60 40 82 24  ."M.@.$
+00000260: 00 00 80 66 6f 6f 61 20  ...fooa
+00000268: 6c 6f 6e 67 65 72 20 73  longer s
+00000270: 74 72 69 6e 67 79 65 74  tringyet
+00000278: 20 61 6e 6f 74 68 65 72   another
+00000280: 20 73 74 72 69 6e 67 00   string.
+00000288: 00 00 00 00 00 00 00 00  ........
+00000290: 18 00 00 00 00 00 00 00  ........
+00000298: 04 22 4d 18 60 40 82 13  ."M.@..
+000002a0: 00 00 00 22 40 00 01 00  ..."@...
+000002a8: 12 80 07 00 90 00 0a 00  ........
+000002b0: 00 00 00 00 00 00 00 00  ........
+000002b8: 00 00 00 00 00 00 00 00  ........
+000002c0: ff ff ff ff 00 00 00 00  ........
+000002c8: 10 00 00 00 0c 00 14 00  ........
+000002d0: 06 00 08 00 0c 00 10 00  ........
+000002d8: 0c 00 00 00 00 00 04 00  ........
+000002e0: 34 00 00 00 24 00 00 00  4...$...
+000002e8: 04 00 00 00 01 00 00 00  ........
+000002f0: e8 00 00 00 00 00 00 00  ........
+000002f8: 10 01 00 00 00 00 00 00  ........
+00000300: c8 00 00 00 00 00 00 00  ........
+00000308: 00 00 00 00 08 00 08 00  ........
+00000310: 00 00 04 00 08 00 00 00  ........
+00000318: 04 00 00 00 03 00 00 00  ........
+00000320: 74 00 00 00 38 00 00 00  t...8...
+00000328: 04 00 00 00 a8 ff ff ff  ........
+00000330: 00 00 01 02 10 00 00 00  ........
+00000338: 18 00 00 00 04 00 00 00  ........
+00000340: 00 00 00 00 04 00 00 00  ........
+00000348: 76 61 6c 32 00 00 00 00  val2....
+00000350: 9c ff ff ff 00 00 00 01  ........
+00000358: 40 00 00 00 d8 ff ff ff  @.......
+00000360: 00 00 01 05 10 00 00 00  ........
+00000368: 18 00 00 00 04 00 00 00  ........
+00000370: 00 00 00 00 03 00 00 00  ........
+00000378: 76 61 6c 00 04 00 04 00  val.....
+00000380: 04 00 00 00 10 00 14 00  ........
+00000388: 08 00 06 00 07 00 0c 00  ........
+00000390: 00 00 10 00 10 00 00 00  ........
+00000398: 00 00 01 02 10 00 00 00  ........
+000003a0: 1c 00 00 00 04 00 00 00  ........
+000003a8: 00 00 00 00 02 00 00 00  ........
+000003b0: 69 64 00 00 08 00 0c 00  id......
+000003b8: 08 00 07 00 08 00 00 00  ........
+000003c0: 00 00 00 01 40 00 00 00  ....@...
+000003c8: 00 01 00 00 41 52 52 4f  ....ARRO
+000003d0: 57 31                    W1
+ +Arrow looks quite…intimidating…at first glance. There’s a giant header, and lots of things that don’t seem related to our dataset at all, plus mysterious padding that seems to exist solely to take up space. But the important thing is that **the overhead is fixed**. Whether you’re transferring one row or a billion, the overhead doesn’t change. And unlike PostgreSQL, **no per-value parsing is required**. + +Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer. Strings do still require a length per value, but the overhead isn’t added where it isn’t otherwise needed. And nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values in the first place, saving space. Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay\! + +Even the header isn’t actually the disadvantage it looks like. The header contains the schema, which makes the data stream self-describing. With PostgreSQL, you need to get the schema from somewhere else. So we aren’t making an apples-to-apples comparison in the first place: PostgreSQL still has to transfer the schema, it’s just not part of the “binary format” that we’re looking at here. +Meanwhile, there’s actually a more insidious problem with PostgreSQL we’ve overlooked so far: alignment. Remember that 2 byte field count at the start of every row? Well, that means all the 4 byte integers after it are now unaligned…so you can’t use them without copying them (or doing a very slow unaligned load). Arrow, on the other hand, strategically adds some padding (overhead) to align the data, and lets you use little-endian or big-endian byte order depending on your data. And Arrow doesn’t apply expensive encodings to the data that require further parsing; there’s just optional compression that can be enabled if it suits your data[^2]. So **you can use Arrow data as-is without having to parse every value**. + +That’s the benefit of Arrow being a standardized, in-memory data format. Data coming off the wire is already in Arrow format, and can be passed on directly to DuckDB, pandas, polars, cuDF, DataFusion, or any number of systems. Even if the PostgreSQL format fixed many of these problems we’ve discussed—adding padding to align fields, using little-endian or making endianness configurable, trimming the overhead—you’d still end up having to convert the data to another format to use downstream. And even then, if you really did want to use the PostgreSQL binary format[^3], the documentation rather unfortunately points you to the source code for the details. Arrow, on the other hand, has a specification, documentation, and multiple implementations (including third-party ones) across a dozen languages for you to pick up and use in your own applications. + +Now, we don’t mean to pick on PostgreSQL here. Obviously, PostgreSQL is a full-featured database with a storied history and many happy users. Arrow isn’t trying to compete in that space anyways. But their domains do intersect. PostgreSQL’s wire protocol has [become a de facto standard](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html), with even brand new products like Google’s AlloyDB using it, and so its design affects many projects[^4]. In fact, AlloyDB is a great example of a shiny new columnar query engine being locked behind a row-oriented client protocol from the 90s. So [Amdahl’s law](https://en.wikipedia.org/wiki/Amdahl's_law) rears its head again—optimizing the “front” and “back” of your data pipeline doesn’t matter when the middle is slow as molasses. + +# A quiver of Arrow (projects) + +So if Arrow is so great, how can we actually use it to build our own protocols? Luckily, Arrow comes with a variety of building blocks for different situations. + +* We just talked about [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc) before. Where Arrow is the in-memory format defining how arrays of data are laid out, er, in memory, Arrow IPC defines how to serialize and deserialize Arrow data so it can be sent somewhere else—whether that means being written to a file, to a socket, into a shared buffer, or otherwise. Arrow IPC organizes data as a sequence of messages, making it easy to stream over your favorite transport, like WebSockets. +* [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http) is “just” streaming Arrow IPC over HTTP. It’s standardized so that different clients agree on how exactly to do this, and there’s examples of clients and servers across several languages, how to use HTTP Range requests, using multipart/mixed requests to send combined JSON and Arrow responses, and more. While not a full protocol in and of itself, it’ll fit right in when building REST APIs. +* [**Disassociated IPC**](https://arrow.apache.org/docs/format/DissociatedIPC.html) combines Arrow with advanced network transports like [UCX](https://openucx.org/) and [libfabric](https://ofiwg.github.io/libfabric/). For those who require the absolute best performance and have the specialized hardware to boot, this allows you to send Arrow data at full throttle, taking advantage of scatter-gather, Infiniband, and more. +* [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html) is a fully defined protocol for accessing relational databases. Think of it as an alternative to the full PostgreSQL wire protocol: it defines how to connect to a database, execute queries, fetch results, view the catalog, and so on. For database developers, Flight SQL provides a fully Arrow-native protocol with clients for several programming languages and drivers for ADBC, JDBC, and ODBC—all of which you don’t have to build yourself\! +* And finally, [**ADBC**](https://arrow.apache.org/docs/format/ADBC.html) actually isn’t a protocol. Instead, it’s an API abstraction layer for working with databases (like JDBC and ODBC—bet you didn’t see that coming), that’s Arrow-native and doesn’t require transposing or converting columnar data back and forth. ADBC gives you a single API to access data from multiple databases, whether they use Flight SQL or something else under the hood, and if a conversion is absolutely necessary, ADBC handles the details so that you don’t have to build out a dozen connectors on your own. + +So to summarize: + +* If you’re *using* a database or other data system, you want **ADBC**. +* If you’re *building* a database, you want **Arrow Flight SQL**. +* If you’re working with specialized networking hardware (you’ll know if you are—that stuff doesn’t come cheap\!), you want **Disassociated IPC**. +* If you’re *designing* a REST-ish API, you want **Arrow HTTP**. (gRPC users: stay tuned.) +* And otherwise, you can roll-your-own with **Arrow IPC**. + +![][image1] + +# Conclusion + +Existing client protocols can be absurdly wasteful, but Arrow offers better efficiency and avoids design pitfalls from the past. And Arrow makes it easy to build and consume data APIs with a variety of standards like Arrow IPC, Arrow HTTP, and ADBC. By using Arrow serialization in protocols, everyone benefits from easier, faster, and simpler data access, and we can avoid accidentally holding data captive behind slow and inefficient interfaces. + +[^1]: Of course, it’s not fully wasted, as null/not-null is data as well. But for accounting purposes, we’ll be consistent and call lengths, padding, bitmaps, etc. “overhead”. + +[^2]: And if your data really benefits from heavy compression, you can always use something like Apache Parquet, which implements lots of fancy encodings to save space and can still be decoded to Arrow data reasonably quickly. + +[^3]: [And people do…](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html) + +[^4]: [We have some experience with the PostgreSQL wire protocol, too.](https://github.com/apache/arrow-adbc/blob/ed18b8b221af23c7b32312411da10f6532eb3488/c/driver/postgresql/copy/reader.h) From d2d9d6b60f4c797ca1745288bf6e60405ac1b8bf Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 17 Feb 2025 02:21:10 -0500 Subject: [PATCH 02/14] Apply suggestions from code review Co-authored-by: Bryce Mecum --- _posts/2025-02-04-data-wants-to-be-free.md | 25 ++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index 271074e3c34d..5cc4c16d5296 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -50,7 +50,7 @@ Arrow as a data interchange format for databases and query engines._ As data practitioners, we often find our data “held hostage”. Instead of being able to use data as soon as we get it, we have to spend time—time to parse and -clean up inefficient CSV files, time to wait for an outdated query engine to +clean up inefficient and messy CSV files, time to wait for an outdated query engine to struggle with a few gigabytes of data, and time to wait for the data to make it across a socket. It’s that last point we’ll focus on today. In an age of multi-gigabit networks, why is it even a problem in the first place? And it is @@ -67,7 +67,7 @@ PostgreSQL and Arrow encode the same data to illustrate the impact of the data serialization format. Then we’ll tour various ways to build protocols with Arrow, like Arrow HTTP and Arrow Flight, and how you might use each of them. -# PostgreSQL vs Arrow: Data Serialization +## PostgreSQL vs Arrow: Data Serialization Let’s compare the [PostgreSQL binary format](https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.4) @@ -264,39 +264,42 @@ How does Arrow compare? We can use [ADBC](https://arrow.apache.org/adbc/current/ Arrow looks quite…intimidating…at first glance. There’s a giant header, and lots of things that don’t seem related to our dataset at all, plus mysterious padding that seems to exist solely to take up space. But the important thing is that **the overhead is fixed**. Whether you’re transferring one row or a billion, the overhead doesn’t change. And unlike PostgreSQL, **no per-value parsing is required**. -Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer. Strings do still require a length per value, but the overhead isn’t added where it isn’t otherwise needed. And nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values in the first place, saving space. Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay\! +Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer. Strings do still require a length per value, but the overhead isn’t added where it isn’t otherwise needed. And nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values in the first place, saving space. Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay. Even the header isn’t actually the disadvantage it looks like. The header contains the schema, which makes the data stream self-describing. With PostgreSQL, you need to get the schema from somewhere else. So we aren’t making an apples-to-apples comparison in the first place: PostgreSQL still has to transfer the schema, it’s just not part of the “binary format” that we’re looking at here. Meanwhile, there’s actually a more insidious problem with PostgreSQL we’ve overlooked so far: alignment. Remember that 2 byte field count at the start of every row? Well, that means all the 4 byte integers after it are now unaligned…so you can’t use them without copying them (or doing a very slow unaligned load). Arrow, on the other hand, strategically adds some padding (overhead) to align the data, and lets you use little-endian or big-endian byte order depending on your data. And Arrow doesn’t apply expensive encodings to the data that require further parsing; there’s just optional compression that can be enabled if it suits your data[^2]. So **you can use Arrow data as-is without having to parse every value**. -That’s the benefit of Arrow being a standardized, in-memory data format. Data coming off the wire is already in Arrow format, and can be passed on directly to DuckDB, pandas, polars, cuDF, DataFusion, or any number of systems. Even if the PostgreSQL format fixed many of these problems we’ve discussed—adding padding to align fields, using little-endian or making endianness configurable, trimming the overhead—you’d still end up having to convert the data to another format to use downstream. And even then, if you really did want to use the PostgreSQL binary format[^3], the documentation rather unfortunately points you to the source code for the details. Arrow, on the other hand, has a specification, documentation, and multiple implementations (including third-party ones) across a dozen languages for you to pick up and use in your own applications. +That’s the benefit of Arrow being a standardized, in-memory data format. Data coming off the wire is already in Arrow format, and can be passed on directly to [DuckDB](https://duckdb.org), [pandas](https://pandas.pydata.org), [polars](https://pola.rs), [cuDF](https://docs.rapids.ai/api/cudf/stable/), [DataFusion](https://datafusion.apache.org), or any number of systems. Even if the PostgreSQL format fixed many of these problems we’ve discussed—adding padding to align fields, using little-endian or making endianness configurable, trimming the overhead—you’d still end up having to convert the data to another format to use downstream. And even then, if you really did want to use the PostgreSQL binary format[^3], the documentation rather unfortunately points you to the source code for the details. Arrow, on the other hand, has a specification, documentation, and multiple implementations (including third-party ones) across a dozen languages for you to pick up and use in your own applications. -Now, we don’t mean to pick on PostgreSQL here. Obviously, PostgreSQL is a full-featured database with a storied history and many happy users. Arrow isn’t trying to compete in that space anyways. But their domains do intersect. PostgreSQL’s wire protocol has [become a de facto standard](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html), with even brand new products like Google’s AlloyDB using it, and so its design affects many projects[^4]. In fact, AlloyDB is a great example of a shiny new columnar query engine being locked behind a row-oriented client protocol from the 90s. So [Amdahl’s law](https://en.wikipedia.org/wiki/Amdahl's_law) rears its head again—optimizing the “front” and “back” of your data pipeline doesn’t matter when the middle is slow as molasses. +Now, we don’t mean to pick on PostgreSQL here. Obviously, PostgreSQL is a full-featured database with a storied history, a different set of goals and constraints than Arrow, and many happy users. Arrow isn’t trying to compete in that space anyways. But their domains do intersect. PostgreSQL’s wire protocol has [become a de facto standard](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html), with even brand new products like Google’s AlloyDB using it, and so its design affects many projects[^4]. In fact, AlloyDB is a great example of a shiny new columnar query engine being locked behind a row-oriented client protocol from the 90s. So [Amdahl’s law](https://en.wikipedia.org/wiki/Amdahl's_law) rears its head again—optimizing the “front” and “back” of your data pipeline doesn’t matter when the middle is slow as molasses. -# A quiver of Arrow (projects) +## A quiver of Arrow (projects) So if Arrow is so great, how can we actually use it to build our own protocols? Luckily, Arrow comes with a variety of building blocks for different situations. * We just talked about [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc) before. Where Arrow is the in-memory format defining how arrays of data are laid out, er, in memory, Arrow IPC defines how to serialize and deserialize Arrow data so it can be sent somewhere else—whether that means being written to a file, to a socket, into a shared buffer, or otherwise. Arrow IPC organizes data as a sequence of messages, making it easy to stream over your favorite transport, like WebSockets. * [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http) is “just” streaming Arrow IPC over HTTP. It’s standardized so that different clients agree on how exactly to do this, and there’s examples of clients and servers across several languages, how to use HTTP Range requests, using multipart/mixed requests to send combined JSON and Arrow responses, and more. While not a full protocol in and of itself, it’ll fit right in when building REST APIs. * [**Disassociated IPC**](https://arrow.apache.org/docs/format/DissociatedIPC.html) combines Arrow with advanced network transports like [UCX](https://openucx.org/) and [libfabric](https://ofiwg.github.io/libfabric/). For those who require the absolute best performance and have the specialized hardware to boot, this allows you to send Arrow data at full throttle, taking advantage of scatter-gather, Infiniband, and more. -* [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html) is a fully defined protocol for accessing relational databases. Think of it as an alternative to the full PostgreSQL wire protocol: it defines how to connect to a database, execute queries, fetch results, view the catalog, and so on. For database developers, Flight SQL provides a fully Arrow-native protocol with clients for several programming languages and drivers for ADBC, JDBC, and ODBC—all of which you don’t have to build yourself\! +* [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html) is a fully defined protocol for accessing relational databases. Think of it as an alternative to the full PostgreSQL wire protocol: it defines how to connect to a database, execute queries, fetch results, view the catalog, and so on. For database developers, Flight SQL provides a fully Arrow-native protocol with clients for several programming languages and drivers for ADBC, JDBC, and ODBC—all of which you don’t have to build yourself. * And finally, [**ADBC**](https://arrow.apache.org/docs/format/ADBC.html) actually isn’t a protocol. Instead, it’s an API abstraction layer for working with databases (like JDBC and ODBC—bet you didn’t see that coming), that’s Arrow-native and doesn’t require transposing or converting columnar data back and forth. ADBC gives you a single API to access data from multiple databases, whether they use Flight SQL or something else under the hood, and if a conversion is absolutely necessary, ADBC handles the details so that you don’t have to build out a dozen connectors on your own. So to summarize: * If you’re *using* a database or other data system, you want **ADBC**. -* If you’re *building* a database, you want **Arrow Flight SQL**. -* If you’re working with specialized networking hardware (you’ll know if you are—that stuff doesn’t come cheap\!), you want **Disassociated IPC**. +* If you’re *building* a database, you want [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html). +* If you’re working with specialized networking hardware (you’ll know if you are—that stuff doesn’t come cheap), you want the [**Disassociated IPC Protocol**](https://arrow.apache.org/docs/format/DissociatedIPC.html). * If you’re *designing* a REST-ish API, you want **Arrow HTTP**. (gRPC users: stay tuned.) -* And otherwise, you can roll-your-own with **Arrow IPC**. +* And otherwise, you can roll-your-own with [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc). ![][image1] -# Conclusion +## Conclusion Existing client protocols can be absurdly wasteful, but Arrow offers better efficiency and avoids design pitfalls from the past. And Arrow makes it easy to build and consume data APIs with a variety of standards like Arrow IPC, Arrow HTTP, and ADBC. By using Arrow serialization in protocols, everyone benefits from easier, faster, and simpler data access, and we can avoid accidentally holding data captive behind slow and inefficient interfaces. + +## Footnotes + [^1]: Of course, it’s not fully wasted, as null/not-null is data as well. But for accounting purposes, we’ll be consistent and call lengths, padding, bitmaps, etc. “overhead”. [^2]: And if your data really benefits from heavy compression, you can always use something like Apache Parquet, which implements lots of fancy encodings to save space and can still be decoded to Arrow data reasonably quickly. From 9faaa4cf050fadabcbe274446463285fa1ac0a08 Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 17 Feb 2025 02:26:12 -0500 Subject: [PATCH 03/14] Add link --- _posts/2025-02-04-data-wants-to-be-free.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index 5cc4c16d5296..cc98b96d3362 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -285,7 +285,7 @@ So if Arrow is so great, how can we actually use it to build our own protocols? So to summarize: -* If you’re *using* a database or other data system, you want **ADBC**. +* If you’re *using* a database or other data system, you want [**ADBC**](https://arrow.apache.org/adbc/). * If you’re *building* a database, you want [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html). * If you’re working with specialized networking hardware (you’ll know if you are—that stuff doesn’t come cheap), you want the [**Disassociated IPC Protocol**](https://arrow.apache.org/docs/format/DissociatedIPC.html). * If you’re *designing* a REST-ish API, you want **Arrow HTTP**. (gRPC users: stay tuned.) From 297ec1fb55bc61d8cfaf3668744e3f0aad430c13 Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 17 Feb 2025 02:26:45 -0500 Subject: [PATCH 04/14] Add link --- _posts/2025-02-04-data-wants-to-be-free.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index cc98b96d3362..554b9fc8f118 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -55,7 +55,7 @@ struggle with a few gigabytes of data, and time to wait for the data to make it across a socket. It’s that last point we’ll focus on today. In an age of multi-gigabit networks, why is it even a problem in the first place? And it is a problem—research by Mark Raasveldt and Hannes Mühleisen in their [2017 -paper](https://ir.cwi.nl/pub/26415/p852-muehleisen.pdf) demonstrated that some +paper](https://doi.org/10.14778/3115404.3115408) demonstrated that some systems take over **ten minutes** to transfer a dataset that should only take ten *seconds*. From d8141781da8911efaf1a6ebf1b8082f771bbdaf9 Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 17 Feb 2025 02:48:36 -0500 Subject: [PATCH 05/14] Other updates --- _posts/2025-02-04-data-wants-to-be-free.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index 554b9fc8f118..6522614aec78 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -76,6 +76,12 @@ IPC](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interp on the same dataset, and show how Arrow (with all the benefit of hindsight) makes better trade-offs than its predecessors. +When you execute a query with PostgreSQL, your client/driver uses the +PostgreSQL wire protocol to send the query and get back the result. Inside +that protocol, the results are encoded with the PostgreSQL binary format[^textbinary]. + +[^textbinary]: There is a text format too, and that is often the default used by many clients. We won't discuss it here. + First, we’ll create a table and fill it with data: ``` @@ -267,7 +273,8 @@ Arrow looks quite…intimidating…at first glance. There’s a giant header, an Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer. Strings do still require a length per value, but the overhead isn’t added where it isn’t otherwise needed. And nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values in the first place, saving space. Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay. Even the header isn’t actually the disadvantage it looks like. The header contains the schema, which makes the data stream self-describing. With PostgreSQL, you need to get the schema from somewhere else. So we aren’t making an apples-to-apples comparison in the first place: PostgreSQL still has to transfer the schema, it’s just not part of the “binary format” that we’re looking at here. -Meanwhile, there’s actually a more insidious problem with PostgreSQL we’ve overlooked so far: alignment. Remember that 2 byte field count at the start of every row? Well, that means all the 4 byte integers after it are now unaligned…so you can’t use them without copying them (or doing a very slow unaligned load). Arrow, on the other hand, strategically adds some padding (overhead) to align the data, and lets you use little-endian or big-endian byte order depending on your data. And Arrow doesn’t apply expensive encodings to the data that require further parsing; there’s just optional compression that can be enabled if it suits your data[^2]. So **you can use Arrow data as-is without having to parse every value**. + +Meanwhile, there’s actually another problem with PostgreSQL we’ve overlooked so far: alignment. Remember that 2 byte field count at the start of every row? Well, that means all the 4 byte integers after it are now unaligned. That requires extra effort to handle properly (e.g. using explicit unaligned load APIs), lest you suffer [undefined behavior](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p7), a performance penalty, or even a runtime error (depending on the CPU). Arrow, on the other hand, strategically adds some padding (overhead) to align the data, and lets you use little-endian or big-endian byte order depending on your data. And Arrow doesn’t apply expensive encodings to the data that require further parsing; there’s just optional compression that can be enabled if it suits your data[^2]. So **you can use Arrow data as-is without having to parse every value**. That’s the benefit of Arrow being a standardized, in-memory data format. Data coming off the wire is already in Arrow format, and can be passed on directly to [DuckDB](https://duckdb.org), [pandas](https://pandas.pydata.org), [polars](https://pola.rs), [cuDF](https://docs.rapids.ai/api/cudf/stable/), [DataFusion](https://datafusion.apache.org), or any number of systems. Even if the PostgreSQL format fixed many of these problems we’ve discussed—adding padding to align fields, using little-endian or making endianness configurable, trimming the overhead—you’d still end up having to convert the data to another format to use downstream. And even then, if you really did want to use the PostgreSQL binary format[^3], the documentation rather unfortunately points you to the source code for the details. Arrow, on the other hand, has a specification, documentation, and multiple implementations (including third-party ones) across a dozen languages for you to pick up and use in your own applications. @@ -278,7 +285,7 @@ Now, we don’t mean to pick on PostgreSQL here. Obviously, PostgreSQL is a full So if Arrow is so great, how can we actually use it to build our own protocols? Luckily, Arrow comes with a variety of building blocks for different situations. * We just talked about [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc) before. Where Arrow is the in-memory format defining how arrays of data are laid out, er, in memory, Arrow IPC defines how to serialize and deserialize Arrow data so it can be sent somewhere else—whether that means being written to a file, to a socket, into a shared buffer, or otherwise. Arrow IPC organizes data as a sequence of messages, making it easy to stream over your favorite transport, like WebSockets. -* [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http) is “just” streaming Arrow IPC over HTTP. It’s standardized so that different clients agree on how exactly to do this, and there’s examples of clients and servers across several languages, how to use HTTP Range requests, using multipart/mixed requests to send combined JSON and Arrow responses, and more. While not a full protocol in and of itself, it’ll fit right in when building REST APIs. +* [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http) is “just” streaming Arrow IPC over HTTP. The Arrow community is working on standardizing it, so that different clients agree on how exactly to do this. As part of the standardization effort, there’s examples of clients and servers across several languages, how to use HTTP Range requests, using multipart/mixed requests to send combined JSON and Arrow responses, and more. While not a full protocol in and of itself, it’ll fit right in when building REST APIs. * [**Disassociated IPC**](https://arrow.apache.org/docs/format/DissociatedIPC.html) combines Arrow with advanced network transports like [UCX](https://openucx.org/) and [libfabric](https://ofiwg.github.io/libfabric/). For those who require the absolute best performance and have the specialized hardware to boot, this allows you to send Arrow data at full throttle, taking advantage of scatter-gather, Infiniband, and more. * [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html) is a fully defined protocol for accessing relational databases. Think of it as an alternative to the full PostgreSQL wire protocol: it defines how to connect to a database, execute queries, fetch results, view the catalog, and so on. For database developers, Flight SQL provides a fully Arrow-native protocol with clients for several programming languages and drivers for ADBC, JDBC, and ODBC—all of which you don’t have to build yourself. * And finally, [**ADBC**](https://arrow.apache.org/docs/format/ADBC.html) actually isn’t a protocol. Instead, it’s an API abstraction layer for working with databases (like JDBC and ODBC—bet you didn’t see that coming), that’s Arrow-native and doesn’t require transposing or converting columnar data back and forth. ADBC gives you a single API to access data from multiple databases, whether they use Flight SQL or something else under the hood, and if a conversion is absolutely necessary, ADBC handles the details so that you don’t have to build out a dozen connectors on your own. @@ -288,7 +295,7 @@ So to summarize: * If you’re *using* a database or other data system, you want [**ADBC**](https://arrow.apache.org/adbc/). * If you’re *building* a database, you want [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html). * If you’re working with specialized networking hardware (you’ll know if you are—that stuff doesn’t come cheap), you want the [**Disassociated IPC Protocol**](https://arrow.apache.org/docs/format/DissociatedIPC.html). -* If you’re *designing* a REST-ish API, you want **Arrow HTTP**. (gRPC users: stay tuned.) +* If you’re *designing* a REST-ish API, you want **Arrow HTTP**. * And otherwise, you can roll-your-own with [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc). ![][image1] From fdd79b753a2656c9d6522f1cee8399eb13527a44 Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 17 Feb 2025 03:07:35 -0500 Subject: [PATCH 06/14] Don't use feather --- _posts/2025-02-04-data-wants-to-be-free.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index 6522614aec78..4ab5a5fb51c6 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -134,12 +134,14 @@ How does Arrow compare? We can use [ADBC](https://arrow.apache.org/adbc/current/ ```console >>> import adbc_driver_postgresql.dbapi ->>> import pyarrow.feather +>>> import pyarrow.ipc >>> conn = adbc_driver_postgresql.dbapi.connect("...") >>> cur = conn.cursor() >>> cur.execute("SELECT * FROM demo") >>> data = cur.fetchallarrow() ->>> pyarrow.feather.write_feather(data, "demo.arrow") +>>> writer = pyarrow.ipc.new_file("demo.arrow", data.schema) +>>> writer.write_table(data) +>>> writer.close() ``` (Aside: look how easy that is!) From 3a51d53808602b4c491ef242852af9bb6523c57e Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 17 Feb 2025 21:39:34 -0500 Subject: [PATCH 07/14] Add missing image --- _posts/2025-02-04-data-wants-to-be-free.md | 2 +- assets/data_wants_to_be_free/flowchart.png | Bin 0 -> 74155 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/data_wants_to_be_free/flowchart.png diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index 4ab5a5fb51c6..b5fc1c45ce03 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -300,7 +300,7 @@ So to summarize: * If you’re *designing* a REST-ish API, you want **Arrow HTTP**. * And otherwise, you can roll-your-own with [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc). -![][image1] +![A flowchart of the decision points.]({{ site.baseurl }}/assets/data_wants_to_be_free/flowchart.png){:class="img-responsive" width="100%"} ## Conclusion diff --git a/assets/data_wants_to_be_free/flowchart.png b/assets/data_wants_to_be_free/flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..93a96722d7240461b7bdfdb0c20d51c5cc8d6cf0 GIT binary patch literal 74155 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 literal 0 HcmV?d00001 From eeb8c40f78d3a8624217c0bd4585c782218f276c Mon Sep 17 00:00:00 2001 From: David Li Date: Tue, 18 Feb 2025 02:24:34 -0500 Subject: [PATCH 08/14] Finish annotating PostgreSQL output --- _posts/2025-02-04-data-wants-to-be-free.md | 68 ++++++++++++---------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index b5fc1c45ce03..0a87daba1f93 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -32,16 +32,20 @@ limitations under the License. @@ -50,14 +54,14 @@ Arrow as a data interchange format for databases and query engines._ As data practitioners, we often find our data “held hostage”. Instead of being able to use data as soon as we get it, we have to spend time—time to parse and -clean up inefficient and messy CSV files, time to wait for an outdated query engine to -struggle with a few gigabytes of data, and time to wait for the data to make -it across a socket. It’s that last point we’ll focus on today. In an age of -multi-gigabit networks, why is it even a problem in the first place? And it is -a problem—research by Mark Raasveldt and Hannes Mühleisen in their [2017 -paper](https://doi.org/10.14778/3115404.3115408) demonstrated that some -systems take over **ten minutes** to transfer a dataset that should only take -ten *seconds*. +clean up inefficient and messy CSV files, time to wait for an outdated query +engine to struggle with a few gigabytes of data, and time to wait for the data +to make it across a socket. It’s that last point we’ll focus on today. In an +age of multi-gigabit networks, why is it even a problem in the first place? +And make no mistake, it is a problem—research by Mark Raasveldt and Hannes +Mühleisen in their [2017 paper](https://doi.org/10.14778/3115404.3115408) +found that some systems take over **ten minutes** to transfer a dataset that +should only take ten *seconds*. Why are we waiting 60 times as long as we need to? [As we've argued before, serialization overheads plague our @@ -104,32 +108,32 @@ Then we can look at the actual bytes of the data and annotate them, based on the 00000008: 0d 0a 00 00 00 00 00 00 ........ and extension 00000010: 00 00 00 00 03 00 00 00 ........ Values in row 00000018: 08 00 00 00 00 00 00 00 ........ Length of value -00000020: 01 00 00 00 03 66 6f 6f .....foo Data -00000028: 00 00 00 08 00 00 00 00 ........ -00000030: 00 00 00 40 00 03 00 00 ...@.... -00000038: 00 08 00 00 00 00 00 00 ........ -00000040: 00 02 00 00 00 0f 61 20 ......a -00000048: 6c 6f 6e 67 65 72 20 73 longer s -00000050: 74 72 69 6e 67 00 00 00 tring... -00000058: 08 00 00 00 00 00 00 00 ........ -00000060: 80 00 03 00 00 00 08 00 ........ -00000068: 00 00 00 00 00 00 03 00 ........ -00000070: 00 00 12 79 65 74 20 61 ...yet a -00000078: 6e 6f 74 68 65 72 20 73 nother s -00000080: 74 72 69 6e 67 00 00 00 tring... -00000088: 08 00 00 00 00 00 00 00 ........ -00000090: 0a ff ff ... - -Honestly, PostgreSQL’s binary format is quite understandable, and pretty compact at first glance. But a closer look isn’t so favorable. **PostgreSQL has overheads proportional to the number of rows and columns**: +00000020: 01 00 00 00 03 66 6f 6f .....foo Data +00000028: 00 00 00 08 00 00 00 00 ........ +00000030: 00 00 00 40 00 03 00 00 ...@.... +00000038: 00 08 00 00 00 00 00 00 ........ +00000040: 00 02 00 00 00 0f 61 20 ......a +00000048: 6c 6f 6e 67 65 72 20 73 longer s +00000050: 74 72 69 6e 67 00 00 00 tring... +00000058: 08 00 00 00 00 00 00 00 ........ +00000060: 80 00 03 00 00 00 08 00 ........ +00000068: 00 00 00 00 00 00 03 00 ........ +00000070: 00 00 12 79 65 74 20 61 ...yet a +00000078: 6e 6f 74 68 65 72 20 73 nother s +00000080: 74 72 69 6e 67 00 00 00 tring... +00000088: 08 00 00 00 00 00 00 00 ........ +00000090: 0a ff ff ... End of stream + +Honestly, PostgreSQL’s binary format is quite understandable, and compact at first glance. It's just a series of length-prefixed fields. But a closer look isn’t so favorable. **PostgreSQL has overheads proportional to the number of rows and columns**: * Every row has a 2 byte prefix for the number of values in the row. *But the data is tabular—we already know this info, and it doesn’t change\!* * Every value of every row has a 4 byte prefix for the length of the following data, or \-1 if the value is NULL. *But we know the data types, and those don’t change—most values have a fixed, known length\!* * All values are big-endian. *But most of our devices are little-endian, so the data has to be converted.* -Sure, we need to store if a value is NULL or not, but 4 bytes is a *bit* much for a boolean. String data and other non-fixed-length types need per-value lengths, but PostgreSQL adds the length for *every* type of value. And converting big-endian to little-endian is pretty trivial…but it’s still work that stands in between you and your data. To PostgreSQL’s credit, its format is at least cheap and easy to parse—[other formats](https://protobuf.dev/programming-guides/encoding/) get fancy with tricks like “varint” encoding which are quite expensive. - For example, a single column of int32 values would have 4 bytes of data and 6 bytes of overhead per row—**60% is “wasted\!”**[^1] The ratio gets a little better with more columns (but not with more rows); in the limit we approach “only” 50% overhead. +Sure, we need to store if a value is NULL or not, but 4 bytes is a *bit* much for a boolean. String data and other non-fixed-length types need per-value lengths, but PostgreSQL adds the length for *every* type of value. And converting big-endian to little-endian is pretty trivial…but it’s still work that stands in between you and your data. To PostgreSQL’s credit, its format is at least cheap and easy to parse—[other formats](https://protobuf.dev/programming-guides/encoding/) get fancy with tricks like “varint” encoding which are quite expensive. + How does Arrow compare? We can use [ADBC](https://arrow.apache.org/adbc/current/driver/postgresql.html) to pull the PostgreSQL table into an Arrow table, then annotate it like before: ```console @@ -297,7 +301,7 @@ So to summarize: * If you’re *using* a database or other data system, you want [**ADBC**](https://arrow.apache.org/adbc/). * If you’re *building* a database, you want [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html). * If you’re working with specialized networking hardware (you’ll know if you are—that stuff doesn’t come cheap), you want the [**Disassociated IPC Protocol**](https://arrow.apache.org/docs/format/DissociatedIPC.html). -* If you’re *designing* a REST-ish API, you want **Arrow HTTP**. +* If you’re *designing* a REST-ish API, you want [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http). * And otherwise, you can roll-your-own with [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc). ![A flowchart of the decision points.]({{ site.baseurl }}/assets/data_wants_to_be_free/flowchart.png){:class="img-responsive" width="100%"} From a20b3f503d675b1eada3506848f592b5796f8669 Mon Sep 17 00:00:00 2001 From: David Li Date: Fri, 21 Feb 2025 02:20:20 -0500 Subject: [PATCH 09/14] Annotate --- _posts/2025-02-04-data-wants-to-be-free.md | 155 ++++----------------- 1 file changed, 27 insertions(+), 128 deletions(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index 0a87daba1f93..a5cd6c6c054b 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -143,140 +143,39 @@ How does Arrow compare? We can use [ADBC](https://arrow.apache.org/adbc/current/ >>> cur = conn.cursor() >>> cur.execute("SELECT * FROM demo") >>> data = cur.fetchallarrow() ->>> writer = pyarrow.ipc.new_file("demo.arrow", data.schema) +>>> writer = pyarrow.ipc.new_stream("demo.arrows", data.schema) >>> writer.write_table(data) >>> writer.close() ``` (Aside: look how easy that is!) -
00000000: 41 52 52 4f 57 31 00 00  ARROW1..
-00000008: ff ff ff ff d8 00 00 00  ........
-00000010: 10 00 00 00 00 00 0a 00  ........
-00000018: 0c 00 06 00 05 00 08 00  ........
-00000020: 0a 00 00 00 00 01 04 00  ........
-00000028: 0c 00 00 00 08 00 08 00  ........
-00000030: 00 00 04 00 08 00 00 00  ........
-00000038: 04 00 00 00 03 00 00 00  ........
-00000040: 74 00 00 00 38 00 00 00  t...8...
-00000048: 04 00 00 00 a8 ff ff ff  ........
-00000050: 00 00 01 02 10 00 00 00  ........
-00000058: 18 00 00 00 04 00 00 00  ........
-00000060: 00 00 00 00 04 00 00 00  ........
-00000068: 76 61 6c 32 00 00 00 00  val2....
-00000070: 9c ff ff ff 00 00 00 01  ........
-00000078: 40 00 00 00 d8 ff ff ff  @.......
-00000080: 00 00 01 05 10 00 00 00  ........
-00000088: 18 00 00 00 04 00 00 00  ........
-00000090: 00 00 00 00 03 00 00 00  ........
-00000098: 76 61 6c 00 04 00 04 00  val.....
-000000a0: 04 00 00 00 10 00 14 00  ........
-000000a8: 08 00 06 00 07 00 0c 00  ........
-000000b0: 00 00 10 00 10 00 00 00  ........
-000000b8: 00 00 01 02 10 00 00 00  ........
-000000c0: 1c 00 00 00 04 00 00 00  ........
-000000c8: 00 00 00 00 02 00 00 00  ........
-000000d0: 69 64 00 00 08 00 0c 00  id......
-000000d8: 08 00 07 00 08 00 00 00  ........
-000000e0: 00 00 00 01 40 00 00 00  ....@...
-000000e8: ff ff ff ff 08 01 00 00  ........
-000000f0: 14 00 00 00 00 00 00 00  ........
-000000f8: 0c 00 18 00 06 00 05 00  ........
-00000100: 08 00 0c 00 0c 00 00 00  ........
-00000108: 00 03 04 00 1c 00 00 00  ........
-00000110: c8 00 00 00 00 00 00 00  ........
-00000118: 00 00 00 00 0c 00 1c 00  ........
-00000120: 10 00 04 00 08 00 0c 00  ........
-00000128: 0c 00 00 00 98 00 00 00  ........
-00000130: 1c 00 00 00 14 00 00 00  ........
-00000138: 03 00 00 00 00 00 00 00  ........
-00000140: 00 00 00 00 04 00 04 00  ........
-00000148: 04 00 00 00 07 00 00 00  ........
-00000150: 00 00 00 00 00 00 00 00  ........
-00000158: 00 00 00 00 00 00 00 00  ........
-00000160: 00 00 00 00 00 00 00 00  ........
-00000168: 2a 00 00 00 00 00 00 00  *.......
-00000170: 30 00 00 00 00 00 00 00  0.......
-00000178: 00 00 00 00 00 00 00 00  ........
-00000180: 30 00 00 00 00 00 00 00  0.......
-00000188: 27 00 00 00 00 00 00 00  .......
-00000190: 58 00 00 00 00 00 00 00  X.......
-00000198: 3b 00 00 00 00 00 00 00  ;.......
-000001a0: 98 00 00 00 00 00 00 00  ........
-000001a8: 00 00 00 00 00 00 00 00  ........
-000001b0: 98 00 00 00 00 00 00 00  ........
-000001b8: 2a 00 00 00 00 00 00 00  *.......
-000001c0: 00 00 00 00 03 00 00 00  ........
-000001c8: 03 00 00 00 00 00 00 00  ........
-000001d0: 00 00 00 00 00 00 00 00  ........
-000001d8: 03 00 00 00 00 00 00 00  ........
-000001e0: 00 00 00 00 00 00 00 00  ........
-000001e8: 03 00 00 00 00 00 00 00  ........
-000001f0: 00 00 00 00 00 00 00 00  ........
-000001f8: 18 00 00 00 00 00 00 00  ........
-00000200: 04 22 4d 18 60 40 82 13  ."M.@..
-00000208: 00 00 00 22 01 00 01 00  ..."....
-00000210: 12 02 07 00 90 00 03 00  ........
-00000218: 00 00 00 00 00 00 00 00  ........
-00000220: 00 00 00 00 00 00 00 00  ........
-00000228: 10 00 00 00 00 00 00 00  ........
-00000230: 04 22 4d 18 60 40 82 10  ."M.@..
-00000238: 00 00 80 00 00 00 00 03  ........
-00000240: 00 00 00 12 00 00 00 24  .......$
-00000248: 00 00 00 00 00 00 00 00  ........
-00000250: 24 00 00 00 00 00 00 00  $.......
-00000258: 04 22 4d 18 60 40 82 24  ."M.@.$
-00000260: 00 00 80 66 6f 6f 61 20  ...fooa
-00000268: 6c 6f 6e 67 65 72 20 73  longer s
-00000270: 74 72 69 6e 67 79 65 74  tringyet
-00000278: 20 61 6e 6f 74 68 65 72   another
-00000280: 20 73 74 72 69 6e 67 00   string.
-00000288: 00 00 00 00 00 00 00 00  ........
-00000290: 18 00 00 00 00 00 00 00  ........
-00000298: 04 22 4d 18 60 40 82 13  ."M.@..
-000002a0: 00 00 00 22 40 00 01 00  ..."@...
-000002a8: 12 80 07 00 90 00 0a 00  ........
-000002b0: 00 00 00 00 00 00 00 00  ........
-000002b8: 00 00 00 00 00 00 00 00  ........
-000002c0: ff ff ff ff 00 00 00 00  ........
-000002c8: 10 00 00 00 0c 00 14 00  ........
-000002d0: 06 00 08 00 0c 00 10 00  ........
-000002d8: 0c 00 00 00 00 00 04 00  ........
-000002e0: 34 00 00 00 24 00 00 00  4...$...
-000002e8: 04 00 00 00 01 00 00 00  ........
-000002f0: e8 00 00 00 00 00 00 00  ........
-000002f8: 10 01 00 00 00 00 00 00  ........
-00000300: c8 00 00 00 00 00 00 00  ........
-00000308: 00 00 00 00 08 00 08 00  ........
-00000310: 00 00 04 00 08 00 00 00  ........
-00000318: 04 00 00 00 03 00 00 00  ........
-00000320: 74 00 00 00 38 00 00 00  t...8...
-00000328: 04 00 00 00 a8 ff ff ff  ........
-00000330: 00 00 01 02 10 00 00 00  ........
-00000338: 18 00 00 00 04 00 00 00  ........
-00000340: 00 00 00 00 04 00 00 00  ........
-00000348: 76 61 6c 32 00 00 00 00  val2....
-00000350: 9c ff ff ff 00 00 00 01  ........
-00000358: 40 00 00 00 d8 ff ff ff  @.......
-00000360: 00 00 01 05 10 00 00 00  ........
-00000368: 18 00 00 00 04 00 00 00  ........
-00000370: 00 00 00 00 03 00 00 00  ........
-00000378: 76 61 6c 00 04 00 04 00  val.....
-00000380: 04 00 00 00 10 00 14 00  ........
-00000388: 08 00 06 00 07 00 0c 00  ........
-00000390: 00 00 10 00 10 00 00 00  ........
-00000398: 00 00 01 02 10 00 00 00  ........
-000003a0: 1c 00 00 00 04 00 00 00  ........
-000003a8: 00 00 00 00 02 00 00 00  ........
-000003b0: 69 64 00 00 08 00 0c 00  id......
-000003b8: 08 00 07 00 08 00 00 00  ........
-000003c0: 00 00 00 01 40 00 00 00  ....@...
-000003c8: 00 01 00 00 41 52 52 4f  ....ARRO
-000003d0: 57 31                    W1
- -Arrow looks quite…intimidating…at first glance. There’s a giant header, and lots of things that don’t seem related to our dataset at all, plus mysterious padding that seems to exist solely to take up space. But the important thing is that **the overhead is fixed**. Whether you’re transferring one row or a billion, the overhead doesn’t change. And unlike PostgreSQL, **no per-value parsing is required**. - -Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer. Strings do still require a length per value, but the overhead isn’t added where it isn’t otherwise needed. And nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values in the first place, saving space. Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay. +
00000000: ff ff ff ff d8 00 00 00  ........
+00000008: 10 00 00 00 00 00 0a 00  ........
+⋮         (208 bytes)
+000000e0: ff ff ff ff f8 00 00 00  ........
+000000e8: 14 00 00 00 00 00 00 00  ........
+⋮         (240 bytes)
+000001e0: 01 00 00 00 00 00 00 00  ........
+000001e8: 02 00 00 00 00 00 00 00  ........
+000001f0: 03 00 00 00 00 00 00 00  ........
+000001f8: 00 00 00 00 03 00 00 00  ........
+00000200: 12 00 00 00 24 00 00 00  ....$...
+00000208: 66 6f 6f 61 20 6c 6f 6e  fooa lon
+00000210: 67 65 72 20 73 74 72 69  ger stri
+00000218: 6e 67 79 65 74 20 61 6e  ngyet an
+00000220: 6f 74 68 65 72 20 73 74  other st
+00000228: 72 69 6e 67 00 00 00 00  ring....
+00000230: 40 00 00 00 00 00 00 00  @.......
+00000238: 80 00 00 00 00 00 00 00  ........
+00000240: 0a 00 00 00 00 00 00 00  ........
+00000248: ff ff ff ff 00 00 00 00  ........
+ +Arrow looks quite…intimidating…at first glance. There’s a giant header that don’t seem related to our dataset at all, plus mysterious padding that seems to exist solely to take up space. But the important thing is that **the overhead is fixed**. Whether you’re transferring one row or a billion, the overhead doesn’t change. And unlike PostgreSQL, **no per-value parsing is required**. + +Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer[^header]. Strings do still require a length per value, but the overhead isn’t added where it isn’t otherwise needed. And nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values in the first place (as it is here), saving space. Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay. + +[^header]: That's what's being stored in that ginormous header (among other things)—the lengths of all the buffers. Even the header isn’t actually the disadvantage it looks like. The header contains the schema, which makes the data stream self-describing. With PostgreSQL, you need to get the schema from somewhere else. So we aren’t making an apples-to-apples comparison in the first place: PostgreSQL still has to transfer the schema, it’s just not part of the “binary format” that we’re looking at here. From e46f2c015586bcd80647614af83016fe48b9767b Mon Sep 17 00:00:00 2001 From: David Li Date: Sat, 22 Feb 2025 11:38:34 +0900 Subject: [PATCH 10/14] More updates --- _posts/2025-02-04-data-wants-to-be-free.md | 64 +++++++++++----------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index a5cd6c6c054b..374e6c329d34 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -59,9 +59,12 @@ engine to struggle with a few gigabytes of data, and time to wait for the data to make it across a socket. It’s that last point we’ll focus on today. In an age of multi-gigabit networks, why is it even a problem in the first place? And make no mistake, it is a problem—research by Mark Raasveldt and Hannes -Mühleisen in their [2017 paper](https://doi.org/10.14778/3115404.3115408) +Mühleisen in their [2017 paper](https://doi.org/10.14778/3115404.3115408)[^freepdf] found that some systems take over **ten minutes** to transfer a dataset that -should only take ten *seconds*. +should only take ten *seconds*[^ten]. + +[^freepdf]: The paper is freely available from [VLDB](https://www.vldb.org/pvldb/vol10/p1022-muehleisen.pdf). +[^ten]: Figure 1 in the paper shows Hive and MongoDB taking over 600 seconds vs the baseline of 10 seconds for netcat to transfer the CSV file. Of course, that means the comparison is not entirely fair since the CSV file is not being parsed, but it gives an idea of the magnitudes involved. Why are we waiting 60 times as long as we need to? [As we've argued before, serialization overheads plague our @@ -80,9 +83,9 @@ IPC](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interp on the same dataset, and show how Arrow (with all the benefit of hindsight) makes better trade-offs than its predecessors. -When you execute a query with PostgreSQL, your client/driver uses the +When you execute a query with PostgreSQL, the client/driver uses the PostgreSQL wire protocol to send the query and get back the result. Inside -that protocol, the results are encoded with the PostgreSQL binary format[^textbinary]. +that protocol, the result set is encoded with the PostgreSQL binary format[^textbinary]. [^textbinary]: There is a text format too, and that is often the default used by many clients. We won't discuss it here. @@ -102,7 +105,7 @@ postgres=# COPY demo TO '/tmp/demo.bin' WITH BINARY; COPY 3 ``` -Then we can look at the actual bytes of the data and annotate them, based on the [documentation](https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.4) for parsing the PostgreSQL binary format: +Then we can annotate the actual bytes of the data based on the [documentation](https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.4):
00000000: 50 47 43 4f 50 59 0a ff  PGCOPY..  COPY signature, flags,
 00000008: 0d 0a 00 00 00 00 00 00  ........  and extension
@@ -127,12 +130,10 @@ Then we can look at the actual bytes of the data and annotate them, based on the
 Honestly, PostgreSQL’s binary format is quite understandable, and compact at first glance. It's just a series of length-prefixed fields. But a closer look isn’t so favorable. **PostgreSQL has overheads proportional to the number of rows and columns**:
 
 * Every row has a 2 byte prefix for the number of values in the row. *But the data is tabular—we already know this info, and it doesn’t change\!*
-* Every value of every row has a 4 byte prefix for the length of the following data, or \-1 if the value is NULL. *But we know the data types, and those don’t change—most values have a fixed, known length\!*
+* Every value of every row has a 4 byte prefix for the length of the following data, or \-1 if the value is NULL. *But we know the data types, and those don’t change—plus, values of most types have a fixed, known length\!*
 * All values are big-endian. *But most of our devices are little-endian, so the data has to be converted.*
 
-For example, a single column of int32 values would have 4 bytes of data and 6 bytes of overhead per row—**60% is “wasted\!”**[^1] The ratio gets a little better with more columns (but not with more rows); in the limit we approach “only” 50% overhead.
-
-Sure, we need to store if a value is NULL or not, but 4 bytes is a *bit* much for a boolean. String data and other non-fixed-length types need per-value lengths, but PostgreSQL adds the length for *every* type of value. And converting big-endian to little-endian is pretty trivial…but it’s still work that stands in between you and your data. To PostgreSQL’s credit, its format is at least cheap and easy to parse—[other formats](https://protobuf.dev/programming-guides/encoding/) get fancy with tricks like “varint” encoding which are quite expensive.
+For example, a single column of int32 values would have 4 bytes of data and 6 bytes of overhead per row—**60% is “wasted\!”**[^1] The ratio gets a little better with more columns (but not with more rows); in the limit we approach “only” 50% overhead.  And then each of the values has to be converted (even if endian-swapping is trivial).  To PostgreSQL’s credit, its format is at least cheap and easy to parse—[other formats](https://protobuf.dev/programming-guides/encoding/) get fancy with tricks like “varint” encoding which are quite expensive.
 
 How does Arrow compare? We can use [ADBC](https://arrow.apache.org/adbc/current/driver/postgresql.html) to pull the PostgreSQL table into an Arrow table, then annotate it like before:
 
@@ -148,50 +149,52 @@ How does Arrow compare? We can use [ADBC](https://arrow.apache.org/adbc/current/
 >>> writer.close()
 ```
 
-(Aside: look how easy that is!)
-
-
00000000: ff ff ff ff d8 00 00 00  ........
-00000008: 10 00 00 00 00 00 0a 00  ........
+
00000000: ff ff ff ff d8 00 00 00  ........  IPC message length
+00000008: 10 00 00 00 00 00 0a 00  ........  IPC schema(208 bytes)
-000000e0: ff ff ff ff f8 00 00 00  ........
-000000e8: 14 00 00 00 00 00 00 00  ........
+000000e0: ff ff ff ff f8 00 00 00  ........  IPC message length
+000000e8: 14 00 00 00 00 00 00 00  ........  IPC record batch(240 bytes)
-000001e0: 01 00 00 00 00 00 00 00  ........
+000001e0: 01 00 00 00 00 00 00 00  ........  Data for column #1
 000001e8: 02 00 00 00 00 00 00 00  ........
 000001f0: 03 00 00 00 00 00 00 00  ........
-000001f8: 00 00 00 00 03 00 00 00  ........
+000001f8: 00 00 00 00 03 00 00 00  ........  String offsets
 00000200: 12 00 00 00 24 00 00 00  ....$...
-00000208: 66 6f 6f 61 20 6c 6f 6e  fooa lon
+00000208: 66 6f 6f 61 20 6c 6f 6e  fooa lon  Data for column #2
 00000210: 67 65 72 20 73 74 72 69  ger stri
 00000218: 6e 67 79 65 74 20 61 6e  ngyet an
 00000220: 6f 74 68 65 72 20 73 74  other st
-00000228: 72 69 6e 67 00 00 00 00  ring....
-00000230: 40 00 00 00 00 00 00 00  @.......
+00000228: 72 69 6e 67 00 00 00 00  ring....  Alignment padding
+00000230: 40 00 00 00 00 00 00 00  @.......  Data for column #3
 00000238: 80 00 00 00 00 00 00 00  ........
 00000240: 0a 00 00 00 00 00 00 00  ........
-00000248: ff ff ff ff 00 00 00 00  ........
+00000248: ff ff ff ff 00 00 00 00 ........ IPC end-of-stream
Arrow looks quite…intimidating…at first glance. There’s a giant header that don’t seem related to our dataset at all, plus mysterious padding that seems to exist solely to take up space. But the important thing is that **the overhead is fixed**. Whether you’re transferring one row or a billion, the overhead doesn’t change. And unlike PostgreSQL, **no per-value parsing is required**. -Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer[^header]. Strings do still require a length per value, but the overhead isn’t added where it isn’t otherwise needed. And nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values in the first place (as it is here), saving space. Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay. +Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer[^header]. Overhead isn't added where it isn't otherwise needed. Strings still have a length per value. Nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values (as it is here). Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay. [^header]: That's what's being stored in that ginormous header (among other things)—the lengths of all the buffers. -Even the header isn’t actually the disadvantage it looks like. The header contains the schema, which makes the data stream self-describing. With PostgreSQL, you need to get the schema from somewhere else. So we aren’t making an apples-to-apples comparison in the first place: PostgreSQL still has to transfer the schema, it’s just not part of the “binary format” that we’re looking at here. +Even the header isn’t actually the disadvantage it looks like. The header contains the schema, which makes the data stream self-describing. With PostgreSQL, you need to get the schema from somewhere else. So we aren’t making an apples-to-apples comparison in the first place: PostgreSQL still has to transfer the schema, it’s just not part of the “binary format” that we’re looking at here[^binaryheader]. + +[^binaryheader]: And conversely, the PGCOPY header is specific to the COPY command we executed to get a bulk response. -Meanwhile, there’s actually another problem with PostgreSQL we’ve overlooked so far: alignment. Remember that 2 byte field count at the start of every row? Well, that means all the 4 byte integers after it are now unaligned. That requires extra effort to handle properly (e.g. using explicit unaligned load APIs), lest you suffer [undefined behavior](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p7), a performance penalty, or even a runtime error (depending on the CPU). Arrow, on the other hand, strategically adds some padding (overhead) to align the data, and lets you use little-endian or big-endian byte order depending on your data. And Arrow doesn’t apply expensive encodings to the data that require further parsing; there’s just optional compression that can be enabled if it suits your data[^2]. So **you can use Arrow data as-is without having to parse every value**. +There’s actually another problem with PostgreSQL: alignment. The 2 byte field count at the start of every row means all the 8 byte integers after it are unaligned. And that requires extra effort to handle properly (e.g. explicit unaligned load idioms), lest you suffer [undefined behavior](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p7), a performance penalty, or even a runtime error. Arrow, on the other hand, strategically adds some padding to keep data aligned, and lets you use little-endian or big-endian byte order depending on your data. And Arrow doesn’t apply expensive encodings to the data that require further parsing. So generally, **you can use Arrow data as-is without having to parse every value**. -That’s the benefit of Arrow being a standardized, in-memory data format. Data coming off the wire is already in Arrow format, and can be passed on directly to [DuckDB](https://duckdb.org), [pandas](https://pandas.pydata.org), [polars](https://pola.rs), [cuDF](https://docs.rapids.ai/api/cudf/stable/), [DataFusion](https://datafusion.apache.org), or any number of systems. Even if the PostgreSQL format fixed many of these problems we’ve discussed—adding padding to align fields, using little-endian or making endianness configurable, trimming the overhead—you’d still end up having to convert the data to another format to use downstream. And even then, if you really did want to use the PostgreSQL binary format[^3], the documentation rather unfortunately points you to the source code for the details. Arrow, on the other hand, has a specification, documentation, and multiple implementations (including third-party ones) across a dozen languages for you to pick up and use in your own applications. +That’s the benefit of Arrow being a standardized data format. By using Arrow for serialization, data coming off the wire is already in Arrow format, and can furthermore be directly passed on to [DuckDB](https://duckdb.org), [pandas](https://pandas.pydata.org), [polars](https://pola.rs), [cuDF](https://docs.rapids.ai/api/cudf/stable/), [DataFusion](https://datafusion.apache.org), or any number of systems. Meanwhile, even if the PostgreSQL format addressed these problems—adding padding to align fields, using little-endian or making endianness configurable, trimming the overhead—you’d still end up having to convert the data to another format (probably Arrow) to use downstream. -Now, we don’t mean to pick on PostgreSQL here. Obviously, PostgreSQL is a full-featured database with a storied history, a different set of goals and constraints than Arrow, and many happy users. Arrow isn’t trying to compete in that space anyways. But their domains do intersect. PostgreSQL’s wire protocol has [become a de facto standard](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html), with even brand new products like Google’s AlloyDB using it, and so its design affects many projects[^4]. In fact, AlloyDB is a great example of a shiny new columnar query engine being locked behind a row-oriented client protocol from the 90s. So [Amdahl’s law](https://en.wikipedia.org/wiki/Amdahl's_law) rears its head again—optimizing the “front” and “back” of your data pipeline doesn’t matter when the middle is slow as molasses. +Even if you really did want to use the PostgreSQL binary format everywhere[^3], the documentation rather unfortunately points you to the C source code as the documentation. Arrow, on the other hand, has a [specification](https://github.com/apache/arrow/tree/main/format), [documentation](https://arrow.apache.org/docs/format/Columnar.html), and multiple [implementations](https://arrow.apache.org/docs/#implementations) (including third-party ones) across a dozen languages for you to pick up and use in your own applications. + +Now, we don’t mean to pick on PostgreSQL here. Obviously, PostgreSQL is a full-featured database with a storied history, a different set of goals and constraints than Arrow, and many happy users. Arrow isn’t trying to compete in that space. But their domains do intersect. PostgreSQL’s wire protocol has [become a de facto standard](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html), with even brand new products like Google’s AlloyDB using it, and so its design affects many projects[^4]. In fact, AlloyDB is a great example of a shiny new columnar query engine being locked behind a row-oriented client protocol from the 90s. So [Amdahl’s law](https://en.wikipedia.org/wiki/Amdahl's_law) rears its head again—optimizing the “front” and “back” of your data pipeline doesn’t matter when the middle is what's holding you back. ## A quiver of Arrow (projects) So if Arrow is so great, how can we actually use it to build our own protocols? Luckily, Arrow comes with a variety of building blocks for different situations. * We just talked about [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc) before. Where Arrow is the in-memory format defining how arrays of data are laid out, er, in memory, Arrow IPC defines how to serialize and deserialize Arrow data so it can be sent somewhere else—whether that means being written to a file, to a socket, into a shared buffer, or otherwise. Arrow IPC organizes data as a sequence of messages, making it easy to stream over your favorite transport, like WebSockets. -* [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http) is “just” streaming Arrow IPC over HTTP. The Arrow community is working on standardizing it, so that different clients agree on how exactly to do this. As part of the standardization effort, there’s examples of clients and servers across several languages, how to use HTTP Range requests, using multipart/mixed requests to send combined JSON and Arrow responses, and more. While not a full protocol in and of itself, it’ll fit right in when building REST APIs. -* [**Disassociated IPC**](https://arrow.apache.org/docs/format/DissociatedIPC.html) combines Arrow with advanced network transports like [UCX](https://openucx.org/) and [libfabric](https://ofiwg.github.io/libfabric/). For those who require the absolute best performance and have the specialized hardware to boot, this allows you to send Arrow data at full throttle, taking advantage of scatter-gather, Infiniband, and more. +* [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http) is “just” streaming Arrow IPC over HTTP. The Arrow community is working on standardizing it, so that different clients agree on how exactly to do this. There’s examples of clients and servers across several languages, how to use HTTP Range requests, using multipart/mixed requests to send combined JSON and Arrow responses, and more. While not a full protocol in and of itself, it’ll fit right in when building REST APIs. +* [**Disassociated IPC**](https://arrow.apache.org/docs/format/DissociatedIPC.html) combines Arrow IPC with advanced network transports like [UCX](https://openucx.org/) and [libfabric](https://ofiwg.github.io/libfabric/). For those who require the absolute best performance and have the specialized hardware to boot, this allows you to send Arrow data at full throttle, taking advantage of scatter-gather, Infiniband, and more. * [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html) is a fully defined protocol for accessing relational databases. Think of it as an alternative to the full PostgreSQL wire protocol: it defines how to connect to a database, execute queries, fetch results, view the catalog, and so on. For database developers, Flight SQL provides a fully Arrow-native protocol with clients for several programming languages and drivers for ADBC, JDBC, and ODBC—all of which you don’t have to build yourself. * And finally, [**ADBC**](https://arrow.apache.org/docs/format/ADBC.html) actually isn’t a protocol. Instead, it’s an API abstraction layer for working with databases (like JDBC and ODBC—bet you didn’t see that coming), that’s Arrow-native and doesn’t require transposing or converting columnar data back and forth. ADBC gives you a single API to access data from multiple databases, whether they use Flight SQL or something else under the hood, and if a conversion is absolutely necessary, ADBC handles the details so that you don’t have to build out a dozen connectors on your own. @@ -207,10 +210,9 @@ So to summarize: ## Conclusion -Existing client protocols can be absurdly wasteful, but Arrow offers better efficiency and avoids design pitfalls from the past. And Arrow makes it easy to build and consume data APIs with a variety of standards like Arrow IPC, Arrow HTTP, and ADBC. By using Arrow serialization in protocols, everyone benefits from easier, faster, and simpler data access, and we can avoid accidentally holding data captive behind slow and inefficient interfaces. - +Existing client protocols can be wasteful. Arrow offers better efficiency and avoids design pitfalls from the past. And Arrow makes it easy to build and consume data APIs with a variety of standards like Arrow IPC, Arrow HTTP, and ADBC. By using Arrow serialization in protocols, everyone benefits from easier, faster, and simpler data access, and we can avoid accidentally holding data captive behind slow and inefficient interfaces. -## Footnotes +--- [^1]: Of course, it’s not fully wasted, as null/not-null is data as well. But for accounting purposes, we’ll be consistent and call lengths, padding, bitmaps, etc. “overhead”. From febacac158324584427d0af22e32c7f483a8a5d7 Mon Sep 17 00:00:00 2001 From: David Li Date: Fri, 21 Feb 2025 21:41:15 -0500 Subject: [PATCH 11/14] Update _posts/2025-02-04-data-wants-to-be-free.md Co-authored-by: Ian Cook --- _posts/2025-02-04-data-wants-to-be-free.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-04-data-wants-to-be-free.md index 374e6c329d34..cb709bd7669b 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-04-data-wants-to-be-free.md @@ -50,7 +50,8 @@ limitations under the License. _This is the second in a series of posts that aims to demystify the use of -Arrow as a data interchange format for databases and query engines._ +Arrow as a data interchange format for databases and query engines. +[Read the first post here.](https://arrow.apache.org/blog/2025/01/10/arrow-result-transfer/)_ As data practitioners, we often find our data “held hostage”. Instead of being able to use data as soon as we get it, we have to spend time—time to parse and From 809d753491d491b994d7fb9bb5706ff65ee74498 Mon Sep 17 00:00:00 2001 From: David Li Date: Sat, 22 Feb 2025 12:00:37 +0900 Subject: [PATCH 12/14] More updates --- _includes/arrow_result_transfer_series.md | 23 +++++++++++++++++++ _posts/2025-01-10-arrow-result-transfer.md | 2 ++ ...md => 2025-02-28-data-wants-to-be-free.md} | 7 +++--- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 _includes/arrow_result_transfer_series.md rename _posts/{2025-02-04-data-wants-to-be-free.md => 2025-02-28-data-wants-to-be-free.md} (99%) diff --git a/_includes/arrow_result_transfer_series.md b/_includes/arrow_result_transfer_series.md new file mode 100644 index 000000000000..2b13c1c309ab --- /dev/null +++ b/_includes/arrow_result_transfer_series.md @@ -0,0 +1,23 @@ + + +Posts in this series: + +1. [How the Apache Arrow Format Accelerates Query Result Transfer]({% link _posts/2025-01-10-arrow-result-transfer.md %}) +1. [ Data wants to be free: fast data exchange with Apache Arrow ]({% link _posts/2025-02-28-data-wants-to-be-free.md %}) diff --git a/_posts/2025-01-10-arrow-result-transfer.md b/_posts/2025-01-10-arrow-result-transfer.md index c5aff340deb3..0a57e7616274 100644 --- a/_posts/2025-01-10-arrow-result-transfer.md +++ b/_posts/2025-01-10-arrow-result-transfer.md @@ -32,6 +32,8 @@ limitations under the License. _This is the first in a series of posts that aims to demystify the use of Arrow as a data interchange format for databases and query engines._ +{% include arrow_result_transfer_series.md %} + “Why is this taking so long?” diff --git a/_posts/2025-02-04-data-wants-to-be-free.md b/_posts/2025-02-28-data-wants-to-be-free.md similarity index 99% rename from _posts/2025-02-04-data-wants-to-be-free.md rename to _posts/2025-02-28-data-wants-to-be-free.md index cb709bd7669b..07edf29ab329 100644 --- a/_posts/2025-02-04-data-wants-to-be-free.md +++ b/_posts/2025-02-28-data-wants-to-be-free.md @@ -2,7 +2,7 @@ layout: post title: "Data wants to be free: fast data exchange with Apache Arrow" description: "" -date: "2025-02-04 00:00:00" +date: "2025-02-28 00:00:00" author: David Li, Ian Cook, Matt Topol categories: [application] image: @@ -50,8 +50,9 @@ limitations under the License. _This is the second in a series of posts that aims to demystify the use of -Arrow as a data interchange format for databases and query engines. -[Read the first post here.](https://arrow.apache.org/blog/2025/01/10/arrow-result-transfer/)_ +Arrow as a data interchange format for databases and query engines._ + +{% include arrow_result_transfer_series.md %} As data practitioners, we often find our data “held hostage”. Instead of being able to use data as soon as we get it, we have to spend time—time to parse and From b33d8d61654e8154bbe574a748da7990d539bc1f Mon Sep 17 00:00:00 2001 From: David Li Date: Sat, 22 Feb 2025 12:18:21 +0900 Subject: [PATCH 13/14] Title case --- _posts/2025-02-28-data-wants-to-be-free.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_posts/2025-02-28-data-wants-to-be-free.md b/_posts/2025-02-28-data-wants-to-be-free.md index 07edf29ab329..0d64be85ef00 100644 --- a/_posts/2025-02-28-data-wants-to-be-free.md +++ b/_posts/2025-02-28-data-wants-to-be-free.md @@ -1,6 +1,6 @@ --- layout: post -title: "Data wants to be free: fast data exchange with Apache Arrow" +title: "Data Wants to Be Free: Fast Data Exchange with Apache Arrow" description: "" date: "2025-02-28 00:00:00" author: David Li, Ian Cook, Matt Topol From 49ce83add54286f952688d08ffef5261f58ce210 Mon Sep 17 00:00:00 2001 From: David Li Date: Sat, 22 Feb 2025 12:18:35 +0900 Subject: [PATCH 14/14] Title case --- _includes/arrow_result_transfer_series.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_includes/arrow_result_transfer_series.md b/_includes/arrow_result_transfer_series.md index 2b13c1c309ab..dddd4b4dfcad 100644 --- a/_includes/arrow_result_transfer_series.md +++ b/_includes/arrow_result_transfer_series.md @@ -20,4 +20,4 @@ limitations under the License. Posts in this series: 1. [How the Apache Arrow Format Accelerates Query Result Transfer]({% link _posts/2025-01-10-arrow-result-transfer.md %}) -1. [ Data wants to be free: fast data exchange with Apache Arrow ]({% link _posts/2025-02-28-data-wants-to-be-free.md %}) +1. [Data Wants to Be Free: Fast Data Exchange with Apache Arrow]({% link _posts/2025-02-28-data-wants-to-be-free.md %})