Skip to content

Updating data with MERGE INTO statement doesn't store just delta of the changed rows but creates copy of all new data #462

Description

@onnimonni

What happens?

Hey,

I would want to efficiently track only the delta of public datasets in ducklake. I asked about it in #392 where @guillesd recommended to use the MERGE INTO statement.

I didn't include the geometry column into this example because of #461 and I wanted this example simpler to understand.

Again it might have been just my poor communication and this got misunderstood.

To Reproduce

ATTACH 'ducklake:osm.ducklake'
AS osm;

INSTALL spatial; LOAD spatial;
LOAD httpfs;

CREATE OR REPLACE MACRO load_osm(area_with_country_and_date) AS TABLE
    SELECT
        kind::varchar as kind,
        id,
        tags
    FROM ST_ReadOSM('https://download.geofabrik.de/' || area_with_country_and_date || '.osm.pbf');

-- Initiate the monaco data from January 2025
CREATE TABLE osm.monaco AS
FROM load_osm('europe/monaco-250101');

-- Update the monaco data from September 2025
BEGIN;

-- FIXME: This step is very very inefficient for the storage
-- Step 1: UPDATE old rows
MERGE INTO osm.monaco
    USING load_osm('europe/monaco-250901')
    USING (kind, id)
WHEN MATCHED THEN UPDATE;

-- Step 2: Add completely new rows
MERGE INTO osm.monaco
    USING load_osm('europe/monaco-250901')
    USING (kind, id)
WHEN NOT MATCHED THEN INSERT;

-- Step 3: Delete rows which were not in the new set
WITH missing_rows AS (
    SELECT kind, id FROM osm.monaco
    ANTI JOIN load_osm('europe/monaco-250901') USING(kind, id)
)
DELETE FROM osm.monaco current_rows USING missing_rows
WHERE current_rows.id = missing_rows.id AND current_rows.kind = missing_rows.kind;

COMMIT;

After we have run the SQL above let's look at the results:

$ tree -h osm.ducklake.files/
[  96]  osm.ducklake.files/
└── [  96]  main
    └── [ 192]  monaco
        ├── [403K]  ducklake-019971a3-b894-7c2c-8e19-21cfcca25b1a.parquet <-- Initial data set here
        ├── [584K]  ducklake-019971a3-e18c-72f2-bf16-b04dd18365a8.parquet <-- Step 1: Updated rows
        ├── [177K]  ducklake-019971a3-e19c-73db-a9e2-74e783ceb4bf-delete.parquet <-- Step 3: Deleted rows
        └── [ 24K]  ducklake-019971a3-f12f-7f69-949d-84e5944cd1fe.parquet <-- Step 2: New rows

3 directories, 4 files

When you look at the parquet file which contains updated rows I assume it should be smaller?

Using the load_osm MACRO we can see that almost all rows were exactly same between both datasets and only 10% of the data changed between:

WITH changed_rows AS (
      SELECT *, COUNT(*) as count
      FROM (
          FROM load_osm('europe/monaco-250101')
          UNION ALL
          FROM load_osm('europe/monaco-250901')
      )
      GROUP BY kind, id, tags
  )
  SELECT
      CASE count
          WHEN 1 THEN 'only-in-other-dataset'
          WHEN 2 THEN 'in-both-datasets'
      END as row_exists,
      COUNT(*) as count
  FROM changed_rows
  GROUP BY row_exists;
┌───────────────────────┬───────┐
│      row_exists       │ count │
│        varchar        │ int64 │
├───────────────────────┼───────┤
│ only-in-other-dataset │  4186 │
│ in-both-datasets      │ 44038 │
└───────────────────────┴───────┘

I would have assumed that the parquet file which contains only the updated rows would have been ~10% of the size from the initial import.

I might be holding it in a wrong way? What is the recommended way to track data like this in ducklake where only small portion of it changes? Ideally I would only want to store the changed rows or even better would be just the columns which changed.

My goal would be to save money for the storage by just storing the delta and still be able to go back in history to certain date. Thanks in advance 🙇 . If this is not a bug but my misunderstading I hope the conversation will at least teach others.

OS:

MacOS 15.6.1 aarch64

DuckDB Version:

v1.4.0 (Andium) b8a06e4a22

DuckLake Version:

45788f0

DuckDB Client:

CLI

Hardware:

Macbook Pro M1 Pro

Full Name:

Onni Hakala

Affiliation:

Freelancer

What is the latest build you tested with? If possible, we recommend testing with the latest nightly build.

I have tested with a stable release

Did you include all relevant data sets for reproducing the issue?

Yes

Did you include all code required to reproduce the issue?

  • Yes, I have

Did you include all relevant configuration (e.g., CPU architecture, Python version, Linux distribution) to reproduce the issue?

  • Yes, I have

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    expected behaviorThe behavior described in the issue is expected

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions