At the start of the year, Twitter become all a flutter when people started posting their results for the 1 billion row challenge.

image

The challenge was thrown out to the community as an exercise in Java processing (because as we know, people love dumping on Java performance) 😀.

image

My Java interest (and skills) are low so I only had a passing interest in the challenge, besides seeing the results that people been posting. But when it comes to billions of rows, then besides doing benchmarks and the like, then typically the best place for storing data like that is … in a database!

So I posted a quick tweet after I loaded the flat file into the database showing why databases are just so damn cool at dealing with data.

image

(This tweet got some push back with people saying I was also dumping on Java or trying to hijack the challenge, but my post was purely to elucidate the benefits of processing data in a database. I’d rather write a few lines of SQL that a Java program to process 1 billion rows).

And that’s where I left it. I was supposed to be done with this. 😀

But then friend Gerald posted a far more in-depth exploration of the challenge, looking at how the database (and the database server) can be so easily scaled to process rows incredibly quickly.

Before proceeding, to get the context of what follows, go take a read of the post – its a great read. Go on, I’ll wait 😀

Gerald’s results were in the following table, the key one for me (because I’m a database guy) being the blazingly fast last one.

image

Even then… I was done, nothing more for me to do.

Until Gerald tacked this as a suffix to the his post.

image

All I can say to that is …

damn_you

There’s goes my Saturday 😀

So after loading the data into a database table, the first thing I did to improve performance was to compress it with ALTER TABLE MOVE. I then ran Gerald’s query with a parallel degree of 8 on my desktop


SQL> SELECT /*+ PARALLEL(8) */
  2         '{' ||
  3            LISTAGG(station_name || '=' || min_measurement || '/' ||
  4                    mean_measurement || '/' || max_measurement, ', ')
  5              WITHIN GROUP (ORDER BY station_name) ||
  6         '}' AS "1BRC"
  7   FROM
  8    (SELECT station_name,
  9            MIN(measurement) AS min_measurement,
 10            ROUND(AVG(measurement), 1) AS mean_measurement,
 11            MAX(measurement) AS max_measurement
 12      FROM measurements
 13       GROUP BY station_name
 14    );

1 row selected.

Elapsed: 00:00:02.22

That’s not too bad, but hardly worthy of an addenda to Gerald’s results. Then I recalled our In-Memory technology, which lets you clone relational data into a columnar compressed format that is an excellent source for analytical style queries like this one. It’s literally a simple one liner.


SQL> alter table MEASUREMENTS inmemory;

Table altered.

Once the in-memory store was populated, I could then re-run the query.


SQL> SELECT /*+ PARALLEL(8) */
  2         '{' ||
  3            LISTAGG(station_name || '=' || min_measurement || '/' ||
  4                    mean_measurement || '/' || max_measurement, ', ')
  5              WITHIN GROUP (ORDER BY station_name) ||
  6         '}' AS "1BRC"
  7   FROM
  8    (SELECT station_name,
  9            MIN(measurement) AS min_measurement,
 10            ROUND(AVG(measurement), 1) AS mean_measurement,
 11            MAX(measurement) AS max_measurement
 12      FROM measurements
 13       GROUP BY station_name
 14    );

1 row selected.

Elapsed: 00:00:00.68

OK, now we are getting places! We are sub-second for a billion rows on a standard desktop machine.  I tried several different levels of parallelism but 8 seemed to be the sweet spot. But one thing I did notice was that this optimal level was not saturating my CPU, in fact, it was less than 50% utilised. I could get closer to 100% with higher level of parallelism but the response times were not as good. This gave me an idea – maybe its the memory speed  that is the limiting factor here?

So I unloaded the table using DataPump and migrated the table to my gaming laptop. It has a slower CPU, and less cores than my desktop, but one thing it does have…is blazingly fast memory.

And on this machine, we got a result quite special even if I say so myself.


SQL> SELECT /*+ PARALLEL(6) */
  2         '{' ||
  3            LISTAGG(station_name || '=' || min_measurement || '/' ||
  4                    mean_measurement || '/' || max_measurement, ', ')
  5              WITHIN GROUP (ORDER BY station_name) ||
  6         '}' AS "1BRC"
  7   FROM
  8    (SELECT station_name,
  9            MIN(measurement) AS min_measurement,
 10            ROUND(AVG(measurement), 1) AS mean_measurement,
 11            MAX(measurement) AS max_measurement
 12      FROM measurements
 13       GROUP BY station_name
 14    );

1 row selected.

Elapsed: 00:00:00.43

Less than half a second with only a parallel degree of 6, on a $1000 laptop. Imagine what could be achieved with a genuine server machine with high speed RAM and cores to burn. As Gerald said – there’s is so much cool tech in the Oracle Database just waiting for you to exploit.

A quick note for the doom sayers who will be quick say “Yeah, but what does that In-Memory option cost?”  My billion row table consumed around 8GB of RAM in the In-Memory store, and in Oracle Database 19c your first 16GB of In-Memory is free!

btw, if you don’t want to go to the effort of installing Java, Maven and generating the flat file, you can get a a DataPump copy (original schema = MCDONAC) of the table so you can load it straight into your database here.

17 responses to “One Billion Rows – Gerald’s Challenge”

  1. Great article – especially the one by Gerald – thanks for pointing it out.

    Made me think whether any further improvement could be achieved by creating the internal table with “PCTFREE 0”.

    The default is “PCTFREE 10” which looks like what will have happened in Gerald’s CREATE TABLE statement.

    By packing each block to 100% of its capacity instead of 90% (the default) we should reduce the number of logical read counts required by the query, and theoretically, at least, improve the response time.

    Interesting input of yours re. use of the in-memory option – forgot you get the first 15gb free.

    ps. tried to comment directly but that wordpress login / facebook didn’t work for me. You should use a proper Oracle-driven CMS application for your blog Connor.

    1. “alter table move compress” automatically set a table to pctfree 0. Its one of the cool things about compression, it “knows” that if you want to compress the data, then pctfree isn’t your friend.

  2. What would the results be on Autonomous Database or Exadata.

  3. What about Partition by hash (station_name) partitions (x) + parallel x? 🙂 it’s much better to parallel per partition than parallel by rowid 🙂

    1. Excellent idea, and one I’ll try out

      1. Plus few more things to try out.

        1) Materialized view (for aggregation) with Query rewrite capabilities.
        2) In memory on materailized view.
        3) how about having “CELLMEMORY” on this table ?

  4. Hi
    I am trying to import your table from the dump file but am having difficulty. Below is my code

    ___

    [oracle@B560M TEST_DATA]$ impdp tables=measurements directory=TEST_DATA dumpfile=measurements.dmp tables=MEASUREMENTS logfile=impdp_measurements.log

    Import: Release 21.0.0.0.0 – Production on Sat Feb 3 20:45:46 2024
    Version 21.9.0.0.0

    Copyright (c) 1982, 2021, Oracle and/or its affiliates. All rights reserved.

    Username: gbmdba@P211
    Password:

    Connected to: Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 – Production
    ORA-39002: invalid operation
    ORA-39166: Object GBMDBA.MEASUREMENTS was not found or could not be exported or imported.

    [oracle@B560M TEST_DATA]$ ls -al
    total 16522988
    drwxrwxr-x. 2 oracle oinstall 183 Feb 3 20:46 .
    drwxrwxr-x. 3 oracle oracle 23 Feb 3 16:05 ..
    -rw-r–r–. 1 root root 32 Feb 3 18:47 afiedt.buf
    -rw-r–r–. 1 root root 12288 Feb 3 18:48 .afiedt.buf.swp
    -rw-r–r–. 1 oracle oracle 0 Feb 3 16:28 impdb_measurements.log
    -rw-r–r–. 1 oracle oracle 631 Feb 3 20:46 impdp_measurements.log
    -rw-r–r–. 1 oracle oracle 319 Feb 3 20:45 import.log
    -rw-r–r–. 1 oracle oinstall 16919511040 Feb 3 16:06 measurements.dmp
    -rw-r–r–. 1 root root 0 Feb 3 18:41 sysdba.txt
    -rw-r–r–. 1 oracle oracle 5 Feb 3 17:05 test.txt
    [oracle@B560M TEST_DATA]$

    1. Ah, probably user mapping. Original schema was MCDONAC, so try FROMUSER with that. I’ll update the post

      1. This worked for me

        DIRECTORY=TEST_DATA
        DUMPFILE=measurements.dmp
        REMAP_SCHEMA=mcdonac:gbmdba
        REMAP_TABLESPACE=LARGETS:DATABF01
        TABLES=MCDONAC.MEASUREMENTS
        #CONTENT=METADATA_ONLY
        LOGFILE=impdp_measurements.log
        PARALLEL=8

  5. I steered clear of that because I figured if I fired up a full Exadata with a lot of cores, I’d be getting some questions internally about who is blowing their cost code 🙂

    1. My Results
      Database on intel i7 16 cores and 64GB RAM (no idea about the speed)
      Connecting from my Desktop using SQLcl
      Parallel 1 => 05.006 s
      Parallel 8 => 00.860 s
      Parallel 16 => 00.608

      1. And I was using the inmemory

        1. And using Oracle 21c

  6. Hi Connor,
    thanks for pointing out the challange and the interesting articles by Gerald and yourself. One question: did you remove the dump file from onedrive in the meantime?

    1. apologies – i moved it and did not hit “save” on the post. Link should be OK now

  7. A usual great information.
    But I cannot access the OneDrive file either…

    1. apologies – i moved it and did not hit “save” on the post. Link should be OK now

Got some thoughts? Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Trending

Blog at WordPress.com.