Fl4m3Ph03n1x

Fl4m3Ph03n1x

Background

PS: the following situation describes an hypothetical scenario, where I own a company that sells things to customers.

I have an Ecto query that is so big, that my machine cannot handle it. With billions of results returned, there is probably not enough RAM in the world that can handle it.

The solution here (or so my research indicates) is to use streams. Streams were made for potentially infinite sets of results, which would fit my use case.

Problem

So lets imagine that I want to delete All users that bought a given item. Maybe that item was not really legal in their country, and now me, the poor guy in IT, has to fix things so the world doesn’t come down crashing.

Naive way:

item_id = "123asdasd123"

purchase_ids =
      Purchases
      |> where([p], p.item_id == ^item_id)
      |> select([p], p.id)
      |> Repo.all()

Users
    |> where([u], u.purchase_id in ^purchase_ids)
    |> Repo.delete_all()

This is the naive way. I call it naive, because of 2 issues:

  • We have so many purchases, that the machine’s memory will overflow (looking at purchase_ids query)
  • purchase_ids will likely have more than 100K ids, so the second query (where we delete things) will fail as it hits Postgres parameters limit of 32K: https://stackoverflow.com/a/42251312/1337392

What can I say, our product is highly addictive and very well priced!
Our customers simply cant get enough of it. Don’t know why. Nope. No reason comes to mind. None at all.

With these problems in mind, I cannot help my customers and grow my empire, I mean, little home owned business.

I did find this possible solution:

Stream way:

item_id = "123asdasd123"

purchase_ids =
      Purchases
      |> where([p], p.item_id == ^item_id)
      |> select([p], p.id)

stream = Repo.stream(purchase_ids)

Repo.transacion(fn -> 
  ids = Enum.to_list(stream)

  Users
    |> where([u], u.purchase_id in ^ids)
    |> Repo.delete_all()
end)

Questions

However, I am not convinced this will work:

  • I am using Enum.to_list and saving everything into a variable, placing everything into memory again. So I am not gaining any advantage by using Repo.stream.
  • I still have too many ids for my Repo.delete_all to work without blowing up

I guess the one advantage here is that this now a transaction, so either everything goes or nothing goes.

So, the following questions arise:

  • How do I properly make use of streams in this scenario?
  • Can I delete items by streaming parameters (ids) or do I have to manually batch them?
  • Can I stream ids to Repo.delete_all ?

Showing Posts 1 to 10

LostKobrakai

LostKobrakai

I know it might not be the solution you’re looking for, but instead of two separate queries you could use subqueries to make them a single, but nested query. No need to send the results of the first query back to elixir in the first place.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

Given the the final objective is to perform a deletion, and that POSTGRES SQL has a limitation for deletes that basically forces me to perform 2 queries (the first where I get the ids, and the second where I delete the users) I don’t see how I could implement your solution.

Could you elaborate more? I am genuinely curious.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

The SQL for this would be:

DELETE FROM users u
USING purchases p
WHERE u.purchase_id = p.id and p.item_id = $1

In Ecto you can do this as simply as:

query = from u in Users,
  join: p in assoc(u, :purchase),
  where: p.item_id == ^item_id

Repo.delete_all(query)
LostKobrakai

LostKobrakai

What @benwilson512 suggested is even better than a subquery, but for more information on subqueries see:

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

But wont this still blow up if the results dont fit into memory ?
Or is Repo.delete_all free from memory?

cmo

cmo

What results are you expecting from delete_all?

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

The results do not go to ecto at all, it runs a DELETE on the postgres side. Postgres is tuned to deal with essentially arbitrary amounts of data through a mix of temporary working spaces in both systems ram and disk. A properly tuned postgres will not run out of memory no matter how many records you are deleting.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

It is my understanding that if possible delete_all will return {non_neg_integer(), nil | [term()]}, where term is the deleted item, if the DB beneath supports it, which in my case does.

Am I missing something there?

@benwilson512 Comparing this solution to another solution using streams, which one would you pick?
For the sake of this post, I will copy a solution from SO:

Repo.transacion(fn ->
  max_rows = 500

  purchase_ids
  |> Repo.stream(max_rows: max_rows)
  |> Stream.chunk_every(max_rows)
  |> Stream.each(fn ids ->
     Users
     |> where([u], u.purchase_id in ^ids)
     |> Repo.delete_all()
  end)
  |> Stream.run()
end, timeout: :infinity)

The original post can be found:

The streaming solution sounds pretty awesome to me, and given both do the same, which one would have more resilience to failure?
(I know that transactions hold a connection, but I am not near the connection limit)

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

What you’re missing is that it is only returned if you ask it to be returned. By default, it returns nothing at all except a count of the entities deleted.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

In regards to the query:

query = from u in Users,
  join: p in assoc(u, :purchase),
  where: p.item_id == ^item_id

Repo.delete_all(query)

I get the error:

** (Ecto.QueryError) iex:41: could not find association `purchase` on schema Users in query:
...

Is there some alteration I need to do to the schema of Users or Purchase?
The SQL code did work though, so I am having a hard trouble understanding why the Ecto one doesn’t. I am assuming this is because I have an issue in some schema?

Also, I cannot use:

query = from u in Users,
  join: p in assoc(u, Purchase),
  where: p.item_id == ^item_id

even though Purchase is aliased, as this will give me a different error (basically I understand this only works with atoms).

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews