ruby sql 65 lines · 3 tabs

Bulk-Upsert Inventory Counts in One Query From a Sidekiq Job

Shared by codesnips Jul 2026
3 tabs
class InventoryLevel < ApplicationRecord
  belongs_to :warehouse

  UPSERT_COLUMNS = %w[warehouse_id sku on_hand reserved updated_at].freeze

  def self.upsert_counts(rows)
    return if rows.blank?

    payload = rows.map do |row|
      row.stringify_keys.slice(*UPSERT_COLUMNS)
    end

    upsert_all(
      payload,
      unique_by: :index_inventory_levels_on_warehouse_and_sku,
      returning: %w[sku on_hand]
    )
  end

  def available
    on_hand - reserved
  end
end
3 files · ruby, sql Explain with highlit

This snippet shows how to reconcile a warehouse feed of inventory counts against the database using a single bulk upsert instead of thousands of per-row updates. The problem it solves is throughput: a periodic feed may contain tens of thousands of SKU counts, and updating each row individually issues one UPDATE per record, saturating the connection pool and holding transactions open far too long. Folding the whole batch into one statement turns that into a single round-trip that the database can plan and execute efficiently.

In InventoryLevel model, upsert_counts wraps ActiveRecord's upsert_all, which compiles to a Postgres INSERT ... ON CONFLICT DO UPDATE. The unique_by: option names the unique index that defines a conflict, and the class filters incoming rows so only whitelisted columns are written. Passing updated_at explicitly is necessary because upsert_all bypasses model callbacks and timestamp auto-management, so anything not in the payload simply will not change. The method returns the result so callers can inspect affected keys.

schema.sql defines the backing table and, crucially, the composite unique index on (warehouse_id, sku). That index is what ON CONFLICT targets — without a matching unique constraint the upsert cannot decide what counts as a duplicate and Postgres raises an error. A partial or expression index would not match unless referenced exactly.

ReconcileInventoryJob is the Sidekiq worker that assembles the payload. It normalizes each feed row into a hash, stamps a single now timestamp so the whole batch shares one write time, and slices the array into BATCH_SIZE chunks. Chunking matters because a single statement with too many rows can exceed the bind-parameter limit and balloon memory; slicing keeps each query bounded while still amortizing round-trips.

The design is naturally idempotent: replaying the same feed produces the same final counts because the conflict target collapses duplicates into updates rather than inserting new rows. A subtle trade-off is that upsert_all skips validations and callbacks, so any invariants must live in database constraints. Developers reach for this pattern whenever a job must sync large external datasets quickly and safely.


Related snips

Share this code

Here's the card — post it anywhere.

Bulk-Upsert Inventory Counts in One Query From a Sidekiq Job — share card
Link copied