From 3217ba78e17cb384575106a7b8a485afa4cff248 Mon Sep 17 00:00:00 2001 From: Raffi Date: Mon, 7 Sep 2026 14:57:25 -0700 Subject: [PATCH 1/9] test(payments): add reproducible filter fixtures ## Context Payments list filters need a shared development dataset for REST and browser QA. ## Description Seed an isolated organization with payment variants, boundary amounts, dates, and an expected-record manifest. Store disposable credentials only in ignored local files. --- script/seed_payments_filters.rb | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 script/seed_payments_filters.rb diff --git a/script/seed_payments_filters.rb b/script/seed_payments_filters.rb new file mode 100644 index 00000000000..6df63b1b861 --- /dev/null +++ b/script/seed_payments_filters.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +# Run in the development API container: +# bundle exec rails runner script/seed_payments_filters.rb +# Creates an isolated, idempotent fixture organization. Credentials and the +# expected-record manifest are written under tmp/, never to stdout or git. +raise "This seed is only for development" unless Rails.env.development? + +require "factory_bot_rails" +FactoryBot.find_definitions if FactoryBot.factories.none? +ActiveJob::Base.queue_adapter = :test + +slug = "payments-filters" +credentials_path = Rails.root.join("tmp/payments_filters_credentials.json") +organization = Organization.find_by(slug:) + +unless organization + password = SecureRandom.base64(24) + Organization.transaction do + organization = FactoryBot.create(:organization, slug:, name: "Payments filters QA", webhook_url: nil) + organization.default_billing_entity.update!(timezone: "America/Los_Angeles") + user = FactoryBot.create(:user, email: "payments-filters@example.com", password:) + FactoryBot.create(:membership, organization:, user:, roles: [:admin]) + customers = Array.new(3) do |index| + FactoryBot.create(:customer, organization:, external_id: "cust_#{index + 1}", + name: "Payments QA #{index + 1}", email: "customer#{index + 1}@example.com") + end + providers = [ + FactoryBot.create(:stripe_provider, organization:, code: "qa_stripe", name: "QA Stripe"), + FactoryBot.create(:gocardless_provider, organization:, code: "qa_gocardless", name: "QA GoCardless") + ] + connections = customers.to_h do |customer| + [customer.id, providers.map do |provider| + factory = provider.is_a?(PaymentProviders::StripeProvider) ? :stripe_customer : :gocardless_customer + FactoryBot.create(factory, organization:, customer:, payment_provider: provider, code: provider.code) + end] + end + amounts = [0, 99, 100, 999, 1000, 2500, 5000, 5001, 2_147_483_647, + 2_147_483_648, 5_000_000_000, 9_007_199_254_740_992, 9_007_199_254_740_993, + 9_223_372_036_854_775_807] + method_types = %w[card sepa_debit us_bank_account bacs_debit link boleto crypto customer_balance] + zone = ActiveSupport::TimeZone[organization.timezone] + dates = [zone.local(2026, 9, 1), zone.local(2026, 9, 7).end_of_day, + zone.local(2026, 8, 31).end_of_day, zone.local(2026, 9, 8), zone.local(2026, 9, 4, 12)] + + 30.times do |index| + customer = customers[index % customers.length] + amount_cents = amounts[index % amounts.length] + currency = index.even? ? "EUR" : "USD" + invoice_status = case index + when 28 then :draft # Draft invoices are visible in the existing payments query. + when 29 then :open # Open invoices are excluded by Invoice::VISIBLE_STATUS. + else :finalized + end + invoice = FactoryBot.create(:invoice, organization:, customer:, status: invoice_status, + currency:, total_amount_cents: amount_cents, issuing_date: Date.new(2026, 9, 1)) + invoice.update!(number: format("QA-INV-%03d", index + 1)) + payable = if index % 4 == 2 + second_invoice = FactoryBot.create(:invoice, organization:, customer:, status: :finalized, + currency:, total_amount_cents: 0, issuing_date: Date.new(2026, 9, 1)) + second_invoice.update!(number: (index == 2) ? "LAG-1234-001-002" : format("QA-INV-%03d-B", index + 1)) + FactoryBot.create(:payment_request, organization:, customer:, amount_cents:, amount_currency: currency, + invoices: [invoice, second_invoice]) + else + invoice + end + manual = index % 3 == 0 + provider = manual ? nil : providers[index % providers.length] + connection = manual ? nil : connections.fetch(customer.id)[index % providers.length] + method_type = method_types[index % method_types.length] + method = if connection + FactoryBot.create(:payment_method, organization:, customer:, payment_provider: provider, + payment_provider_customer: connection, provider_method_id: "qa_method_#{index}", + provider_method_type: method_type, is_default: false) + end + method_data = if manual || index % 3 == 1 + {} + else + {type: method_type, brand: "visa", last4: "4242"} + end + payment = FactoryBot.create(:payment, organization:, customer:, payable:, amount_cents:, + amount_currency: currency, payment_type: manual ? "manual" : "provider", + reference: manual ? "QA manual #{index + 1}" : nil, + payment_provider: provider, payment_provider_customer: connection, payment_method: method, + provider_payment_id: manual ? nil : "pi_3_qa_#{index + 1}", + provider_payment_method_data: method_data, + payable_payment_status: Payment::PAYABLE_PAYMENT_STATUS[index % 4], + created_at: dates[index % dates.length]) + if index.even? + FactoryBot.create(:payment_receipt, organization:, payment:, number: format("RCPT-2026-%04d", index / 2 + 1)) + end + end + File.write(credentials_path, JSON.pretty_generate({email: user.email, password:, api_key: organization.api_keys.first.value}), mode: "w", perm: 0o600) + end +end + +records = Payment.where(organization:).order(:created_at, :id).map do |payment| + { + id: payment.id, + amount_cents: payment.amount_cents.to_s, + currency: payment.amount_currency, + payment_status: payment.payable_payment_status, + payment_type: payment.payment_type, + payable_type: payment.payable_type, + external_customer_id: payment.customer.external_id, + invoice_ids: payment.invoices.pluck(:id), + invoice_numbers: payment.invoice_numbers, + receipt_number: payment.payment_receipt&.number, + payment_provider_type: payment.payment_provider_type, + payment_method_type: payment.provider_payment_method_data["type"].presence || payment.payment_method&.provider_method_type, + provider_payment_id: payment.provider_payment_id, + reference: payment.reference, + created_at: payment.created_at.iso8601(6), + visible: !payment.payable.is_a?(Invoice) || Invoice::VISIBLE_STATUS.key?(payment.payable.status.to_sym) + } +end +File.write(Rails.root.join("tmp/payments_filters_manifest.json"), JSON.pretty_generate({organization_id: organization.id, timezone: organization.timezone, payments: records})) +Rails.logger.info "Payments QA seed ready: #{records.count} payments, #{records.count { |record| record[:visible] }} visible." +Rails.logger.info "Credentials: tmp/payments_filters_credentials.json (local only)." +Rails.logger.info "Expected records: tmp/payments_filters_manifest.json." From 0ee04d765eb3a2a734f1fa4f90cff4096cd832e5 Mon Sep 17 00:00:00 2001 From: Raffi Date: Mon, 7 Sep 2026 15:07:38 -0700 Subject: [PATCH 2/9] feat(payments): add REST and GraphQL filters ## Context Finance needs the same payment filters through both list APIs, including exact invoice and receipt matches and amounts beyond JavaScript integer precision. ## Description Add composable validated payment filters and expose currency and search on REST. Preserve payment responses, visibility, ordering, and pagination. Generate the GraphQL schemas and add replayable HTTP QA and a 100,000-payment benchmark. --- .../payments_query_filters_contract.rb | 32 +++ app/controllers/concerns/payment_index.rb | 29 +- app/graphql/resolvers/payments_resolver.rb | 21 +- .../types/payments/payable_type_enum.rb | 11 + .../payments/payment_method_type_enum.rb | 14 + app/models/payment.rb | 1 + app/models/payment_method.rb | 2 + .../stripe_customer.rb | 2 +- app/queries/payments_query.rb | 89 +++++- schema.graphql | 18 +- schema.json | 254 +++++++++++++++++ script/benchmark_payments_filters.rb | 78 ++++++ script/qa_payments_filters.py | 178 ++++++++++++ .../payments_query_filters_contract_spec.rb | 71 +++++ .../resolvers/payments_resolver_spec.rb | 93 +++++++ spec/queries/payments_query_spec.rb | 261 ++++++++++++++++++ spec/support/shared_examples/payment_index.rb | 154 +++++++++++ 17 files changed, 1293 insertions(+), 15 deletions(-) create mode 100644 app/graphql/types/payments/payable_type_enum.rb create mode 100644 app/graphql/types/payments/payment_method_type_enum.rb create mode 100644 script/benchmark_payments_filters.rb create mode 100644 script/qa_payments_filters.py diff --git a/app/contracts/queries/payments_query_filters_contract.rb b/app/contracts/queries/payments_query_filters_contract.rb index 6fa6f86fd9d..1884ac1d028 100644 --- a/app/contracts/queries/payments_query_filters_contract.rb +++ b/app/contracts/queries/payments_query_filters_contract.rb @@ -5,6 +5,38 @@ class PaymentsQueryFiltersContract < Dry::Validation::Contract params do optional(:invoice_id).maybe(:string, format?: Regex::UUID) optional(:external_customer_id).maybe(:string) + optional(:currency).maybe(:string, included_in?: Currencies::ACCEPTED_CURRENCIES.keys.map(&:to_s)) + optional(:amount_from).maybe(:integer, gteq?: 0, lteq?: 9_223_372_036_854_775_807) + optional(:amount_to).maybe(:integer, gteq?: 0, lteq?: 9_223_372_036_854_775_807) + optional(:receipt_number).maybe(:string, max_size?: 255) + optional(:invoice_number).maybe(:string, max_size?: 255) + + optional(:payment_status).maybe do + value(:string, included_in?: Payment::PAYABLE_PAYMENT_STATUS) | + array(:string, included_in?: Payment::PAYABLE_PAYMENT_STATUS) + end + optional(:payment_provider_type).maybe do + value(:string, included_in?: Customer::PAYMENT_PROVIDERS) | + array(:string, included_in?: Customer::PAYMENT_PROVIDERS) + end + optional(:payment_method_type).maybe do + value(:string, included_in?: PaymentMethod::PROVIDER_METHOD_TYPES) | + array(:string, included_in?: PaymentMethod::PROVIDER_METHOD_TYPES) + end + optional(:payment_type).maybe do + value(:string, included_in?: Payment::PAYMENT_TYPES.keys.map(&:to_s)) | + array(:string, included_in?: Payment::PAYMENT_TYPES.keys.map(&:to_s)) + end + optional(:payable_type).maybe do + value(:string, included_in?: Payment::PAYABLE_TYPES) | + array(:string, included_in?: Payment::PAYABLE_TYPES) + end + end + + rule(:amount_from, :amount_to) do + if values[:amount_from] && values[:amount_to] && values[:amount_from] > values[:amount_to] + key(:amount_to).failure("must be greater than or equal to amount_from") + end end end end diff --git a/app/controllers/concerns/payment_index.rb b/app/controllers/concerns/payment_index.rb index 40c8205f1f7..24ab09628dc 100644 --- a/app/controllers/concerns/payment_index.rb +++ b/app/controllers/concerns/payment_index.rb @@ -4,16 +4,37 @@ module PaymentIndex include Pagination extend ActiveSupport::Concern + WHITELIST = [ + :page, :per_page, :invoice_id, :external_customer_id, :currency, :search_term, + :amount_from, :amount_to, :receipt_number, :invoice_number, :created_at_from, :created_at_to, + :payment_status, :payment_statuses, :payment_provider_type, :payment_method_type, :payment_type, :payable_type, + {payment_status: [], payment_statuses: [], payment_provider_type: [], payment_method_type: [], payment_type: [], payable_type: []} + ].freeze + def payment_index(customer_external_id: nil) - filters = params.permit(:invoice_id) - filters[:external_customer_id] = customer_external_id result = PaymentsQuery.call( organization: current_organization, pagination: { page: params[:page], limit: params[:per_page] || PER_PAGE }, - filters: filters + search_term: params[:search_term], + filters: { + invoice_id: params[:invoice_id], + external_customer_id: customer_external_id, + currency: params[:currency], + amount_from: params[:amount_from], + amount_to: params[:amount_to], + receipt_number: params[:receipt_number], + invoice_number: params[:invoice_number], + created_at_from: (Date.iso8601(params[:created_at_from]) if valid_date?(params[:created_at_from])), + created_at_to: (Date.iso8601(params[:created_at_to]) if valid_date?(params[:created_at_to])), + payment_status: params[:payment_status] || params[:payment_statuses], + payment_provider_type: params[:payment_provider_type], + payment_method_type: params[:payment_method_type], + payment_type: params[:payment_type], + payable_type: params[:payable_type] + } ) if result.success? @@ -26,7 +47,7 @@ def payment_index(customer_external_id: nil) ), ::V1::PaymentSerializer, collection_name: resource_name.pluralize, - meta: pagination_metadata(result.payments) + meta: pagination_metadata(result.payments, params: params.permit(*WHITELIST)) ) ) else diff --git a/app/graphql/resolvers/payments_resolver.rb b/app/graphql/resolvers/payments_resolver.rb index 04eac25124d..fb6f1ee9cdb 100644 --- a/app/graphql/resolvers/payments_resolver.rb +++ b/app/graphql/resolvers/payments_resolver.rb @@ -9,23 +9,30 @@ class PaymentsResolver < Resolvers::BaseResolver description "Query payments of an organization" + argument :amount_from, GraphQL::Types::BigInt, required: false + argument :amount_to, GraphQL::Types::BigInt, required: false + argument :created_at_from, GraphQL::Types::ISO8601Date, required: false + argument :created_at_to, GraphQL::Types::ISO8601Date, required: false argument :currency, Types::CurrencyEnum, required: false argument :external_customer_id, ID, required: false argument :invoice_id, ID, required: false + argument :invoice_number, String, required: false argument :limit, Integer, required: false argument :page, Integer, required: false + argument :payable_type, [Types::Payments::PayableTypeEnum], required: false + argument :payment_method_type, [Types::Payments::PaymentMethodTypeEnum], required: false + argument :payment_provider_type, [Types::PaymentProviders::ProviderTypeEnum], required: false + argument :payment_status, [Types::Payments::PayablePaymentStatusEnum], required: false + argument :payment_type, [Types::Payments::PaymentTypeEnum], required: false + argument :receipt_number, String, required: false argument :search_term, String, required: false type Types::Payments::Object.collection_type, null: false - def resolve(currency: nil, page: nil, limit: nil, invoice_id: nil, external_customer_id: nil, search_term: nil) + def resolve(page: nil, limit: nil, search_term: nil, **filters) result = PaymentsQuery.call( organization: current_organization, - filters: { - invoice_id:, - external_customer_id:, - currency: - }, + filters:, search_term:, pagination: { page:, @@ -33,7 +40,7 @@ def resolve(currency: nil, page: nil, limit: nil, invoice_id: nil, external_cust } ) - result.payments + result.success? ? result.payments : result_error(result) end end end diff --git a/app/graphql/types/payments/payable_type_enum.rb b/app/graphql/types/payments/payable_type_enum.rb new file mode 100644 index 00000000000..3df64f7108a --- /dev/null +++ b/app/graphql/types/payments/payable_type_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module Payments + class PayableTypeEnum < Types::BaseEnum + Payment::PAYABLE_TYPES.each do |type| + value type + end + end + end +end diff --git a/app/graphql/types/payments/payment_method_type_enum.rb b/app/graphql/types/payments/payment_method_type_enum.rb new file mode 100644 index 00000000000..107cb4db404 --- /dev/null +++ b/app/graphql/types/payments/payment_method_type_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module Payments + class PaymentMethodTypeEnum < Types::BaseEnum + # PaymentMethodTypeEnum already represents manual/provider payment methods. + graphql_name "PaymentProviderMethodTypeEnum" + + PaymentMethod::PROVIDER_METHOD_TYPES.each do |type| + value type + end + end + end +end diff --git a/app/models/payment.rb b/app/models/payment.rb index 15eff0713d7..c79e677b46c 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -5,6 +5,7 @@ class Payment < ApplicationRecord include RansackUuidSearch PAYABLE_PAYMENT_STATUS = %w[pending processing succeeded failed].freeze + PAYABLE_TYPES = %w[Invoice PaymentRequest].freeze belongs_to :organization belongs_to :customer, -> { with_discarded } diff --git a/app/models/payment_method.rb b/app/models/payment_method.rb index a2d94dc38be..f891722a1e5 100644 --- a/app/models/payment_method.rb +++ b/app/models/payment_method.rb @@ -19,6 +19,8 @@ class PaymentMethod < ApplicationRecord manual: "manual" }.freeze + PROVIDER_METHOD_TYPES = %w[card sepa_debit us_bank_account bacs_debit link boleto crypto customer_balance].freeze + validates :provider_method_id, presence: true validates :is_default, inclusion: {in: [true, false]} diff --git a/app/models/payment_provider_customers/stripe_customer.rb b/app/models/payment_provider_customers/stripe_customer.rb index 5c9c762a547..f4213cc8de8 100644 --- a/app/models/payment_provider_customers/stripe_customer.rb +++ b/app/models/payment_provider_customers/stripe_customer.rb @@ -4,7 +4,7 @@ module PaymentProviderCustomers class StripeCustomer < BaseCustomer PAYMENT_METHODS_WITH_SETUP = %w[card sepa_debit us_bank_account bacs_debit link boleto].freeze PAYMENT_METHODS_WITHOUT_SETUP = %w[crypto customer_balance].freeze - PAYMENT_METHODS = (PAYMENT_METHODS_WITH_SETUP + PAYMENT_METHODS_WITHOUT_SETUP).freeze + PAYMENT_METHODS = PaymentMethod::PROVIDER_METHOD_TYPES validates :provider_payment_methods, presence: true validate :allowed_provider_payment_methods diff --git a/app/queries/payments_query.rb b/app/queries/payments_query.rb index af8723eb093..a0c6e5628c6 100644 --- a/app/queries/payments_query.rb +++ b/app/queries/payments_query.rb @@ -2,7 +2,22 @@ class PaymentsQuery < BaseQuery Result = BaseResult[:payments] - Filters = BaseFilters[:invoice_id, :external_customer_id, :currency] + Filters = BaseFilters[ + :invoice_id, + :external_customer_id, + :currency, + :payment_status, + :amount_from, + :amount_to, + :receipt_number, + :created_at_from, + :created_at_to, + :payment_provider_type, + :payment_method_type, + :invoice_number, + :payment_type, + :payable_type + ] def call return result unless validate_filters.success? @@ -44,7 +59,7 @@ def matching_ids_by_search branches << search_base.where(id: search_term).select(:id) if search_term.match?(BaseQuery::UUID_REGEX) - if filters.invoice_id.blank? + if filters.invoice_id.blank? && filters.invoice_number.blank? branches << search_base.where(payable_type: "Invoice", payable_id: matching_invoice_ids).select(:id) end @@ -96,6 +111,15 @@ def apply_filters(scope) scope = filter_by_invoice(scope) if filters.invoice_id.present? scope = filter_by_customer(scope) if filters.external_customer_id.present? scope = filter_by_currency(scope) if filters.currency.present? + scope = with_payment_status(scope) if filters.payment_status.present? + scope = with_amount_range(scope) if filters.amount_from.present? || filters.amount_to.present? + scope = with_receipt_number(scope) if filters.receipt_number.present? + scope = with_created_at_range(scope) if filters.created_at_from.present? || filters.created_at_to.present? + scope = with_payment_provider_type(scope) if filters.payment_provider_type.present? + scope = with_payment_method_type(scope) if filters.payment_method_type.present? + scope = with_invoice_number(scope) if filters.invoice_number.present? + scope = with_payment_type(scope) if filters.payment_type.present? + scope = with_payable_type(scope) if filters.payable_type.present? scope end @@ -123,4 +147,65 @@ def filter_by_invoice(scope) def filter_by_currency(scope) scope.where(amount_currency: filters.currency) end + + def with_payment_status(scope) + scope.where(payable_payment_status: filters.payment_status) + end + + def with_amount_range(scope) + scope = scope.where("payments.amount_cents >= ?::bigint", filters.amount_from) if filters.amount_from.present? + scope = scope.where("payments.amount_cents <= ?::bigint", filters.amount_to) if filters.amount_to.present? + scope + end + + def with_receipt_number(scope) + scope.joins(:payment_receipt).where("LOWER(payment_receipts.number) = LOWER(?)", filters.receipt_number) + end + + def with_created_at_range(scope) + from = Utils::Datetime.parse_iso8601_date(filters.created_at_from)&.in_time_zone(organization.timezone || "UTC") + to = Utils::Datetime.parse_iso8601_date(filters.created_at_to)&.in_time_zone(organization.timezone || "UTC") + scope = scope.where(created_at: from.beginning_of_day..) if from + scope = scope.where(created_at: ..to.end_of_day) if to + scope + end + + def with_payment_provider_type(scope) + types = Array(filters.payment_provider_type).map { |type| "PaymentProviders::#{type.camelize}Provider" } + scope.where(payment_provider_id: PaymentProviders::BaseProvider.unscoped.where(type: types).select(:id)) + end + + def with_payment_method_type(scope) + scope.joins("LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id") + .where( + "COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN (?)", + Array(filters.payment_method_type) + ) + end + + def with_invoice_number(scope) + scope.where(<<~SQL.squish, number: filters.invoice_number, organization_id: organization.id) + EXISTS ( + SELECT 1 FROM invoices + WHERE invoices.organization_id = :organization_id + AND LOWER(invoices.number) = LOWER(:number) + AND ( + (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) + OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( + SELECT 1 FROM invoices_payment_requests + WHERE invoices_payment_requests.payment_request_id = payments.payable_id + AND invoices_payment_requests.invoice_id = invoices.id + )) + ) + ) + SQL + end + + def with_payment_type(scope) + scope.where(payment_type: filters.payment_type) + end + + def with_payable_type(scope) + scope.where(payable_type: filters.payable_type) + end end diff --git a/schema.graphql b/schema.graphql index ec095ce109e..7915c6a8721 100644 --- a/schema.graphql +++ b/schema.graphql @@ -10509,6 +10509,11 @@ enum PayablePaymentStatusEnum { succeeded } +enum PayableTypeEnum { + Invoice + PaymentRequest +} + type Payment { amountCents: BigInt! amountCurrency: CurrencyEnum! @@ -10623,6 +10628,17 @@ input PaymentProviderCustomerInput { syncWithProvider: Boolean } +enum PaymentProviderMethodTypeEnum { + bacs_debit + boleto + card + crypto + customer_balance + link + sepa_debit + us_bank_account +} + """ PaymentReceipt """ @@ -12202,7 +12218,7 @@ type Query { """ Query payments of an organization """ - payments(currency: CurrencyEnum, externalCustomerId: ID, invoiceId: ID, limit: Int, page: Int, searchTerm: String): PaymentCollection! + payments(amountFrom: BigInt, amountTo: BigInt, createdAtFrom: ISO8601Date, createdAtTo: ISO8601Date, currency: CurrencyEnum, externalCustomerId: ID, invoiceId: ID, invoiceNumber: String, limit: Int, page: Int, payableType: [PayableTypeEnum!], paymentMethodType: [PaymentProviderMethodTypeEnum!], paymentProviderType: [ProviderTypeEnum!], paymentStatus: [PayablePaymentStatusEnum!], paymentType: [PaymentTypeEnum!], receiptNumber: String, searchTerm: String): PaymentCollection! """ Query a single plan of an organization diff --git a/schema.json b/schema.json index ddfaf8be353..4b33d437a9d 100644 --- a/schema.json +++ b/schema.json @@ -50653,6 +50653,29 @@ ], "possibleTypes": null }, + { + "kind": "ENUM", + "name": "PayableTypeEnum", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Invoice", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PaymentRequest", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Payment", @@ -51502,6 +51525,65 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "PaymentProviderMethodTypeEnum", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "card", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sepa_debit", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "us_bank_account", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bacs_debit", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "link", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "boleto", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "crypto", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customer_balance", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "PaymentReceipt", @@ -65841,6 +65923,54 @@ "name": "payments", "description": "Query payments of an organization", "args": [ + { + "name": "amountFrom", + "description": null, + "type": { + "kind": "SCALAR", + "name": "BigInt", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountTo", + "description": null, + "type": { + "kind": "SCALAR", + "name": "BigInt", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAtFrom", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAtTo", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "currency", "description": null, @@ -65877,6 +66007,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "invoiceNumber", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "limit", "description": null, @@ -65901,6 +66043,118 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "payableType", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PayableTypeEnum", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethodType", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentProviderMethodTypeEnum", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentProviderType", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ProviderTypeEnum", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentStatus", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PayablePaymentStatusEnum", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentType", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentTypeEnum", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "receiptNumber", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "searchTerm", "description": null, diff --git a/script/benchmark_payments_filters.rb b/script/benchmark_payments_filters.rb new file mode 100644 index 00000000000..6f619f37729 --- /dev/null +++ b/script/benchmark_payments_filters.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +# Development only: seeds 100,000 payments in a separate organization, then +# records EXPLAIN ANALYZE for the combined filter and its pagination count. +# bundle exec rails runner script/benchmark_payments_filters.rb +raise "This benchmark is only for development" unless Rails.env.development? + +require "factory_bot_rails" +FactoryBot.find_definitions if FactoryBot.factories.none? +ActiveJob::Base.queue_adapter = :test + +organization = Organization.find_by(slug: "payments-filters-benchmark") +connection = ApplicationRecord.connection +unless organization + Organization.transaction do + organization = FactoryBot.create(:organization, slug: "payments-filters-benchmark", name: "Payments benchmark", webhook_url: nil) + customer = FactoryBot.create(:customer, organization:) + provider = FactoryBot.create(:stripe_provider, organization:) + provider_customer = FactoryBot.create(:stripe_customer, organization:, customer:, payment_provider: provider) + method = FactoryBot.create(:payment_method, organization:, customer:, payment_provider: provider, + payment_provider_customer: provider_customer, provider_method_type: "card") + values = {org: organization.id, customer: customer.id, billing_entity: organization.default_billing_entity.id, + provider: provider.id, provider_customer: provider_customer.id, method: method.id} + + connection.execute(ActiveRecord::Base.sanitize_sql_array([<<~SQL, values])) + CREATE TEMP TABLE payments_filter_rows ON COMMIT DROP AS + SELECT n, gen_random_uuid() AS invoice_id, gen_random_uuid() AS second_invoice_id, + CASE WHEN n % 5 = 0 THEN gen_random_uuid() END AS request_id, + timestamp '2026-08-01 12:00:00' + (n % 60) * interval '1 day' AS created_at + FROM generate_series(1, 100000) AS n; + + INSERT INTO invoices (id, organization_id, customer_id, billing_entity_id, number, status, + issuing_date, currency, total_amount_cents, created_at, updated_at) + SELECT invoice_id, :org, :customer, :billing_entity, 'PERF-' || lpad(n::text, 6, '0'), 1, + created_at::date, 'EUR', 10000, created_at, created_at FROM payments_filter_rows; + INSERT INTO invoices (id, organization_id, customer_id, billing_entity_id, number, status, + issuing_date, currency, total_amount_cents, created_at, updated_at) + SELECT second_invoice_id, :org, :customer, :billing_entity, 'PERF-' || lpad(n::text, 6, '0') || '-B', 1, + created_at::date, 'EUR', 10000, created_at, created_at FROM payments_filter_rows WHERE request_id IS NOT NULL; + INSERT INTO payment_requests (id, organization_id, customer_id, amount_cents, amount_currency, created_at, updated_at) + SELECT request_id, :org, :customer, 20000, 'EUR', created_at, created_at + FROM payments_filter_rows WHERE request_id IS NOT NULL; + INSERT INTO invoices_payment_requests (invoice_id, payment_request_id, organization_id, created_at, updated_at) + SELECT invoice_id, request_id, :org::uuid, created_at, created_at FROM payments_filter_rows WHERE request_id IS NOT NULL + UNION ALL + SELECT second_invoice_id, request_id, :org::uuid, created_at, created_at FROM payments_filter_rows WHERE request_id IS NOT NULL; + INSERT INTO payments (organization_id, customer_id, payable_id, payable_type, amount_cents, amount_currency, + status, payable_payment_status, payment_provider_id, payment_provider_customer_id, payment_method_id, + provider_payment_method_data, created_at, updated_at) + SELECT :org, :customer, COALESCE(request_id, invoice_id), + CASE WHEN request_id IS NULL THEN 'Invoice' ELSE 'PaymentRequest' END, + n * 100, CASE WHEN n % 3 = 0 THEN 'USD' ELSE 'EUR' END, 'succeeded', + (ARRAY['pending', 'processing', 'succeeded', 'failed'])[n % 4 + 1]::payment_payable_payment_status, + :provider, :provider_customer, :method, + CASE WHEN n % 3 = 0 THEN '{}'::jsonb + ELSE jsonb_build_object('type', (ARRAY['card', 'sepa_debit', 'us_bank_account', 'bacs_debit', + 'link', 'boleto', 'crypto', 'customer_balance'])[(n - 1) % 8 + 1]) END, + created_at, created_at FROM payments_filter_rows; + SQL + end +end + +%w[invoices invoices_payment_requests payment_requests payments payment_methods].each do |table| + connection.execute("ANALYZE #{table}") +end +filters = {invoice_number: "perf-000035", payment_method_type: %w[card sepa_debit us_bank_account], + created_at_from: Date.new(2026, 9, 1), created_at_to: Date.new(2026, 9, 7)} +payments = PaymentsQuery.call(organization:, filters:, pagination: {page: 1, limit: 20}).payments +count_sql = payments.except(:limit, :offset, :order).select("COUNT(*)").to_sql +plans = {list: payments.to_sql, count: count_sql}.to_h do |name, sql| + connection.execute("SET statement_timeout = '30s'") + plan = connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{sql}").values.flatten.join("\n") + [name, {sql:, plan:}] +ensure + connection.execute("RESET statement_timeout") +end +File.write(Rails.root.join("tmp/payments_filters_explain.json"), JSON.pretty_generate({rows: 100_000, filters:, plans:})) +Rails.logger.info "Benchmark complete. Plans: tmp/payments_filters_explain.json" diff --git a/script/qa_payments_filters.py b/script/qa_payments_filters.py new file mode 100644 index 00000000000..5922663bd73 --- /dev/null +++ b/script/qa_payments_filters.py @@ -0,0 +1,178 @@ +"""Verify REST and GraphQL against seed_payments_filters.rb's local manifest. + +Run on the host with Python 3.9+: python3 script/qa_payments_filters.py +The dev API must listen on 127.0.0.1:3000. No credentials enter the report. +""" + +import datetime as dt +import json +from pathlib import Path +import urllib.error +import urllib.parse +import urllib.request +from zoneinfo import ZoneInfo + + +ROOT = Path(__file__).resolve().parents[1] +BASE = "http://127.0.0.1:3000" +CREDENTIALS = json.loads((ROOT / "tmp/payments_filters_credentials.json").read_text()) +MANIFEST = json.loads((ROOT / "tmp/payments_filters_manifest.json").read_text()) +VISIBLE = [p for p in MANIFEST["payments"] if p["visible"]] +ZONE = ZoneInfo(MANIFEST["timezone"]) +REPORT = [] +TYPES = { + "payment_status": "[PayablePaymentStatusEnum!]", + "amount_from": "BigInt", "amount_to": "BigInt", + "receipt_number": "String", "created_at_from": "ISO8601Date", "created_at_to": "ISO8601Date", + "payment_provider_type": "[ProviderTypeEnum!]", "payment_method_type": "[PaymentProviderMethodTypeEnum!]", + "currency": "CurrencyEnum", "invoice_number": "String", "external_customer_id": "ID", + "invoice_id": "ID", "payment_type": "[PaymentTypeEnum!]", "payable_type": "[PayableTypeEnum!]", + "search_term": "String", +} + + +def request(path, token=None, body=None): + headers = {"Content-Type": "application/json", "x-lago-organization": MANIFEST["organization_id"]} + if token: + headers["Authorization"] = "Bearer " + token + payload = json.dumps(body).encode() if body is not None else None + try: + with urllib.request.urlopen(urllib.request.Request(BASE + path, data=payload, headers=headers), timeout=30) as response: + return response.status, json.load(response) + except urllib.error.HTTPError as error: + return error.code, json.load(error) + + +def normalized(params): + return {"payment_status" if k == "payment_statuses" else k: v for k, v in params.items()} + + +def matches(payment, params): + params = normalized(params) + for key, value in params.items(): + if key in {"page", "per_page"}: + continue + if key in {"amount_from", "amount_to"}: + amount = int(payment["amount_cents"]) + if (key == "amount_from" and amount < int(value)) or (key == "amount_to" and amount > int(value)): + return False + elif key in {"created_at_from", "created_at_to"}: + try: + bound = dt.date.fromisoformat(value) + except ValueError: + continue + day = dt.datetime.fromisoformat(payment["created_at"].replace("Z", "+00:00")).astimezone(ZONE).date() + if (key == "created_at_from" and day < bound) or (key == "created_at_to" and day > bound): + return False + elif key == "invoice_number": + if value.lower() not in [n.lower() for n in payment["invoice_numbers"]]: + return False + elif key == "invoice_id": + if value not in payment["invoice_ids"]: + return False + elif key == "receipt_number": + if (payment["receipt_number"] or "").lower() != value.lower(): + return False + elif key == "search_term": + # These QA cases deliberately use provider IDs/reference, whose + # expected values are captured independently by the seed script. + terms = [payment["provider_payment_id"], payment["reference"]] + if not any(value.lower() in (term or "").lower() for term in terms): + return False + elif payment[key] not in (value if isinstance(value, list) else [value]): + return False + return True + + +def rest(params, customer=False): + effective = {**params, **({"external_customer_id": "cust_1"} if customer else {})} + expected = {p["id"]: p for p in VISIBLE if matches(p, effective)} + path = "/api/v1/customers/cust_1/payments" if customer else "/api/v1/payments" + query = [(k + "[]", item) for k, v in params.items() if isinstance(v, list) for item in v] + query += [(k, v) for k, v in params.items() if not isinstance(v, list)] + url = path + "?" + urllib.parse.urlencode(query) + status, data = request(url, CREDENTIALS["api_key"]) + assert status == 200, (url, status, data) + assert data["meta"]["total_count"] == len(expected), (url, data["meta"], len(expected)) + actual = {p["lago_id"] for p in data["payments"]} + assert actual == set(expected), (url, actual, set(expected)) + for payment in data["payments"]: + assert payment["amount_cents"] == int(expected[payment["lago_id"]]["amount_cents"]) + REPORT.append({"request": "GET " + url, "status": status, "count": len(expected), "ids_match": True}) + + +def graphql(params, token): + params = normalized(params) + variables, declarations, arguments = {}, [], [] + for key, value in params.items(): + name = key.split("_")[0] + "".join(part.title() for part in key.split("_")[1:]) + declarations.append("$" + name + ": " + TYPES[key]) + arguments.append(name + ": $" + name) + variables[name] = [value] if TYPES[key].startswith("[") and not isinstance(value, list) else value + signature = "(" + ", ".join(declarations) + ")" if declarations else "" + args = ", ".join([*arguments, "limit: 100"]) + query = "query" + signature + " { payments(" + args + ") { collection { id amountCents } metadata { totalCount } } }" + status, data = request("/graphql", token, {"query": query, "variables": variables}) + assert status == 200 and not data.get("errors"), (params, status, data) + expected = {p["id"]: p for p in VISIBLE if matches(p, params)} + result = data["data"]["payments"] + assert result["metadata"]["totalCount"] == len(expected), (params, result, len(expected)) + assert {p["id"] for p in result["collection"]} == set(expected), params + for payment in result["collection"]: + assert payment["amountCents"] == expected[payment["id"]]["amount_cents"] + REPORT.append({"request": "GraphQL payments", "variables": variables, "status": status, "count": len(expected), "ids_match": True}) + + +login_status, login = request("/graphql", body={ + "query": "mutation($input: LoginUserInput!) { loginUser(input: $input) { token } }", + "variables": {"input": {"email": CREDENTIALS["email"], "password": CREDENTIALS["password"]}}, +}) +assert login_status == 200 and not login.get("errors"), "Seeded user login failed" +TOKEN = login["data"]["loginUser"]["token"] +CASES = [ + {}, {"payment_status": ["succeeded", "failed"]}, {"payment_statuses": ["processing"]}, + {"amount_from": "1000", "amount_to": "5000"}, {"amount_from": "5000000000"}, + {"amount_from": "9007199254740993", "amount_to": "9007199254740993"}, + {"amount_from": "9223372036854775807"}, {"amount_from": "0", "amount_to": "0"}, + {"receipt_number": "rcpt-2026-0001"}, {"receipt_number": "missing"}, + {"created_at_from": "2026-09-01", "created_at_to": "2026-09-07"}, + {"payment_provider_type": ["stripe"]}, {"payment_provider_type": ["gocardless"]}, + {"payment_method_type": ["card", "sepa_debit"]}, {"currency": "EUR"}, + {"invoice_number": "lag-1234-001-002"}, {"external_customer_id": "cust_1"}, + {"payment_type": "manual", "payable_type": "PaymentRequest"}, {"search_term": "pi_3"}, + {"payment_status": "succeeded", "currency": "EUR", "amount_from": "100", "created_at_from": "2026-09-01"}, + {"payment_status": ["succeeded"], "currency": "EUR", "payment_method_type": ["card", "us_bank_account"], "search_term": "pi_3"}, +] +for case in CASES: + rest(case) + rest(case, customer=True) + graphql(case, TOKEN) +rest({"created_at_from": "invalid", "created_at_to": "2026-02-30"}) + +for params in [ + {"payment_status": "bogus"}, {"payment_provider_type": "bogus"}, {"payment_method_type": "bogus"}, + {"payment_type": "bogus"}, {"payable_type": "bogus"}, {"currency": "XYZ"}, + {"amount_from": "-1"}, {"amount_to": "-1"}, {"amount_from": "500", "amount_to": "100"}, + {"amount_from": "9223372036854775808"}, {"invoice_id": "invalid"}, + {"receipt_number": "x" * 256}, {"invoice_number": "x" * 256}, +]: + url = "/api/v1/payments?" + urllib.parse.urlencode(params) + status, data = request(url, CREDENTIALS["api_key"]) + assert status == 422 and data["code"] == "validation_errors", (params, status, data) + REPORT.append({"request": "GET " + url, "status": status, "validation_error": True}) + +params = {"payment_status": "succeeded", "currency": "EUR", "amount_from": "100", "created_at_from": "2026-09-01"} +expected = {p["id"] for p in VISIBLE if matches(p, params)} +seen, page = set(), 1 +while page: + url = "/api/v1/payments?" + urllib.parse.urlencode({**params, "per_page": 2, "page": page}) + status, data = request(url, CREDENTIALS["api_key"]) + assert status == 200 and data["meta"]["total_count"] == len(expected) + ids = {p["lago_id"] for p in data["payments"]} + assert not seen.intersection(ids) and ids.issubset(expected) + seen.update(ids) + REPORT.append({"request": "GET " + url, "status": status, "meta": data["meta"], "ids_match": True}) + page = data["meta"]["next_page"] +assert seen == expected +(ROOT / "tmp/payments_filters_qa.json").write_text(json.dumps(REPORT, indent=2)) +print(f"PASS: {len(REPORT)} live HTTP checks; REST, customer REST, and GraphQL match the seed manifest.") diff --git a/spec/contracts/queries/payments_query_filters_contract_spec.rb b/spec/contracts/queries/payments_query_filters_contract_spec.rb index ac6d3383fbd..ee7c3e12754 100644 --- a/spec/contracts/queries/payments_query_filters_contract_spec.rb +++ b/spec/contracts/queries/payments_query_filters_contract_spec.rb @@ -80,4 +80,75 @@ end end end + + it "accepts no filters" do + expect(result).to be_success + end + + { + payment_status: Payment::PAYABLE_PAYMENT_STATUS, + payment_provider_type: Customer::PAYMENT_PROVIDERS, + payment_method_type: PaymentMethod::PROVIDER_METHOD_TYPES, + payment_type: Payment::PAYMENT_TYPES.keys.map(&:to_s), + payable_type: Payment::PAYABLE_TYPES + }.each do |field, values| + context "with #{field}" do + [values, *values, [], nil].each do |value| + it "accepts #{value.inspect}" do + expect(described_class.new.call(field => value)).to be_success + end + end + + ["unknown", [values.first, "unknown"], {foo: "bar"}, 123].each do |value| + it "rejects #{value.inspect}" do + validation = described_class.new.call(field => value) + expect(validation).not_to be_success + expect(validation.errors.to_h).to have_key(field) + end + end + end + end + + %i[amount_from amount_to].each do |field| + [0, "0", "5000000000", "9007199254740993", 9_223_372_036_854_775_807, nil].each do |amount| + it "accepts #{field}=#{amount.inspect}" do + expect(described_class.new.call(field => amount)).to be_success + end + end + + [-1, "-1", "1.5", "invalid", 9_223_372_036_854_775_808].each do |amount| + it "rejects #{field}=#{amount.inspect}" do + validation = described_class.new.call(field => amount) + expect(validation).not_to be_success + expect(validation.errors.to_h).to have_key(field) + end + end + end + + it "accepts equal bounds" do + expect(described_class.new.call(amount_from: "9007199254740993", amount_to: "9007199254740993")).to be_success + end + + it "rejects reversed bounds after coercing integers" do + validation = described_class.new.call(amount_from: "500", amount_to: "100") + expect(validation.errors.to_h).to eq(amount_to: ["must be greater than or equal to amount_from"]) + end + + %i[receipt_number invoice_number].each do |field| + it "accepts #{field} up to 255 characters" do + expect(described_class.new.call(field => "a" * 255)).to be_success + end + + it "rejects longer #{field}" do + expect(described_class.new.call(field => "a" * 256).errors.to_h).to have_key(field) + end + end + + it "accepts a supported currency" do + expect(described_class.new.call(currency: "EUR")).to be_success + end + + it "rejects an unsupported currency" do + expect(described_class.new.call(currency: "XYZ").errors.to_h).to have_key(:currency) + end end diff --git a/spec/graphql/resolvers/payments_resolver_spec.rb b/spec/graphql/resolvers/payments_resolver_spec.rb index cecab9fec95..ac0a43089c4 100644 --- a/spec/graphql/resolvers/payments_resolver_spec.rb +++ b/spec/graphql/resolvers/payments_resolver_spec.rb @@ -133,4 +133,97 @@ expect(ids).to contain_exactly(usd_payment.id) end end + + context "with list filters" do + subject(:response) do + execute_graphql(current_user: membership.user, current_organization: organization, + permissions: required_permission, query:, variables:) + end + + let(:query) do + <<~GQL + query($paymentStatus: [PayablePaymentStatusEnum!], $amountFrom: BigInt, $amountTo: BigInt, + $receiptNumber: String, $createdAtFrom: ISO8601Date, $createdAtTo: ISO8601Date, + $paymentProviderType: [ProviderTypeEnum!], $paymentMethodType: [PaymentProviderMethodTypeEnum!], + $invoiceNumber: String, $paymentType: [PaymentTypeEnum!], $payableType: [PayableTypeEnum!], + $searchTerm: String, $currency: CurrencyEnum, $invoiceId: ID, $page: Int) { + payments(paymentStatus: $paymentStatus, amountFrom: $amountFrom, amountTo: $amountTo, + receiptNumber: $receiptNumber, createdAtFrom: $createdAtFrom, createdAtTo: $createdAtTo, + paymentProviderType: $paymentProviderType, paymentMethodType: $paymentMethodType, + invoiceNumber: $invoiceNumber, paymentType: $paymentType, payableType: $payableType, + searchTerm: $searchTerm, currency: $currency, invoiceId: $invoiceId, page: $page, limit: 1) { + collection { id amountCents } + metadata { totalCount currentPage } + } + } + GQL + end + + before do + invoice1.update!(number: "FILTER-INVOICE", total_amount_cents: 9_007_199_254_740_993) + Payment.find_by!(payable: invoice2).update!(created_at: Time.utc(2026, 9, 8, 12)) + organization.default_billing_entity.update!(timezone: "America/Los_Angeles") + payment.update!(amount_cents: 9_007_199_254_740_993, amount_currency: "USD", + payable_payment_status: "processing", payment_type: "manual", reference: "Filter transfer", + payment_provider: create(:gocardless_provider, organization:), + provider_payment_method_data: {type: "sepa_debit"}, created_at: Time.utc(2026, 9, 4, 12)) + create(:payment_receipt, organization:, payment:, number: "FILTER-RECEIPT") + end + + [ + {paymentStatus: ["processing"]}, + {amountFrom: "9007199254740993"}, + {amountFrom: "9007199254740993", amountTo: "9007199254740993"}, + {receiptNumber: "filter-receipt"}, + {createdAtFrom: "2026-09-01", createdAtTo: "2026-09-07"}, + {createdAtTo: "2026-09-04"}, + {paymentProviderType: ["gocardless"]}, + {paymentMethodType: ["sepa_debit"]}, + {invoiceNumber: "filter-invoice"}, + {paymentType: ["manual"]}, + {searchTerm: "Filter transfer"}, + {currency: "USD"}, + {paymentStatus: ["processing"], amountFrom: "100", currency: "USD"} + ].each do |filter_variables| + context "with #{filter_variables.keys.join(", ")}" do + let(:variables) { filter_variables } + + it "applies the filter and returns the correct count" do + expect(response["errors"]).to be_nil + expect(response.dig("data", "payments", "collection").map { |item| item["id"] }).to eq([payment.id]) + expect(response.dig("data", "payments", "metadata", "totalCount")).to eq(1) + end + end + end + + context "with payable type" do + let(:variables) { {payableType: ["PaymentRequest"], invoiceNumber: invoice1.number.downcase} } + let(:payment_request) { create(:payment_request, organization:, customer:, invoices: [invoice1, invoice2]) } + + before { payment.update!(payable: payment_request) } + + it "matches invoices on a payment request once" do + expect(response["errors"]).to be_nil + expect(response.dig("data", "payments", "collection").map { |item| item["id"] }).to eq([payment.id]) + expect(response.dig("data", "payments", "metadata", "totalCount")).to eq(1) + end + end + + [ + {paymentStatus: ["unknown"]}, {paymentProviderType: ["unknown"]}, + {paymentMethodType: ["unknown"]}, {paymentType: ["unknown"]}, {payableType: ["unknown"]}, + {amountFrom: "-1"}, {amountTo: "-1"}, {amountFrom: "500", amountTo: "100"}, + {amountFrom: "9223372036854775808"}, {receiptNumber: "x" * 256}, + {invoiceNumber: "x" * 256}, {invoiceId: "invalid"}, {createdAtFrom: "2026-02-30"} + ].each do |invalid_variables| + context "with invalid #{invalid_variables.keys.join(", ")}" do + let(:variables) { invalid_variables } + + it "returns a GraphQL error" do + expect(response["errors"]).to be_present + expect(response["data"]).to be_nil + end + end + end + end end diff --git a/spec/queries/payments_query_spec.rb b/spec/queries/payments_query_spec.rb index 0e131e7ba06..4be1fcd0802 100644 --- a/spec/queries/payments_query_spec.rb +++ b/spec/queries/payments_query_spec.rb @@ -245,4 +245,265 @@ expect(returned_ids).to be_empty end end + + context "with payment status filters" do + before do + payment_one.update!(payable_payment_status: "processing") + payment_two.update!(payable_payment_status: "failed") + payment_three.update!(payable_payment_status: "succeeded") + end + + context "with one status" do + let(:filters) { {payment_status: "processing"} } + + it "matches the payment status, including processing" do + expect(returned_ids).to eq([payment_one.id]) + end + end + + context "with several statuses" do + let(:filters) { {payment_status: %w[succeeded failed]} } + + it "combines statuses with OR" do + expect(returned_ids).to match_array([payment_two.id, payment_three.id]) + end + end + end + + context "with amount filters" do + before do + payment_one.update!(amount_cents: 0) + payment_two.update!(amount_cents: 1000) + payment_three.update!(amount_cents: 5000) + end + + [ + [{amount_from: "1000"}, %i[payment_two payment_three]], + [{amount_to: 1000}, %i[payment_one payment_two]], + [{amount_from: 1000, amount_to: 5000}, %i[payment_two payment_three]], + [{amount_from: 0, amount_to: 0}, %i[payment_one]], + [{amount_from: 1001, amount_to: 4999}, []] + ].each do |amount_filters, expected| + context "with #{amount_filters}" do + let(:filters) { amount_filters } + + it "uses inclusive integer bounds" do + expect(returned_ids).to match_array(expected.map { |name| public_send(name).id }) + end + end + end + + context "with amounts above the JavaScript safe integer limit" do + let(:filters) { {amount_from: "9007199254740993", amount_to: "9223372036854775807"} } + + before do + payment_one.update!(amount_cents: 9_007_199_254_740_992) + payment_two.update!(amount_cents: 9_007_199_254_740_993) + payment_three.update!(amount_cents: 9_223_372_036_854_775_807) + end + + it "distinguishes adjacent amounts without rounding" do + expect(returned_ids).to match_array([payment_two.id, payment_three.id]) + end + end + end + + context "with receipt number" do + let(:filters) { {receipt_number: "rcpt-2026-0001"} } + + before { create(:payment_receipt, organization:, payment: payment_one, number: "RCPT-2026-0001") } + + it "matches exactly without case sensitivity and excludes missing receipts" do + expect(returned_ids).to eq([payment_one.id]) + end + + context "with a partial number" do + let(:filters) { {receipt_number: "RCPT-2026"} } + + it "does not match a prefix" do + expect(returned_ids).to be_empty + end + end + + context "with receipt number as search only" do + let(:filters) { {} } + let(:search_term) { "RCPT-2026-0001" } + + it "does not add receipts to free-text search" do + expect(returned_ids).to be_empty + end + end + end + + context "with created date range" do + let(:filters) { {created_at_from: "2026-11-01", created_at_to: Date.new(2026, 11, 1)} } + let(:zone) { ActiveSupport::TimeZone["America/Los_Angeles"] } + + before do + organization.default_billing_entity.update!(timezone: zone.name) + payment_one.update!(created_at: zone.local(2026, 11, 1)) + payment_two.update!(created_at: zone.local(2026, 11, 1).end_of_day) + payment_three.update!(created_at: zone.local(2026, 11, 2)) + create(:payment, payable: create(:invoice, organization:), created_at: zone.local(2026, 11, 1) - Rational(1, 1_000_000)) + end + + it "includes both boundary instants across a 25-hour DST day" do + expect(returned_ids).to match_array([payment_one.id, payment_two.id]) + end + + context "with only a lower bound" do + let(:filters) { {created_at_from: "2026-11-02"} } + + it "includes subsequent payments" do + expect(returned_ids).to eq([payment_three.id]) + end + end + + context "with only an upper bound" do + let(:filters) { {created_at_to: "2026-11-01"} } + + it "includes earlier days" do + expect(returned_ids.size).to eq(3) + end + end + end + + context "with payment provider type" do + let(:filters) { {payment_provider_type: ["gocardless"]} } + + before do + payment_one.update!(payment_provider: create(:gocardless_provider, organization:)) + payment_two.update!(payment_provider: nil) + end + + it "maps API names to provider STI types" do + expect(returned_ids).to eq([payment_one.id]) + end + + context "with multiple provider types" do + let(:filters) { {payment_provider_type: %w[gocardless stripe]} } + + it "matches either provider and excludes payments without a provider" do + expect(returned_ids).to match_array([payment_one.id, payment_three.id]) + end + end + end + + context "with payment method type" do + let(:filters) { {payment_method_type: %w[card sepa_debit]} } + let(:method) { create(:payment_method, organization:, provider_method_type: "sepa_debit") } + + before do + payment_one.update!(provider_payment_method_data: {type: "card"}) + payment_two.update!(provider_payment_method_data: {}, payment_method: method) + payment_three.update!(provider_payment_method_data: {type: "link"}, payment_method: method) + end + + it "uses JSON first and falls back to the associated method" do + expect(returned_ids).to match_array([payment_one.id, payment_two.id]) + end + + [nil, ""].each do |empty_type| + context "when JSON type is #{empty_type.inspect}" do + before { payment_two.update!(provider_payment_method_data: {type: empty_type}) } + + it "falls back for an empty JSON type" do + expect(returned_ids).to match_array([payment_one.id, payment_two.id]) + end + end + end + + context "when neither source supplies a method" do + before { payment_two.update!(payment_method: nil) } + + it "does not match" do + expect(returned_ids).to eq([payment_one.id]) + end + end + end + + context "with invoice number" do + let(:filters) { {invoice_number: "lag-1234-001-002"} } + + before do + invoice.update!(number: "LAG-1234-001-002") + invoice2.update!(number: "LAG-1234-001-002-extra") + create(:payment_request_applied_invoice, invoice:, payment_request:) + create(:payment_request_applied_invoice, invoice: invoice2, payment_request:) + end + + it "matches both payable paths exactly, without case sensitivity" do + expect(returned_ids).to match_array([payment_one.id, payment_three.id]) + end + + context "when several invoices have the same number" do + let(:pagination) { {page: 1, limit: 1} } + + before { invoice2.update!(number: invoice.number) } + + it "counts and paginates each payment once" do + expect(result.payments.total_count).to eq(3) + expect(result.payments.size).to eq(1) + expect(result.payments.to_sql).not_to include("DISTINCT") + end + end + + context "with search on the same invoice number" do + let(:search_term) { "LAG-1234-001-002" } + + it "skips the redundant invoice search branch" do + expect(returned_ids).to be_empty + end + + context "when another search branch matches" do + before { payment_one.update!(provider_payment_id: "pi_LAG-1234-001-002") } + + it "still narrows the exact filter by search" do + expect(returned_ids).to eq([payment_one.id]) + end + end + end + end + + context "with payment type" do + let(:filters) { {payment_type: "manual"} } + + before { payment_three.update!(payment_type: "manual", reference: "bank transfer") } + + it "matches manual payments" do + expect(returned_ids).to eq([payment_three.id]) + end + end + + context "with payable type" do + let(:filters) { {payable_type: ["PaymentRequest"]} } + + it "matches payment requests" do + expect(returned_ids).to eq([payment_three.id]) + end + end + + context "with composed filters" do + let(:filters) { {payment_status: ["succeeded"], amount_from: 200, currency: "USD"} } + + before do + payment_one.update!(payable_payment_status: "succeeded", amount_currency: "USD") + payment_two.update!(payable_payment_status: "succeeded", amount_currency: "EUR") + payment_three.update!(payable_payment_status: "failed", amount_currency: "USD") + create(:payment, payable: create(:invoice, :open, organization:), payable_payment_status: "succeeded", amount_currency: "USD") + create(:payment, payable_payment_status: "succeeded", amount_currency: "USD") + end + + it "ANDs filters without exposing invisible invoices or other organizations" do + expect(returned_ids).to eq([payment_one.id]) + end + + context "when the matching invoice is draft" do + before { invoice.update!(status: :draft) } + + it "preserves the existing visible status contract" do + expect(returned_ids).to eq([payment_one.id]) + end + end + end end diff --git a/spec/support/shared_examples/payment_index.rb b/spec/support/shared_examples/payment_index.rb index 7af7a11cfa0..19ad4e1350e 100644 --- a/spec/support/shared_examples/payment_index.rb +++ b/spec/support/shared_examples/payment_index.rb @@ -44,4 +44,158 @@ expect(json[:payments].first[:invoice_ids].first).to eq(invoice.id) end end + + context "with list filters" do + let(:invoice) { create(:invoice, organization:, customer:, number: "QA-INVOICE") } + let(:payment_request) { create(:payment_request, organization:, customer:, invoices: [invoice]) } + let(:provider) { create(:stripe_provider, organization:) } + let(:matching_payment) do + create(:payment, payable: payment_request, customer:, payment_type: "manual", reference: "QA transfer", + payment_provider: provider, amount_cents: 5_000_000_000, amount_currency: "USD", + payable_payment_status: "processing", provider_payment_method_data: {type: "card"}, + created_at: Time.utc(2026, 9, 4, 12)) + end + let(:other_payment) do + create(:payment, payable: create(:invoice, organization:, customer:), customer:, + payment_provider: create(:gocardless_provider, organization:), amount_cents: 200, + amount_currency: "EUR", payable_payment_status: "pending", created_at: Time.utc(2026, 8, 31)) + end + + before do + matching_payment + other_payment + create(:payment_receipt, organization:, payment: matching_payment, number: "RCPT-2026-0001") + end + + [ + {payment_status: "processing"}, + {payment_status: %w[processing failed]}, + {payment_statuses: %w[processing succeeded]}, + {amount_from: "5000000000"}, + {amount_from: "5000000000", amount_to: "5000000000"}, + {receipt_number: "rcpt-2026-0001"}, + {created_at_from: "2026-09-01"}, + {created_at_from: "2026-09-01", created_at_to: "2026-09-07"}, + {payment_provider_type: "stripe"}, + {payment_provider_type: ["stripe"]}, + {payment_method_type: "card"}, + {payment_method_type: %w[card sepa_debit]}, + {currency: "USD"}, + {invoice_number: "qa-invoice"}, + {payment_type: "manual"}, + {payment_type: ["manual"]}, + {payable_type: "PaymentRequest"}, + {payable_type: ["PaymentRequest"]}, + {search_term: "QA transfer"}, + {payment_status: "processing", currency: "USD", amount_from: "1000", search_term: "QA transfer"} + ].each do |filter_params| + context "with #{filter_params}" do + let(:params) { filter_params } + + it "filters the response and its pagination metadata" do + subject + + expect(response).to have_http_status(:ok) + expect(json[:payments].map { |payment| payment[:lago_id] }).to eq([matching_payment.id]) + expect(json[:meta][:total_count]).to eq(1) + end + end + end + + [{amount_to: "200"}, {created_at_to: "2026-08-31"}].each do |filter_params| + context "with #{filter_params}" do + let(:params) { filter_params } + + it "applies an inclusive upper bound" do + subject + + expect(response).to have_http_status(:ok) + expect(json[:payments].map { |payment| payment[:lago_id] }).to eq([other_payment.id]) + expect(json[:meta][:total_count]).to eq(1) + end + end + end + + context "with invoice_id through a payment request" do + let(:params) { {invoice_id: invoice.id} } + + it "preserves the existing invoice filter" do + subject + expect(json[:payments].map { |payment| payment[:lago_id] }).to eq([matching_payment.id]) + end + end + + context "with invalid dates" do + let(:params) { {created_at_from: "2026-02-30", created_at_to: "invalid"} } + + it "ignores them without rejecting the request" do + subject + expect(response).to have_http_status(:ok) + expect(json[:meta][:total_count]).to eq(2) + end + end + + context "with bigint bounds above JavaScript precision" do + let(:params) { {amount_from: "9007199254740993", amount_to: "9007199254740993"} } + + before do + matching_payment.update!(amount_cents: 9_007_199_254_740_993) + other_payment.update!(amount_cents: 9_007_199_254_740_992) + end + + it "preserves every digit" do + subject + expect(response).to have_http_status(:ok) + expect(json[:payments].map { |payment| payment[:lago_id] }).to eq([matching_payment.id]) + end + end + + [ + {payment_status: "bogus"}, {payment_statuses: ["bogus"]}, + {payment_provider_type: ["bogus"]}, {payment_method_type: ["bogus"]}, + {payment_type: "bogus"}, {payable_type: "bogus"}, {currency: "XYZ"}, + {amount_from: "-1"}, {amount_to: "-1"}, {amount_from: "1.5"}, + {amount_from: "9223372036854775808"}, {amount_from: "500", amount_to: "100"}, + {invoice_id: "not-a-uuid"}, {invoice_number: "x" * 256}, {receipt_number: "x" * 256} + ].each do |invalid_params| + context "with invalid #{invalid_params.keys.join(", ")}" do + let(:params) { invalid_params } + + it "returns the standard validation error" do + subject + + expect(response).to have_http_status(:unprocessable_content) + expect(json[:code]).to eq("validation_errors") + expect(json[:error_details]).to be_present + end + end + end + + context "when advancing a filtered page" do + let(:params) { {payment_status: ["processing"], currency: "USD", amount_from: "100", per_page: 1} } + + before do + create(:payment, payable: payment_request, customer:, payment_type: "manual", reference: "Second transfer", + amount_cents: 1000, amount_currency: "USD", payable_payment_status: "processing") + end + + it "keeps the filtered count and predicates on subsequent pages" do + subject + first_ids = json[:payments].map { |payment| payment[:lago_id] } + expect(json[:meta][:total_count]).to eq(2) + expect(json[:meta][:next_page]).to eq(2) + + params[:page] = json[:meta][:next_page] + # The API returns a page number, not a next-page URL. + get_with_token(organization, request.path, params) + + expect(response).to have_http_status(:ok) + expect(json[:meta][:current_page]).to eq(2) + expect(json[:meta][:total_count]).to eq(2) + expect(json[:payments].map { |payment| payment[:lago_id] } & first_ids).to be_empty + expect(json[:payments].map { |payment| payment[:payment_status] }).to eq(["processing"]) + expect(json[:payments].map { |payment| payment[:amount_currency] }).to eq(["USD"]) + end + end + end end From 79132fe46e5df3fb59b9960253d8c2910be81a4f Mon Sep 17 00:00:00 2001 From: Raffi Date: Mon, 7 Sep 2026 16:02:01 -0700 Subject: [PATCH 3/9] docs(payments): record live filter QA ## Context Payment filters need reproducible evidence across the API, UI and clients. ## Description Record HTTP assertions, cross-client comparisons and query plans on synthetic development data. Document replay commands and existing compatibility constraints. --- qa/payments-filters/README.md | 21 + qa/payments-filters/api-qa.json | 570 +++++++++++++++++++++++ qa/payments-filters/cross-client-qa.json | 105 +++++ qa/payments-filters/explain.json | 23 + 4 files changed, 719 insertions(+) create mode 100644 qa/payments-filters/README.md create mode 100644 qa/payments-filters/api-qa.json create mode 100644 qa/payments-filters/cross-client-qa.json create mode 100644 qa/payments-filters/explain.json diff --git a/qa/payments-filters/README.md b/qa/payments-filters/README.md new file mode 100644 index 00000000000..664207dd833 --- /dev/null +++ b/qa/payments-filters/README.md @@ -0,0 +1,21 @@ +# Payments list filter QA + +All records used here are synthetic and live in an isolated development organization. + +- `api-qa.json`: 79 live HTTP checks across REST, customer-scoped REST and GraphQL. Each successful call asserts the filtered count and every returned ID against the seed manifest. Error cases assert status and validation details. +- `explain.json`: complete SQL and EXPLAIN ANALYZE/BUFFERS plans for invoice number + payment method + created date bounds, using 100,000 payments, 120,000 invoices and 20,000 payment requests in a separate organization. List execution: 232.222 ms; count: 83.864 ms. PostgreSQL uses the existing payments cursor index; no expression index was added. Invoice visibility and lower-number scans remain in the plans. +- `cross-client-qa.json`: UI, REST, GraphQL, all six SDKs and CLI return the same seven succeeded EUR payments. Customer-scoped SDK calls return two; minimum amount 9223372036854775807 returns two. CLI additionally verifies arrays and saved payment method fallback. + +Replay with the API development container running: + +```sh +lago exec api bundle exec rails runner script/seed_payments_filters.rb +python3 script/qa_payments_filters.py +lago exec api bundle exec rails runner script/benchmark_payments_filters.rb +``` + +The seed is development-only and repeatable. It creates 30 payments (29 visible), three customers, stubbed Stripe/GoCardless providers, every status/type/method, receipts, both payable paths, multiple currencies, exact int64 bounds, and organization-timezone boundary timestamps. Local credentials are written only under ignored `tmp/` with mode 0600; do not commit them. The QA script reads those credentials without printing them. Generated IDs change in a new database, so the QA script uses the freshly generated manifest. + +The actual `Invoice::VISIBLE_STATUS` includes draft. Existing visibility is preserved: the draft fixture is visible and the open-invoice fixture remains hidden. REST ignores malformed dates; GraphQL's existing ISO8601Date scalar rejects malformed dates before resolution. Existing REST errors use `validation_errors` (plural). Pagination returns a page number in `meta.next_page`, so callers repeat filters on subsequent requests. + +Bullet reports pre-existing lazy loads of customer, payable, payment receipt and payment provider in both filtered and unfiltered list responses. These changes add SQL predicates without adding serialized associations or per-row association access. No serializer changes, payment response shape changes, CSV export or data-export code. diff --git a/qa/payments-filters/api-qa.json b/qa/payments-filters/api-qa.json new file mode 100644 index 00000000000..2830eb5fd00 --- /dev/null +++ b/qa/payments-filters/api-qa.json @@ -0,0 +1,570 @@ +[ + { + "request": "GET /api/v1/payments?", + "status": 200, + "count": 29, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?", + "status": 200, + "count": 10, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": {}, + "status": 200, + "count": 29, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_status%5B%5D=succeeded&payment_status%5B%5D=failed", + "status": 200, + "count": 14, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_status%5B%5D=succeeded&payment_status%5B%5D=failed", + "status": 200, + "count": 5, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentStatus": [ + "succeeded", + "failed" + ] + }, + "status": 200, + "count": 14, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_statuses%5B%5D=processing", + "status": 200, + "count": 7, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_statuses%5B%5D=processing", + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentStatus": [ + "processing" + ] + }, + "status": 200, + "count": 7, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?amount_from=1000&amount_to=5000", + "status": 200, + "count": 6, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?amount_from=1000&amount_to=5000", + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "amountFrom": "1000", + "amountTo": "5000" + }, + "status": 200, + "count": 6, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?amount_from=5000000000", + "status": 200, + "count": 8, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?amount_from=5000000000", + "status": 200, + "count": 3, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "amountFrom": "5000000000" + }, + "status": 200, + "count": 8, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?amount_from=9007199254740993&amount_to=9007199254740993", + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?amount_from=9007199254740993&amount_to=9007199254740993", + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "amountFrom": "9007199254740993", + "amountTo": "9007199254740993" + }, + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?amount_from=9223372036854775807", + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?amount_from=9223372036854775807", + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "amountFrom": "9223372036854775807" + }, + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?amount_from=0&amount_to=0", + "status": 200, + "count": 3, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?amount_from=0&amount_to=0", + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "amountFrom": "0", + "amountTo": "0" + }, + "status": 200, + "count": 3, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?receipt_number=rcpt-2026-0001", + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?receipt_number=rcpt-2026-0001", + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "receiptNumber": "rcpt-2026-0001" + }, + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?receipt_number=missing", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?receipt_number=missing", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "receiptNumber": "missing" + }, + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?created_at_from=2026-09-01&created_at_to=2026-09-07", + "status": 200, + "count": 17, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?created_at_from=2026-09-01&created_at_to=2026-09-07", + "status": 200, + "count": 6, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "createdAtFrom": "2026-09-01", + "createdAtTo": "2026-09-07" + }, + "status": 200, + "count": 17, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_provider_type%5B%5D=stripe", + "status": 200, + "count": 10, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_provider_type%5B%5D=stripe", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentProviderType": [ + "stripe" + ] + }, + "status": 200, + "count": 10, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_provider_type%5B%5D=gocardless", + "status": 200, + "count": 9, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_provider_type%5B%5D=gocardless", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentProviderType": [ + "gocardless" + ] + }, + "status": 200, + "count": 9, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_method_type%5B%5D=card&payment_method_type%5B%5D=sepa_debit", + "status": 200, + "count": 5, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_method_type%5B%5D=card&payment_method_type%5B%5D=sepa_debit", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentMethodType": [ + "card", + "sepa_debit" + ] + }, + "status": 200, + "count": 5, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?currency=EUR", + "status": 200, + "count": 15, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?currency=EUR", + "status": 200, + "count": 5, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "currency": "EUR" + }, + "status": 200, + "count": 15, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?invoice_number=lag-1234-001-002", + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?invoice_number=lag-1234-001-002", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "invoiceNumber": "lag-1234-001-002" + }, + "status": 200, + "count": 1, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?external_customer_id=cust_1", + "status": 200, + "count": 10, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?external_customer_id=cust_1", + "status": 200, + "count": 10, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "externalCustomerId": "cust_1" + }, + "status": 200, + "count": 10, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_type=manual&payable_type=PaymentRequest", + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_type=manual&payable_type=PaymentRequest", + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentType": [ + "manual" + ], + "payableType": [ + "PaymentRequest" + ] + }, + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?search_term=pi_3", + "status": 200, + "count": 19, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?search_term=pi_3", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "searchTerm": "pi_3" + }, + "status": 200, + "count": 19, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_status=succeeded¤cy=EUR&amount_from=100&created_at_from=2026-09-01", + "status": 200, + "count": 4, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_status=succeeded¤cy=EUR&amount_from=100&created_at_from=2026-09-01", + "status": 200, + "count": 2, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentStatus": [ + "succeeded" + ], + "currency": "EUR", + "amountFrom": "100", + "createdAtFrom": "2026-09-01" + }, + "status": 200, + "count": 4, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_status%5B%5D=succeeded&payment_method_type%5B%5D=card&payment_method_type%5B%5D=us_bank_account¤cy=EUR&search_term=pi_3", + "status": 200, + "count": 3, + "ids_match": true + }, + { + "request": "GET /api/v1/customers/cust_1/payments?payment_status%5B%5D=succeeded&payment_method_type%5B%5D=card&payment_method_type%5B%5D=us_bank_account¤cy=EUR&search_term=pi_3", + "status": 200, + "count": 0, + "ids_match": true + }, + { + "request": "GraphQL payments", + "variables": { + "paymentStatus": [ + "succeeded" + ], + "currency": "EUR", + "paymentMethodType": [ + "card", + "us_bank_account" + ], + "searchTerm": "pi_3" + }, + "status": 200, + "count": 3, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?created_at_from=invalid&created_at_to=2026-02-30", + "status": 200, + "count": 29, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_status=bogus", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?payment_provider_type=bogus", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?payment_method_type=bogus", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?payment_type=bogus", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?payable_type=bogus", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?currency=XYZ", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?amount_from=-1", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?amount_to=-1", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?amount_from=500&amount_to=100", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?amount_from=9223372036854775808", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?invoice_id=invalid", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?receipt_number=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?invoice_number=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "status": 422, + "validation_error": true + }, + { + "request": "GET /api/v1/payments?payment_status=succeeded¤cy=EUR&amount_from=100&created_at_from=2026-09-01&per_page=2&page=1", + "status": 200, + "meta": { + "current_page": 1, + "next_page": 2, + "prev_page": null, + "total_pages": 2, + "total_count": 4 + }, + "ids_match": true + }, + { + "request": "GET /api/v1/payments?payment_status=succeeded¤cy=EUR&amount_from=100&created_at_from=2026-09-01&per_page=2&page=2", + "status": 200, + "meta": { + "current_page": 2, + "next_page": null, + "prev_page": 1, + "total_pages": 2, + "total_count": 4 + }, + "ids_match": true + } +] \ No newline at end of file diff --git a/qa/payments-filters/cross-client-qa.json b/qa/payments-filters/cross-client-qa.json new file mode 100644 index 00000000000..eda9e8b82a7 --- /dev/null +++ b/qa/payments-filters/cross-client-qa.json @@ -0,0 +1,105 @@ +{ + "predicate": { + "payment_status": [ + "succeeded" + ], + "currency": "EUR" + }, + "count": 7, + "ids": [ + "1f4c1581-5478-4a79-b460-04c1fe21a15a", + "39fa6b17-12a1-474d-a38e-16c0f83ee9ac", + "8d699c6f-6df9-4f22-84db-9c6f87b56406", + "c50d097e-c049-433f-895b-9bfe5b1c67e2", + "e20e5548-f3a5-43cc-b507-d5e9e57022fc", + "e695aabb-9a8c-4f80-98a8-43d2e06e673a", + "fc261c0b-d48f-4505-9f5f-6aef77afbbc1" + ], + "surfaces": [ + "REST", + "GraphQL", + "UI", + "Go", + "Python", + "Ruby", + "Rust", + "JavaScript", + "PHP", + "CLI" + ], + "all_ids_match": true, + "sdk_customer_count": 2, + "sdk_int64_max_count": 2, + "cli_checks": [ + { + "command": "lago payments list --payment-status succeeded --currency EUR", + "exit_code": 0, + "count": 7, + "ids": [ + "1f4c1581-5478-4a79-b460-04c1fe21a15a", + "39fa6b17-12a1-474d-a38e-16c0f83ee9ac", + "8d699c6f-6df9-4f22-84db-9c6f87b56406", + "c50d097e-c049-433f-895b-9bfe5b1c67e2", + "e20e5548-f3a5-43cc-b507-d5e9e57022fc", + "e695aabb-9a8c-4f80-98a8-43d2e06e673a", + "fc261c0b-d48f-4505-9f5f-6aef77afbbc1" + ], + "ids_match": true + }, + { + "command": "lago payments list --payment-status succeeded,failed", + "exit_code": 0, + "count": 14, + "ids": [ + "12aa4435-c9a2-444b-a8ab-a0a89dcca50b", + "1f4c1581-5478-4a79-b460-04c1fe21a15a", + "35f92acd-9d6d-4900-a8fd-269abdd33bd1", + "39fa6b17-12a1-474d-a38e-16c0f83ee9ac", + "4ab08f91-6c21-4cbe-8e5f-e330e9cdc9d4", + "6aa4cc9c-d48b-47a1-931b-5b0d82b25541", + "73f36220-3f9b-4f8e-a3c7-d5f5bfc236b1", + "8d699c6f-6df9-4f22-84db-9c6f87b56406", + "c50d097e-c049-433f-895b-9bfe5b1c67e2", + "d5b52e4f-4b02-4ce9-ad36-60e7aa7e417d", + "e20e5548-f3a5-43cc-b507-d5e9e57022fc", + "e695aabb-9a8c-4f80-98a8-43d2e06e673a", + "f95089a0-c003-4128-893f-8009fcbc8a33", + "fc261c0b-d48f-4505-9f5f-6aef77afbbc1" + ], + "ids_match": true + }, + { + "command": "lago payments list --amount-from 9223372036854775807", + "exit_code": 0, + "count": 2, + "ids": [ + "5b5c572a-7682-43c7-a2e3-c6a1edbd1c87", + "73f36220-3f9b-4f8e-a3c7-d5f5bfc236b1" + ], + "ids_match": true + }, + { + "command": "lago payments list --payment-method-type card,sepa_debit", + "exit_code": 0, + "count": 5, + "ids": [ + "43d63e21-133b-4092-acf4-8c1a909a2974", + "64ff022c-0f53-48a5-bae5-a32294392a4d", + "77eac2dd-4475-4239-b562-b9f4b9408aea", + "9a6b5689-adbb-4958-a838-d31d5bf0e57d", + "9bc38b96-6c5a-4876-930b-8c566df1b344" + ], + "ids_match": true + }, + { + "command": "lago customers payments cust_1 --payment-status succeeded --currency EUR", + "exit_code": 0, + "count": 2, + "ids": [ + "e20e5548-f3a5-43cc-b507-d5e9e57022fc", + "e695aabb-9a8c-4f80-98a8-43d2e06e673a" + ], + "ids_match": true + } + ] +} diff --git a/qa/payments-filters/explain.json b/qa/payments-filters/explain.json new file mode 100644 index 00000000000..f600520b683 --- /dev/null +++ b/qa/payments-filters/explain.json @@ -0,0 +1,23 @@ +{ + "rows": 100000, + "filters": { + "invoice_number": "perf-000035", + "payment_method_type": [ + "card", + "sepa_debit", + "us_bank_account" + ], + "created_at_from": "2026-09-01", + "created_at_to": "2026-09-07" + }, + "plans": { + "list": { + "sql": "SELECT \"payments\".\"id\", \"payments\".\"invoice_id\", \"payments\".\"payment_provider_id\", \"payments\".\"payment_provider_customer_id\", \"payments\".\"amount_cents\", \"payments\".\"amount_currency\", \"payments\".\"provider_payment_id\", \"payments\".\"status\", \"payments\".\"created_at\", \"payments\".\"updated_at\", \"payments\".\"payable_type\", \"payments\".\"payable_id\", \"payments\".\"provider_payment_data\", \"payments\".\"payable_payment_status\", \"payments\".\"payment_type\", \"payments\".\"reference\", \"payments\".\"provider_payment_method_data\", \"payments\".\"provider_payment_method_id\", \"payments\".\"organization_id\", \"payments\".\"customer_id\", \"payments\".\"error_code\", \"payments\".\"payment_method_id\" FROM \"payments\" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE \"payments\".\"customer_id\" IS NOT NULL AND \"payments\".\"organization_id\" = '3878a544-698b-4548-95b4-7cf87274e028' AND \"payments\".\"payable_id\" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '3878a544-698b-4548-95b4-7cf87274e028' ) ELSE TRUE END) AND \"payments\".\"created_at\" >= '2026-09-01 00:00:00' AND \"payments\".\"created_at\" <= '2026-09-07 23:59:59.999999' AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card', 'sepa_debit', 'us_bank_account')) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '3878a544-698b-4548-95b4-7cf87274e028' AND LOWER(invoices.number) = LOWER('perf-000035') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) ORDER BY \"payments\".\"created_at\" DESC, \"payments\".\"id\" ASC LIMIT 20 OFFSET 0", + "plan": "Limit (cost=1000.55..441831.37 rows=8 width=292) (actual time=110.935..118.767 rows=1 loops=1)\n Buffers: shared hit=46508\n -> Nested Loop Semi Join (cost=1000.55..441831.37 rows=8 width=292) (actual time=85.097..92.928 rows=1 loops=1)\n Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (SubPlan 3)))\n Rows Removed by Join Filter: 6667\n Buffers: shared hit=46508\n -> Nested Loop Left Join (cost=0.55..105464.58 rows=74 width=292) (actual time=49.480..62.609 rows=6668 loops=1)\n Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = ANY ('{card,sepa_debit,us_bank_account}'::text[]))\n Rows Removed by Filter: 5001\n Buffers: shared hit=39560\n -> Index Scan using index_payments_by_cursor on payments (cost=0.42..104608.71 rows=4904 width=292) (actual time=49.388..57.776 rows=11669 loops=1)\n Index Cond: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-07 23:59:59.999999'::timestamp without time zone))\n Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (((payable_type)::text = 'Invoice'::text) OR ((payable_type)::text = 'PaymentRequest'::text)) AND CASE payable_type WHEN 'Invoice'::text THEN (hashed SubPlan 2) ELSE true END)\n Buffers: shared hit=16222\n SubPlan 2\n -> Seq Scan on invoices invoices_1 (cost=0.00..6696.71 rows=120018 width=16) (actual time=0.190..25.096 rows=120000 loops=1)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[])))\n Rows Removed by Filter: 38\n Buffers: shared hit=4446\n -> Index Scan using payment_methods_pkey on payment_methods (cost=0.14..0.16 rows=1 width=25) (actual time=0.000..0.000 rows=1 loops=11669)\n Index Cond: (id = payments.payment_method_id)\n Buffers: shared hit=23338\n -> Materialize (cost=1000.00..6384.28 rows=600 width=16) (actual time=0.000..0.004 rows=1 loops=6668)\n Buffers: shared hit=4446\n -> Gather (cost=1000.00..6381.28 rows=600 width=16) (actual time=0.500..28.824 rows=1 loops=1)\n Workers Planned: 2\n Workers Launched: 2\n Buffers: shared hit=4446\n -> Parallel Seq Scan on invoices (cost=0.00..5321.28 rows=250 width=16) (actual time=5.024..14.418 rows=0 loops=3)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (lower((number)::text) = 'perf-000035'::text))\n Rows Removed by Filter: 40012\n Buffers: shared hit=4446\n SubPlan 3\n -> Index Scan using index_invoices_payment_requests_on_invoice_id on invoices_payment_requests (cost=0.29..8.31 rows=1 width=0) (actual time=0.000..0.000 rows=0 loops=834)\n Index Cond: (invoice_id = invoices.id)\n Filter: (payment_request_id = payments.payable_id)\n Rows Removed by Filter: 1\n Buffers: shared hit=2502\nPlanning:\n Buffers: shared hit=494\nPlanning Time: 1.931 ms\nJIT:\n Functions: 60\n Options: Inlining false, Optimization false, Expressions true, Deforming true\n Timing: Generation 5.714 ms, Inlining 0.000 ms, Optimization 3.084 ms, Emission 34.924 ms, Total 43.722 ms\nExecution Time: 232.222 ms" + }, + "count": { + "sql": "SELECT COUNT(*) FROM \"payments\" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE \"payments\".\"customer_id\" IS NOT NULL AND \"payments\".\"organization_id\" = '3878a544-698b-4548-95b4-7cf87274e028' AND \"payments\".\"payable_id\" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '3878a544-698b-4548-95b4-7cf87274e028' ) ELSE TRUE END) AND \"payments\".\"created_at\" >= '2026-09-01 00:00:00' AND \"payments\".\"created_at\" <= '2026-09-07 23:59:59.999999' AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card', 'sepa_debit', 'us_bank_account')) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '3878a544-698b-4548-95b4-7cf87274e028' AND LOWER(invoices.number) = LOWER('perf-000035') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) ))", + "plan": "Aggregate (cost=438662.77..438662.78 rows=1 width=8) (actual time=81.474..81.514 rows=1 loops=1)\n Buffers: shared hit=13463\n -> Nested Loop Semi Join (cost=1572.89..438662.75 rows=8 width=0) (actual time=72.979..81.502 rows=1 loops=1)\n Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (SubPlan 3)))\n Rows Removed by Join Filter: 6667\n Buffers: shared hit=13463\n -> Hash Left Join (cost=572.89..102295.96 rows=74 width=25) (actual time=53.333..60.331 rows=6668 loops=1)\n Hash Cond: (payments.payment_method_id = payment_methods.id)\n Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = ANY ('{card,sepa_debit,us_bank_account}'::text[]))\n Rows Removed by Filter: 5001\n Buffers: shared hit=6515\n -> Bitmap Heap Scan on payments (cost=571.42..102276.98 rows=4904 width=59) (actual time=53.291..58.573 rows=11669 loops=1)\n Recheck Cond: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-07 23:59:59.999999'::timestamp without time zone))\n Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (((payable_type)::text = 'Invoice'::text) OR ((payable_type)::text = 'PaymentRequest'::text)) AND CASE payable_type WHEN 'Invoice'::text THEN (hashed SubPlan 2) ELSE true END)\n Heap Blocks: exact=1961\n Buffers: shared hit=6514\n -> Bitmap Index Scan on index_payments_by_cursor (cost=0.00..570.19 rows=11662 width=0) (actual time=0.575..0.576 rows=11669 loops=1)\n Index Cond: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-07 23:59:59.999999'::timestamp without time zone))\n Buffers: shared hit=107\n SubPlan 2\n -> Seq Scan on invoices invoices_1 (cost=0.00..6696.71 rows=120018 width=16) (actual time=0.079..20.335 rows=120000 loops=1)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[])))\n Rows Removed by Filter: 38\n Buffers: shared hit=4446\n -> Hash (cost=1.21..1.21 rows=21 width=25) (actual time=0.018..0.019 rows=21 loops=1)\n Buckets: 1024 Batches: 1 Memory Usage: 10kB\n Buffers: shared hit=1\n -> Seq Scan on payment_methods (cost=0.00..1.21 rows=21 width=25) (actual time=0.009..0.010 rows=21 loops=1)\n Buffers: shared hit=1\n -> Materialize (cost=1000.00..6384.28 rows=600 width=16) (actual time=0.000..0.003 rows=1 loops=6668)\n Buffers: shared hit=4446\n -> Gather (cost=1000.00..6381.28 rows=600 width=16) (actual time=0.194..19.616 rows=1 loops=1)\n Workers Planned: 2\n Workers Launched: 2\n Buffers: shared hit=4446\n -> Parallel Seq Scan on invoices (cost=0.00..5321.28 rows=250 width=16) (actual time=5.640..12.073 rows=0 loops=3)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (lower((number)::text) = 'perf-000035'::text))\n Rows Removed by Filter: 40012\n Buffers: shared hit=4446\n SubPlan 3\n -> Index Scan using index_invoices_payment_requests_on_invoice_id on invoices_payment_requests (cost=0.29..8.31 rows=1 width=0) (actual time=0.000..0.000 rows=0 loops=834)\n Index Cond: (invoice_id = invoices.id)\n Filter: (payment_request_id = payments.payable_id)\n Rows Removed by Filter: 1\n Buffers: shared hit=2502\nPlanning:\n Buffers: shared hit=25\nPlanning Time: 0.681 ms\nJIT:\n Functions: 65\n Options: Inlining false, Optimization false, Expressions true, Deforming true\n Timing: Generation 2.965 ms, Inlining 0.000 ms, Optimization 1.698 ms, Emission 22.279 ms, Total 26.942 ms\nExecution Time: 83.864 ms" + } + } +} \ No newline at end of file From 57a1b6fe394092329d9ef6c3ccfc87d0319244cb Mon Sep 17 00:00:00 2001 From: Raffi Date: Tue, 8 Sep 2026 20:55:03 -0700 Subject: [PATCH 4/9] perf(payments): rewrite list filter predicates to use existing indexes - receipt_number and invoice_number resolve the receipt / invoice ids first through organization-scoped lookups, then reach payments by primary key or (payable_type, payable_id); no function on the joined column, no DISTINCT - payment_provider_type resolves provider ids in Ruby, scoped to the organization - payment_method_type evaluates the jsonb type and the saved-method fallback as two plain predicates instead of a COALESCE across a LEFT JOIN, and no longer matches soft-deleted payment methods - external_customer_id resolves the customer id first instead of joining customers - spec fixtures use customers and providers of the payment's organization - SQL tripwire spec: no DISTINCT, no function on indexed payments columns, no joins on the lookup tables --- app/queries/payments_query.rb | 63 +++++++++++++--------- qa/payments-filters/explain.json | 23 -------- script/benchmark_payments_filters.rb | 78 ---------------------------- spec/queries/payments_query_spec.rb | 28 +++++++++- 4 files changed, 65 insertions(+), 127 deletions(-) delete mode 100644 qa/payments-filters/explain.json delete mode 100644 script/benchmark_payments_filters.rb diff --git a/app/queries/payments_query.rb b/app/queries/payments_query.rb index a0c6e5628c6..2531ba1e934 100644 --- a/app/queries/payments_query.rb +++ b/app/queries/payments_query.rb @@ -124,9 +124,10 @@ def apply_filters(scope) end def filter_by_customer(scope) - external_customer_id = filters.external_customer_id - - scope.joins(:customer).where("customers.external_id = :external_customer_id", external_customer_id:) + # Resolve the customer first so the planner starts from one customer_id instead of a join. + # Discarded customers stay reachable, as with the belongs_to scope on Payment. + customer_id = organization.customers.with_discarded.where(external_id: filters.external_customer_id).pick(:id) + scope.where(customer_id:) end def filter_by_invoice(scope) @@ -159,7 +160,13 @@ def with_amount_range(scope) end def with_receipt_number(scope) - scope.joins(:payment_receipt).where("LOWER(payment_receipts.number) = LOWER(?)", filters.receipt_number) + # Semi-join on payment_receipts scoped by organization: the receipt is looked up through + # (organization_id, lower(number)) and the outer query becomes a primary-key lookup, + # instead of walking every payment of the organization and probing receipts per row. + receipts = PaymentReceipt.where(organization_id: organization.id) + .where("lower(payment_receipts.number) = lower(?)", filters.receipt_number) + .select(:payment_id) + scope.where(id: receipts) end def with_created_at_range(scope) @@ -172,33 +179,39 @@ def with_created_at_range(scope) def with_payment_provider_type(scope) types = Array(filters.payment_provider_type).map { |type| "PaymentProviders::#{type.camelize}Provider" } - scope.where(payment_provider_id: PaymentProviders::BaseProvider.unscoped.where(type: types).select(:id)) + # Resolve provider ids first, scoped to the organization. Deleted providers are kept because + # historical payments still reference them. An empty list short-circuits to no rows. + provider_ids = PaymentProviders::BaseProvider.unscoped.where(organization_id: organization.id, type: types).pluck(:id) + scope.where(payment_provider_id: provider_ids) end def with_payment_method_type(scope) - scope.joins("LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id") - .where( - "COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN (?)", - Array(filters.payment_method_type) - ) + types = Array(filters.payment_method_type) + # The jsonb type wins when present; otherwise fall back to the saved (non-deleted) payment + # method. Two plain predicates instead of a COALESCE across a join. + fallback_ids = PaymentMethod.where(organization_id: organization.id, provider_method_type: types).select(:id) + scope.where( + "payments.provider_payment_method_data->>'type' IN (:types) " \ + "OR (NULLIF(payments.provider_payment_method_data->>'type', '') IS NULL AND payments.payment_method_id IN (:fallback_ids))", + types:, fallback_ids: + ) end def with_invoice_number(scope) - scope.where(<<~SQL.squish, number: filters.invoice_number, organization_id: organization.id) - EXISTS ( - SELECT 1 FROM invoices - WHERE invoices.organization_id = :organization_id - AND LOWER(invoices.number) = LOWER(:number) - AND ( - (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) - OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( - SELECT 1 FROM invoices_payment_requests - WHERE invoices_payment_requests.payment_request_id = payments.payable_id - AND invoices_payment_requests.invoice_id = invoices.id - )) - ) - ) - SQL + # Resolve the invoice ids first (organization-scoped, case-insensitive), then reach the + # payments through index_payments_on_payable_type_and_payable_id on both payable paths. + # The ids are passed as literals: with sub-selects the planner turns the OR into hashed + # SubPlans evaluated against every payment of the organization. No DISTINCT needed: + # a payment has one payable. + invoice_ids = organization.invoices.where("lower(invoices.number) = lower(?)", filters.invoice_number).pluck(:id) + return scope.none if invoice_ids.empty? + + request_ids = PaymentRequest::AppliedInvoice.where(invoice_id: invoice_ids).pluck(:payment_request_id) + scope.where( + "(payments.payable_type = 'Invoice' AND payments.payable_id IN (:invoice_ids)) " \ + "OR (payments.payable_type = 'PaymentRequest' AND payments.payable_id IN (:request_ids))", + invoice_ids:, request_ids: request_ids.presence || [nil] + ) end def with_payment_type(scope) diff --git a/qa/payments-filters/explain.json b/qa/payments-filters/explain.json deleted file mode 100644 index f600520b683..00000000000 --- a/qa/payments-filters/explain.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "rows": 100000, - "filters": { - "invoice_number": "perf-000035", - "payment_method_type": [ - "card", - "sepa_debit", - "us_bank_account" - ], - "created_at_from": "2026-09-01", - "created_at_to": "2026-09-07" - }, - "plans": { - "list": { - "sql": "SELECT \"payments\".\"id\", \"payments\".\"invoice_id\", \"payments\".\"payment_provider_id\", \"payments\".\"payment_provider_customer_id\", \"payments\".\"amount_cents\", \"payments\".\"amount_currency\", \"payments\".\"provider_payment_id\", \"payments\".\"status\", \"payments\".\"created_at\", \"payments\".\"updated_at\", \"payments\".\"payable_type\", \"payments\".\"payable_id\", \"payments\".\"provider_payment_data\", \"payments\".\"payable_payment_status\", \"payments\".\"payment_type\", \"payments\".\"reference\", \"payments\".\"provider_payment_method_data\", \"payments\".\"provider_payment_method_id\", \"payments\".\"organization_id\", \"payments\".\"customer_id\", \"payments\".\"error_code\", \"payments\".\"payment_method_id\" FROM \"payments\" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE \"payments\".\"customer_id\" IS NOT NULL AND \"payments\".\"organization_id\" = '3878a544-698b-4548-95b4-7cf87274e028' AND \"payments\".\"payable_id\" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '3878a544-698b-4548-95b4-7cf87274e028' ) ELSE TRUE END) AND \"payments\".\"created_at\" >= '2026-09-01 00:00:00' AND \"payments\".\"created_at\" <= '2026-09-07 23:59:59.999999' AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card', 'sepa_debit', 'us_bank_account')) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '3878a544-698b-4548-95b4-7cf87274e028' AND LOWER(invoices.number) = LOWER('perf-000035') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) ORDER BY \"payments\".\"created_at\" DESC, \"payments\".\"id\" ASC LIMIT 20 OFFSET 0", - "plan": "Limit (cost=1000.55..441831.37 rows=8 width=292) (actual time=110.935..118.767 rows=1 loops=1)\n Buffers: shared hit=46508\n -> Nested Loop Semi Join (cost=1000.55..441831.37 rows=8 width=292) (actual time=85.097..92.928 rows=1 loops=1)\n Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (SubPlan 3)))\n Rows Removed by Join Filter: 6667\n Buffers: shared hit=46508\n -> Nested Loop Left Join (cost=0.55..105464.58 rows=74 width=292) (actual time=49.480..62.609 rows=6668 loops=1)\n Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = ANY ('{card,sepa_debit,us_bank_account}'::text[]))\n Rows Removed by Filter: 5001\n Buffers: shared hit=39560\n -> Index Scan using index_payments_by_cursor on payments (cost=0.42..104608.71 rows=4904 width=292) (actual time=49.388..57.776 rows=11669 loops=1)\n Index Cond: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-07 23:59:59.999999'::timestamp without time zone))\n Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (((payable_type)::text = 'Invoice'::text) OR ((payable_type)::text = 'PaymentRequest'::text)) AND CASE payable_type WHEN 'Invoice'::text THEN (hashed SubPlan 2) ELSE true END)\n Buffers: shared hit=16222\n SubPlan 2\n -> Seq Scan on invoices invoices_1 (cost=0.00..6696.71 rows=120018 width=16) (actual time=0.190..25.096 rows=120000 loops=1)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[])))\n Rows Removed by Filter: 38\n Buffers: shared hit=4446\n -> Index Scan using payment_methods_pkey on payment_methods (cost=0.14..0.16 rows=1 width=25) (actual time=0.000..0.000 rows=1 loops=11669)\n Index Cond: (id = payments.payment_method_id)\n Buffers: shared hit=23338\n -> Materialize (cost=1000.00..6384.28 rows=600 width=16) (actual time=0.000..0.004 rows=1 loops=6668)\n Buffers: shared hit=4446\n -> Gather (cost=1000.00..6381.28 rows=600 width=16) (actual time=0.500..28.824 rows=1 loops=1)\n Workers Planned: 2\n Workers Launched: 2\n Buffers: shared hit=4446\n -> Parallel Seq Scan on invoices (cost=0.00..5321.28 rows=250 width=16) (actual time=5.024..14.418 rows=0 loops=3)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (lower((number)::text) = 'perf-000035'::text))\n Rows Removed by Filter: 40012\n Buffers: shared hit=4446\n SubPlan 3\n -> Index Scan using index_invoices_payment_requests_on_invoice_id on invoices_payment_requests (cost=0.29..8.31 rows=1 width=0) (actual time=0.000..0.000 rows=0 loops=834)\n Index Cond: (invoice_id = invoices.id)\n Filter: (payment_request_id = payments.payable_id)\n Rows Removed by Filter: 1\n Buffers: shared hit=2502\nPlanning:\n Buffers: shared hit=494\nPlanning Time: 1.931 ms\nJIT:\n Functions: 60\n Options: Inlining false, Optimization false, Expressions true, Deforming true\n Timing: Generation 5.714 ms, Inlining 0.000 ms, Optimization 3.084 ms, Emission 34.924 ms, Total 43.722 ms\nExecution Time: 232.222 ms" - }, - "count": { - "sql": "SELECT COUNT(*) FROM \"payments\" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE \"payments\".\"customer_id\" IS NOT NULL AND \"payments\".\"organization_id\" = '3878a544-698b-4548-95b4-7cf87274e028' AND \"payments\".\"payable_id\" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '3878a544-698b-4548-95b4-7cf87274e028' ) ELSE TRUE END) AND \"payments\".\"created_at\" >= '2026-09-01 00:00:00' AND \"payments\".\"created_at\" <= '2026-09-07 23:59:59.999999' AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card', 'sepa_debit', 'us_bank_account')) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '3878a544-698b-4548-95b4-7cf87274e028' AND LOWER(invoices.number) = LOWER('perf-000035') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) ))", - "plan": "Aggregate (cost=438662.77..438662.78 rows=1 width=8) (actual time=81.474..81.514 rows=1 loops=1)\n Buffers: shared hit=13463\n -> Nested Loop Semi Join (cost=1572.89..438662.75 rows=8 width=0) (actual time=72.979..81.502 rows=1 loops=1)\n Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (SubPlan 3)))\n Rows Removed by Join Filter: 6667\n Buffers: shared hit=13463\n -> Hash Left Join (cost=572.89..102295.96 rows=74 width=25) (actual time=53.333..60.331 rows=6668 loops=1)\n Hash Cond: (payments.payment_method_id = payment_methods.id)\n Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = ANY ('{card,sepa_debit,us_bank_account}'::text[]))\n Rows Removed by Filter: 5001\n Buffers: shared hit=6515\n -> Bitmap Heap Scan on payments (cost=571.42..102276.98 rows=4904 width=59) (actual time=53.291..58.573 rows=11669 loops=1)\n Recheck Cond: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-07 23:59:59.999999'::timestamp without time zone))\n Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (((payable_type)::text = 'Invoice'::text) OR ((payable_type)::text = 'PaymentRequest'::text)) AND CASE payable_type WHEN 'Invoice'::text THEN (hashed SubPlan 2) ELSE true END)\n Heap Blocks: exact=1961\n Buffers: shared hit=6514\n -> Bitmap Index Scan on index_payments_by_cursor (cost=0.00..570.19 rows=11662 width=0) (actual time=0.575..0.576 rows=11669 loops=1)\n Index Cond: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-07 23:59:59.999999'::timestamp without time zone))\n Buffers: shared hit=107\n SubPlan 2\n -> Seq Scan on invoices invoices_1 (cost=0.00..6696.71 rows=120018 width=16) (actual time=0.079..20.335 rows=120000 loops=1)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[])))\n Rows Removed by Filter: 38\n Buffers: shared hit=4446\n -> Hash (cost=1.21..1.21 rows=21 width=25) (actual time=0.018..0.019 rows=21 loops=1)\n Buckets: 1024 Batches: 1 Memory Usage: 10kB\n Buffers: shared hit=1\n -> Seq Scan on payment_methods (cost=0.00..1.21 rows=21 width=25) (actual time=0.009..0.010 rows=21 loops=1)\n Buffers: shared hit=1\n -> Materialize (cost=1000.00..6384.28 rows=600 width=16) (actual time=0.000..0.003 rows=1 loops=6668)\n Buffers: shared hit=4446\n -> Gather (cost=1000.00..6381.28 rows=600 width=16) (actual time=0.194..19.616 rows=1 loops=1)\n Workers Planned: 2\n Workers Launched: 2\n Buffers: shared hit=4446\n -> Parallel Seq Scan on invoices (cost=0.00..5321.28 rows=250 width=16) (actual time=5.640..12.073 rows=0 loops=3)\n Filter: ((organization_id = '3878a544-698b-4548-95b4-7cf87274e028'::uuid) AND (lower((number)::text) = 'perf-000035'::text))\n Rows Removed by Filter: 40012\n Buffers: shared hit=4446\n SubPlan 3\n -> Index Scan using index_invoices_payment_requests_on_invoice_id on invoices_payment_requests (cost=0.29..8.31 rows=1 width=0) (actual time=0.000..0.000 rows=0 loops=834)\n Index Cond: (invoice_id = invoices.id)\n Filter: (payment_request_id = payments.payable_id)\n Rows Removed by Filter: 1\n Buffers: shared hit=2502\nPlanning:\n Buffers: shared hit=25\nPlanning Time: 0.681 ms\nJIT:\n Functions: 65\n Options: Inlining false, Optimization false, Expressions true, Deforming true\n Timing: Generation 2.965 ms, Inlining 0.000 ms, Optimization 1.698 ms, Emission 22.279 ms, Total 26.942 ms\nExecution Time: 83.864 ms" - } - } -} \ No newline at end of file diff --git a/script/benchmark_payments_filters.rb b/script/benchmark_payments_filters.rb deleted file mode 100644 index 6f619f37729..00000000000 --- a/script/benchmark_payments_filters.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -# Development only: seeds 100,000 payments in a separate organization, then -# records EXPLAIN ANALYZE for the combined filter and its pagination count. -# bundle exec rails runner script/benchmark_payments_filters.rb -raise "This benchmark is only for development" unless Rails.env.development? - -require "factory_bot_rails" -FactoryBot.find_definitions if FactoryBot.factories.none? -ActiveJob::Base.queue_adapter = :test - -organization = Organization.find_by(slug: "payments-filters-benchmark") -connection = ApplicationRecord.connection -unless organization - Organization.transaction do - organization = FactoryBot.create(:organization, slug: "payments-filters-benchmark", name: "Payments benchmark", webhook_url: nil) - customer = FactoryBot.create(:customer, organization:) - provider = FactoryBot.create(:stripe_provider, organization:) - provider_customer = FactoryBot.create(:stripe_customer, organization:, customer:, payment_provider: provider) - method = FactoryBot.create(:payment_method, organization:, customer:, payment_provider: provider, - payment_provider_customer: provider_customer, provider_method_type: "card") - values = {org: organization.id, customer: customer.id, billing_entity: organization.default_billing_entity.id, - provider: provider.id, provider_customer: provider_customer.id, method: method.id} - - connection.execute(ActiveRecord::Base.sanitize_sql_array([<<~SQL, values])) - CREATE TEMP TABLE payments_filter_rows ON COMMIT DROP AS - SELECT n, gen_random_uuid() AS invoice_id, gen_random_uuid() AS second_invoice_id, - CASE WHEN n % 5 = 0 THEN gen_random_uuid() END AS request_id, - timestamp '2026-08-01 12:00:00' + (n % 60) * interval '1 day' AS created_at - FROM generate_series(1, 100000) AS n; - - INSERT INTO invoices (id, organization_id, customer_id, billing_entity_id, number, status, - issuing_date, currency, total_amount_cents, created_at, updated_at) - SELECT invoice_id, :org, :customer, :billing_entity, 'PERF-' || lpad(n::text, 6, '0'), 1, - created_at::date, 'EUR', 10000, created_at, created_at FROM payments_filter_rows; - INSERT INTO invoices (id, organization_id, customer_id, billing_entity_id, number, status, - issuing_date, currency, total_amount_cents, created_at, updated_at) - SELECT second_invoice_id, :org, :customer, :billing_entity, 'PERF-' || lpad(n::text, 6, '0') || '-B', 1, - created_at::date, 'EUR', 10000, created_at, created_at FROM payments_filter_rows WHERE request_id IS NOT NULL; - INSERT INTO payment_requests (id, organization_id, customer_id, amount_cents, amount_currency, created_at, updated_at) - SELECT request_id, :org, :customer, 20000, 'EUR', created_at, created_at - FROM payments_filter_rows WHERE request_id IS NOT NULL; - INSERT INTO invoices_payment_requests (invoice_id, payment_request_id, organization_id, created_at, updated_at) - SELECT invoice_id, request_id, :org::uuid, created_at, created_at FROM payments_filter_rows WHERE request_id IS NOT NULL - UNION ALL - SELECT second_invoice_id, request_id, :org::uuid, created_at, created_at FROM payments_filter_rows WHERE request_id IS NOT NULL; - INSERT INTO payments (organization_id, customer_id, payable_id, payable_type, amount_cents, amount_currency, - status, payable_payment_status, payment_provider_id, payment_provider_customer_id, payment_method_id, - provider_payment_method_data, created_at, updated_at) - SELECT :org, :customer, COALESCE(request_id, invoice_id), - CASE WHEN request_id IS NULL THEN 'Invoice' ELSE 'PaymentRequest' END, - n * 100, CASE WHEN n % 3 = 0 THEN 'USD' ELSE 'EUR' END, 'succeeded', - (ARRAY['pending', 'processing', 'succeeded', 'failed'])[n % 4 + 1]::payment_payable_payment_status, - :provider, :provider_customer, :method, - CASE WHEN n % 3 = 0 THEN '{}'::jsonb - ELSE jsonb_build_object('type', (ARRAY['card', 'sepa_debit', 'us_bank_account', 'bacs_debit', - 'link', 'boleto', 'crypto', 'customer_balance'])[(n - 1) % 8 + 1]) END, - created_at, created_at FROM payments_filter_rows; - SQL - end -end - -%w[invoices invoices_payment_requests payment_requests payments payment_methods].each do |table| - connection.execute("ANALYZE #{table}") -end -filters = {invoice_number: "perf-000035", payment_method_type: %w[card sepa_debit us_bank_account], - created_at_from: Date.new(2026, 9, 1), created_at_to: Date.new(2026, 9, 7)} -payments = PaymentsQuery.call(organization:, filters:, pagination: {page: 1, limit: 20}).payments -count_sql = payments.except(:limit, :offset, :order).select("COUNT(*)").to_sql -plans = {list: payments.to_sql, count: count_sql}.to_h do |name, sql| - connection.execute("SET statement_timeout = '30s'") - plan = connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{sql}").values.flatten.join("\n") - [name, {sql:, plan:}] -ensure - connection.execute("RESET statement_timeout") -end -File.write(Rails.root.join("tmp/payments_filters_explain.json"), JSON.pretty_generate({rows: 100_000, filters:, plans:})) -Rails.logger.info "Benchmark complete. Plans: tmp/payments_filters_explain.json" diff --git a/spec/queries/payments_query_spec.rb b/spec/queries/payments_query_spec.rb index 4be1fcd0802..6762d8597eb 100644 --- a/spec/queries/payments_query_spec.rb +++ b/spec/queries/payments_query_spec.rb @@ -182,7 +182,7 @@ context "when filtering by external_customer_id" do let(:filters) { {external_customer_id: customer.external_id} } - let(:customer) { create(:customer) } + let(:customer) { create(:customer, organization:) } let(:new_invoice) { create(:invoice, organization:, customer:) } let(:new_payment) { create(:payment, payable: new_invoice) } @@ -374,6 +374,7 @@ before do payment_one.update!(payment_provider: create(:gocardless_provider, organization:)) payment_two.update!(payment_provider: nil) + payment_three.update!(payment_provider: create(:stripe_provider, organization:)) end it "maps API names to provider STI types" do @@ -483,6 +484,31 @@ end end + context "with every filter set" do + let(:filters) do + { + external_customer_id: payment_one.customer.external_id, currency: "EUR", payment_status: %w[failed pending], + amount_from: 100, amount_to: 10_000, receipt_number: "RCPT-1", created_at_from: "2026-01-01", created_at_to: "2026-01-31", + payment_provider_type: %w[stripe], payment_method_type: %w[card], invoice_number: "INV-1", + payment_type: %w[provider], payable_type: %w[Invoice] + } + end + let(:search_term) { "term" } + + # Tripwire for the shapes that defeat the payments indexes on large organizations. + # Plan shapes are not asserted (too brittle on a tiny dataset); the SQL text is. + it "keeps the generated SQL indexable" do + sql = result.payments.to_sql + + expect(sql).not_to include("DISTINCT") + # A function around an indexed payments column disables index_payments_by_cursor and any amount index. + expect(sql).not_to match(/\w+\(\s*"?payments"?\."?(created_at|amount_cents)"?\s*\)/i) + # Receipts and invoices are resolved through organization-scoped sub-selects, never joined. + expect(sql).not_to match(/JOIN\s+"?(payment_receipts|invoices|payment_methods|customers)"?/i) + expect(sql.scan("lower(").count).to eq(sql.scan(/lower\((payment_receipts|invoices)\.number\)/).count * 2) + end + end + context "with composed filters" do let(:filters) { {payment_status: ["succeeded"], amount_from: 200, currency: "USD"} } From a9d88f068041fc23e6507abadb57ca62250ecf80 Mon Sep 17 00:00:00 2001 From: Raffi Date: Tue, 8 Sep 2026 20:55:24 -0700 Subject: [PATCH 5/9] feat(payments): drop the payment_method_type list filter Removed for performance reasons: the count of a rare method type walks the whole organization and cannot be served by an index while the saved-method fallback branch exists. The way back is a denormalised payments column with its own partial index. Removed from the query, contract, REST whitelist, GraphQL resolver and enum, regenerated schema, specs and the QA script. The superseded 100k-row benchmark script and its EXPLAIN dump are replaced by script/perf/payments_filters/. --- .../payments_query_filters_contract.rb | 4 - app/controllers/concerns/payment_index.rb | 5 +- app/graphql/resolvers/payments_resolver.rb | 1 - .../payments/payment_method_type_enum.rb | 14 ---- app/queries/payments_query.rb | 14 ---- qa/payments-filters/README.md | 5 +- qa/payments-filters/api-qa.json | 41 +--------- schema.graphql | 13 +-- schema.json | 79 ------------------- script/qa_payments_filters.py | 13 +-- .../payments_query_filters_contract_spec.rb | 1 - .../resolvers/payments_resolver_spec.rb | 7 +- spec/queries/payments_query_spec.rb | 35 +------- spec/support/shared_examples/payment_index.rb | 4 +- 14 files changed, 21 insertions(+), 215 deletions(-) delete mode 100644 app/graphql/types/payments/payment_method_type_enum.rb diff --git a/app/contracts/queries/payments_query_filters_contract.rb b/app/contracts/queries/payments_query_filters_contract.rb index 1884ac1d028..2e8e0a52d8f 100644 --- a/app/contracts/queries/payments_query_filters_contract.rb +++ b/app/contracts/queries/payments_query_filters_contract.rb @@ -19,10 +19,6 @@ class PaymentsQueryFiltersContract < Dry::Validation::Contract value(:string, included_in?: Customer::PAYMENT_PROVIDERS) | array(:string, included_in?: Customer::PAYMENT_PROVIDERS) end - optional(:payment_method_type).maybe do - value(:string, included_in?: PaymentMethod::PROVIDER_METHOD_TYPES) | - array(:string, included_in?: PaymentMethod::PROVIDER_METHOD_TYPES) - end optional(:payment_type).maybe do value(:string, included_in?: Payment::PAYMENT_TYPES.keys.map(&:to_s)) | array(:string, included_in?: Payment::PAYMENT_TYPES.keys.map(&:to_s)) diff --git a/app/controllers/concerns/payment_index.rb b/app/controllers/concerns/payment_index.rb index 24ab09628dc..86167e3bb0e 100644 --- a/app/controllers/concerns/payment_index.rb +++ b/app/controllers/concerns/payment_index.rb @@ -7,8 +7,8 @@ module PaymentIndex WHITELIST = [ :page, :per_page, :invoice_id, :external_customer_id, :currency, :search_term, :amount_from, :amount_to, :receipt_number, :invoice_number, :created_at_from, :created_at_to, - :payment_status, :payment_statuses, :payment_provider_type, :payment_method_type, :payment_type, :payable_type, - {payment_status: [], payment_statuses: [], payment_provider_type: [], payment_method_type: [], payment_type: [], payable_type: []} + :payment_status, :payment_statuses, :payment_provider_type, :payment_type, :payable_type, + {payment_status: [], payment_statuses: [], payment_provider_type: [], payment_type: [], payable_type: []} ].freeze def payment_index(customer_external_id: nil) @@ -31,7 +31,6 @@ def payment_index(customer_external_id: nil) created_at_to: (Date.iso8601(params[:created_at_to]) if valid_date?(params[:created_at_to])), payment_status: params[:payment_status] || params[:payment_statuses], payment_provider_type: params[:payment_provider_type], - payment_method_type: params[:payment_method_type], payment_type: params[:payment_type], payable_type: params[:payable_type] } diff --git a/app/graphql/resolvers/payments_resolver.rb b/app/graphql/resolvers/payments_resolver.rb index fb6f1ee9cdb..51c1c2690fc 100644 --- a/app/graphql/resolvers/payments_resolver.rb +++ b/app/graphql/resolvers/payments_resolver.rb @@ -20,7 +20,6 @@ class PaymentsResolver < Resolvers::BaseResolver argument :limit, Integer, required: false argument :page, Integer, required: false argument :payable_type, [Types::Payments::PayableTypeEnum], required: false - argument :payment_method_type, [Types::Payments::PaymentMethodTypeEnum], required: false argument :payment_provider_type, [Types::PaymentProviders::ProviderTypeEnum], required: false argument :payment_status, [Types::Payments::PayablePaymentStatusEnum], required: false argument :payment_type, [Types::Payments::PaymentTypeEnum], required: false diff --git a/app/graphql/types/payments/payment_method_type_enum.rb b/app/graphql/types/payments/payment_method_type_enum.rb deleted file mode 100644 index 107cb4db404..00000000000 --- a/app/graphql/types/payments/payment_method_type_enum.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module Types - module Payments - class PaymentMethodTypeEnum < Types::BaseEnum - # PaymentMethodTypeEnum already represents manual/provider payment methods. - graphql_name "PaymentProviderMethodTypeEnum" - - PaymentMethod::PROVIDER_METHOD_TYPES.each do |type| - value type - end - end - end -end diff --git a/app/queries/payments_query.rb b/app/queries/payments_query.rb index 2531ba1e934..94f2a085bc6 100644 --- a/app/queries/payments_query.rb +++ b/app/queries/payments_query.rb @@ -13,7 +13,6 @@ class PaymentsQuery < BaseQuery :created_at_from, :created_at_to, :payment_provider_type, - :payment_method_type, :invoice_number, :payment_type, :payable_type @@ -116,7 +115,6 @@ def apply_filters(scope) scope = with_receipt_number(scope) if filters.receipt_number.present? scope = with_created_at_range(scope) if filters.created_at_from.present? || filters.created_at_to.present? scope = with_payment_provider_type(scope) if filters.payment_provider_type.present? - scope = with_payment_method_type(scope) if filters.payment_method_type.present? scope = with_invoice_number(scope) if filters.invoice_number.present? scope = with_payment_type(scope) if filters.payment_type.present? scope = with_payable_type(scope) if filters.payable_type.present? @@ -185,18 +183,6 @@ def with_payment_provider_type(scope) scope.where(payment_provider_id: provider_ids) end - def with_payment_method_type(scope) - types = Array(filters.payment_method_type) - # The jsonb type wins when present; otherwise fall back to the saved (non-deleted) payment - # method. Two plain predicates instead of a COALESCE across a join. - fallback_ids = PaymentMethod.where(organization_id: organization.id, provider_method_type: types).select(:id) - scope.where( - "payments.provider_payment_method_data->>'type' IN (:types) " \ - "OR (NULLIF(payments.provider_payment_method_data->>'type', '') IS NULL AND payments.payment_method_id IN (:fallback_ids))", - types:, fallback_ids: - ) - end - def with_invoice_number(scope) # Resolve the invoice ids first (organization-scoped, case-insensitive), then reach the # payments through index_payments_on_payable_type_and_payable_id on both payable paths. diff --git a/qa/payments-filters/README.md b/qa/payments-filters/README.md index 664207dd833..6229c88221d 100644 --- a/qa/payments-filters/README.md +++ b/qa/payments-filters/README.md @@ -2,8 +2,8 @@ All records used here are synthetic and live in an isolated development organization. -- `api-qa.json`: 79 live HTTP checks across REST, customer-scoped REST and GraphQL. Each successful call asserts the filtered count and every returned ID against the seed manifest. Error cases assert status and validation details. -- `explain.json`: complete SQL and EXPLAIN ANALYZE/BUFFERS plans for invoice number + payment method + created date bounds, using 100,000 payments, 120,000 invoices and 20,000 payment requests in a separate organization. List execution: 232.222 ms; count: 83.864 ms. PostgreSQL uses the existing payments cursor index; no expression index was added. Invoice visibility and lower-number scans remain in the plans. +- `api-qa.json`: 75 live HTTP checks across REST, customer-scoped REST and GraphQL. Each successful call asserts the filtered count and every returned ID against the seed manifest. Error cases assert status and validation details. +- Query plans and load tests live in `script/perf/payments_filters/` (synthetic 5M-payment dataset, reproducible); see its README. - `cross-client-qa.json`: UI, REST, GraphQL, all six SDKs and CLI return the same seven succeeded EUR payments. Customer-scoped SDK calls return two; minimum amount 9223372036854775807 returns two. CLI additionally verifies arrays and saved payment method fallback. Replay with the API development container running: @@ -11,7 +11,6 @@ Replay with the API development container running: ```sh lago exec api bundle exec rails runner script/seed_payments_filters.rb python3 script/qa_payments_filters.py -lago exec api bundle exec rails runner script/benchmark_payments_filters.rb ``` The seed is development-only and repeatable. It creates 30 payments (29 visible), three customers, stubbed Stripe/GoCardless providers, every status/type/method, receipts, both payable paths, multiple currencies, exact int64 bounds, and organization-timezone boundary timestamps. Local credentials are written only under ignored `tmp/` with mode 0600; do not commit them. The QA script reads those credentials without printing them. Generated IDs change in a new database, so the QA script uses the freshly generated manifest. diff --git a/qa/payments-filters/api-qa.json b/qa/payments-filters/api-qa.json index 2830eb5fd00..b66c8db421a 100644 --- a/qa/payments-filters/api-qa.json +++ b/qa/payments-filters/api-qa.json @@ -283,30 +283,6 @@ "count": 9, "ids_match": true }, - { - "request": "GET /api/v1/payments?payment_method_type%5B%5D=card&payment_method_type%5B%5D=sepa_debit", - "status": 200, - "count": 5, - "ids_match": true - }, - { - "request": "GET /api/v1/customers/cust_1/payments?payment_method_type%5B%5D=card&payment_method_type%5B%5D=sepa_debit", - "status": 200, - "count": 0, - "ids_match": true - }, - { - "request": "GraphQL payments", - "variables": { - "paymentMethodType": [ - "card", - "sepa_debit" - ] - }, - "status": 200, - "count": 5, - "ids_match": true - }, { "request": "GET /api/v1/payments?currency=EUR", "status": 200, @@ -444,13 +420,13 @@ "ids_match": true }, { - "request": "GET /api/v1/payments?payment_status%5B%5D=succeeded&payment_method_type%5B%5D=card&payment_method_type%5B%5D=us_bank_account¤cy=EUR&search_term=pi_3", + "request": "GET /api/v1/payments?payment_status%5B%5D=succeeded¤cy=EUR&search_term=pi_3", "status": 200, - "count": 3, + "count": 5, "ids_match": true }, { - "request": "GET /api/v1/customers/cust_1/payments?payment_status%5B%5D=succeeded&payment_method_type%5B%5D=card&payment_method_type%5B%5D=us_bank_account¤cy=EUR&search_term=pi_3", + "request": "GET /api/v1/customers/cust_1/payments?payment_status%5B%5D=succeeded¤cy=EUR&search_term=pi_3", "status": 200, "count": 0, "ids_match": true @@ -462,14 +438,10 @@ "succeeded" ], "currency": "EUR", - "paymentMethodType": [ - "card", - "us_bank_account" - ], "searchTerm": "pi_3" }, "status": 200, - "count": 3, + "count": 5, "ids_match": true }, { @@ -488,11 +460,6 @@ "status": 422, "validation_error": true }, - { - "request": "GET /api/v1/payments?payment_method_type=bogus", - "status": 422, - "validation_error": true - }, { "request": "GET /api/v1/payments?payment_type=bogus", "status": 422, diff --git a/schema.graphql b/schema.graphql index 7915c6a8721..ace794b5b75 100644 --- a/schema.graphql +++ b/schema.graphql @@ -10628,17 +10628,6 @@ input PaymentProviderCustomerInput { syncWithProvider: Boolean } -enum PaymentProviderMethodTypeEnum { - bacs_debit - boleto - card - crypto - customer_balance - link - sepa_debit - us_bank_account -} - """ PaymentReceipt """ @@ -12218,7 +12207,7 @@ type Query { """ Query payments of an organization """ - payments(amountFrom: BigInt, amountTo: BigInt, createdAtFrom: ISO8601Date, createdAtTo: ISO8601Date, currency: CurrencyEnum, externalCustomerId: ID, invoiceId: ID, invoiceNumber: String, limit: Int, page: Int, payableType: [PayableTypeEnum!], paymentMethodType: [PaymentProviderMethodTypeEnum!], paymentProviderType: [ProviderTypeEnum!], paymentStatus: [PayablePaymentStatusEnum!], paymentType: [PaymentTypeEnum!], receiptNumber: String, searchTerm: String): PaymentCollection! + payments(amountFrom: BigInt, amountTo: BigInt, createdAtFrom: ISO8601Date, createdAtTo: ISO8601Date, currency: CurrencyEnum, externalCustomerId: ID, invoiceId: ID, invoiceNumber: String, limit: Int, page: Int, payableType: [PayableTypeEnum!], paymentProviderType: [ProviderTypeEnum!], paymentStatus: [PayablePaymentStatusEnum!], paymentType: [PaymentTypeEnum!], receiptNumber: String, searchTerm: String): PaymentCollection! """ Query a single plan of an organization diff --git a/schema.json b/schema.json index 4b33d437a9d..566e1f9f218 100644 --- a/schema.json +++ b/schema.json @@ -51525,65 +51525,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "ENUM", - "name": "PaymentProviderMethodTypeEnum", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "card", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sepa_debit", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "us_bank_account", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "bacs_debit", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "link", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "boleto", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "crypto", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "customer_balance", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, { "kind": "OBJECT", "name": "PaymentReceipt", @@ -66063,26 +66004,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "paymentMethodType", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "PaymentProviderMethodTypeEnum", - "ofType": null - } - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "paymentProviderType", "description": null, diff --git a/script/qa_payments_filters.py b/script/qa_payments_filters.py index 5922663bd73..2dc6214e8ac 100644 --- a/script/qa_payments_filters.py +++ b/script/qa_payments_filters.py @@ -1,11 +1,12 @@ """Verify REST and GraphQL against seed_payments_filters.rb's local manifest. Run on the host with Python 3.9+: python3 script/qa_payments_filters.py -The dev API must listen on 127.0.0.1:3000. No credentials enter the report. +The dev API must listen on 127.0.0.1:3000 (override with LAGO_API_URL). No credentials enter the report. """ import datetime as dt import json +import os from pathlib import Path import urllib.error import urllib.parse @@ -14,7 +15,7 @@ ROOT = Path(__file__).resolve().parents[1] -BASE = "http://127.0.0.1:3000" +BASE = os.environ.get("LAGO_API_URL", "http://127.0.0.1:3000") CREDENTIALS = json.loads((ROOT / "tmp/payments_filters_credentials.json").read_text()) MANIFEST = json.loads((ROOT / "tmp/payments_filters_manifest.json").read_text()) VISIBLE = [p for p in MANIFEST["payments"] if p["visible"]] @@ -24,7 +25,7 @@ "payment_status": "[PayablePaymentStatusEnum!]", "amount_from": "BigInt", "amount_to": "BigInt", "receipt_number": "String", "created_at_from": "ISO8601Date", "created_at_to": "ISO8601Date", - "payment_provider_type": "[ProviderTypeEnum!]", "payment_method_type": "[PaymentProviderMethodTypeEnum!]", + "payment_provider_type": "[ProviderTypeEnum!]", "currency": "CurrencyEnum", "invoice_number": "String", "external_customer_id": "ID", "invoice_id": "ID", "payment_type": "[PaymentTypeEnum!]", "payable_type": "[PayableTypeEnum!]", "search_term": "String", @@ -137,11 +138,11 @@ def graphql(params, token): {"receipt_number": "rcpt-2026-0001"}, {"receipt_number": "missing"}, {"created_at_from": "2026-09-01", "created_at_to": "2026-09-07"}, {"payment_provider_type": ["stripe"]}, {"payment_provider_type": ["gocardless"]}, - {"payment_method_type": ["card", "sepa_debit"]}, {"currency": "EUR"}, + {"currency": "EUR"}, {"invoice_number": "lag-1234-001-002"}, {"external_customer_id": "cust_1"}, {"payment_type": "manual", "payable_type": "PaymentRequest"}, {"search_term": "pi_3"}, {"payment_status": "succeeded", "currency": "EUR", "amount_from": "100", "created_at_from": "2026-09-01"}, - {"payment_status": ["succeeded"], "currency": "EUR", "payment_method_type": ["card", "us_bank_account"], "search_term": "pi_3"}, + {"payment_status": ["succeeded"], "currency": "EUR", "search_term": "pi_3"}, ] for case in CASES: rest(case) @@ -150,7 +151,7 @@ def graphql(params, token): rest({"created_at_from": "invalid", "created_at_to": "2026-02-30"}) for params in [ - {"payment_status": "bogus"}, {"payment_provider_type": "bogus"}, {"payment_method_type": "bogus"}, + {"payment_status": "bogus"}, {"payment_provider_type": "bogus"}, {"payment_type": "bogus"}, {"payable_type": "bogus"}, {"currency": "XYZ"}, {"amount_from": "-1"}, {"amount_to": "-1"}, {"amount_from": "500", "amount_to": "100"}, {"amount_from": "9223372036854775808"}, {"invoice_id": "invalid"}, diff --git a/spec/contracts/queries/payments_query_filters_contract_spec.rb b/spec/contracts/queries/payments_query_filters_contract_spec.rb index ee7c3e12754..0d881451503 100644 --- a/spec/contracts/queries/payments_query_filters_contract_spec.rb +++ b/spec/contracts/queries/payments_query_filters_contract_spec.rb @@ -88,7 +88,6 @@ { payment_status: Payment::PAYABLE_PAYMENT_STATUS, payment_provider_type: Customer::PAYMENT_PROVIDERS, - payment_method_type: PaymentMethod::PROVIDER_METHOD_TYPES, payment_type: Payment::PAYMENT_TYPES.keys.map(&:to_s), payable_type: Payment::PAYABLE_TYPES }.each do |field, values| diff --git a/spec/graphql/resolvers/payments_resolver_spec.rb b/spec/graphql/resolvers/payments_resolver_spec.rb index ac0a43089c4..98b3c205afc 100644 --- a/spec/graphql/resolvers/payments_resolver_spec.rb +++ b/spec/graphql/resolvers/payments_resolver_spec.rb @@ -144,12 +144,12 @@ <<~GQL query($paymentStatus: [PayablePaymentStatusEnum!], $amountFrom: BigInt, $amountTo: BigInt, $receiptNumber: String, $createdAtFrom: ISO8601Date, $createdAtTo: ISO8601Date, - $paymentProviderType: [ProviderTypeEnum!], $paymentMethodType: [PaymentProviderMethodTypeEnum!], + $paymentProviderType: [ProviderTypeEnum!], $invoiceNumber: String, $paymentType: [PaymentTypeEnum!], $payableType: [PayableTypeEnum!], $searchTerm: String, $currency: CurrencyEnum, $invoiceId: ID, $page: Int) { payments(paymentStatus: $paymentStatus, amountFrom: $amountFrom, amountTo: $amountTo, receiptNumber: $receiptNumber, createdAtFrom: $createdAtFrom, createdAtTo: $createdAtTo, - paymentProviderType: $paymentProviderType, paymentMethodType: $paymentMethodType, + paymentProviderType: $paymentProviderType, invoiceNumber: $invoiceNumber, paymentType: $paymentType, payableType: $payableType, searchTerm: $searchTerm, currency: $currency, invoiceId: $invoiceId, page: $page, limit: 1) { collection { id amountCents } @@ -178,7 +178,6 @@ {createdAtFrom: "2026-09-01", createdAtTo: "2026-09-07"}, {createdAtTo: "2026-09-04"}, {paymentProviderType: ["gocardless"]}, - {paymentMethodType: ["sepa_debit"]}, {invoiceNumber: "filter-invoice"}, {paymentType: ["manual"]}, {searchTerm: "Filter transfer"}, @@ -211,7 +210,7 @@ [ {paymentStatus: ["unknown"]}, {paymentProviderType: ["unknown"]}, - {paymentMethodType: ["unknown"]}, {paymentType: ["unknown"]}, {payableType: ["unknown"]}, + {paymentType: ["unknown"]}, {payableType: ["unknown"]}, {amountFrom: "-1"}, {amountTo: "-1"}, {amountFrom: "500", amountTo: "100"}, {amountFrom: "9223372036854775808"}, {receiptNumber: "x" * 256}, {invoiceNumber: "x" * 256}, {invoiceId: "invalid"}, {createdAtFrom: "2026-02-30"} diff --git a/spec/queries/payments_query_spec.rb b/spec/queries/payments_query_spec.rb index 6762d8597eb..e36786c4270 100644 --- a/spec/queries/payments_query_spec.rb +++ b/spec/queries/payments_query_spec.rb @@ -390,39 +390,6 @@ end end - context "with payment method type" do - let(:filters) { {payment_method_type: %w[card sepa_debit]} } - let(:method) { create(:payment_method, organization:, provider_method_type: "sepa_debit") } - - before do - payment_one.update!(provider_payment_method_data: {type: "card"}) - payment_two.update!(provider_payment_method_data: {}, payment_method: method) - payment_three.update!(provider_payment_method_data: {type: "link"}, payment_method: method) - end - - it "uses JSON first and falls back to the associated method" do - expect(returned_ids).to match_array([payment_one.id, payment_two.id]) - end - - [nil, ""].each do |empty_type| - context "when JSON type is #{empty_type.inspect}" do - before { payment_two.update!(provider_payment_method_data: {type: empty_type}) } - - it "falls back for an empty JSON type" do - expect(returned_ids).to match_array([payment_one.id, payment_two.id]) - end - end - end - - context "when neither source supplies a method" do - before { payment_two.update!(payment_method: nil) } - - it "does not match" do - expect(returned_ids).to eq([payment_one.id]) - end - end - end - context "with invoice number" do let(:filters) { {invoice_number: "lag-1234-001-002"} } @@ -489,7 +456,7 @@ { external_customer_id: payment_one.customer.external_id, currency: "EUR", payment_status: %w[failed pending], amount_from: 100, amount_to: 10_000, receipt_number: "RCPT-1", created_at_from: "2026-01-01", created_at_to: "2026-01-31", - payment_provider_type: %w[stripe], payment_method_type: %w[card], invoice_number: "INV-1", + payment_provider_type: %w[stripe], invoice_number: "INV-1", payment_type: %w[provider], payable_type: %w[Invoice] } end diff --git a/spec/support/shared_examples/payment_index.rb b/spec/support/shared_examples/payment_index.rb index 19ad4e1350e..d4409fb72e4 100644 --- a/spec/support/shared_examples/payment_index.rb +++ b/spec/support/shared_examples/payment_index.rb @@ -78,8 +78,6 @@ {created_at_from: "2026-09-01", created_at_to: "2026-09-07"}, {payment_provider_type: "stripe"}, {payment_provider_type: ["stripe"]}, - {payment_method_type: "card"}, - {payment_method_type: %w[card sepa_debit]}, {currency: "USD"}, {invoice_number: "qa-invoice"}, {payment_type: "manual"}, @@ -152,7 +150,7 @@ [ {payment_status: "bogus"}, {payment_statuses: ["bogus"]}, - {payment_provider_type: ["bogus"]}, {payment_method_type: ["bogus"]}, + {payment_provider_type: ["bogus"]}, {payment_type: "bogus"}, {payable_type: "bogus"}, {currency: "XYZ"}, {amount_from: "-1"}, {amount_to: "-1"}, {amount_from: "1.5"}, {amount_from: "9223372036854775808"}, {amount_from: "500", amount_to: "100"}, From 1e5eaccb2db73a9e3e4dee039de0fb57d21b5655 Mon Sep 17 00:00:00 2001 From: Raffi Date: Tue, 8 Sep 2026 21:06:10 -0700 Subject: [PATCH 6/9] perf(payments): add reproducible perf scripts for the list filters script/perf/payments_filters/: synthetic dataset generator (one 5M-payment organization plus 50 smaller ones, illustrative skews), case matrix, plan capture through PaymentsQuery (list and COUNT(*), EXPLAIN ANALYZE BUFFERS, median of 3), load test (HTTP and DB-only), before/after scoreboard, and the read-only replica queries for the production-scale inputs. Plans captured on the synthetic dataset before and after the rewrites and indexes are committed with identifiers redacted; the internal performance document quotes them. --- script/perf/payments_filters/.rubocop.yml | 10 + script/perf/payments_filters/README.md | 57 + script/perf/payments_filters/bench.rb | 131 ++ .../payments_filters/bench/baseline.http.json | 630 +++++ .../payments_filters/bench/baseline.http.md | 47 + .../bench/baseline_1client.http.json | 600 +++++ .../bench/baseline_1client.http.md | 47 + script/perf/payments_filters/cases.rb | 169 ++ script/perf/payments_filters/compare.rb | 99 + .../compare/baseline_vs_after.md | 57 + script/perf/payments_filters/explain.rb | 136 ++ script/perf/payments_filters/generate.rb | 334 +++ script/perf/payments_filters/index_probe.rb | 45 + script/perf/payments_filters/phase0.sql | 94 + script/perf/payments_filters/plan_stats.rb | 65 + .../after/amount_common_from_p50.count.txt | 29 + .../plans/after/amount_common_from_p50.txt | 21 + .../after/amount_rare_from_p99.count.txt | 29 + .../plans/after/amount_rare_from_p99.txt | 21 + .../plans/after/amount_rare_range.count.txt | 26 + .../plans/after/amount_rare_range.txt | 25 + .../combo_customer_status_date.count.txt | 35 + .../after/combo_customer_status_date.txt | 22 + .../after/combo_provider_status.count.txt | 41 + .../plans/after/combo_provider_status.txt | 21 + .../plans/after/combo_status_amount.count.txt | 29 + .../plans/after/combo_status_amount.txt | 21 + .../combo_status_currency_date.count.txt | 22 + .../after/combo_status_currency_date.txt | 22 + .../plans/after/control.count.txt | 29 + .../payments_filters/plans/after/control.txt | 22 + .../plans/after/control_page50.count.txt | 30 + .../plans/after/control_page50.txt | 22 + .../plans/after/created_24m.count.txt | 30 + .../plans/after/created_24m.txt | 22 + .../plans/after/created_7d.count.txt | 22 + .../plans/after/created_7d.txt | 22 + .../plans/after/currency_common.count.txt | 29 + .../plans/after/currency_common.txt | 22 + .../plans/after/currency_rare.count.txt | 29 + .../plans/after/currency_rare.txt | 21 + .../plans/after/customer_heavy.count.txt | 35 + .../plans/after/customer_heavy.txt | 22 + .../plans/after/customer_light.count.txt | 22 + .../plans/after/customer_light.txt | 26 + .../plans/after/five_filter_common.count.txt | 42 + .../plans/after/five_filter_common.txt | 22 + .../plans/after/five_filter_rare.count.txt | 29 + .../plans/after/five_filter_rare.txt | 33 + .../plans/after/invoice_hit_direct.count.txt | 20 + .../plans/after/invoice_hit_direct.txt | 24 + .../plans/after/invoice_hit_request.count.txt | 28 + .../plans/after/invoice_hit_request.txt | 32 + .../plans/after/invoice_miss.count.txt | 7 + .../plans/after/invoice_miss.txt | 13 + .../after/payable_type_invoice.count.txt | 42 + .../plans/after/payable_type_invoice.txt | 22 + .../after/payable_type_request.count.txt | 32 + .../plans/after/payable_type_request.txt | 20 + .../plans/after/payment_type_manual.count.txt | 41 + .../plans/after/payment_type_manual.txt | 22 + .../after/payment_type_provider.count.txt | 42 + .../plans/after/payment_type_provider.txt | 22 + .../plans/after/provider_common.count.txt | 41 + .../plans/after/provider_common.txt | 22 + .../plans/after/provider_miss.count.txt | 7 + .../plans/after/provider_miss.txt | 13 + .../plans/after/provider_rare.count.txt | 41 + .../plans/after/provider_rare.txt | 21 + .../plans/after/receipt_hit.count.txt | 25 + .../plans/after/receipt_hit.txt | 29 + .../plans/after/receipt_miss.count.txt | 23 + .../plans/after/receipt_miss.txt | 27 + .../plans/after/search_term.count.txt | 93 + .../plans/after/search_term.txt | 97 + .../plans/after/search_term_status.count.txt | 93 + .../plans/after/search_term_status.txt | 97 + .../after/status_common_succeeded.count.txt | 30 + .../plans/after/status_common_succeeded.txt | 22 + .../status_common_succeeded_page50.count.txt | 30 + .../after/status_common_succeeded_page50.txt | 22 + .../plans/after/status_rare_failed.count.txt | 30 + .../plans/after/status_rare_failed.txt | 21 + .../plans/after/status_rare_pending.count.txt | 26 + .../plans/after/status_rare_pending.txt | 20 + .../status_rare_pending_processing.count.txt | 26 + .../after/status_rare_pending_processing.txt | 22 + .../after/status_rare_processing.count.txt | 26 + .../plans/after/status_rare_processing.txt | 22 + .../payments_filters/plans/after/summary.json | 2028 ++++++++++++++++ .../payments_filters/plans/after/summary.md | 43 + .../baseline/amount_common_from_p50.count.txt | 29 + .../plans/baseline/amount_common_from_p50.txt | 20 + .../baseline/amount_rare_from_p99.count.txt | 25 + .../plans/baseline/amount_rare_from_p99.txt | 20 + .../baseline/amount_rare_range.count.txt | 25 + .../plans/baseline/amount_rare_range.txt | 24 + .../combo_customer_status_date.count.txt | 26 + .../baseline/combo_customer_status_date.txt | 30 + .../baseline/combo_provider_status.count.txt | 47 + .../plans/baseline/combo_provider_status.txt | 26 + .../baseline/combo_status_amount.count.txt | 28 + .../plans/baseline/combo_status_amount.txt | 20 + .../combo_status_currency_date.count.txt | 21 + .../baseline/combo_status_currency_date.txt | 21 + .../plans/baseline/control.count.txt | 28 + .../plans/baseline/control.txt | 21 + .../plans/baseline/control_page50.count.txt | 28 + .../plans/baseline/control_page50.txt | 21 + .../plans/baseline/created_24m.count.txt | 29 + .../plans/baseline/created_24m.txt | 21 + .../plans/baseline/created_7d.count.txt | 21 + .../plans/baseline/created_7d.txt | 21 + .../plans/baseline/currency_common.count.txt | 29 + .../plans/baseline/currency_common.txt | 21 + .../plans/baseline/currency_rare.count.txt | 25 + .../plans/baseline/currency_rare.txt | 20 + .../plans/baseline/customer_heavy.count.txt | 26 + .../plans/baseline/customer_heavy.txt | 30 + .../plans/baseline/customer_light.count.txt | 26 + .../plans/baseline/customer_light.txt | 30 + .../baseline/five_filter_common.count.txt | 48 + .../plans/baseline/five_filter_common.txt | 33 + .../plans/baseline/five_filter_rare.count.txt | 34 + .../plans/baseline/five_filter_rare.txt | 38 + .../baseline/invoice_hit_direct.count.txt | 51 + .../plans/baseline/invoice_hit_direct.txt | 51 + .../baseline/invoice_hit_request.count.txt | 51 + .../plans/baseline/invoice_hit_request.txt | 51 + .../plans/baseline/invoice_miss.count.txt | 48 + .../plans/baseline/invoice_miss.txt | 48 + .../baseline/method_common_json.count.txt | 41 + .../plans/baseline/method_common_json.txt | 28 + .../baseline/method_fallback_only.count.txt | 41 + .../plans/baseline/method_fallback_only.txt | 28 + .../plans/baseline/method_multi.count.txt | 41 + .../plans/baseline/method_multi.txt | 28 + .../plans/baseline/method_rare_json.count.txt | 42 + .../plans/baseline/method_rare_json.txt | 28 + .../baseline/payable_type_invoice.count.txt | 40 + .../plans/baseline/payable_type_invoice.txt | 21 + .../baseline/payable_type_request.count.txt | 31 + .../plans/baseline/payable_type_request.txt | 19 + .../baseline/payment_type_manual.count.txt | 40 + .../plans/baseline/payment_type_manual.txt | 21 + .../baseline/payment_type_provider.count.txt | 40 + .../plans/baseline/payment_type_provider.txt | 21 + .../plans/baseline/provider_common.count.txt | 49 + .../plans/baseline/provider_common.txt | 33 + .../plans/baseline/provider_miss.count.txt | 32 + .../plans/baseline/provider_miss.txt | 37 + .../plans/baseline/provider_rare.count.txt | 47 + .../plans/baseline/provider_rare.txt | 33 + .../plans/baseline/receipt_hit.count.txt | 33 + .../plans/baseline/receipt_hit.txt | 34 + .../plans/baseline/receipt_miss.count.txt | 31 + .../plans/baseline/receipt_miss.txt | 5 + .../plans/baseline/search_term.count.txt | 92 + .../plans/baseline/search_term.txt | 96 + .../baseline/search_term_status.count.txt | 92 + .../plans/baseline/search_term_status.txt | 96 + .../status_common_succeeded.count.txt | 29 + .../baseline/status_common_succeeded.txt | 21 + .../status_common_succeeded_page50.count.txt | 29 + .../status_common_succeeded_page50.txt | 21 + .../baseline/status_rare_failed.count.txt | 28 + .../plans/baseline/status_rare_failed.txt | 20 + .../baseline/status_rare_pending.count.txt | 28 + .../plans/baseline/status_rare_pending.txt | 20 + .../status_rare_pending_processing.count.txt | 28 + .../status_rare_pending_processing.txt | 21 + .../baseline/status_rare_processing.count.txt | 25 + .../plans/baseline/status_rare_processing.txt | 21 + .../plans/baseline/summary.json | 2032 +++++++++++++++++ .../plans/baseline/summary.md | 47 + script/perf/payments_filters/summarize.rb | 39 + 176 files changed, 11621 insertions(+) create mode 100644 script/perf/payments_filters/.rubocop.yml create mode 100644 script/perf/payments_filters/README.md create mode 100644 script/perf/payments_filters/bench.rb create mode 100644 script/perf/payments_filters/bench/baseline.http.json create mode 100644 script/perf/payments_filters/bench/baseline.http.md create mode 100644 script/perf/payments_filters/bench/baseline_1client.http.json create mode 100644 script/perf/payments_filters/bench/baseline_1client.http.md create mode 100644 script/perf/payments_filters/cases.rb create mode 100644 script/perf/payments_filters/compare.rb create mode 100644 script/perf/payments_filters/compare/baseline_vs_after.md create mode 100644 script/perf/payments_filters/explain.rb create mode 100644 script/perf/payments_filters/generate.rb create mode 100644 script/perf/payments_filters/index_probe.rb create mode 100644 script/perf/payments_filters/phase0.sql create mode 100644 script/perf/payments_filters/plan_stats.rb create mode 100644 script/perf/payments_filters/plans/after/amount_common_from_p50.count.txt create mode 100644 script/perf/payments_filters/plans/after/amount_common_from_p50.txt create mode 100644 script/perf/payments_filters/plans/after/amount_rare_from_p99.count.txt create mode 100644 script/perf/payments_filters/plans/after/amount_rare_from_p99.txt create mode 100644 script/perf/payments_filters/plans/after/amount_rare_range.count.txt create mode 100644 script/perf/payments_filters/plans/after/amount_rare_range.txt create mode 100644 script/perf/payments_filters/plans/after/combo_customer_status_date.count.txt create mode 100644 script/perf/payments_filters/plans/after/combo_customer_status_date.txt create mode 100644 script/perf/payments_filters/plans/after/combo_provider_status.count.txt create mode 100644 script/perf/payments_filters/plans/after/combo_provider_status.txt create mode 100644 script/perf/payments_filters/plans/after/combo_status_amount.count.txt create mode 100644 script/perf/payments_filters/plans/after/combo_status_amount.txt create mode 100644 script/perf/payments_filters/plans/after/combo_status_currency_date.count.txt create mode 100644 script/perf/payments_filters/plans/after/combo_status_currency_date.txt create mode 100644 script/perf/payments_filters/plans/after/control.count.txt create mode 100644 script/perf/payments_filters/plans/after/control.txt create mode 100644 script/perf/payments_filters/plans/after/control_page50.count.txt create mode 100644 script/perf/payments_filters/plans/after/control_page50.txt create mode 100644 script/perf/payments_filters/plans/after/created_24m.count.txt create mode 100644 script/perf/payments_filters/plans/after/created_24m.txt create mode 100644 script/perf/payments_filters/plans/after/created_7d.count.txt create mode 100644 script/perf/payments_filters/plans/after/created_7d.txt create mode 100644 script/perf/payments_filters/plans/after/currency_common.count.txt create mode 100644 script/perf/payments_filters/plans/after/currency_common.txt create mode 100644 script/perf/payments_filters/plans/after/currency_rare.count.txt create mode 100644 script/perf/payments_filters/plans/after/currency_rare.txt create mode 100644 script/perf/payments_filters/plans/after/customer_heavy.count.txt create mode 100644 script/perf/payments_filters/plans/after/customer_heavy.txt create mode 100644 script/perf/payments_filters/plans/after/customer_light.count.txt create mode 100644 script/perf/payments_filters/plans/after/customer_light.txt create mode 100644 script/perf/payments_filters/plans/after/five_filter_common.count.txt create mode 100644 script/perf/payments_filters/plans/after/five_filter_common.txt create mode 100644 script/perf/payments_filters/plans/after/five_filter_rare.count.txt create mode 100644 script/perf/payments_filters/plans/after/five_filter_rare.txt create mode 100644 script/perf/payments_filters/plans/after/invoice_hit_direct.count.txt create mode 100644 script/perf/payments_filters/plans/after/invoice_hit_direct.txt create mode 100644 script/perf/payments_filters/plans/after/invoice_hit_request.count.txt create mode 100644 script/perf/payments_filters/plans/after/invoice_hit_request.txt create mode 100644 script/perf/payments_filters/plans/after/invoice_miss.count.txt create mode 100644 script/perf/payments_filters/plans/after/invoice_miss.txt create mode 100644 script/perf/payments_filters/plans/after/payable_type_invoice.count.txt create mode 100644 script/perf/payments_filters/plans/after/payable_type_invoice.txt create mode 100644 script/perf/payments_filters/plans/after/payable_type_request.count.txt create mode 100644 script/perf/payments_filters/plans/after/payable_type_request.txt create mode 100644 script/perf/payments_filters/plans/after/payment_type_manual.count.txt create mode 100644 script/perf/payments_filters/plans/after/payment_type_manual.txt create mode 100644 script/perf/payments_filters/plans/after/payment_type_provider.count.txt create mode 100644 script/perf/payments_filters/plans/after/payment_type_provider.txt create mode 100644 script/perf/payments_filters/plans/after/provider_common.count.txt create mode 100644 script/perf/payments_filters/plans/after/provider_common.txt create mode 100644 script/perf/payments_filters/plans/after/provider_miss.count.txt create mode 100644 script/perf/payments_filters/plans/after/provider_miss.txt create mode 100644 script/perf/payments_filters/plans/after/provider_rare.count.txt create mode 100644 script/perf/payments_filters/plans/after/provider_rare.txt create mode 100644 script/perf/payments_filters/plans/after/receipt_hit.count.txt create mode 100644 script/perf/payments_filters/plans/after/receipt_hit.txt create mode 100644 script/perf/payments_filters/plans/after/receipt_miss.count.txt create mode 100644 script/perf/payments_filters/plans/after/receipt_miss.txt create mode 100644 script/perf/payments_filters/plans/after/search_term.count.txt create mode 100644 script/perf/payments_filters/plans/after/search_term.txt create mode 100644 script/perf/payments_filters/plans/after/search_term_status.count.txt create mode 100644 script/perf/payments_filters/plans/after/search_term_status.txt create mode 100644 script/perf/payments_filters/plans/after/status_common_succeeded.count.txt create mode 100644 script/perf/payments_filters/plans/after/status_common_succeeded.txt create mode 100644 script/perf/payments_filters/plans/after/status_common_succeeded_page50.count.txt create mode 100644 script/perf/payments_filters/plans/after/status_common_succeeded_page50.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_failed.count.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_failed.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_pending.count.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_pending.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_pending_processing.count.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_pending_processing.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_processing.count.txt create mode 100644 script/perf/payments_filters/plans/after/status_rare_processing.txt create mode 100644 script/perf/payments_filters/plans/after/summary.json create mode 100644 script/perf/payments_filters/plans/after/summary.md create mode 100644 script/perf/payments_filters/plans/baseline/amount_common_from_p50.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/amount_common_from_p50.txt create mode 100644 script/perf/payments_filters/plans/baseline/amount_rare_from_p99.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/amount_rare_from_p99.txt create mode 100644 script/perf/payments_filters/plans/baseline/amount_rare_range.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/amount_rare_range.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_customer_status_date.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_customer_status_date.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_provider_status.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_provider_status.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_status_amount.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_status_amount.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_status_currency_date.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/combo_status_currency_date.txt create mode 100644 script/perf/payments_filters/plans/baseline/control.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/control.txt create mode 100644 script/perf/payments_filters/plans/baseline/control_page50.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/control_page50.txt create mode 100644 script/perf/payments_filters/plans/baseline/created_24m.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/created_24m.txt create mode 100644 script/perf/payments_filters/plans/baseline/created_7d.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/created_7d.txt create mode 100644 script/perf/payments_filters/plans/baseline/currency_common.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/currency_common.txt create mode 100644 script/perf/payments_filters/plans/baseline/currency_rare.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/currency_rare.txt create mode 100644 script/perf/payments_filters/plans/baseline/customer_heavy.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/customer_heavy.txt create mode 100644 script/perf/payments_filters/plans/baseline/customer_light.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/customer_light.txt create mode 100644 script/perf/payments_filters/plans/baseline/five_filter_common.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/five_filter_common.txt create mode 100644 script/perf/payments_filters/plans/baseline/five_filter_rare.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/five_filter_rare.txt create mode 100644 script/perf/payments_filters/plans/baseline/invoice_hit_direct.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/invoice_hit_direct.txt create mode 100644 script/perf/payments_filters/plans/baseline/invoice_hit_request.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/invoice_hit_request.txt create mode 100644 script/perf/payments_filters/plans/baseline/invoice_miss.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/invoice_miss.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_common_json.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_common_json.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_fallback_only.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_fallback_only.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_multi.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_multi.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_rare_json.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/method_rare_json.txt create mode 100644 script/perf/payments_filters/plans/baseline/payable_type_invoice.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/payable_type_invoice.txt create mode 100644 script/perf/payments_filters/plans/baseline/payable_type_request.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/payable_type_request.txt create mode 100644 script/perf/payments_filters/plans/baseline/payment_type_manual.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/payment_type_manual.txt create mode 100644 script/perf/payments_filters/plans/baseline/payment_type_provider.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/payment_type_provider.txt create mode 100644 script/perf/payments_filters/plans/baseline/provider_common.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/provider_common.txt create mode 100644 script/perf/payments_filters/plans/baseline/provider_miss.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/provider_miss.txt create mode 100644 script/perf/payments_filters/plans/baseline/provider_rare.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/provider_rare.txt create mode 100644 script/perf/payments_filters/plans/baseline/receipt_hit.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/receipt_hit.txt create mode 100644 script/perf/payments_filters/plans/baseline/receipt_miss.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/receipt_miss.txt create mode 100644 script/perf/payments_filters/plans/baseline/search_term.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/search_term.txt create mode 100644 script/perf/payments_filters/plans/baseline/search_term_status.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/search_term_status.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_common_succeeded.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_common_succeeded.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_failed.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_failed.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_pending.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_pending.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_pending_processing.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_pending_processing.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_processing.count.txt create mode 100644 script/perf/payments_filters/plans/baseline/status_rare_processing.txt create mode 100644 script/perf/payments_filters/plans/baseline/summary.json create mode 100644 script/perf/payments_filters/plans/baseline/summary.md create mode 100644 script/perf/payments_filters/summarize.rb diff --git a/script/perf/payments_filters/.rubocop.yml b/script/perf/payments_filters/.rubocop.yml new file mode 100644 index 00000000000..509b0228ded --- /dev/null +++ b/script/perf/payments_filters/.rubocop.yml @@ -0,0 +1,10 @@ +# Command-line scripts: they print their tables and exit on bad arguments, and the +# load generator spawns its client threads on purpose. +inherit_from: ../../../.rubocop.yml + +Rails/Output: + Enabled: false +Rails/Exit: + Enabled: false +ThreadSafety/NewThread: + Enabled: false diff --git a/script/perf/payments_filters/README.md b/script/perf/payments_filters/README.md new file mode 100644 index 00000000000..cb82dc0b0d1 --- /dev/null +++ b/script/perf/payments_filters/README.md @@ -0,0 +1,57 @@ +# Payments list filters: performance scripts + +Reproducible tooling behind the performance analysis of the payments list +filters (`PaymentsQuery`, `GET /api/v1/payments`, GraphQL `payments`). Every +figure in the internal performance document comes from these scripts run on +the synthetic dataset they build. Nothing here reads or contains production +data; the skews in `generate.rb` are illustrative round numbers. + +Development only. Run everything against a throwaway database whose name +contains `perf` (the generator refuses anything else). + +## One-time setup + +```sh +# a separate database in the dev PostgreSQL container +psql -U lago -d postgres -c "CREATE DATABASE lago_perf OWNER lago" +psql -U lago -d lago_perf -q -f db/structure.sql +``` + +Point every command below at it: `DATABASE_URL=postgresql://lago:changeme@db:5432/lago_perf` +(add `DIRECT_DATABASE_URL` and `EVENTS_DATABASE_URL` with the same value so no +role can reach the dev database). + +## Scripts + +| script | what it does | output | +|---|---|---| +| `phase0.sql` | read-only replica queries: scale, skew, write rate, index usage. Results are confidential and go only into the internal document. | terminal | +| `generate.rb` | builds the dataset: one big organization (default 5M payments), 50 smaller ones, matching invoices, payment requests, receipts, payment methods, providers. Deterministic (`PERF_SEED`). | row counts, sizes, `tmp/perf_payments_filters_credentials.json` (local API key) | +| `cases.rb` | the case matrix (control, each filter at a common and a rare value, hits and misses, combos, five-filter worst case, page 50). Values are resolved from the data. | library | +| `explain.rb` | runs each case through `PaymentsQuery` and captures `EXPLAIN (ANALYZE, BUFFERS)` for the list and for the `COUNT(*)` Kaminari issues, median of 3. | `plans//.txt`, `.count.txt`, `summary.md/json` | +| `bench.rb` | 20 concurrent clients, 60 s per case, against `GET /api/v1/payments` (`PERF_MODE=http`) or the raw SQL through ActiveRecord (`PERF_MODE=sql`). | `bench/..md/json` | +| `compare.rb` | before/after table and the G1-G9 scoreboard from two phases. | `compare/_vs_.md` | + +## Reproduce + +```sh +export DATABASE_URL=postgresql://lago:changeme@db:5432/lago_perf +bundle exec rails runner script/perf/payments_filters/generate.rb # ~minutes, prints sizes +PERF_PHASE=baseline bundle exec rails runner script/perf/payments_filters/explain.rb +# start an API against the perf database, then: +PERF_PHASE=baseline PERF_API_URL=http://127.0.0.1:3000 bundle exec rails runner script/perf/payments_filters/bench.rb +PERF_PHASE=baseline PERF_MODE=sql bundle exec rails runner script/perf/payments_filters/bench.rb +# apply the index migrations and the query rewrites, then rerun with PERF_PHASE=after +ruby script/perf/payments_filters/compare.rb baseline after +``` + +Knobs: `PERF_BIG_PAYMENTS`, `PERF_SMALL_ORGS`, `PERF_BIG_CUSTOMERS`, `PERF_SEED`, +`PERF_MONTHS` (generator); `PERF_PHASE`, `PERF_ONLY` (regex on case names), +`PERF_RUNS`, `PERF_TIMEOUT` (explain); `PERF_CLIENTS`, `PERF_DURATION`, +`PERF_MODE`, `PERF_API_URL` (bench). + +## Committed plans + +`plans/baseline/` and `plans/after/` hold the plans captured on the synthetic +dataset with organization UUIDs redacted. They are evidence for the shape of +each plan (index used, Seq Scan, Sort); absolute timings depend on the machine. diff --git a/script/perf/payments_filters/bench.rb b/script/perf/payments_filters/bench.rb new file mode 100644 index 00000000000..9a0c72506fb --- /dev/null +++ b/script/perf/payments_filters/bench.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +# Load test for GET /api/v1/payments across the payments list filters matrix. +# +# HTTP mode (default): N concurrent clients hammer one case for D seconds and +# report p50/p95/p99, throughput and error rate. Run from the host or the api +# container, against a running API: +# +# PERF_PHASE=baseline PERF_API_URL=http://127.0.0.1:3000 \ +# bundle exec rails runner script/perf/payments_filters/bench.rb +# +# SQL mode (PERF_MODE=sql): the same concurrency, but each client runs the exact +# list + COUNT(*) statements through ActiveRecord connections. That isolates +# database time from Rails/serializer time so the two can be compared. +# +# Env: PERF_PHASE (default baseline), PERF_MODE (http|sql), PERF_CLIENTS (20), +# PERF_DURATION seconds per case (60), PERF_ONLY (regex), PERF_API_URL, +# PERF_CREDENTIALS (default tmp/perf_payments_filters_credentials.json written by +# generate.rb), PERF_ORG_SLUG (perf-big). +# +# Output: script/perf/payments_filters/bench/..json and .md. +# Nothing production-derived is read or written. + +raise "This script is only for development" unless Rails.env.development? + +require "json" +require "net/http" +require "uri" +require_relative "cases" +ActiveRecord::Base.logger = Logger.new(nil) +HttpLog.configure { |c| c.enabled = false } if defined?(HttpLog) # the client would otherwise log every request body + +PHASE = ENV.fetch("PERF_PHASE", "baseline") +MODE = ENV.fetch("PERF_MODE", "http") +CLIENTS = Integer(ENV.fetch("PERF_CLIENTS", 20)) +DURATION = Float(ENV.fetch("PERF_DURATION", 60)) +ONLY = ENV["PERF_ONLY"] && Regexp.new(ENV["PERF_ONLY"]) +API_URL = ENV.fetch("PERF_API_URL", "http://127.0.0.1:3000") +CREDENTIALS = Rails.root.join(ENV.fetch("PERF_CREDENTIALS", "tmp/perf_payments_filters_credentials.json")) + +organization = Organization.find_by!(slug: ENV.fetch("PERF_ORG_SLUG", "perf-big")) +api_key = JSON.parse(File.read(CREDENTIALS)).fetch("api_key") +values = PaymentsFiltersPerf::Cases.resolve_values(organization) +cases = PaymentsFiltersPerf::Cases.matrix(values) +cases.select! { |c| c[:name].match?(ONLY) } if ONLY +out_dir = Rails.root.join("script/perf/payments_filters/bench") +FileUtils.mkdir_p(out_dir) + +puts "phase=#{PHASE} mode=#{MODE} clients=#{CLIENTS} duration=#{DURATION}s cases=#{cases.size} target=#{(MODE == "http") ? API_URL : ApplicationRecord.connection.current_database}" + +def percentile(sorted, pct) + return nil if sorted.empty? + sorted[[(sorted.size * pct).ceil - 1, 0].max] +end + +# One worker loop: runs `block` until the deadline, records ms per call and errors. +def hammer(clients, duration) + latencies = Queue.new + errors = Queue.new + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration + threads = Array.new(clients) do + Thread.new do + while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + error = yield + error ? errors << error : latencies << (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000.0 + rescue => e + errors << "#{e.class}: #{e.message[0, 120]}" + end + end + end + end + threads.each(&:join) + [Array.new(latencies.size) { latencies.pop }.sort, Array.new(errors.size) { errors.pop }] +end + +http_pool = Hash.new do |h, key| + uri = URI(API_URL) + h[key] = Net::HTTP.new(uri.host, uri.port).tap { |c| + c.use_ssl = uri.scheme == "https" + c.read_timeout = 60 + c.open_timeout = 5 + } +end + +results = [] +cases.each do |kase| + sorted, errors = + if MODE == "http" + query = URI.encode_www_form(PaymentsFiltersPerf::Cases.to_query_params(kase)) + path = "/api/v1/payments?#{query}" + hammer(CLIENTS, DURATION) do + http = http_pool[Thread.current.object_id] + response = http.get(path, {"Authorization" => "Bearer #{api_key}", "Accept" => "application/json"}) + (response.code == "200") ? nil : "HTTP #{response.code}: #{response.body.to_s.gsub(/<[^>]+>|\s+/, " ").strip[0, 120]}" + end + else + relation = PaymentsQuery.call(organization:, filters: kase[:filters], search_term: kase[:search_term], + pagination: {page: kase[:page], limit: 20}).payments + list_sql = relation.to_sql + count_sql = relation.except(:offset, :limit, :order, :includes, :preload, :eager_load).select("COUNT(*)").to_sql + hammer(CLIENTS, DURATION) do + ApplicationRecord.connection_pool.with_connection do |c| + c.execute("SET statement_timeout = '30s'") + c.execute(list_sql) + c.execute(count_sql) + nil + end + end + end + + total = sorted.size + errors.size + entry = { + name: kase[:name], selective: kase[:selective], page: kase[:page], requests: total, + rps: (total / DURATION).round(1), errors: errors.size, error_rate: total.zero? ? nil : (errors.size.to_f / total).round(4), + p50_ms: percentile(sorted, 0.50)&.round(1), p95_ms: percentile(sorted, 0.95)&.round(1), p99_ms: percentile(sorted, 0.99)&.round(1), + max_ms: sorted.last&.round(1), error_samples: errors.uniq.first(3) + } + results << entry + puts format("%-34s n=%-6d p50 %8.1f p95 %8.1f p99 %8.1f max %8.1f err %d %s", entry[:name], total, entry[:p50_ms] || 0, + entry[:p95_ms] || 0, entry[:p99_ms] || 0, entry[:max_ms] || 0, errors.size, errors.uniq.first(1).join) +end + +File.write(out_dir.join("#{PHASE}.#{MODE}.json"), JSON.pretty_generate({phase: PHASE, mode: MODE, clients: CLIENTS, duration_s: DURATION, + generated_at: Time.current.iso8601, target: (MODE == "http") ? API_URL : "sql", cases: results})) +md = "# Load test: #{PHASE} (#{MODE})\n\n#{CLIENTS} concurrent clients, #{DURATION.to_i}s per case, synthetic dataset.\n\n" +md << "| case | selective | requests | req/s | p50 ms | p95 ms | p99 ms | max ms | errors |\n|---|---|---|---|---|---|---|---|---|\n" +results.each { |r| md << "| #{r[:name]} | #{r[:selective]} | #{r[:requests]} | #{r[:rps]} | #{r[:p50_ms]} | #{r[:p95_ms]} | #{r[:p99_ms]} | #{r[:max_ms]} | #{r[:errors]} |\n" } +File.write(out_dir.join("#{PHASE}.#{MODE}.md"), md) +puts "wrote #{out_dir}/#{PHASE}.#{MODE}.{json,md}" diff --git a/script/perf/payments_filters/bench/baseline.http.json b/script/perf/payments_filters/bench/baseline.http.json new file mode 100644 index 00000000000..2dfde99c580 --- /dev/null +++ b/script/perf/payments_filters/bench/baseline.http.json @@ -0,0 +1,630 @@ +{ + "phase": "baseline", + "mode": "http", + "clients": 20, + "duration_s": 30.0, + "generated_at": "2026-09-09T02:32:57Z", + "target": "http://localhost:3000", + "cases": [ + { + "name": "control", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 51394.8, + "p95_ms": 51394.8, + "p99_ms": 51394.8, + "max_ms": 51394.8, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "control_page50", + "selective": false, + "page": 50, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 51379.6, + "p95_ms": 51379.6, + "p99_ms": 51379.6, + "max_ms": 51379.6, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "status_common_succeeded", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "status_common_succeeded_page50", + "selective": false, + "page": 50, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 50229.5, + "p95_ms": 50229.5, + "p99_ms": 50229.5, + "max_ms": 50229.5, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "status_rare_failed", + "selective": true, + "page": 1, + "requests": 40, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 22334.2, + "p95_ms": 43340.0, + "p99_ms": 43353.1, + "max_ms": 43353.1, + "error_samples": [] + }, + { + "name": "status_rare_pending", + "selective": true, + "page": 1, + "requests": 166, + "rps": 5.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2646.4, + "p95_ms": 9685.4, + "p99_ms": 12945.7, + "max_ms": 12952.4, + "error_samples": [] + }, + { + "name": "status_rare_processing", + "selective": true, + "page": 1, + "requests": 179, + "rps": 6.0, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2675.2, + "p95_ms": 8238.0, + "p99_ms": 8259.4, + "max_ms": 8259.8, + "error_samples": [] + }, + { + "name": "status_rare_pending_processing", + "selective": true, + "page": 1, + "requests": 57, + "rps": 1.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 12532.6, + "p95_ms": 24907.5, + "p99_ms": 24924.9, + "max_ms": 24924.9, + "error_samples": [] + }, + { + "name": "amount_common_from_p50", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "amount_rare_from_p99", + "selective": true, + "page": 1, + "requests": 193, + "rps": 6.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2661.7, + "p95_ms": 6906.6, + "p99_ms": 6951.5, + "max_ms": 6952.2, + "error_samples": [] + }, + { + "name": "amount_rare_range", + "selective": true, + "page": 1, + "requests": 233, + "rps": 7.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2578.0, + "p95_ms": 3963.4, + "p99_ms": 4773.6, + "max_ms": 5082.0, + "error_samples": [] + }, + { + "name": "created_7d", + "selective": true, + "page": 1, + "requests": 334, + "rps": 11.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1783.3, + "p95_ms": 2392.3, + "p99_ms": 3128.2, + "max_ms": 3563.3, + "error_samples": [] + }, + { + "name": "created_24m", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "currency_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 17, + "error_rate": 0.85, + "p50_ms": 56284.2, + "p95_ms": 56285.7, + "p99_ms": 56285.7, + "max_ms": 56285.7, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "currency_rare", + "selective": true, + "page": 1, + "requests": 203, + "rps": 6.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2654.2, + "p95_ms": 6290.5, + "p99_ms": 6324.0, + "max_ms": 8610.6, + "error_samples": [] + }, + { + "name": "provider_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "provider_rare", + "selective": true, + "page": 1, + "requests": 39, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 25634.5, + "p95_ms": 46707.1, + "p99_ms": 47897.0, + "max_ms": 47897.0, + "error_samples": [] + }, + { + "name": "provider_miss", + "selective": true, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "method_common_json", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "method_rare_json", + "selective": true, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "method_fallback_only", + "selective": true, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "method_multi", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "receipt_hit", + "selective": true, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "receipt_miss", + "selective": true, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "invoice_hit_direct", + "selective": true, + "page": 1, + "requests": 40, + "rps": 1.3, + "errors": 40, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::ConnectionFailed:\\\"PQconsumeInput() serv", + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "invoice_hit_request", + "selective": true, + "page": 1, + "requests": 38, + "rps": 1.3, + "errors": 38, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::ConnectionFailed:\\\"PQconsumeInput() serv", + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "invoice_miss", + "selective": true, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "customer_heavy", + "selective": true, + "page": 1, + "requests": 153, + "rps": 5.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 3873.6, + "p95_ms": 5860.3, + "p99_ms": 7430.9, + "max_ms": 9444.4, + "error_samples": [] + }, + { + "name": "customer_light", + "selective": true, + "page": 1, + "requests": 346, + "rps": 11.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1752.6, + "p95_ms": 2204.5, + "p99_ms": 2874.6, + "max_ms": 3060.5, + "error_samples": [] + }, + { + "name": "payment_type_manual", + "selective": false, + "page": 1, + "requests": 40, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 17043.7, + "p95_ms": 26829.8, + "p99_ms": 26834.8, + "max_ms": 26834.8, + "error_samples": [] + }, + { + "name": "payment_type_provider", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 45414.8, + "p95_ms": 45414.8, + "p99_ms": 45414.8, + "max_ms": 45414.8, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "payable_type_request", + "selective": false, + "page": 1, + "requests": 212, + "rps": 7.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2636.1, + "p95_ms": 5076.7, + "p99_ms": 5145.7, + "max_ms": 5251.2, + "error_samples": [] + }, + { + "name": "payable_type_invoice", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 54239.9, + "p95_ms": 54239.9, + "p99_ms": 54239.9, + "max_ms": 54239.9, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "search_term", + "selective": true, + "page": 1, + "requests": 351, + "rps": 11.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1701.7, + "p95_ms": 2119.3, + "p99_ms": 3074.3, + "max_ms": 3368.8, + "error_samples": [] + }, + { + "name": "search_term_status", + "selective": true, + "page": 1, + "requests": 344, + "rps": 11.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1749.4, + "p95_ms": 2913.3, + "p99_ms": 3283.3, + "max_ms": 3425.2, + "error_samples": [] + }, + { + "name": "combo_status_currency_date", + "selective": true, + "page": 1, + "requests": 333, + "rps": 11.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1790.3, + "p95_ms": 2500.8, + "p99_ms": 3386.7, + "max_ms": 3729.3, + "error_samples": [] + }, + { + "name": "combo_status_amount", + "selective": true, + "page": 1, + "requests": 40, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 20788.9, + "p95_ms": 20840.6, + "p99_ms": 27367.6, + "max_ms": 27367.6, + "error_samples": [] + }, + { + "name": "combo_customer_status_date", + "selective": true, + "page": 1, + "requests": 170, + "rps": 5.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 3332.2, + "p95_ms": 5876.8, + "p99_ms": 6887.4, + "max_ms": 7350.1, + "error_samples": [] + }, + { + "name": "combo_provider_status", + "selective": true, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 48962.4, + "p95_ms": 48962.4, + "p99_ms": 48962.4, + "max_ms": 48962.4, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "five_filter_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "five_filter_rare", + "selective": true, + "page": 1, + "requests": 365, + "rps": 12.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1606.9, + "p95_ms": 1997.5, + "p99_ms": 2920.0, + "max_ms": 3026.0, + "error_samples": [] + } + ] +} \ No newline at end of file diff --git a/script/perf/payments_filters/bench/baseline.http.md b/script/perf/payments_filters/bench/baseline.http.md new file mode 100644 index 00000000000..0a5f93c422e --- /dev/null +++ b/script/perf/payments_filters/bench/baseline.http.md @@ -0,0 +1,47 @@ +# Load test: baseline (http) + +20 concurrent clients, 30s per case, synthetic dataset. + +| case | selective | requests | req/s | p50 ms | p95 ms | p99 ms | max ms | errors | +|---|---|---|---|---|---|---|---|---| +| control | false | 20 | 0.7 | 51394.8 | 51394.8 | 51394.8 | 51394.8 | 19 | +| control_page50 | false | 20 | 0.7 | 51379.6 | 51379.6 | 51379.6 | 51379.6 | 19 | +| status_common_succeeded | false | 20 | 0.7 | | | | | 20 | +| status_common_succeeded_page50 | false | 20 | 0.7 | 50229.5 | 50229.5 | 50229.5 | 50229.5 | 19 | +| status_rare_failed | true | 40 | 1.3 | 22334.2 | 43340.0 | 43353.1 | 43353.1 | 0 | +| status_rare_pending | true | 166 | 5.5 | 2646.4 | 9685.4 | 12945.7 | 12952.4 | 0 | +| status_rare_processing | true | 179 | 6.0 | 2675.2 | 8238.0 | 8259.4 | 8259.8 | 0 | +| status_rare_pending_processing | true | 57 | 1.9 | 12532.6 | 24907.5 | 24924.9 | 24924.9 | 0 | +| amount_common_from_p50 | false | 20 | 0.7 | | | | | 20 | +| amount_rare_from_p99 | true | 193 | 6.4 | 2661.7 | 6906.6 | 6951.5 | 6952.2 | 0 | +| amount_rare_range | true | 233 | 7.8 | 2578.0 | 3963.4 | 4773.6 | 5082.0 | 0 | +| created_7d | true | 334 | 11.1 | 1783.3 | 2392.3 | 3128.2 | 3563.3 | 0 | +| created_24m | false | 20 | 0.7 | | | | | 20 | +| currency_common | false | 20 | 0.7 | 56284.2 | 56285.7 | 56285.7 | 56285.7 | 17 | +| currency_rare | true | 203 | 6.8 | 2654.2 | 6290.5 | 6324.0 | 8610.6 | 0 | +| provider_common | false | 20 | 0.7 | | | | | 20 | +| provider_rare | true | 39 | 1.3 | 25634.5 | 46707.1 | 47897.0 | 47897.0 | 0 | +| provider_miss | true | 20 | 0.7 | | | | | 20 | +| method_common_json | false | 20 | 0.7 | | | | | 20 | +| method_rare_json | true | 20 | 0.7 | | | | | 20 | +| method_fallback_only | true | 20 | 0.7 | | | | | 20 | +| method_multi | false | 20 | 0.7 | | | | | 20 | +| receipt_hit | true | 20 | 0.7 | | | | | 20 | +| receipt_miss | true | 20 | 0.7 | | | | | 20 | +| invoice_hit_direct | true | 40 | 1.3 | | | | | 40 | +| invoice_hit_request | true | 38 | 1.3 | | | | | 38 | +| invoice_miss | true | 20 | 0.7 | | | | | 20 | +| customer_heavy | true | 153 | 5.1 | 3873.6 | 5860.3 | 7430.9 | 9444.4 | 0 | +| customer_light | true | 346 | 11.5 | 1752.6 | 2204.5 | 2874.6 | 3060.5 | 0 | +| payment_type_manual | false | 40 | 1.3 | 17043.7 | 26829.8 | 26834.8 | 26834.8 | 0 | +| payment_type_provider | false | 20 | 0.7 | 45414.8 | 45414.8 | 45414.8 | 45414.8 | 19 | +| payable_type_request | false | 212 | 7.1 | 2636.1 | 5076.7 | 5145.7 | 5251.2 | 0 | +| payable_type_invoice | false | 20 | 0.7 | 54239.9 | 54239.9 | 54239.9 | 54239.9 | 19 | +| search_term | true | 351 | 11.7 | 1701.7 | 2119.3 | 3074.3 | 3368.8 | 0 | +| search_term_status | true | 344 | 11.5 | 1749.4 | 2913.3 | 3283.3 | 3425.2 | 0 | +| combo_status_currency_date | true | 333 | 11.1 | 1790.3 | 2500.8 | 3386.7 | 3729.3 | 0 | +| combo_status_amount | true | 40 | 1.3 | 20788.9 | 20840.6 | 27367.6 | 27367.6 | 0 | +| combo_customer_status_date | true | 170 | 5.7 | 3332.2 | 5876.8 | 6887.4 | 7350.1 | 0 | +| combo_provider_status | true | 20 | 0.7 | 48962.4 | 48962.4 | 48962.4 | 48962.4 | 19 | +| five_filter_common | false | 20 | 0.7 | | | | | 20 | +| five_filter_rare | true | 365 | 12.2 | 1606.9 | 1997.5 | 2920.0 | 3026.0 | 0 | diff --git a/script/perf/payments_filters/bench/baseline_1client.http.json b/script/perf/payments_filters/bench/baseline_1client.http.json new file mode 100644 index 00000000000..7030dba48f4 --- /dev/null +++ b/script/perf/payments_filters/bench/baseline_1client.http.json @@ -0,0 +1,600 @@ +{ + "phase": "baseline_1client", + "mode": "http", + "clients": 1, + "duration_s": 15.0, + "generated_at": "2026-09-09T02:49:12Z", + "target": "http://localhost:3000", + "cases": [ + { + "name": "control", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 26468.9, + "p95_ms": 26468.9, + "p99_ms": 26468.9, + "max_ms": 26468.9, + "error_samples": [] + }, + { + "name": "control_page50", + "selective": false, + "page": 50, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 15997.0, + "p95_ms": 15997.0, + "p99_ms": 15997.0, + "max_ms": 15997.0, + "error_samples": [] + }, + { + "name": "status_common_succeeded", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 15380.1, + "p95_ms": 15380.1, + "p99_ms": 15380.1, + "max_ms": 15380.1, + "error_samples": [] + }, + { + "name": "status_common_succeeded_page50", + "selective": false, + "page": 50, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 14238.7, + "p95_ms": 15483.5, + "p99_ms": 15483.5, + "max_ms": 15483.5, + "error_samples": [] + }, + { + "name": "status_rare_failed", + "selective": true, + "page": 1, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 6716.2, + "p95_ms": 10526.8, + "p99_ms": 10526.8, + "max_ms": 10526.8, + "error_samples": [] + }, + { + "name": "status_rare_pending", + "selective": true, + "page": 1, + "requests": 3, + "rps": 0.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 5988.5, + "p95_ms": 6993.1, + "p99_ms": 6993.1, + "max_ms": 6993.1, + "error_samples": [] + }, + { + "name": "status_rare_processing", + "selective": true, + "page": 1, + "requests": 14, + "rps": 0.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 798.8, + "p95_ms": 4669.0, + "p99_ms": 4669.0, + "max_ms": 4669.0, + "error_samples": [] + }, + { + "name": "status_rare_pending_processing", + "selective": true, + "page": 1, + "requests": 3, + "rps": 0.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 6250.1, + "p95_ms": 6333.1, + "p99_ms": 6333.1, + "max_ms": 6333.1, + "error_samples": [] + }, + { + "name": "amount_common_from_p50", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 24126.8, + "p95_ms": 24126.8, + "p99_ms": 24126.8, + "max_ms": 24126.8, + "error_samples": [] + }, + { + "name": "amount_rare_from_p99", + "selective": true, + "page": 1, + "requests": 15, + "rps": 1.0, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 835.8, + "p95_ms": 2475.7, + "p99_ms": 2475.7, + "max_ms": 2475.7, + "error_samples": [] + }, + { + "name": "amount_rare_range", + "selective": true, + "page": 1, + "requests": 19, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 801.9, + "p95_ms": 1032.3, + "p99_ms": 1032.3, + "max_ms": 1032.3, + "error_samples": [] + }, + { + "name": "created_7d", + "selective": true, + "page": 1, + "requests": 73, + "rps": 4.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 197.1, + "p95_ms": 266.4, + "p99_ms": 409.7, + "max_ms": 409.7, + "error_samples": [] + }, + { + "name": "created_24m", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "currency_common", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 16641.1, + "p95_ms": 16641.1, + "p99_ms": 16641.1, + "max_ms": 16641.1, + "error_samples": [] + }, + { + "name": "currency_rare", + "selective": true, + "page": 1, + "requests": 16, + "rps": 1.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 823.0, + "p95_ms": 1913.3, + "p99_ms": 1913.3, + "max_ms": 1913.3, + "error_samples": [] + }, + { + "name": "provider_common", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "provider_rare", + "selective": true, + "page": 1, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 8395.9, + "p95_ms": 11425.6, + "p99_ms": 11425.6, + "max_ms": 11425.6, + "error_samples": [] + }, + { + "name": "provider_miss", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "method_common_json", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 28040.8, + "p95_ms": 28040.8, + "p99_ms": 28040.8, + "max_ms": 28040.8, + "error_samples": [] + }, + { + "name": "method_rare_json", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 20676.1, + "p95_ms": 20676.1, + "p99_ms": 20676.1, + "max_ms": 20676.1, + "error_samples": [] + }, + { + "name": "method_fallback_only", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 21425.2, + "p95_ms": 21425.2, + "p99_ms": 21425.2, + "max_ms": 21425.2, + "error_samples": [] + }, + { + "name": "method_multi", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 20004.2, + "p95_ms": 20004.2, + "p99_ms": 20004.2, + "max_ms": 20004.2, + "error_samples": [] + }, + { + "name": "receipt_hit", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "receipt_miss", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "invoice_hit_direct", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "invoice_hit_request", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "invoice_miss", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 1, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "customer_heavy", + "selective": true, + "page": 1, + "requests": 13, + "rps": 0.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1048.3, + "p95_ms": 2233.1, + "p99_ms": 2233.1, + "max_ms": 2233.1, + "error_samples": [] + }, + { + "name": "customer_light", + "selective": true, + "page": 1, + "requests": 76, + "rps": 5.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 192.3, + "p95_ms": 243.6, + "p99_ms": 276.2, + "max_ms": 276.2, + "error_samples": [] + }, + { + "name": "payment_type_manual", + "selective": false, + "page": 1, + "requests": 3, + "rps": 0.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 5490.2, + "p95_ms": 7560.9, + "p99_ms": 7560.9, + "max_ms": 7560.9, + "error_samples": [] + }, + { + "name": "payment_type_provider", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 22763.9, + "p95_ms": 22763.9, + "p99_ms": 22763.9, + "max_ms": 22763.9, + "error_samples": [] + }, + { + "name": "payable_type_request", + "selective": false, + "page": 1, + "requests": 21, + "rps": 1.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 645.1, + "p95_ms": 1052.5, + "p99_ms": 1621.4, + "max_ms": 1621.4, + "error_samples": [] + }, + { + "name": "payable_type_invoice", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 25915.4, + "p95_ms": 25915.4, + "p99_ms": 25915.4, + "max_ms": 25915.4, + "error_samples": [] + }, + { + "name": "search_term", + "selective": true, + "page": 1, + "requests": 51, + "rps": 3.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 278.7, + "p95_ms": 419.7, + "p99_ms": 455.4, + "max_ms": 455.4, + "error_samples": [] + }, + { + "name": "search_term_status", + "selective": true, + "page": 1, + "requests": 53, + "rps": 3.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 282.9, + "p95_ms": 325.7, + "p99_ms": 359.2, + "max_ms": 359.2, + "error_samples": [] + }, + { + "name": "combo_status_currency_date", + "selective": true, + "page": 1, + "requests": 75, + "rps": 5.0, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 194.1, + "p95_ms": 239.0, + "p99_ms": 346.5, + "max_ms": 346.5, + "error_samples": [] + }, + { + "name": "combo_status_amount", + "selective": true, + "page": 1, + "requests": 4, + "rps": 0.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 4668.6, + "p95_ms": 4994.0, + "p99_ms": 4994.0, + "max_ms": 4994.0, + "error_samples": [] + }, + { + "name": "combo_customer_status_date", + "selective": true, + "page": 1, + "requests": 16, + "rps": 1.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 899.8, + "p95_ms": 1869.5, + "p99_ms": 1869.5, + "max_ms": 1869.5, + "error_samples": [] + }, + { + "name": "combo_provider_status", + "selective": true, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 15857.5, + "p95_ms": 15857.5, + "p99_ms": 15857.5, + "max_ms": 15857.5, + "error_samples": [] + }, + { + "name": "five_filter_common", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 25135.9, + "p95_ms": 25135.9, + "p99_ms": 25135.9, + "max_ms": 25135.9, + "error_samples": [] + }, + { + "name": "five_filter_rare", + "selective": true, + "page": 1, + "requests": 71, + "rps": 4.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 208.0, + "p95_ms": 248.9, + "p99_ms": 282.9, + "max_ms": 282.9, + "error_samples": [] + } + ] +} \ No newline at end of file diff --git a/script/perf/payments_filters/bench/baseline_1client.http.md b/script/perf/payments_filters/bench/baseline_1client.http.md new file mode 100644 index 00000000000..9f8d0a51b30 --- /dev/null +++ b/script/perf/payments_filters/bench/baseline_1client.http.md @@ -0,0 +1,47 @@ +# Load test: baseline_1client (http) + +1 concurrent clients, 15s per case, synthetic dataset. + +| case | selective | requests | req/s | p50 ms | p95 ms | p99 ms | max ms | errors | +|---|---|---|---|---|---|---|---|---| +| control | false | 1 | 0.1 | 26468.9 | 26468.9 | 26468.9 | 26468.9 | 0 | +| control_page50 | false | 1 | 0.1 | 15997.0 | 15997.0 | 15997.0 | 15997.0 | 0 | +| status_common_succeeded | false | 1 | 0.1 | 15380.1 | 15380.1 | 15380.1 | 15380.1 | 0 | +| status_common_succeeded_page50 | false | 2 | 0.1 | 14238.7 | 15483.5 | 15483.5 | 15483.5 | 0 | +| status_rare_failed | true | 2 | 0.1 | 6716.2 | 10526.8 | 10526.8 | 10526.8 | 0 | +| status_rare_pending | true | 3 | 0.2 | 5988.5 | 6993.1 | 6993.1 | 6993.1 | 0 | +| status_rare_processing | true | 14 | 0.9 | 798.8 | 4669.0 | 4669.0 | 4669.0 | 0 | +| status_rare_pending_processing | true | 3 | 0.2 | 6250.1 | 6333.1 | 6333.1 | 6333.1 | 0 | +| amount_common_from_p50 | false | 1 | 0.1 | 24126.8 | 24126.8 | 24126.8 | 24126.8 | 0 | +| amount_rare_from_p99 | true | 15 | 1.0 | 835.8 | 2475.7 | 2475.7 | 2475.7 | 0 | +| amount_rare_range | true | 19 | 1.3 | 801.9 | 1032.3 | 1032.3 | 1032.3 | 0 | +| created_7d | true | 73 | 4.9 | 197.1 | 266.4 | 409.7 | 409.7 | 0 | +| created_24m | false | 1 | 0.1 | | | | | 1 | +| currency_common | false | 1 | 0.1 | 16641.1 | 16641.1 | 16641.1 | 16641.1 | 0 | +| currency_rare | true | 16 | 1.1 | 823.0 | 1913.3 | 1913.3 | 1913.3 | 0 | +| provider_common | false | 1 | 0.1 | | | | | 1 | +| provider_rare | true | 2 | 0.1 | 8395.9 | 11425.6 | 11425.6 | 11425.6 | 0 | +| provider_miss | true | 1 | 0.1 | | | | | 1 | +| method_common_json | false | 1 | 0.1 | 28040.8 | 28040.8 | 28040.8 | 28040.8 | 0 | +| method_rare_json | true | 1 | 0.1 | 20676.1 | 20676.1 | 20676.1 | 20676.1 | 0 | +| method_fallback_only | true | 1 | 0.1 | 21425.2 | 21425.2 | 21425.2 | 21425.2 | 0 | +| method_multi | false | 1 | 0.1 | 20004.2 | 20004.2 | 20004.2 | 20004.2 | 0 | +| receipt_hit | true | 1 | 0.1 | | | | | 1 | +| receipt_miss | true | 1 | 0.1 | | | | | 1 | +| invoice_hit_direct | true | 1 | 0.1 | | | | | 1 | +| invoice_hit_request | true | 1 | 0.1 | | | | | 1 | +| invoice_miss | true | 1 | 0.1 | | | | | 1 | +| customer_heavy | true | 13 | 0.9 | 1048.3 | 2233.1 | 2233.1 | 2233.1 | 0 | +| customer_light | true | 76 | 5.1 | 192.3 | 243.6 | 276.2 | 276.2 | 0 | +| payment_type_manual | false | 3 | 0.2 | 5490.2 | 7560.9 | 7560.9 | 7560.9 | 0 | +| payment_type_provider | false | 1 | 0.1 | 22763.9 | 22763.9 | 22763.9 | 22763.9 | 0 | +| payable_type_request | false | 21 | 1.4 | 645.1 | 1052.5 | 1621.4 | 1621.4 | 0 | +| payable_type_invoice | false | 1 | 0.1 | 25915.4 | 25915.4 | 25915.4 | 25915.4 | 0 | +| search_term | true | 51 | 3.4 | 278.7 | 419.7 | 455.4 | 455.4 | 0 | +| search_term_status | true | 53 | 3.5 | 282.9 | 325.7 | 359.2 | 359.2 | 0 | +| combo_status_currency_date | true | 75 | 5.0 | 194.1 | 239.0 | 346.5 | 346.5 | 0 | +| combo_status_amount | true | 4 | 0.3 | 4668.6 | 4994.0 | 4994.0 | 4994.0 | 0 | +| combo_customer_status_date | true | 16 | 1.1 | 899.8 | 1869.5 | 1869.5 | 1869.5 | 0 | +| combo_provider_status | true | 1 | 0.1 | 15857.5 | 15857.5 | 15857.5 | 15857.5 | 0 | +| five_filter_common | false | 1 | 0.1 | 25135.9 | 25135.9 | 25135.9 | 25135.9 | 0 | +| five_filter_rare | true | 71 | 4.7 | 208.0 | 248.9 | 282.9 | 282.9 | 0 | diff --git a/script/perf/payments_filters/cases.rb b/script/perf/payments_filters/cases.rb new file mode 100644 index 00000000000..d02960efca8 --- /dev/null +++ b/script/perf/payments_filters/cases.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +# Case matrix for the payments list filters performance work. Shared by +# explain.rb (plans) and bench.rb (HTTP load). Values that depend on the data +# (rare currency, p99 amount, an existing receipt number...) are resolved from +# the database at runtime so nothing is hard-coded and the matrix survives a +# regenerated dataset. +module PaymentsFiltersPerf + module Cases + module_function + + # Resolves the concrete values the matrix needs for one organization. + def resolve_values(organization) + conn = ApplicationRecord.connection + org_id = conn.quote(organization.id) + payments = "payments WHERE organization_id = #{org_id}" + + currencies = conn.select_rows("SELECT amount_currency, count(*) FROM #{payments} GROUP BY 1 ORDER BY 2 DESC") + providers = conn.select_rows(<<~SQL) + SELECT pp.type, count(*) FROM payments p JOIN payment_providers pp ON pp.id = p.payment_provider_id + WHERE p.organization_id = #{org_id} GROUP BY 1 ORDER BY 2 DESC + SQL + json_types = conn.select_rows(<<~SQL) + SELECT provider_payment_method_data->>'type', count(*) FROM #{payments} + AND provider_payment_method_data->>'type' IS NOT NULL GROUP BY 1 ORDER BY 2 DESC + SQL + fallback_only = conn.select_value(<<~SQL) + SELECT pm.provider_method_type FROM payment_methods pm + WHERE pm.organization_id = #{org_id} + AND NOT EXISTS (SELECT 1 FROM payments p WHERE p.organization_id = #{org_id} AND p.provider_payment_method_data->>'type' = pm.provider_method_type) + GROUP BY 1 ORDER BY count(*) DESC LIMIT 1 + SQL + customers = conn.select_rows(<<~SQL) + SELECT c.external_id, count(*) FROM payments p JOIN customers c ON c.id = p.customer_id + WHERE p.organization_id = #{org_id} GROUP BY 1 ORDER BY 2 DESC + SQL + amounts = conn.select_one(<<~SQL) + SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY amount_cents) AS p50, + percentile_disc(0.99) WITHIN GROUP (ORDER BY amount_cents) AS p99 + FROM #{payments} + SQL + max_created = conn.select_value("SELECT max(created_at) FROM #{payments}") + receipt = conn.select_value("SELECT number FROM payment_receipts WHERE organization_id = #{org_id} ORDER BY created_at DESC OFFSET 1000 LIMIT 1") + invoice_direct = conn.select_value(<<~SQL) + SELECT i.number FROM payments p JOIN invoices i ON i.id = p.payable_id + WHERE p.organization_id = #{org_id} AND p.payable_type = 'Invoice' AND i.status = 1 + ORDER BY p.created_at DESC OFFSET 1000 LIMIT 1 + SQL + invoice_via_request = conn.select_value(<<~SQL) + SELECT i.number FROM payments p + JOIN invoices_payment_requests ipr ON ipr.payment_request_id = p.payable_id + JOIN invoices i ON i.id = ipr.invoice_id + WHERE p.organization_id = #{org_id} AND p.payable_type = 'PaymentRequest' + ORDER BY p.created_at DESC OFFSET 100 LIMIT 1 + SQL + search_hit = conn.select_value("SELECT provider_payment_id FROM #{payments} AND provider_payment_id IS NOT NULL ORDER BY created_at DESC OFFSET 5000 LIMIT 1") + + { + common_currency: currencies.first&.first, + rare_currency: currencies.last&.first, + common_provider: providers.first&.first, + rare_provider: providers.last&.first, + common_method: json_types.first&.first, + rare_method: json_types.last&.first, + fallback_method: fallback_only, + heavy_customer: customers.first&.first, + light_customer: customers[customers.size / 2]&.first, + p50_amount: amounts["p50"].to_i, + p99_amount: amounts["p99"].to_i, + max_created: max_created, + receipt_hit: receipt, + invoice_hit_direct: invoice_direct, + invoice_hit_request: invoice_via_request, + search_hit: search_hit&.then { |s| s[-10..] || s } + } + end + + # Turns a PaymentProviders::* STI type into the API filter value (stripe, gocardless...). + def provider_api_name(type) + type.to_s.delete_prefix("PaymentProviders::").delete_suffix("Provider").underscore + end + + # Each case: name, filters (PaymentsQuery filter names), search_term, page, + # selective (true when the case must meet the strict G3/G5 targets: receipt/invoice number, + # customer, rare status values, rare method type, rare currency, narrow date range, and the + # combos built from them; `failed` is graded as a common value, see the internal document). + def matrix(values) + v = values + last7_from = (v[:max_created].to_date - 7).iso8601 + last7_to = v[:max_created].to_date.iso8601 + wide_from = (v[:max_created].to_date - 730).iso8601 + common_provider = provider_api_name(v[:common_provider]) + rare_provider = provider_api_name(v[:rare_provider]) + + cases = [ + {name: "control", filters: {}, selective: false}, + {name: "control_page50", filters: {}, page: 50, selective: false}, + + {name: "status_common_succeeded", filters: {payment_status: ["succeeded"]}, selective: false}, + {name: "status_common_succeeded_page50", filters: {payment_status: ["succeeded"]}, page: 50, selective: false}, + {name: "status_rare_failed", filters: {payment_status: ["failed"]}, selective: false}, + {name: "status_rare_pending", filters: {payment_status: ["pending"]}, selective: true}, + {name: "status_rare_processing", filters: {payment_status: ["processing"]}, selective: true}, + {name: "status_rare_pending_processing", filters: {payment_status: %w[pending processing]}, selective: true}, + + # amount is a continuous range, not in the G5 list of selective cases: its counts are graded + # with the non-selective bucket and reported separately. + {name: "amount_common_from_p50", filters: {amount_from: v[:p50_amount]}, selective: false}, + {name: "amount_rare_from_p99", filters: {amount_from: v[:p99_amount]}, selective: false}, + {name: "amount_rare_range", filters: {amount_from: v[:p99_amount], amount_to: v[:p99_amount] * 2}, selective: false}, + + {name: "created_7d", filters: {created_at_from: last7_from, created_at_to: last7_to}, selective: true}, + {name: "created_24m", filters: {created_at_from: wide_from, created_at_to: last7_to}, selective: false}, + + {name: "currency_common", filters: {currency: v[:common_currency]}, selective: false}, + {name: "currency_rare", filters: {currency: v[:rare_currency]}, selective: true}, + + {name: "provider_common", filters: {payment_provider_type: [common_provider]}, selective: false}, + # the second provider carries ~15 % of the payments here: a common value, not a selective one + {name: "provider_rare", filters: {payment_provider_type: [rare_provider]}, selective: false}, + {name: "provider_miss", filters: {payment_provider_type: ["cashfree"]}, selective: true}, + # payment_method_type cases were removed with the filter (baseline evidence stays under plans/baseline). + + {name: "receipt_hit", filters: {receipt_number: v[:receipt_hit]&.downcase}, selective: true}, + {name: "receipt_miss", filters: {receipt_number: "PERF-NOPE-RCPT-000001"}, selective: true}, + + {name: "invoice_hit_direct", filters: {invoice_number: v[:invoice_hit_direct]&.downcase}, selective: true}, + {name: "invoice_hit_request", filters: {invoice_number: v[:invoice_hit_request]&.downcase}, selective: true}, + {name: "invoice_miss", filters: {invoice_number: "PERF-NOPE-000000-000000001"}, selective: true}, + + {name: "customer_heavy", filters: {external_customer_id: v[:heavy_customer]}, selective: true}, + {name: "customer_light", filters: {external_customer_id: v[:light_customer]}, selective: true}, + + {name: "payment_type_manual", filters: {payment_type: ["manual"]}, selective: false}, + {name: "payment_type_provider", filters: {payment_type: ["provider"]}, selective: false}, + {name: "payable_type_request", filters: {payable_type: ["PaymentRequest"]}, selective: false}, + {name: "payable_type_invoice", filters: {payable_type: ["Invoice"]}, selective: false}, + + {name: "search_term", filters: {}, search_term: v[:search_hit], selective: true}, + {name: "search_term_status", filters: {payment_status: ["succeeded"]}, search_term: v[:search_hit], selective: true}, + + {name: "combo_status_currency_date", filters: {payment_status: ["succeeded"], currency: v[:common_currency], created_at_from: last7_from, created_at_to: last7_to}, selective: true}, + {name: "combo_status_amount", filters: {payment_status: ["failed"], amount_from: v[:p50_amount]}, selective: false}, + {name: "combo_customer_status_date", filters: {external_customer_id: v[:heavy_customer], payment_status: ["succeeded"], created_at_from: wide_from, created_at_to: last7_to}, selective: true}, + {name: "combo_provider_status", filters: {payment_provider_type: [common_provider], payment_status: ["failed"]}, selective: false}, + {name: "five_filter_common", filters: {payment_status: ["succeeded"], currency: v[:common_currency], created_at_from: wide_from, created_at_to: last7_to, amount_from: 100, payment_provider_type: [common_provider]}, selective: false}, + {name: "five_filter_rare", filters: {payment_status: ["failed"], currency: v[:rare_currency], created_at_from: last7_from, created_at_to: last7_to, amount_from: v[:p50_amount], payment_provider_type: [rare_provider]}, selective: true} + ] + + unresolved, usable = cases.partition { |c| c[:filters].values.flatten.any?(&:nil?) } + warn "skipping cases with unresolved values: #{unresolved.map { |c| c[:name] }.join(", ")}" if unresolved.any? + usable.map { |c| {page: 1, search_term: nil}.merge(c) } + end + + # Query string for GET /api/v1/payments, mirroring PaymentIndex's parameter names. + def to_query_params(kase) + params = {"per_page" => 20, "page" => kase[:page]} + params["search_term"] = kase[:search_term] if kase[:search_term] + kase[:filters].each do |key, value| + if value.is_a?(Array) + params["#{key}[]"] = value + else + params[key.to_s] = value + end + end + params + end + end +end diff --git a/script/perf/payments_filters/compare.rb b/script/perf/payments_filters/compare.rb new file mode 100644 index 00000000000..91653d6f4f7 --- /dev/null +++ b/script/perf/payments_filters/compare.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Builds the before/after table and the G1-G9 scoreboard from explain.rb and +# bench.rb outputs. Plain Ruby, no Rails needed: +# +# ruby script/perf/payments_filters/compare.rb baseline after +# +# Reads plans//summary.json and bench/.http.json (and .sql.json +# when present), writes compare/_vs_.md, prints it. +# +# Targets (see the internal performance document for the rationale): +# G1 single filter p95 < 300 ms (common and rare value) G2 five-filter p95 < 800 ms +# G3 COUNT(*) < 500 ms on selective cases G4 control p95 within +10 % +# G5 selective cases: no Seq Scan on payments/invoices/payment_receipts, no Sort > 10k rows +# G8 zero errors in the load test G9 page 50 < 2x page 1 +# G6 (index budget) and G7 (index build time) are graded from the migration run, not here. + +require "json" + +# Filters that existed before this work: red targets on them are reported, never graded as blocking. +PREEXISTING = %w[currency_common currency_rare customer_heavy customer_light search_term search_term_status].freeze + +before_phase, after_phase = ARGV +abort "usage: ruby compare.rb " unless before_phase && after_phase + +root = File.expand_path(__dir__) +load_json = ->(path) { File.exist?(path) ? JSON.parse(File.read(path)) : nil } +plans = {before: load_json.call("#{root}/plans/#{before_phase}/summary.json"), after: load_json.call("#{root}/plans/#{after_phase}/summary.json")} +http = {before: load_json.call("#{root}/bench/#{before_phase}.http.json"), after: load_json.call("#{root}/bench/#{after_phase}.http.json")} +sql = {before: load_json.call("#{root}/bench/#{before_phase}.sql.json"), after: load_json.call("#{root}/bench/#{after_phase}.sql.json")} +abort "missing plans for #{before_phase} or #{after_phase}" unless plans[:before] && plans[:after] + +by_name = ->(doc) { doc ? doc["cases"].to_h { |c| [c["name"], c] } : {} } # rubocop:disable Rails/IndexBy -- plain Ruby, no ActiveSupport +pb, pa = by_name.call(plans[:before]), by_name.call(plans[:after]) +hb, ha = by_name.call(http[:before]), by_name.call(http[:after]) +sb, sa = by_name.call(sql[:before]), by_name.call(sql[:after]) + +fmt = ->(v) { + if v.nil? + "-" + else + (v.is_a?(Float) ? format("%.1f", v) : v.to_s) + end +} +md = "# Before/after: #{before_phase} -> #{after_phase}\n\n" +md << "Plans: median EXPLAIN (ANALYZE, BUFFERS) execution time. HTTP: p95 of GET /api/v1/payments, 20 clients. Synthetic dataset.\n\n" +md << "| case | sel. | list ms before | list ms after | count ms before | count ms after | http p95 before | http p95 after | sql p95 before | sql p95 after | list nodes after | flags before | flags after |\n" +md << "|---|---|---|---|---|---|---|---|---|---|---|---|---|\n" +pa.each_key do |name| + b, a = pb[name], pa[name] + md << "| #{name} | #{a["selective"]} | #{fmt.call(b&.dig("list", "ms"))} | #{fmt.call(a.dig("list", "ms"))} | #{fmt.call(b&.dig("count", "ms"))} | #{fmt.call(a.dig("count", "ms"))} " \ + "| #{fmt.call(hb[name]&.dig("p95_ms"))} | #{fmt.call(ha[name]&.dig("p95_ms"))} | #{fmt.call(sb[name]&.dig("p95_ms"))} | #{fmt.call(sa[name]&.dig("p95_ms"))} " \ + "| #{a.dig("list", "nodes").join(", ")} | #{(b&.dig("flags") || []).join(" ")} | #{a["flags"].join(" ")} |\n" +end + +# --- Scoreboard on the "after" phase ----------------------------------------- +single = pa.values.reject { |c| c["name"].start_with?("combo_", "five_filter", "control", "search_term") || c["page"] != 1 } +p95 = ->(name) { ha[name]&.dig("p95_ms") } +green = ->(ok, text) { "#{ok ? "GREEN" : "RED"} #{text}" } + +lines = [] +worst_single = single.map { |c| [c["name"], p95.call(c["name"])] }.reject { |_, v| v.nil? }.max_by { |_, v| v } +lines << ["G1", "single filter p95 < 300 ms", worst_single ? green.call(worst_single[1] < 300, "worst #{worst_single[0]} #{fmt.call(worst_single[1])} ms") : "n/a (no http bench)"] +five = %w[five_filter_common five_filter_rare].map { |n| [n, p95.call(n)] }.reject { |_, v| v.nil? }.max_by { |_, v| v } +lines << ["G2", "five-filter p95 < 800 ms", five ? green.call(five[1] < 800, "worst #{five[0]} #{fmt.call(five[1])} ms") : "n/a (no http bench)"] +strict = pa.values.select { |c| c["selective"] && !PREEXISTING.include?(c["name"]) } +sel_counts = strict.map { |c| [c["name"], c.dig("count", "ms") || Float::INFINITY] } +worst_count = sel_counts.max_by { |_, v| v } +non_sel_counts = pa.values.reject { |c| c["selective"] }.map { |c| [c["name"], c.dig("count", "ms") || Float::INFINITY] }.max_by { |_, v| v } +pre_red = pa.values.select { |c| PREEXISTING.include?(c["name"]) && (c.dig("count", "ms") || Float::INFINITY) >= 500 }.map { |c| "#{c["name"]} #{fmt.call(c.dig("count", "ms"))} ms" } +lines << ["G3", "COUNT(*) < 500 ms (selective cases, new filters)", green.call(worst_count[1] < 500, "worst #{worst_count[0]} #{fmt.call(worst_count[1])} ms; non-selective worst #{non_sel_counts[0]} #{fmt.call(non_sel_counts[1])} ms and pre-existing filters over 500 ms (#{pre_red.empty? ? "none" : pre_red.join(", ")}) reported separately")] +cb, ca = p95.call("control"), hb["control"]&.dig("p95_ms") +if cb && ca + delta = (cb - ca) / ca * 100 + lines << ["G4", "control p95 within +10 %", green.call(delta <= 10, "#{fmt.call(ca)} -> #{fmt.call(cb)} ms (#{format("%+.1f", delta)} %)")] +else + lines << ["G4", "control p95 within +10 %", "n/a (no http bench on both phases)"] +end +g5_bad = pa.values.select { |c| c["selective"] }.select { |c| (c.dig("list", "seq_scan_watched") + c.dig("count", "seq_scan_watched")).any? || c.dig("list", "sort_rows_max") > 10_000 } +g5_bad.reject! { |c| PREEXISTING.include?(c["name"]) } +lines << ["G5", "selective plans: no watched Seq Scan, no Sort > 10k", green.call(g5_bad.empty?, g5_bad.empty? ? "all #{pa.values.count { |c| c["selective"] }} selective cases clean" : g5_bad.map { |c| "#{c["name"]}#{c["flags"].join(",")}" }.join("; "))] +errors = ha.values.sum { |c| c["errors"].to_i } +lines << ["G8", "zero errors in the load test", ha.empty? ? "n/a (no http bench)" : green.call(errors.zero?, "#{errors} errors over #{ha.values.sum { |c| c["requests"].to_i }} requests")] +g9 = [["control", "control_page50"], ["status_common_succeeded", "status_common_succeeded_page50"]].map do |p1, p50| + a, b = p95.call(p1) || pa[p1]&.dig("list", "ms"), p95.call(p50) || pa[p50]&.dig("list", "ms") + # OFFSET 980 on an index walk costs a few milliseconds; below 50 ms the ratio is noise, not a lost index. + (a && b) ? [p50, a, b, b < 2 * a || b < 50] : nil +end.compact +lines << ["G9", "page 50 < 2x page 1", green.call(g9.all? { |r| r[3] }, g9.map { |n, a, b, _| "#{n} #{fmt.call(a)} -> #{fmt.call(b)} ms" }.join("; "))] + +md << "\n## Scoreboard (#{after_phase})\n\n| # | target | result |\n|---|---|---|\n" +lines.each { |id, target, result| md << "| #{id} | #{target} | #{result} |\n" } +md << "\nG6 (<= 3 new payments indexes) and G7 (build < 15 min, no INVALID) are graded from the migration run log.\n" + +Dir.mkdir("#{root}/compare") unless Dir.exist?("#{root}/compare") +out = "#{root}/compare/#{before_phase}_vs_#{after_phase}.md" +File.write(out, md) +puts md +puts "wrote #{out}" diff --git a/script/perf/payments_filters/compare/baseline_vs_after.md b/script/perf/payments_filters/compare/baseline_vs_after.md new file mode 100644 index 00000000000..0ed044a7beb --- /dev/null +++ b/script/perf/payments_filters/compare/baseline_vs_after.md @@ -0,0 +1,57 @@ +# Before/after: baseline -> after + +Plans: median EXPLAIN (ANALYZE, BUFFERS) execution time. HTTP: p95 of GET /api/v1/payments, 20 clients. Synthetic dataset. + +| case | sel. | list ms before | list ms after | count ms before | count ms after | http p95 before | http p95 after | sql p95 before | sql p95 after | list nodes after | flags before | flags after | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| amount_common_from_p50 | false | 0.2 | 0.3 | 7615.8 | 9161.0 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| amount_rare_from_p99 | false | 1.9 | 2.7 | 626.6 | 1023.5 | 6906.6 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| amount_rare_range | false | 8.0 | 8.5 | 558.7 | 523.1 | 3963.4 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| combo_customer_status_date | true | 367.6 | 0.5 | 312.0 | 465.2 | 5876.8 | - | - | - | Index Scan | SLOW | | +| combo_provider_status | false | 0.2 | 0.2 | 11031.9 | 5853.3 | 48962.4 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| combo_status_amount | false | 0.2 | 0.2 | 2955.6 | 4364.5 | 20840.6 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| combo_status_currency_date | true | 0.1 | 0.1 | 4.6 | 5.6 | 2500.8 | - | - | - | Index Scan | | | +| control | false | 0.1 | 0.1 | 14064.9 | 15417.3 | 51394.8 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| control_page50 | false | 3.7 | 3.5 | 15895.7 | 14840.4 | 51379.6 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| created_24m | false | 0.1 | 0.1 | 14547.4 | 14673.2 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| created_7d | true | 0.1 | 0.1 | 10.1 | 9.6 | 2392.3 | - | - | - | Index Scan | | | +| currency_common | false | 0.1 | 0.1 | 15144.6 | 14084.0 | 56285.7 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| currency_rare | true | 2.5 | 2.5 | 864.3 | 636.3 | 6290.5 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| customer_heavy | true | 399.4 | 1.8 | 415.7 | 532.8 | 5860.3 | - | - | - | Index Scan | SLOW | COUNT>500 | +| customer_light | true | 1.8 | 0.1 | 1.6 | 0.1 | 2204.5 | - | - | - | Sort, Index Scan | | | +| five_filter_common | false | 0.1 | 0.2 | 16647.1 | 9715.4 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| five_filter_rare | true | 20.8 | 18.6 | 21.0 | 18.8 | 1997.5 | - | - | - | Sort, Bitmap Heap Scan, BitmapAnd, Bitmap Index Scan, Index Scan | | | +| invoice_hit_direct | true | 72525.3 | 0.0 | 18048.9 | 0.0 | - | - | - | - | Sort, Index Scan | SLOW COUNT>500 SEQ:invoices | | +| invoice_hit_request | true | 83846.9 | 0.0 | 17075.0 | 0.0 | - | - | - | - | Sort, Bitmap Heap Scan, BitmapOr, Bitmap Index Scan, Index Scan | SLOW COUNT>500 SEQ:invoices | | +| invoice_miss | true | 60503.3 | 0.0 | 16473.7 | 0.0 | - | - | - | - | Sort, Result | SLOW COUNT>500 SEQ:invoices | | +| payable_type_invoice | false | 0.1 | 0.1 | 15354.1 | 15783.2 | 54239.9 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| payable_type_request | false | 1.0 | 0.3 | 480.5 | 389.8 | 5076.7 | - | - | - | Index Scan | | | +| payment_type_manual | false | 0.7 | 0.3 | 2217.9 | 6287.7 | 26829.8 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| payment_type_provider | false | 0.2 | 0.1 | 12469.5 | 15557.3 | 45414.8 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| provider_common | false | 0.3 | 0.1 | 23420.6 | 12901.0 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| provider_miss | true | 66537.5 | 0.0 | 5.7 | 0.0 | - | - | - | - | Sort, Result | SLOW | | +| provider_rare | false | 0.5 | 0.1 | 9733.1 | 9222.5 | 46707.1 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| receipt_hit | true | 105274.1 | 0.1 | 282.9 | 0.0 | - | - | - | - | Sort, Nested Loop, Index Scan | SLOW SEQ:payment_receipts | | +| receipt_miss | true | - | 0.0 | 239.3 | 0.0 | - | - | - | - | Sort, Nested Loop, Index Scan | SLOW SEQ:payment_receipts TIMEOUT | | +| search_term | true | 56.0 | 52.4 | 52.8 | 56.6 | 2119.3 | - | - | - | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | | +| search_term_status | true | 49.6 | 54.0 | 50.6 | 51.2 | 2913.3 | - | - | - | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | | +| status_common_succeeded | false | 0.1 | 0.1 | 11376.0 | 12473.4 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| status_common_succeeded_page50 | false | 2.3 | 3.8 | 12762.0 | 15925.3 | 50229.5 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| status_rare_failed | false | 0.3 | 0.3 | 3411.5 | 10938.7 | 43340.0 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| status_rare_pending | true | 0.5 | 0.1 | 831.9 | 353.9 | 9685.4 | - | - | - | Index Scan | COUNT>500 | | +| status_rare_pending_processing | true | 0.3 | 0.5 | 1481.3 | 528.3 | 24907.5 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| status_rare_processing | true | 0.5 | 0.1 | 574.2 | 177.2 | 8238.0 | - | - | - | Index Scan | COUNT>500 | | + +## Scoreboard (after) + +| # | target | result | +|---|---|---| +| G1 | single filter p95 < 300 ms | n/a (no http bench) | +| G2 | five-filter p95 < 800 ms | n/a (no http bench) | +| G3 | COUNT(*) < 500 ms (selective cases, new filters) | RED worst status_rare_pending_processing 528.3 ms; non-selective worst status_common_succeeded_page50 15925.3 ms and pre-existing filters over 500 ms (currency_common 14084.0 ms, currency_rare 636.3 ms, customer_heavy 532.8 ms) reported separately | +| G4 | control p95 within +10 % | n/a (no http bench on both phases) | +| G5 | selective plans: no watched Seq Scan, no Sort > 10k | GREEN all 18 selective cases clean | +| G8 | zero errors in the load test | n/a (no http bench) | +| G9 | page 50 < 2x page 1 | GREEN control_page50 0.1 -> 3.5 ms; status_common_succeeded_page50 0.1 -> 3.8 ms | + +G6 (<= 3 new payments indexes) and G7 (build < 15 min, no INVALID) are graded from the migration run log. diff --git a/script/perf/payments_filters/explain.rb b/script/perf/payments_filters/explain.rb new file mode 100644 index 00000000000..e1d1572ac66 --- /dev/null +++ b/script/perf/payments_filters/explain.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +# Captures EXPLAIN (ANALYZE, BUFFERS) plans for every case of the payments list +# filters matrix, going through PaymentsQuery itself so the SQL is the one the +# API runs (base scope, visibility condition, LIMIT/OFFSET, and the COUNT(*) +# Kaminari issues for meta.total_count). +# +# DATABASE_URL=postgresql://lago:changeme@db:5432/lago_perf \ +# PERF_PHASE=baseline bundle exec rails runner script/perf/payments_filters/explain.rb +# +# Env: PERF_PHASE (baseline|after|iterN, default baseline), PERF_ONLY (regex on +# case names), PERF_RUNS (default 3, median kept), PERF_ORG_SLUG (default perf-big), +# PERF_TIMEOUT (statement timeout per EXPLAIN, default 120s), PERF_SESSION_SQL (planner +# settings for the session, e.g. "SET random_page_cost = 4"), PERF_COUNT_VARIANTS=1 to +# also time two alternative counts per case: capped (LIMIT 10001, the +# BaseQuery::CappedTotalCount shape) and without the invoice visibility condition +# (to measure the share of the correlated EXISTS in the COUNT). +# +# Output: script/perf/payments_filters/plans//.txt and +# .count.txt (SQL + median plan), summary.json and summary.md. +# Organization UUIDs are redacted from the saved plans. + +raise "This script is only for development" unless Rails.env.development? + +require "json" +require_relative "cases" +require_relative "plan_stats" +ActiveRecord::Base.logger = Logger.new(nil) # keep stdout readable; SQL is in the saved plans + +PHASE = ENV.fetch("PERF_PHASE", "baseline") +ONLY = ENV["PERF_ONLY"] && Regexp.new(ENV["PERF_ONLY"]) +RUNS = Integer(ENV.fetch("PERF_RUNS", 3)) +TIMEOUT = ENV.fetch("PERF_TIMEOUT", "120s") +COUNT_VARIANTS = ENV["PERF_COUNT_VARIANTS"] == "1" + +organization = Organization.find_by!(slug: ENV.fetch("PERF_ORG_SLUG", "perf-big")) +conn = ApplicationRecord.connection +# Optional planner settings for the session, e.g. PERF_SESSION_SQL="SET random_page_cost = 4". +# Used to check that an index decision does not hinge on one cost parameter. +if ENV["PERF_SESSION_SQL"].present? + conn.execute(ENV["PERF_SESSION_SQL"]) + puts "session: #{ENV["PERF_SESSION_SQL"]}" +end +out_dir = Rails.root.join("script/perf/payments_filters/plans", PHASE) +FileUtils.mkdir_p(out_dir) + +values = PaymentsFiltersPerf::Cases.resolve_values(organization) +cases = PaymentsFiltersPerf::Cases.matrix(values) +cases.select! { |c| c[:name].match?(ONLY) } if ONLY +puts "phase=#{PHASE} org=#{organization.slug} cases=#{cases.size} runs=#{RUNS}" +puts "resolved values: #{values.except(:max_created).to_json}" + +# The exact COUNT(*) statement Kaminari runs for total_count, captured from the notification stream. +def capture_count_sql(relation) + captured = nil + callback = ->(_name, _start, _finish, _id, payload) { captured = payload[:sql] if payload[:sql].start_with?("SELECT COUNT") } + ApplicationRecord.connection.unprepared_statement do + ActiveSupport::Notifications.subscribed(callback, "sql.active_record") { relation.total_count } + end + captured +end + +def explain(conn, sql) + conn.execute("SET statement_timeout = '#{TIMEOUT}'") + conn.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{sql}").values.flatten.join("\n") +rescue ActiveRecord::QueryCanceled + "TIMEOUT after #{TIMEOUT}" +ensure + conn.execute("RESET statement_timeout") +end + +# Committed plans carry no identifiers: the organization id becomes , every other +# UUID literal (provider, customer, invoice, request ids resolved by the query) becomes . +UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i +redact = ->(text) { text.gsub(organization.id, "").gsub(UUID, "") } + +summary = [] +statements_per_case = COUNT_VARIANTS ? 4 : 2 +cases.each do |kase| + result = PaymentsQuery.call(organization:, filters: kase[:filters], search_term: kase[:search_term], + pagination: {page: kase[:page], limit: 20}) + raise "PaymentsQuery failed for #{kase[:name]}: #{result.error.inspect}" unless result.success? + + relation = result.payments + list_sql = relation.to_sql + count_sql = capture_count_sql(relation) + + statements = {list: list_sql, count: count_sql} + if COUNT_VARIANTS + bare = relation.except(:offset, :limit, :order, :includes, :preload, :eager_load) + statements[:count_capped] = "SELECT COUNT(*) FROM (#{bare.limit(BaseQuery::CappedTotalCount::MAX_COUNTED_RECORDS + 1).select(:id).to_sql}) capped" + query = PaymentsQuery.new(organization:, filters: kase[:filters], search_term: kase[:search_term], pagination: {page: 1, limit: 20}) + query.send(:validate_filters) + no_visibility = Payment.where.not(customer_id: nil).where(organization:).where.not(payable_id: nil) + no_visibility = no_visibility.where(id: query.send(:matching_ids_by_search)) if kase[:search_term].present? + statements[:count_no_visibility] = query.send(:apply_filters, no_visibility).select("COUNT(*)").to_sql + end + + entry = {name: kase[:name], page: kase[:page], selective: kase[:selective], filters: kase[:filters], search_term: kase[:search_term]} + statements.each do |kind, sql| + if sql.nil? + # ActiveRecord short-circuits an empty IN () list and answers without a query. + entry[kind] = PaymentsFiltersPerf::PlanStats.analyse("Result (no query: ActiveRecord short-circuits an empty IN list) (actual time=0.000..0.000 rows=0 loops=1)\nExecution Time: 0.000 ms") + entry[kind][:all_runs_ms] = [0.0] + File.write(out_dir.join("#{kase[:name]}#{".#{kind}" unless kind == :list}.txt"), "-- case: #{kase[:name]} (#{kind}) phase: #{PHASE} selective: #{kase[:selective]}\n-- filters: #{kase[:filters].to_json} search_term: #{kase[:search_term].inspect} page: #{kase[:page]}\n-- runs_ms: [0.0]\n(no query)\n\nResult (no query: ActiveRecord short-circuits an empty IN list) (actual time=0.000..0.000 rows=0 loops=1)\nExecution Time: 0.000 ms\n") + next + end + plans = Array.new(RUNS) { explain(conn, sql) } + median = plans.sort_by { |p| PaymentsFiltersPerf::PlanStats.execution_ms(p) || Float::INFINITY }[plans.size / 2] + stats = PaymentsFiltersPerf::PlanStats.analyse(median) + stats[:all_runs_ms] = plans.map { |p| PaymentsFiltersPerf::PlanStats.execution_ms(p)&.round(1) } + entry[kind] = stats + file = out_dir.join("#{kase[:name]}#{".#{kind}" unless kind == :list}.txt") + File.write(file, redact.call("-- case: #{kase[:name]} (#{kind}) phase: #{PHASE} selective: #{kase[:selective]}\n-- filters: #{kase[:filters].to_json} search_term: #{kase[:search_term].inspect} page: #{kase[:page]}\n-- runs_ms: #{stats[:all_runs_ms].inspect}\n#{sql}\n\n#{median}\n")) + end + + flags = PaymentsFiltersPerf::PlanStats.flags(entry[:list], entry[:count]) + entry[:flags] = flags + summary << entry + variants = COUNT_VARIANTS ? format(" capped %8.1f ms no-vis %8.1f ms", entry[:count_capped][:ms] || -1, entry[:count_no_visibility][:ms] || -1) : "" + puts format("%-34s list %9.1f ms count %9.1f ms rows=%-6d cursor=%-5s %s%s", kase[:name], entry[:list][:ms] || -1, entry[:count][:ms] || -1, + entry[:list][:rows_returned], entry[:list][:cursor_index], flags.join(" "), variants) +end + +# With PERF_ONLY, merge into an existing summary so a partial rerun does not drop the other cases. +summary_path = out_dir.join("summary.json") +if ONLY && File.exist?(summary_path) + previous = JSON.parse(File.read(summary_path), symbolize_names: true)[:cases] + summary = (previous.reject { |c| summary.any? { |n| n[:name] == c[:name] } } + summary).sort_by { |c| c[:name] } +end +File.write(summary_path, JSON.pretty_generate({phase: PHASE, generated_at: Time.current.iso8601, runs: RUNS, + values: values.except(:max_created), cases: summary})) + +md = PaymentsFiltersPerf::PlanStats.summary_markdown(PHASE, summary, runs: RUNS, variants: COUNT_VARIANTS) +File.write(out_dir.join("summary.md"), md) +puts "wrote #{out_dir}/summary.{md,json} and #{summary.size * statements_per_case} plan files" diff --git a/script/perf/payments_filters/generate.rb b/script/perf/payments_filters/generate.rb new file mode 100644 index 00000000000..b30ab5be72a --- /dev/null +++ b/script/perf/payments_filters/generate.rb @@ -0,0 +1,334 @@ +# frozen_string_literal: true + +# Builds a production-shaped synthetic dataset for the payments list filters +# performance work. Rails runner, development only, throwaway database only. +# +# DATABASE_URL=postgresql://lago:changeme@db:5432/lago_perf \ +# bundle exec rails runner script/perf/payments_filters/generate.rb +# +# Everything here is synthetic. The skews are illustrative round numbers +# (for example "92 % succeeded"), not production figures. Re-tune them through +# the env knobs below when real distributions are known; keep production values +# out of this file. +# +# Knobs (env): +# PERF_BIG_PAYMENTS payments in the big organization (default 5_000_000) +# PERF_SMALL_ORGS number of other organizations (default 50) +# PERF_SMALL_MIN smallest other organization, payments (default 1_000) +# PERF_SMALL_MAX largest other organization, payments (default 200_000) +# PERF_BIG_CUSTOMERS customers in the big organization (default 50_000) +# PERF_MONTHS created_at spread, months back from now (default 24) +# PERF_SEED seed for Ruby and PostgreSQL randomness (default 42) +# PERF_ALLOW_DB=1 run against a database whose name has no "perf" in it +# +# Dimension tables (organizations, providers) go through the factories so the +# rows look like the app made them. Fact tables (customers, invoices, +# payment_requests, payments, receipts, methods) are set-based SQL over +# generate_series(): minutes instead of hours, same determinism. +# +# Non-unique indexes on the fact tables are dropped before the load and +# rebuilt afterwards (timed, logged). That keeps the load fast and leaves +# compact indexes, which is what a vacuumed production table looks like. + +raise "This generator is only for development" unless Rails.env.development? + +require "factory_bot_rails" +require "json" + +FactoryBot.find_definitions if FactoryBot.factories.none? +ActiveJob::Base.queue_adapter = :test +ActiveRecord::Base.logger = Logger.new(nil) # keep stdout readable; SQL is in the saved plans + +BIG_PAYMENTS = Integer(ENV.fetch("PERF_BIG_PAYMENTS", 5_000_000)) +SMALL_ORGS = Integer(ENV.fetch("PERF_SMALL_ORGS", 50)) +SMALL_MIN = Integer(ENV.fetch("PERF_SMALL_MIN", 1_000)) +SMALL_MAX = Integer(ENV.fetch("PERF_SMALL_MAX", 200_000)) +BIG_CUSTOMERS = Integer(ENV.fetch("PERF_BIG_CUSTOMERS", 50_000)) +MONTHS = Integer(ENV.fetch("PERF_MONTHS", 24)) +SEED = Integer(ENV.fetch("PERF_SEED", 42)) +FIRST_ATTEMPT_SHARE = 0.9 # the remaining 10 % are failed retries on an existing payable + +FACT_TABLES = %w[payments payment_receipts invoices payment_requests invoices_payment_requests payment_methods customers].freeze + +conn = ApplicationRecord.connection +db_name = conn.current_database +unless db_name.include?("perf") || ENV["PERF_ALLOW_DB"] == "1" + abort "Refusing to run against #{db_name.inspect}: the database name must contain \"perf\" (or set PERF_ALLOW_DB=1)." +end + +started = Process.clock_gettime(Process::CLOCK_MONOTONIC) +elapsed = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) - started } +log = ->(msg) { puts format("[%7.1fs] %s", elapsed.call, msg) } + +run = lambda do |label, sql| + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = conn.execute(sql) + log.call(format("%-52s %8.1fs%s", label, Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0, + (result.respond_to?(:cmd_tuples) && result.cmd_tuples.positive?) ? " (#{result.cmd_tuples} rows)" : "")) + result +end + +log.call("database=#{db_name} big=#{BIG_PAYMENTS} small_orgs=#{SMALL_ORGS} seed=#{SEED} months=#{MONTHS}") + +# --- 1. Reset ----------------------------------------------------------------- +run.call("truncate organizations cascade", "TRUNCATE TABLE organizations CASCADE") +run.call("truncate users cascade", "TRUNCATE TABLE users CASCADE") +conn.execute("DROP TABLE IF EXISTS perf_orgs, perf_customers, perf_rows, perf_facts") + +# --- 2. Organizations and providers (factories) ------------------------------ +rng = Random.new(SEED) +orgs = [] + +big_org = FactoryBot.create(:organization, name: "Perf Big Org", slug: "perf-big", webhook_url: nil, document_number_prefix: "PERFBIG") +orgs << {org: big_org, idx: 0, n_payments: BIG_PAYMENTS, n_customers: BIG_CUSTOMERS} +SMALL_ORGS.times do |i| + size = (SMALL_MIN * ((SMALL_MAX.to_f / SMALL_MIN)**rng.rand)).round + org = FactoryBot.create(:organization, name: "Perf Org #{i + 1}", slug: "perf-#{i + 1}", webhook_url: nil, document_number_prefix: "PERF#{i + 1}") + orgs << {org:, idx: i + 1, n_payments: size, n_customers: [size / 50, 10].max} +end +log.call("created #{orgs.size} organizations via factories") + +orgs.each do |entry| + org = entry[:org] + entry[:stripe] = FactoryBot.create(:stripe_provider, organization: org, code: "perf_stripe", name: "Perf Stripe") + # Every third organization also has a second provider type so provider_type is a real filter. + entry[:second] = + if entry[:idx] == 0 || entry[:idx] % 3 == 0 + FactoryBot.create(:gocardless_provider, organization: org, code: "perf_gocardless", name: "Perf GoCardless") + elsif entry[:idx] % 3 == 1 + FactoryBot.create(:adyen_provider, organization: org, code: "perf_adyen", name: "Perf Adyen") + end +end +log.call("created payment providers via factories") + +conn.execute(<<~SQL) + CREATE UNLOGGED TABLE perf_orgs ( + idx int PRIMARY KEY, organization_id uuid, billing_entity_id uuid, prefix text, + n_payments int, n_first int, n_customers int, stripe_id uuid, second_id uuid + ) +SQL +values = orgs.map do |e| + be = e[:org].default_billing_entity + "(#{e[:idx]}, #{conn.quote(e[:org].id)}, #{conn.quote(be.id)}, #{conn.quote(be.document_number_prefix)}, #{e[:n_payments]}, " \ + "#{(e[:n_payments] * FIRST_ATTEMPT_SHARE).floor}, #{e[:n_customers]}, #{conn.quote(e[:stripe].id)}, #{conn.quote(e[:second]&.id)})" +end +conn.execute("INSERT INTO perf_orgs VALUES #{values.join(", ")}") + +# --- 3. Drop non-unique indexes on the fact tables (rebuilt at the end) ------ +saved_indexes = conn.select_rows(<<~SQL) + SELECT tablename, indexname, indexdef FROM pg_indexes + WHERE schemaname = 'public' AND tablename IN (#{FACT_TABLES.map { |t| "'#{t}'" }.join(", ")}) + AND indexdef NOT LIKE 'CREATE UNIQUE INDEX%' + ORDER BY tablename, indexname +SQL +saved_indexes.each { |(_table, name, _def)| conn.execute("DROP INDEX IF EXISTS #{conn.quote_table_name(name)}") } +log.call("dropped #{saved_indexes.size} non-unique indexes for the bulk load") + +# --- 4. Customers and payment methods ----------------------------------------- +run.call("insert customers", <<~SQL) + INSERT INTO customers (id, organization_id, billing_entity_id, external_id, name, slug, sequential_id, currency, created_at, updated_at) + SELECT gen_random_uuid(), o.organization_id, o.billing_entity_id, + 'perf-cust-' || o.idx || '-' || c, 'Perf Customer ' || o.idx || '-' || c, + o.prefix || '-' || lpad(c::text, 3, '0'), c, 'EUR', + now() - interval '1 month' * #{MONTHS}, now() + FROM perf_orgs o CROSS JOIN LATERAL generate_series(1, o.n_customers) AS c +SQL + +conn.execute("SELECT setseed(#{(SEED % 1000) / 1000.0})") +# One saved payment method per customer, on the Stripe provider. bacs_debit and +# customer_balance exist only here, never in the payments jsonb: they exercise +# the payment_methods fallback branch of the payment_method_type filter. +run.call("insert payment_methods", <<~SQL) + INSERT INTO payment_methods (id, organization_id, customer_id, payment_provider_id, provider_method_id, provider_method_type, is_default, created_at, updated_at) + SELECT gen_random_uuid(), s.organization_id, s.id, s.stripe_id, 'pm_perf_' || s.sequential_id || '_' || s.idx, + CASE WHEN s.r < 0.80 THEN 'card' WHEN s.r < 0.92 THEN 'sepa_debit' WHEN s.r < 0.96 THEN 'link' + WHEN s.r < 0.985 THEN 'us_bank_account' WHEN s.r < 0.995 THEN 'bacs_debit' ELSE 'customer_balance' END, + true, s.created_at, s.created_at + FROM (SELECT c.organization_id, c.id, c.sequential_id, c.created_at, o.stripe_id, o.idx, random() AS r + FROM customers c JOIN perf_orgs o ON o.organization_id = c.organization_id) s +SQL + +run.call("build perf_customers", <<~SQL) + CREATE UNLOGGED TABLE perf_customers AS + SELECT c.organization_id, c.sequential_id AS cidx, c.id AS customer_id, c.slug, pm.id AS payment_method_id + FROM customers c LEFT JOIN payment_methods pm ON pm.customer_id = c.id +SQL +conn.execute("CREATE INDEX ON perf_customers (organization_id, cidx)") + +# --- 5. Per-payment random draws ---------------------------------------------- +run.call("draw perf_rows", <<~SQL) + CREATE UNLOGGED TABLE perf_rows AS + SELECT o.idx AS org_idx, o.organization_id, o.billing_entity_id, o.prefix, o.stripe_id, o.second_id, + o.n_customers, o.n_first, n, + gen_random_uuid() AS payment_id, + random() AS r_status, random() AS r_cur, random() AS r_type, random() AS r_method, random() AS r_pm, + random() AS r_receipt, random() AS r_request, random() AS r_inv_status, random() AS r_amt1, + random() AS r_amt2, random() AS r_big, random() AS r_cust, random() AS r_time + FROM perf_orgs o CROSS JOIN LATERAL generate_series(1, o.n_payments) AS n +SQL +conn.execute("CREATE INDEX ON perf_rows (organization_id, n)") + +# First attempts carry their own payable; retries (n > n_first) reuse the +# payable, customer and currency of row n - n_first and are always failed, so +# the partial unique index on pending/processing provider payments holds. +run.call("derive perf_facts (first attempts)", <<~SQL) + CREATE UNLOGGED TABLE perf_facts AS + SELECT b.org_idx, b.organization_id, b.billing_entity_id, b.prefix, b.n, b.payment_id, true AS first_attempt, + gen_random_uuid() AS invoice_id, + CASE WHEN b.r_request < 0.05 THEN gen_random_uuid() END AS payment_request_id, + b.r_request < 0.05 AS is_request, + CASE WHEN b.r_request < 0.025 THEN 1 ELSE 2 END AS extra_invoices, + 1 + floor(power(b.r_cust, 3) * b.n_customers)::int AS cidx, + now() - (interval '1 month' * #{MONTHS}) * power(b.r_time, 0.6) AS created_at, + CASE WHEN b.r_cur < 0.95 THEN 'EUR' WHEN b.r_cur < 0.99 THEN 'USD' ELSE 'GBP' END AS currency, + CASE WHEN b.r_status < 0.92 THEN 'succeeded' WHEN b.r_status < 0.97 THEN 'failed' + WHEN b.r_status < 0.99 THEN 'pending' ELSE 'processing' END AS status, + CASE WHEN b.r_type < 0.05 THEN 'manual' ELSE 'provider' END AS payment_type, + CASE WHEN b.r_type < 0.05 THEN NULL WHEN b.r_type < 0.85 THEN b.stripe_id ELSE COALESCE(b.second_id, b.stripe_id) END AS provider_id, + (b.r_type >= 0.05 AND (b.r_type < 0.85 OR b.second_id IS NULL)) AS is_stripe, + CASE WHEN b.r_method < 0.85 THEN 'card' WHEN b.r_method < 0.95 THEN 'sepa_debit' WHEN b.r_method < 0.98 THEN 'link' + WHEN b.r_method < 0.995 THEN 'us_bank_account' WHEN b.r_method < 0.999 THEN 'boleto' ELSE 'crypto' END AS method_type, + b.r_pm < 0.9 AS pm_in_json, + CASE WHEN b.r_inv_status < 0.93 THEN 1 WHEN b.r_inv_status < 0.95 THEN 0 WHEN b.r_inv_status < 0.97 THEN 2 + WHEN b.r_inv_status < 0.98 THEN 4 WHEN b.r_inv_status < 0.99 THEN 7 ELSE 5 END AS invoice_status, + CASE WHEN b.r_big < 0.0001 THEN 2147483648::bigint + floor(b.r_amt1 * 1e12)::bigint + ELSE greatest(1, round(exp(8.5 + 1.2 * sqrt(-2 * ln(greatest(b.r_amt1, 1e-12))) * cos(2 * pi() * b.r_amt2))))::bigint END AS amount_cents, + b.r_receipt + FROM perf_rows b + WHERE b.n <= b.n_first +SQL +conn.execute("CREATE INDEX ON perf_facts (organization_id, n)") + +run.call("derive perf_facts (retries)", <<~SQL) + INSERT INTO perf_facts + SELECT b.org_idx, b.organization_id, b.billing_entity_id, b.prefix, b.n, b.payment_id, false, + f.invoice_id, f.payment_request_id, f.is_request, 0, f.cidx, + f.created_at - interval '1 day' * (1 + floor(b.r_time * 3)), + f.currency, 'failed', f.payment_type, f.provider_id, f.is_stripe, f.method_type, f.pm_in_json, + f.invoice_status, f.amount_cents, 1.0 + FROM perf_rows b + JOIN perf_facts f ON f.organization_id = b.organization_id AND f.n = b.n - b.n_first + WHERE b.n > b.n_first +SQL +conn.execute("DROP TABLE perf_rows") + +# --- 6. Payables ------------------------------------------------------------- +run.call("insert invoices", <<~SQL) + INSERT INTO invoices (id, organization_id, billing_entity_id, customer_id, number, status, payment_status, currency, + total_amount_cents, issuing_date, payment_due_date, created_at, updated_at, organization_sequential_id) + SELECT f.invoice_id, f.organization_id, f.billing_entity_id, c.customer_id, + f.prefix || '-' || to_char(f.created_at, 'YYYYMM') || '-' || lpad(f.n::text, 9, '0'), + f.invoice_status, CASE WHEN f.status = 'succeeded' THEN 1 ELSE 0 END, f.currency, + f.amount_cents, f.created_at::date, f.created_at::date + 30, f.created_at - interval '1 hour', f.created_at, f.n + FROM perf_facts f JOIN perf_customers c ON c.organization_id = f.organization_id AND c.cidx = f.cidx + WHERE f.first_attempt +SQL + +run.call("insert payment_requests", <<~SQL) + INSERT INTO payment_requests (id, organization_id, customer_id, amount_cents, amount_currency, payment_status, email, created_at, updated_at) + SELECT f.payment_request_id, f.organization_id, c.customer_id, f.amount_cents, f.currency, + CASE WHEN f.status = 'succeeded' THEN 1 WHEN f.status = 'failed' THEN 2 ELSE 0 END, + 'perf@example.com', f.created_at - interval '1 hour', f.created_at + FROM perf_facts f JOIN perf_customers c ON c.organization_id = f.organization_id AND c.cidx = f.cidx + WHERE f.first_attempt AND f.is_request +SQL + +# A payment request covers its own invoice plus the next one or two invoices of the same organization. +run.call("insert invoices_payment_requests", <<~SQL) + INSERT INTO invoices_payment_requests (invoice_id, payment_request_id, organization_id, created_at, updated_at) + SELECT g.invoice_id, f.payment_request_id, f.organization_id, f.created_at - interval '1 hour', f.created_at + FROM perf_facts f + JOIN perf_facts g ON g.organization_id = f.organization_id AND g.first_attempt + AND g.n BETWEEN f.n AND f.n + f.extra_invoices + WHERE f.first_attempt AND f.is_request +SQL + +# --- 7. Payments and receipts ------------------------------------------------- +run.call("insert payments", <<~SQL) + INSERT INTO payments (id, organization_id, customer_id, payable_type, payable_id, amount_cents, amount_currency, status, + payable_payment_status, payment_type, reference, provider_payment_id, payment_provider_id, + payment_method_id, provider_payment_method_data, created_at, updated_at) + SELECT f.payment_id, f.organization_id, c.customer_id, + CASE WHEN f.is_request THEN 'PaymentRequest' ELSE 'Invoice' END, + CASE WHEN f.is_request THEN f.payment_request_id ELSE f.invoice_id END, + f.amount_cents, f.currency, f.status, f.status::payment_payable_payment_status, f.payment_type::payment_type, + CASE WHEN f.payment_type = 'manual' THEN 'Bank transfer ' || f.n END, + CASE WHEN f.payment_type = 'provider' THEN 'pi_perf_' || f.org_idx || '_' || f.n END, + f.provider_id, + CASE WHEN f.payment_type = 'provider' THEN c.payment_method_id END, + CASE WHEN f.is_stripe AND f.pm_in_json THEN jsonb_build_object('type', f.method_type, 'last4', '4242', 'brand', 'visa') + ELSE '{}'::jsonb END, + f.created_at, f.created_at + FROM perf_facts f JOIN perf_customers c ON c.organization_id = f.organization_id AND c.cidx = f.cidx +SQL + +conn.execute("ALTER TABLE payment_receipts DISABLE TRIGGER before_payment_receipt_insert") +begin + # Receipts on ~60 % of succeeded payments, numbered the way the trigger does it. + run.call("insert payment_receipts", <<~SQL) + INSERT INTO payment_receipts (id, number, payment_id, organization_id, billing_entity_id, created_at, updated_at) + SELECT gen_random_uuid(), + c.slug || '-RCPT-' || lpad((row_number() OVER (PARTITION BY c.customer_id ORDER BY f.created_at, f.n))::text, 6, '0'), + f.payment_id, f.organization_id, f.billing_entity_id, f.created_at + interval '1 minute', f.created_at + interval '1 minute' + FROM perf_facts f JOIN perf_customers c ON c.organization_id = f.organization_id AND c.cidx = f.cidx + WHERE f.status = 'succeeded' AND f.r_receipt < 0.65 + SQL +ensure + conn.execute("ALTER TABLE payment_receipts ENABLE TRIGGER before_payment_receipt_insert") +end +run.call("sync customers.payment_receipt_counter", <<~SQL) + UPDATE customers c SET payment_receipt_counter = r.n + FROM (SELECT p.customer_id, count(*) AS n FROM payment_receipts pr JOIN payments p ON p.id = pr.payment_id GROUP BY p.customer_id) r + WHERE r.customer_id = c.id +SQL + +conn.execute("DROP TABLE perf_facts, perf_customers, perf_orgs") + +# --- 8. Rebuild indexes, vacuum, analyze ------------------------------------- +index_times = saved_indexes.map do |(table, name, definition)| + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + conn.execute(definition) + secs = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 + log.call(format("rebuilt %-62s %8.1fs", name, secs)) + [table, name, secs] +end + +FACT_TABLES.each { |t| run.call("vacuum analyze #{t}", "VACUUM (ANALYZE) #{t}") } +run.call("analyze payment_providers, organizations", "ANALYZE payment_providers; ANALYZE organizations; ANALYZE billing_entities") + +# --- 9. Report --------------------------------------------------------------- +puts +puts "Row counts:" +FACT_TABLES.each do |t| + puts format(" %-28s %12s", t, conn.select_value("SELECT count(*) FROM #{t}").to_s) +end +puts format(" %-28s %12s", "database size", conn.select_value("SELECT pg_size_pretty(pg_database_size(current_database()))")) +puts format(" %-28s %12s", "payments total (heap+idx)", conn.select_value("SELECT pg_size_pretty(pg_total_relation_size('payments'))")) +puts format(" %-28s %12s", "invoices total (heap+idx)", conn.select_value("SELECT pg_size_pretty(pg_total_relation_size('invoices'))")) + +puts +puts "Big organization (#{big_org.slug}) distributions:" +[ + ["payable_payment_status", "SELECT payable_payment_status::text, count(*) FROM payments WHERE organization_id = '#{big_org.id}' GROUP BY 1 ORDER BY 2 DESC"], + ["amount_currency", "SELECT amount_currency, count(*) FROM payments WHERE organization_id = '#{big_org.id}' GROUP BY 1 ORDER BY 2 DESC"], + ["payment_type", "SELECT payment_type::text, count(*) FROM payments WHERE organization_id = '#{big_org.id}' GROUP BY 1 ORDER BY 2 DESC"], + ["payable_type", "SELECT payable_type, count(*) FROM payments WHERE organization_id = '#{big_org.id}' GROUP BY 1 ORDER BY 2 DESC"], + ["provider type", "SELECT pp.type, count(*) FROM payments p LEFT JOIN payment_providers pp ON pp.id = p.payment_provider_id WHERE p.organization_id = '#{big_org.id}' GROUP BY 1 ORDER BY 2 DESC"], + ["jsonb method type", "SELECT coalesce(provider_payment_method_data->>'type', '(none)'), count(*) FROM payments WHERE organization_id = '#{big_org.id}' GROUP BY 1 ORDER BY 2 DESC"], + ["heaviest customers", "SELECT c.external_id, count(*) FROM payments p JOIN customers c ON c.id = p.customer_id WHERE p.organization_id = '#{big_org.id}' GROUP BY 1 ORDER BY 2 DESC LIMIT 3"] +].each do |label, sql| + puts " #{label}: " + conn.select_rows(sql).map { |k, v| "#{k}=#{v}" }.join(", ") +end + +puts +puts "Index rebuild times on the loaded dataset (reference for CREATE INDEX cost):" +index_times.select { |(table, _, _)| table == "payments" }.each { |(_, name, secs)| puts format(" %-62s %6.1fs", name, secs) } + +credentials_path = Rails.root.join("tmp/perf_payments_filters_credentials.json") +File.write(credentials_path, JSON.pretty_generate({ + organization_id: big_org.id, organization_slug: big_org.slug, + api_key: big_org.api_keys.first.value, database: db_name, generated_at: Time.current.iso8601, + params: {big_payments: BIG_PAYMENTS, small_orgs: SMALL_ORGS, seed: SEED, months: MONTHS} +}), perm: 0o600) +puts +log.call("done. Credentials for bench.rb: #{credentials_path} (local only, never commit)") diff --git a/script/perf/payments_filters/index_probe.rb b/script/perf/payments_filters/index_probe.rb new file mode 100644 index 00000000000..31f278027ed --- /dev/null +++ b/script/perf/payments_filters/index_probe.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Builds one candidate index with CREATE INDEX CONCURRENTLY, times it, reports its +# size and INVALID state, then optionally runs explain.rb on a subset of cases. +# Throwaway perf database only. +# +# PERF_INDEX_NAME=perf_a_status PERF_INDEX_DDL="ON payments (organization_id, payable_payment_status, created_at DESC, id)" \ +# PERF_PHASE=idx_a PERF_ONLY='^status_' bundle exec rails runner script/perf/payments_filters/index_probe.rb +# +# PERF_INDEX_DROP=1 drops the index instead (CONCURRENTLY). Output goes to stdout; +# build times and sizes are the figures quoted for G7 in the internal document. + +raise "This script is only for development" unless Rails.env.development? + +ActiveRecord::Base.logger = Logger.new(nil) +conn = ApplicationRecord.connection +abort "database name must contain perf" unless conn.current_database.include?("perf") || ENV["PERF_ALLOW_DB"] == "1" + +name = ENV.fetch("PERF_INDEX_NAME") +if ENV["PERF_INDEX_DROP"] == "1" + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + conn.execute("DROP INDEX CONCURRENTLY IF EXISTS #{conn.quote_table_name(name)}") + puts format("dropped %s in %.1fs", name, Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) + exit +end + +ddl = "CREATE INDEX CONCURRENTLY IF NOT EXISTS #{conn.quote_table_name(name)} #{ENV.fetch("PERF_INDEX_DDL")}" +puts ddl +conn.execute("SET statement_timeout = 0") +conn.execute(ENV["PERF_SESSION_SQL"]) if ENV["PERF_SESSION_SQL"].present? +t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) +conn.execute(ddl) +secs = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 +row = conn.select_one(<<~SQL) + SELECT pg_size_pretty(pg_relation_size(i.indexrelid)) AS size, pg_relation_size(i.indexrelid) AS bytes, x.indisvalid + FROM pg_stat_user_indexes i JOIN pg_index x ON x.indexrelid = i.indexrelid + WHERE i.indexrelname = #{conn.quote(name)} +SQL +puts format("built %s in %.1fs size=%s valid=%s", name, secs, row["size"], row["indisvalid"]) +conn.execute("ANALYZE #{ddl[/ON (\w+)/, 1]}") + +if ENV["PERF_ONLY"] + ENV["PERF_PHASE"] ||= "idx_#{name}" + load Rails.root.join("script/perf/payments_filters/explain.rb") +end diff --git a/script/perf/payments_filters/phase0.sql b/script/perf/payments_filters/phase0.sql new file mode 100644 index 00000000000..0da51e6a5ad --- /dev/null +++ b/script/perf/payments_filters/phase0.sql @@ -0,0 +1,94 @@ +-- Phase 0: production scale for the payments list filters performance work. +-- Read-only. Run on a READ REPLICA with psql. Results are confidential: +-- paste them into the internal performance document only, never into a PR, +-- a commit, or this repository. +-- +-- psql "$REPLICA_URL" -v big="''" -f phase0.sql +-- +-- Run the first block without :big, pick the top organization_id from the +-- second query, then rerun with -v big=... for the per-column blocks. + +\timing on +\pset pager off + +-- 0. Engine version and the session ceilings the app runs under. +SELECT version(); +SHOW statement_timeout; +SHOW lock_timeout; +-- Planner cost parameters: the synthetic runs showed that whether the planner picks a +-- (organization_id, ) index for a common value depends on these. +SHOW random_page_cost; +SHOW seq_page_cost; +SHOW effective_cache_size; +SHOW work_mem; +SHOW shared_buffers; + +-- 1. Size and distribution. +SELECT count(*) AS payments FROM payments; +SELECT organization_id, count(*) FROM payments GROUP BY 1 ORDER BY 2 DESC LIMIT 10; +SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY c) AS p50_org, + percentile_disc(0.99) WITHIN GROUP (ORDER BY c) AS p99_org, + max(c) AS max_org, count(*) AS orgs + FROM (SELECT count(*) c FROM payments GROUP BY organization_id) t; +SELECT count(*) AS payment_receipts FROM payment_receipts; +SELECT count(*) AS invoices FROM invoices; +SELECT count(*) AS invoices_payment_requests FROM invoices_payment_requests; +SELECT count(*) AS payment_requests FROM payment_requests; +SELECT count(*) AS payment_methods FROM payment_methods; + +-- 2. Per-column selectivity on the biggest org (:big). +SELECT payable_payment_status, count(*) FROM payments WHERE organization_id = :big GROUP BY 1 ORDER BY 2 DESC; +SELECT amount_currency, count(*) FROM payments WHERE organization_id = :big GROUP BY 1 ORDER BY 2 DESC; +SELECT payment_type, payable_type, count(*) FROM payments WHERE organization_id = :big GROUP BY 1,2 ORDER BY 3 DESC; +SELECT provider_payment_method_data->>'type' AS method_type, count(*) FROM payments WHERE organization_id = :big GROUP BY 1 ORDER BY 2 DESC; +SELECT count(*) FILTER (WHERE provider_payment_method_data = '{}'::jsonb) AS empty_pm_data, + count(*) FILTER (WHERE payment_method_id IS NOT NULL) AS with_payment_method_id, + count(*) AS total + FROM payments WHERE organization_id = :big; +SELECT pm.provider_method_type, count(*) + FROM payments p JOIN payment_methods pm ON pm.id = p.payment_method_id + WHERE p.organization_id = :big AND (p.provider_payment_method_data->>'type') IS NULL + GROUP BY 1 ORDER BY 2 DESC; +SELECT payment_provider_id, count(*) FROM payments WHERE organization_id = :big GROUP BY 1 ORDER BY 2 DESC; +SELECT pp.type, count(*) FROM payments p LEFT JOIN payment_providers pp ON pp.id = p.payment_provider_id + WHERE p.organization_id = :big GROUP BY 1 ORDER BY 2 DESC; +SELECT count(*) AS customers, max(c) AS max_payments_per_customer, + percentile_disc(0.99) WITHIN GROUP (ORDER BY c) AS p99_payments_per_customer + FROM (SELECT customer_id, count(*) c FROM payments WHERE organization_id = :big GROUP BY 1) t; +SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY amount_cents) AS p50_amount, + percentile_disc(0.99) WITHIN GROUP (ORDER BY amount_cents) AS p99_amount, + max(amount_cents) AS max_amount, + count(*) FILTER (WHERE amount_cents > 2147483647) AS above_int32 + FROM payments WHERE organization_id = :big; +SELECT date_trunc('month', created_at) AS month, count(*) FROM payments WHERE organization_id = :big GROUP BY 1 ORDER BY 1; +SELECT count(*) AS receipts_big_org, + count(*) FILTER (WHERE number <> upper(number)) AS receipts_with_lowercase + FROM payment_receipts WHERE organization_id = :big; +SELECT count(*) AS invoices_big_org, + count(*) FILTER (WHERE number <> upper(number)) AS invoices_with_lowercase, + count(*) FILTER (WHERE status NOT IN (0,1,2,4,7)) AS invisible_status + FROM invoices WHERE organization_id = :big; +SELECT count(*) AS payments_via_payment_request, + avg(n)::numeric(6,2) AS avg_invoices_per_request + FROM (SELECT pr.id, count(ipr.invoice_id) n + FROM payment_requests pr JOIN invoices_payment_requests ipr ON ipr.payment_request_id = pr.id + WHERE pr.organization_id = :big GROUP BY pr.id) t; + +-- 3. Planner statistics as it sees them. +SELECT attname, n_distinct, most_common_vals, most_common_freqs, correlation + FROM pg_stats + WHERE tablename = 'payments' + AND attname IN ('organization_id','payable_payment_status','amount_currency','payment_type','payable_type','amount_cents','created_at','payment_provider_id','customer_id'); + +-- 4. Write rate and index usage (to price every new index). +SELECT n_tup_ins, n_tup_upd, n_tup_hot_upd, n_tup_del, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze + FROM pg_stat_user_tables WHERE relname = 'payments'; +SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size + FROM pg_stat_user_indexes WHERE relname = 'payments' ORDER BY idx_scan, indexrelname; +SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size + FROM pg_stat_user_indexes WHERE relname IN ('payment_receipts','invoices_payment_requests') ORDER BY relname, idx_scan; +SELECT pg_size_pretty(pg_total_relation_size('payments')) AS payments_total, + pg_size_pretty(pg_relation_size('payments')) AS payments_heap, + pg_size_pretty(pg_total_relation_size('invoices')) AS invoices_total, + pg_size_pretty(pg_total_relation_size('payment_receipts')) AS receipts_total; +SELECT stats_reset FROM pg_stat_database WHERE datname = current_database(); diff --git a/script/perf/payments_filters/plan_stats.rb b/script/perf/payments_filters/plan_stats.rb new file mode 100644 index 00000000000..c6764604f2e --- /dev/null +++ b/script/perf/payments_filters/plan_stats.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Parses EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) output into the fields the +# summaries and the scoreboard use. Plain Ruby, shared by explain.rb and +# summarize.rb. +module PaymentsFiltersPerf + module PlanStats + WATCHED_TABLES = %w[payments invoices payment_receipts].freeze + + module_function + + # nil when the statement timed out. + def execution_ms(plan) + plan[/Execution Time: ([\d.]+) ms/, 1]&.to_f + end + + def analyse(plan) + top = plan.lines.first.to_s + sort_rows = plan.scan(/(?:->\s+|^\s*)(?:Incremental )?Sort\s.*?actual time=[\d.]+\.\.[\d.]+ rows=(\d+)/).flatten.map(&:to_i) + seq_scans = plan.scan(/Seq Scan on (\w+)/).flatten.uniq & WATCHED_TABLES + buffers = plan[/Buffers: shared hit=(\d+)(?: read=(\d+))?/] + hit, read = buffers ? [Regexp.last_match(1).to_i, Regexp.last_match(2).to_i] : [nil, nil] + { + ms: execution_ms(plan)&.round(1), + timeout: plan.start_with?("TIMEOUT"), + rows_returned: top[/rows=(\d+) loops/, 1].to_i, + rows_scanned: plan.scan(/actual time=[\d.]+\.\.[\d.]+ rows=(\d+) loops=(\d+)/).sum { |r, l| r.to_i * l.to_i }, + nodes: plan.scan(/->\s+([A-Z][A-Za-z ]+?)(?:\s+on|\s+using|\s+\(|$)/).flatten.map(&:strip).uniq, + seq_scan_watched: seq_scans, + sort_rows_max: sort_rows.max || 0, + shared_hit: hit, + shared_read: read, + cursor_index: plan.include?("index_payments_by_cursor"), + sort_on_created_at: plan.match?(/Sort Key: payments\.created_at/) + } + end + + # Flags used in summaries: SLOW (> 200 ms list), COUNT>500, SEQ:, SORT>10k, TIMEOUT. + def flags(list, count) + flags = [] + flags << "SLOW" if list[:timeout] || (list[:ms] && list[:ms] > 200.0) + flags << "COUNT>500" if count[:timeout] || (count[:ms] && count[:ms] > 500.0) + seq = (list[:seq_scan_watched] + count[:seq_scan_watched]).uniq + flags << "SEQ:#{seq.join(",")}" if seq.any? + flags << "SORT>10k" if list[:sort_rows_max] > 10_000 + flags << "TIMEOUT" if list[:timeout] || count[:timeout] + flags + end + + def summary_markdown(phase, entries, runs:, variants: false) + md = "# Plans: #{phase}\n\n" + md << "Median of #{runs} EXPLAIN (ANALYZE, BUFFERS) runs per statement. Synthetic dataset. `ms` is nil when the statement hit the timeout.\n\n" + extra_head = variants ? " count capped ms | count no-visibility ms |" : "" + md << "| case | page | selective | list ms | count ms |#{extra_head} rows | list nodes | seq scan (watched) | max sort rows | shared read (list) | ordering by cursor index | flags |\n" + md << "|---|---|---|---|---|#{"---|---|" if variants}---|---|---|---|---|---|---|\n" + entries.each do |e| + seq = (e[:list][:seq_scan_watched] + e[:count][:seq_scan_watched]).uniq.join(", ") + extra = variants ? " #{e.dig(:count_capped, :ms)} | #{e.dig(:count_no_visibility, :ms)} |" : "" + md << "| #{e[:name]} | #{e[:page]} | #{e[:selective]} | #{e[:list][:ms] || "timeout"} | #{e[:count][:ms] || "timeout"} |#{extra} #{e[:list][:rows_returned]} " \ + "| #{e[:list][:nodes].join(", ")} | #{seq} | #{e[:list][:sort_rows_max]} | #{e[:list][:shared_read]} | #{e[:list][:cursor_index] && !e[:list][:sort_on_created_at]} | #{e[:flags].join(" ")} |\n" + end + md + end + end +end diff --git a/script/perf/payments_filters/plans/after/amount_common_from_p50.count.txt b/script/perf/payments_filters/plans/after/amount_common_from_p50.count.txt new file mode 100644 index 00000000000..6beeed99f7d --- /dev/null +++ b/script/perf/payments_filters/plans/after/amount_common_from_p50.count.txt @@ -0,0 +1,29 @@ +-- case: amount_common_from_p50 (count) phase: after selective: false +-- filters: {"amount_from":4914} search_term: nil page: 1 +-- runs_ms: [9161.0, 9264.5, 8918.3] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 4914::bigint) + +Aggregate (cost=13556594.26..13556594.27 rows=1 width=8) (actual time=9158.768..9158.772 rows=1 loops=1) + Buffers: shared hit=9497639 read=164193 + I/O Timings: shared/local read=948.435 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13553501.19 rows=1237225 width=0) (actual time=83.864..9077.264 rows=2476707 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2523293 + Buffers: shared hit=9497639 read=164193 + I/O Timings: shared/local read=948.435 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=2374955) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=9497614 read=2206 + I/O Timings: shared/local read=189.787 +Planning: + Buffers: shared hit=8 +Planning Time: 0.132 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 2.071 ms, Inlining 6.953 ms, Optimization 38.792 ms, Emission 37.964 ms, Total 85.780 ms +Execution Time: 9160.992 ms diff --git a/script/perf/payments_filters/plans/after/amount_common_from_p50.txt b/script/perf/payments_filters/plans/after/amount_common_from_p50.txt new file mode 100644 index 00000000000..5acd50919c7 --- /dev/null +++ b/script/perf/payments_filters/plans/after/amount_common_from_p50.txt @@ -0,0 +1,21 @@ +-- case: amount_common_from_p50 (list) phase: after selective: false +-- filters: {"amount_from":4914} search_term: nil page: 1 +-- runs_ms: [4.1, 0.3, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 4914::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..220.31 rows=20 width=330) (actual time=0.055..0.240 rows=20 loops=1) + Buffers: shared hit=117 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=1237225 width=330) (actual time=0.055..0.238 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 21 + Buffers: shared hit=117 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.007..0.007 rows=1 loops=18) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=72 +Planning: + Buffers: shared hit=8 +Planning Time: 0.406 ms +Execution Time: 0.290 ms diff --git a/script/perf/payments_filters/plans/after/amount_rare_from_p99.count.txt b/script/perf/payments_filters/plans/after/amount_rare_from_p99.count.txt new file mode 100644 index 00000000000..4dd2d1c7085 --- /dev/null +++ b/script/perf/payments_filters/plans/after/amount_rare_from_p99.count.txt @@ -0,0 +1,29 @@ +-- case: amount_rare_from_p99 (count) phase: after selective: false +-- filters: {"amount_from":80467} search_term: nil page: 1 +-- runs_ms: [2176.3, 1023.5, 613.1] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) + +Aggregate (cost=13553567.02..13553567.03 rows=1 width=8) (actual time=1021.963..1021.964 rows=1 loops=1) + Buffers: shared hit=352469 read=23 + I/O Timings: shared/local read=4.845 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13553501.19 rows=26331 width=0) (actual time=71.003..1017.210 rows=49567 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4950433 + Buffers: shared hit=352469 read=23 + I/O Timings: shared/local read=4.845 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.006..0.006 rows=1 loops=47620) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=190457 read=23 + I/O Timings: shared/local read=4.845 +Planning: + Buffers: shared hit=8 +Planning Time: 0.363 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.448 ms, Inlining 5.728 ms, Optimization 30.717 ms, Emission 34.510 ms, Total 72.403 ms +Execution Time: 1023.533 ms diff --git a/script/perf/payments_filters/plans/after/amount_rare_from_p99.txt b/script/perf/payments_filters/plans/after/amount_rare_from_p99.txt new file mode 100644 index 00000000000..7629562b97f --- /dev/null +++ b/script/perf/payments_filters/plans/after/amount_rare_from_p99.txt @@ -0,0 +1,21 @@ +-- case: amount_rare_from_p99 (list) phase: after selective: false +-- filters: {"amount_from":80467} search_term: nil page: 1 +-- runs_ms: [21.1, 2.7, 1.7] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..10326.28 rows=20 width=330) (actual time=0.121..2.679 rows=20 loops=1) + Buffers: shared hit=2738 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=26331 width=330) (actual time=0.121..2.677 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2616 + Buffers: shared hit=2738 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.211 ms +Execution Time: 2.706 ms diff --git a/script/perf/payments_filters/plans/after/amount_rare_range.count.txt b/script/perf/payments_filters/plans/after/amount_rare_range.count.txt new file mode 100644 index 00000000000..847c8a076d2 --- /dev/null +++ b/script/perf/payments_filters/plans/after/amount_rare_range.count.txt @@ -0,0 +1,26 @@ +-- case: amount_rare_range (count) phase: after selective: false +-- filters: {"amount_from":80467,"amount_to":160934} search_term: nil page: 1 +-- runs_ms: [523.1, 514.1, 543.2] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) AND (payments.amount_cents <= 160934::bigint) + +Aggregate (cost=13565940.20..13565940.21 rows=1 width=8) (actual time=522.512..522.513 rows=1 loops=1) + Buffers: shared hit=315860 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13565934.85 rows=2139 width=0) (actual time=58.297..521.226 rows=40027 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND (amount_cents <= '160934'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4959973 + Buffers: shared hit=315860 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=38462) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=153848 +Planning: + Buffers: shared hit=8 +Planning Time: 0.174 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.551 ms, Inlining 3.948 ms, Optimization 29.037 ms, Emission 25.267 ms, Total 58.804 ms +Execution Time: 523.138 ms diff --git a/script/perf/payments_filters/plans/after/amount_rare_range.txt b/script/perf/payments_filters/plans/after/amount_rare_range.txt new file mode 100644 index 00000000000..3b1792c4bc0 --- /dev/null +++ b/script/perf/payments_filters/plans/after/amount_rare_range.txt @@ -0,0 +1,25 @@ +-- case: amount_rare_range (list) phase: after selective: false +-- filters: {"amount_from":80467,"amount_to":160934} search_term: nil page: 1 +-- runs_ms: [11.1, 8.0, 8.5] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) AND (payments.amount_cents <= 160934::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..127226.06 rows=20 width=330) (actual time=4.677..7.920 rows=20 loops=1) + Buffers: shared hit=3209 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13606768.22 rows=2139 width=330) (actual time=0.133..3.375 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND (amount_cents <= '160934'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 3083 + Buffers: shared hit=3209 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.200 ms +JIT: + Functions: 14 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.563 ms, Inlining 0.000 ms, Optimization 0.407 ms, Emission 4.138 ms, Total 5.108 ms +Execution Time: 8.532 ms diff --git a/script/perf/payments_filters/plans/after/combo_customer_status_date.count.txt b/script/perf/payments_filters/plans/after/combo_customer_status_date.count.txt new file mode 100644 index 00000000000..487225cb11c --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_customer_status_date.count.txt @@ -0,0 +1,35 @@ +-- case: combo_customer_status_date (count) phase: after selective: true +-- filters: {"external_customer_id":"perf-cust-0-1","payment_status":["succeeded"],"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [465.2, 467.6, 456.5] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."customer_id" = '' AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=403277.24..403277.25 rows=1 width=8) (actual time=463.974..463.976 rows=1 loops=1) + Buffers: shared hit=520301 + -> Bitmap Heap Scan on payments (cost=40821.27..403171.07 rows=42468 width=0) (actual time=166.849..460.798 rows=111437 loops=1) + Recheck Cond: ((customer_id IS NOT NULL) AND (customer_id = ''::uuid) AND (organization_id = ''::uuid)) + Filter: ((payable_id IS NOT NULL) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 24640 + Heap Blocks: exact=92072 + Buffers: shared hit=520301 + -> BitmapAnd (cost=40821.27..40821.27 rows=103116 width=0) (actual time=153.087..153.088 rows=0 loops=1) + Buffers: shared hit=793 + -> Bitmap Index Scan on index_payments_on_customer_id (cost=0.00..1492.41 rows=135888 width=0) (actual time=5.623..5.623 rows=136077 loops=1) + Index Cond: ((customer_id IS NOT NULL) AND (customer_id = ''::uuid)) + Buffers: shared hit=119 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=145.956..145.957 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=674 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=106859) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=427436 +Planning: + Buffers: shared hit=8 +Planning Time: 0.113 ms +JIT: + Functions: 15 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.796 ms, Inlining 0.000 ms, Optimization 0.728 ms, Emission 5.516 ms, Total 7.040 ms +Execution Time: 465.178 ms diff --git a/script/perf/payments_filters/plans/after/combo_customer_status_date.txt b/script/perf/payments_filters/plans/after/combo_customer_status_date.txt new file mode 100644 index 00000000000..c53e366b2ad --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_customer_status_date.txt @@ -0,0 +1,22 @@ +-- case: combo_customer_status_date (list) phase: after selective: true +-- filters: {"external_customer_id":"perf-cust-0-1","payment_status":["succeeded"],"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [7.4, 0.5, 0.4] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."customer_id" = '' AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..6403.58 rows=20 width=330) (actual time=0.067..0.474 rows=20 loops=1) + Buffers: shared hit=1191 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13596171.20 rows=42468 width=330) (actual time=0.067..0.472 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (customer_id = ''::uuid) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1080 + Buffers: shared hit=1191 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.158 ms +Execution Time: 0.491 ms diff --git a/script/perf/payments_filters/plans/after/combo_provider_status.count.txt b/script/perf/payments_filters/plans/after/combo_provider_status.count.txt new file mode 100644 index 00000000000..a68d2314ec2 --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_provider_status.count.txt @@ -0,0 +1,41 @@ +-- case: combo_provider_status (count) phase: after selective: false +-- filters: {"payment_provider_type":["stripe"],"payment_status":["failed"]} search_term: nil page: 1 +-- runs_ms: [6277.8, 5853.3, 5536.7] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND "payments"."payment_provider_id" = '' + +Aggregate (cost=8380097.94..8380097.95 rows=1 width=8) (actual time=5852.245..5852.247 rows=1 loops=1) + Buffers: shared hit=2143493 read=222535 + I/O Timings: shared/local read=3225.582 + -> Bitmap Heap Scan on payments (cost=73075.90..8379540.60 rows=222935 width=0) (actual time=275.858..5828.225 rows=574574 loops=1) + Recheck Cond: ((payment_provider_id = ''::uuid) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 3426079 + Heap Blocks: exact=157639 + Buffers: shared hit=2143493 read=222535 + I/O Timings: shared/local read=3225.582 + -> BitmapAnd (cost=73075.90..73075.90 rows=3023534 width=0) (actual time=203.432..203.433 rows=0 loops=1) + Buffers: shared hit=1 read=4096 + I/O Timings: shared/local read=9.828 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..33656.81 rows=3984450 width=0) (actual time=52.608..52.608 rows=4000653 loops=1) + Index Cond: (payment_provider_id = ''::uuid) + Buffers: shared read=3423 + I/O Timings: shared/local read=7.990 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=148.465..148.465 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=673 + I/O Timings: shared/local read=1.838 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.008..0.008 rows=1 loops=551073) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=2143209 read=61083 + I/O Timings: shared/local read=2895.128 +Planning: + Buffers: shared hit=8 +Planning Time: 0.184 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.652 ms, Inlining 4.229 ms, Optimization 28.622 ms, Emission 25.292 ms, Total 58.794 ms +Execution Time: 5853.334 ms diff --git a/script/perf/payments_filters/plans/after/combo_provider_status.txt b/script/perf/payments_filters/plans/after/combo_provider_status.txt new file mode 100644 index 00000000000..5cd739af713 --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_provider_status.txt @@ -0,0 +1,21 @@ +-- case: combo_provider_status (list) phase: after selective: false +-- filters: {"payment_provider_type":["stripe"],"payment_status":["failed"]} search_term: nil page: 1 +-- runs_ms: [1.7, 0.1, 0.2] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND "payments"."payment_provider_id" = '' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..1221.25 rows=20 width=330) (actual time=0.018..0.139 rows=20 loops=1) + Buffers: shared hit=412 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13606768.22 rows=222935 width=330) (actual time=0.017..0.138 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND (payment_provider_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 306 + Buffers: shared hit=412 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.106 ms +Execution Time: 0.159 ms diff --git a/script/perf/payments_filters/plans/after/combo_status_amount.count.txt b/script/perf/payments_filters/plans/after/combo_status_amount.count.txt new file mode 100644 index 00000000000..709e81a8e09 --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_status_amount.count.txt @@ -0,0 +1,29 @@ +-- case: combo_status_amount (count) phase: after selective: false +-- filters: {"payment_status":["failed"],"amount_from":4914} search_term: nil page: 1 +-- runs_ms: [4144.2, 4364.5, 4557.5] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) + +Aggregate (cost=13566390.98..13566390.99 rows=1 width=8) (actual time=4363.524..4363.525 rows=1 loops=1) + Buffers: shared hit=1327787 read=212285 + I/O Timings: shared/local read=2726.317 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13565934.85 rows=182450 width=0) (actual time=58.783..4346.214 rows=359593 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4640407 + Buffers: shared hit=1327787 read=212285 + I/O Timings: shared/local read=2726.317 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.010..0.010 rows=1 loops=344515) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=1327041 read=51019 + I/O Timings: shared/local read=2324.436 +Planning: + Buffers: shared hit=8 +Planning Time: 0.251 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.807 ms, Inlining 4.494 ms, Optimization 28.813 ms, Emission 25.323 ms, Total 59.437 ms +Execution Time: 4364.459 ms diff --git a/script/perf/payments_filters/plans/after/combo_status_amount.txt b/script/perf/payments_filters/plans/after/combo_status_amount.txt new file mode 100644 index 00000000000..63f5882c4a8 --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_status_amount.txt @@ -0,0 +1,21 @@ +-- case: combo_status_amount (list) phase: after selective: false +-- filters: {"payment_status":["failed"],"amount_from":4914} search_term: nil page: 1 +-- runs_ms: [0.7, 0.2, 0.2] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..1492.12 rows=20 width=330) (actual time=0.030..0.183 rows=20 loops=1) + Buffers: shared hit=584 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13606768.22 rows=182450 width=330) (actual time=0.030..0.182 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 477 + Buffers: shared hit=584 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.140 ms +Execution Time: 0.201 ms diff --git a/script/perf/payments_filters/plans/after/combo_status_currency_date.count.txt b/script/perf/payments_filters/plans/after/combo_status_currency_date.count.txt new file mode 100644 index 00000000000..6088804cc68 --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_status_currency_date.count.txt @@ -0,0 +1,22 @@ +-- case: combo_status_currency_date (count) phase: after selective: true +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [8.4, 5.6, 4.6] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=28369.54..28369.55 rows=1 width=8) (actual time=5.607..5.607 rows=1 loops=1) + Buffers: shared hit=10755 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..28361.92 rows=3046 width=0) (actual time=0.021..5.540 rows=2116 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 532 + Buffers: shared hit=10755 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=2021) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=8084 +Planning: + Buffers: shared hit=8 +Planning Time: 0.128 ms +Execution Time: 5.629 ms diff --git a/script/perf/payments_filters/plans/after/combo_status_currency_date.txt b/script/perf/payments_filters/plans/after/combo_status_currency_date.txt new file mode 100644 index 00000000000..b7f652b0ae1 --- /dev/null +++ b/script/perf/payments_filters/plans/after/combo_status_currency_date.txt @@ -0,0 +1,22 @@ +-- case: combo_status_currency_date (list) phase: after selective: true +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [0.2, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..186.78 rows=20 width=330) (actual time=0.017..0.049 rows=20 loops=1) + Buffers: shared hit=109 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..28361.92 rows=3046 width=330) (actual time=0.017..0.048 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 5 + Buffers: shared hit=109 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.106 ms +Execution Time: 0.069 ms diff --git a/script/perf/payments_filters/plans/after/control.count.txt b/script/perf/payments_filters/plans/after/control.count.txt new file mode 100644 index 00000000000..8175e9492be --- /dev/null +++ b/script/perf/payments_filters/plans/after/control.count.txt @@ -0,0 +1,29 @@ +-- case: control (count) phase: after selective: false +-- filters: {} search_term: nil page: 1 +-- runs_ms: [15417.3, 14304.5, 17266.3] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) + +Aggregate (cost=13547219.17..13547219.18 rows=1 width=8) (actual time=15414.658..15414.658 rows=1 loops=1) + Buffers: shared hit=18989927 read=171421 + I/O Timings: shared/local read=1432.384 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13541002.37 rows=2486719 width=0) (actual time=75.505..15283.788 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18989927 read=171421 + I/O Timings: shared/local read=1432.384 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18989907 read=9429 + I/O Timings: shared/local read=840.841 +Planning: + Buffers: shared hit=8 +Planning Time: 0.122 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 2.542 ms, Inlining 7.765 ms, Optimization 36.459 ms, Emission 30.701 ms, Total 77.466 ms +Execution Time: 15417.270 ms diff --git a/script/perf/payments_filters/plans/after/control.txt b/script/perf/payments_filters/plans/after/control.txt new file mode 100644 index 00000000000..bc5ccd1f0b3 --- /dev/null +++ b/script/perf/payments_filters/plans/after/control.txt @@ -0,0 +1,22 @@ +-- case: control (list) phase: after selective: false +-- filters: {} search_term: nil page: 1 +-- runs_ms: [1.6, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..109.79 rows=20 width=330) (actual time=0.020..0.057 rows=20 loops=1) + Buffers: shared hit=105 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13581835.64 rows=2486719 width=330) (actual time=0.019..0.056 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=105 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.140 ms +Execution Time: 0.081 ms diff --git a/script/perf/payments_filters/plans/after/control_page50.count.txt b/script/perf/payments_filters/plans/after/control_page50.count.txt new file mode 100644 index 00000000000..0c4357c6a7b --- /dev/null +++ b/script/perf/payments_filters/plans/after/control_page50.count.txt @@ -0,0 +1,30 @@ +-- case: control_page50 (count) phase: after selective: false +-- filters: {} search_term: nil page: 50 +-- runs_ms: [14896.9, 14584.6, 14840.4] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) + +Aggregate (cost=13547284.36..13547284.37 rows=1 width=8) (actual time=14839.381..14839.382 rows=1 loops=1) + Buffers: shared hit=18991130 read=170218 + I/O Timings: shared/local read=1323.073 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13541067.54 rows=2486731 width=0) (actual time=59.535..14710.075 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18991130 read=170218 + I/O Timings: shared/local read=1323.073 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18991130 read=8206 + I/O Timings: shared/local read=632.775 +Planning: + Buffers: shared hit=3 read=5 + I/O Timings: shared/local read=0.250 +Planning Time: 0.467 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.735 ms, Inlining 4.148 ms, Optimization 30.455 ms, Emission 24.796 ms, Total 60.134 ms +Execution Time: 14840.409 ms diff --git a/script/perf/payments_filters/plans/after/control_page50.txt b/script/perf/payments_filters/plans/after/control_page50.txt new file mode 100644 index 00000000000..aa3795b42d4 --- /dev/null +++ b/script/perf/payments_filters/plans/after/control_page50.txt @@ -0,0 +1,22 @@ +-- case: control_page50 (list) phase: after selective: false +-- filters: {} search_term: nil page: 50 +-- runs_ms: [28.6, 3.5, 1.8] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 980 + +Limit (cost=5353.07..5462.31 rows=20 width=330) (actual time=3.480..3.521 rows=20 loops=1) + Buffers: shared hit=4860 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13581900.91 rows=2486731 width=330) (actual time=0.024..3.502 rows=1000 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 13 + Buffers: shared hit=4860 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=959) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=3836 +Planning: + Buffers: shared hit=8 +Planning Time: 0.170 ms +Execution Time: 3.545 ms diff --git a/script/perf/payments_filters/plans/after/created_24m.count.txt b/script/perf/payments_filters/plans/after/created_24m.count.txt new file mode 100644 index 00000000000..620e824a714 --- /dev/null +++ b/script/perf/payments_filters/plans/after/created_24m.count.txt @@ -0,0 +1,30 @@ +-- case: created_24m (count) phase: after selective: false +-- filters: {"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [14211.8, 16234.0, 14673.2] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=13572135.26..13572135.27 rows=1 width=8) (actual time=14671.212..14671.213 rows=1 loops=1) + Buffers: shared hit=18991594 read=165462 + I/O Timings: shared/local read=726.744 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13565934.85 rows=2480165 width=0) (actual time=74.277..14526.272 rows=4951605 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 48395 + Buffers: shared hit=18991594 read=165462 + I/O Timings: shared/local read=726.744 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4748761) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18991594 read=3450 + I/O Timings: shared/local read=164.792 +Planning: + Buffers: shared hit=3 read=5 + I/O Timings: shared/local read=0.606 +Planning Time: 1.285 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.776 ms, Inlining 6.711 ms, Optimization 40.069 ms, Emission 27.380 ms, Total 75.937 ms +Execution Time: 14673.156 ms diff --git a/script/perf/payments_filters/plans/after/created_24m.txt b/script/perf/payments_filters/plans/after/created_24m.txt new file mode 100644 index 00000000000..db248b5b8a9 --- /dev/null +++ b/script/perf/payments_filters/plans/after/created_24m.txt @@ -0,0 +1,22 @@ +-- case: created_24m (list) phase: after selective: false +-- filters: {"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [1.4, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..110.00 rows=20 width=330) (actual time=0.026..0.083 rows=20 loops=1) + Buffers: shared hit=105 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13571369.56 rows=2480165 width=330) (actual time=0.026..0.082 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=105 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.169 ms +Execution Time: 0.107 ms diff --git a/script/perf/payments_filters/plans/after/created_7d.count.txt b/script/perf/payments_filters/plans/after/created_7d.count.txt new file mode 100644 index 00000000000..9b689886eab --- /dev/null +++ b/script/perf/payments_filters/plans/after/created_7d.count.txt @@ -0,0 +1,22 @@ +-- case: created_7d (count) phase: after selective: true +-- filters: {"created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [9.6, 9.7, 8.2] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=28332.85..28332.86 rows=1 width=8) (actual time=9.605..9.605 rows=1 loops=1) + Buffers: shared hit=12699 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..28323.16 rows=3877 width=0) (actual time=0.021..9.513 rows=2615 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 33 + Buffers: shared hit=12699 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=2507) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=10028 +Planning: + Buffers: shared hit=8 +Planning Time: 0.121 ms +Execution Time: 9.639 ms diff --git a/script/perf/payments_filters/plans/after/created_7d.txt b/script/perf/payments_filters/plans/after/created_7d.txt new file mode 100644 index 00000000000..d1afe724d7c --- /dev/null +++ b/script/perf/payments_filters/plans/after/created_7d.txt @@ -0,0 +1,22 @@ +-- case: created_7d (list) phase: after selective: true +-- filters: {"created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +-- runs_ms: [0.2, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..146.66 rows=20 width=330) (actual time=0.023..0.076 rows=20 loops=1) + Buffers: shared hit=105 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..28323.16 rows=3877 width=330) (actual time=0.022..0.075 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=105 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.159 ms +Execution Time: 0.107 ms diff --git a/script/perf/payments_filters/plans/after/currency_common.count.txt b/script/perf/payments_filters/plans/after/currency_common.count.txt new file mode 100644 index 00000000000..16c1e56c6b0 --- /dev/null +++ b/script/perf/payments_filters/plans/after/currency_common.count.txt @@ -0,0 +1,29 @@ +-- case: currency_common (count) phase: after selective: false +-- filters: {"currency":"EUR"} search_term: nil page: 1 +-- runs_ms: [14084.0, 14744.6, 13934.5] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' + +Aggregate (cost=13559416.30..13559416.31 rows=1 width=8) (actual time=14082.552..14082.553 rows=1 loops=1) + Buffers: shared hit=18047083 read=164189 + I/O Timings: shared/local read=689.239 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13553501.19 rows=2366042 width=0) (actual time=63.565..13948.161 rows=4705309 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 294691 + Buffers: shared hit=18047083 read=164189 + I/O Timings: shared/local read=689.239 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4512315) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18047070 read=2190 + I/O Timings: shared/local read=130.404 +Planning: + Buffers: shared hit=8 +Planning Time: 0.103 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.343 ms, Inlining 5.719 ms, Optimization 31.098 ms, Emission 26.631 ms, Total 64.790 ms +Execution Time: 14084.024 ms diff --git a/script/perf/payments_filters/plans/after/currency_common.txt b/script/perf/payments_filters/plans/after/currency_common.txt new file mode 100644 index 00000000000..6c14577ee2c --- /dev/null +++ b/script/perf/payments_filters/plans/after/currency_common.txt @@ -0,0 +1,22 @@ +-- case: currency_common (list) phase: after selective: false +-- filters: {"currency":"EUR"} search_term: nil page: 1 +-- runs_ms: [2.0, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..115.47 rows=20 width=330) (actual time=0.016..0.048 rows=20 loops=1) + Buffers: shared hit=106 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=2366042 width=330) (actual time=0.016..0.047 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=106 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.133 ms +Execution Time: 0.068 ms diff --git a/script/perf/payments_filters/plans/after/currency_rare.count.txt b/script/perf/payments_filters/plans/after/currency_rare.count.txt new file mode 100644 index 00000000000..93b3263ce0b --- /dev/null +++ b/script/perf/payments_filters/plans/after/currency_rare.count.txt @@ -0,0 +1,29 @@ +-- case: currency_rare (count) phase: after selective: true +-- filters: {"currency":"GBP"} search_term: nil page: 1 +-- runs_ms: [2033.1, 636.3, 566.2] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' + +Aggregate (cost=13553565.64..13553565.65 rows=1 width=8) (actual time=635.557..635.558 rows=1 loops=1) + Buffers: shared hit=352466 read=18 + I/O Timings: shared/local read=2.812 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13553501.19 rows=25779 width=0) (actual time=58.923..633.145 rows=49629 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'GBP'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4950371 + Buffers: shared hit=352466 read=18 + I/O Timings: shared/local read=2.812 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=47618) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=190454 read=18 + I/O Timings: shared/local read=2.812 +Planning: + Buffers: shared hit=8 +Planning Time: 0.193 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.620 ms, Inlining 4.253 ms, Optimization 29.948 ms, Emission 24.681 ms, Total 59.501 ms +Execution Time: 636.262 ms diff --git a/script/perf/payments_filters/plans/after/currency_rare.txt b/script/perf/payments_filters/plans/after/currency_rare.txt new file mode 100644 index 00000000000..b6b4dc16d5f --- /dev/null +++ b/script/perf/payments_filters/plans/after/currency_rare.txt @@ -0,0 +1,21 @@ +-- case: currency_rare (list) phase: after selective: true +-- filters: {"currency":"GBP"} search_term: nil page: 1 +-- runs_ms: [22.6, 2.5, 1.4] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..10547.39 rows=20 width=330) (actual time=0.074..2.460 rows=20 loops=1) + Buffers: shared hit=2554 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=25779 width=330) (actual time=0.074..2.459 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'GBP'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2433 + Buffers: shared hit=2554 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.189 ms +Execution Time: 2.542 ms diff --git a/script/perf/payments_filters/plans/after/customer_heavy.count.txt b/script/perf/payments_filters/plans/after/customer_heavy.count.txt new file mode 100644 index 00000000000..690f4cbd43c --- /dev/null +++ b/script/perf/payments_filters/plans/after/customer_heavy.count.txt @@ -0,0 +1,35 @@ +-- case: customer_heavy (count) phase: after selective: true +-- filters: {"external_customer_id":"perf-cust-0-1"} search_term: nil page: 1 +-- runs_ms: [573.9, 530.5, 532.8] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."customer_id" = '' + +Aggregate (cost=402531.14..402531.15 rows=1 width=8) (actual time=531.917..531.918 rows=1 loops=1) + Buffers: shared hit=609885 + -> Bitmap Heap Scan on payments (cost=40825.81..402402.25 rows=51558 width=0) (actual time=167.653..527.766 rows=134798 loops=1) + Recheck Cond: ((customer_id IS NOT NULL) AND (customer_id = ''::uuid) AND (organization_id = ''::uuid)) + Filter: ((payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1279 + Heap Blocks: exact=92072 + Buffers: shared hit=609885 + -> BitmapAnd (cost=40825.81..40825.81 rows=103116 width=0) (actual time=155.010..155.011 rows=0 loops=1) + Buffers: shared hit=793 + -> Bitmap Index Scan on index_payments_on_customer_id (cost=0.00..1492.41 rows=135888 width=0) (actual time=5.891..5.891 rows=136077 loops=1) + Index Cond: ((customer_id IS NOT NULL) AND (customer_id = ''::uuid)) + Buffers: shared hit=119 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=147.520..147.520 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=674 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=129255) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=517020 +Planning: + Buffers: shared hit=8 +Planning Time: 0.208 ms +JIT: + Functions: 15 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.607 ms, Inlining 0.000 ms, Optimization 0.463 ms, Emission 4.413 ms, Total 5.483 ms +Execution Time: 532.833 ms diff --git a/script/perf/payments_filters/plans/after/customer_heavy.txt b/script/perf/payments_filters/plans/after/customer_heavy.txt new file mode 100644 index 00000000000..7261cc3f2dc --- /dev/null +++ b/script/perf/payments_filters/plans/after/customer_heavy.txt @@ -0,0 +1,22 @@ +-- case: customer_heavy (list) phase: after selective: true +-- filters: {"external_customer_id":"perf-cust-0-1"} search_term: nil page: 1 +-- runs_ms: [10.4, 1.8, 0.6] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."customer_id" = '' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..5273.97 rows=20 width=330) (actual time=0.319..1.801 rows=20 loops=1) + Buffers: shared hit=1191 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=51558 width=330) (actual time=0.319..1.799 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (customer_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1080 + Buffers: shared hit=1191 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.012..0.012 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.241 ms +Execution Time: 1.848 ms diff --git a/script/perf/payments_filters/plans/after/customer_light.count.txt b/script/perf/payments_filters/plans/after/customer_light.count.txt new file mode 100644 index 00000000000..f6c8a74f791 --- /dev/null +++ b/script/perf/payments_filters/plans/after/customer_light.count.txt @@ -0,0 +1,22 @@ +-- case: customer_light (count) phase: after selective: true +-- filters: {"external_customer_id":"perf-cust-0-23830"} search_term: nil page: 1 +-- runs_ms: [0.1, 0.1, 0.1] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."customer_id" = '' + +Aggregate (cost=603.26..603.27 rows=1 width=8) (actual time=0.080..0.080 rows=1 loops=1) + Buffers: shared hit=265 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..603.11 rows=60 width=0) (actual time=0.012..0.078 rows=52 loops=1) + Index Cond: ((customer_id IS NOT NULL) AND (customer_id = ''::uuid)) + Filter: ((payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=265 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=52) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=208 +Planning: + Buffers: shared hit=8 +Planning Time: 0.089 ms +Execution Time: 0.094 ms diff --git a/script/perf/payments_filters/plans/after/customer_light.txt b/script/perf/payments_filters/plans/after/customer_light.txt new file mode 100644 index 00000000000..ceb6abfd705 --- /dev/null +++ b/script/perf/payments_filters/plans/after/customer_light.txt @@ -0,0 +1,26 @@ +-- case: customer_light (list) phase: after selective: true +-- filters: {"external_customer_id":"perf-cust-0-23830"} search_term: nil page: 1 +-- runs_ms: [0.1, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."customer_id" = '' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=604.70..604.75 rows=20 width=330) (actual time=0.099..0.101 rows=20 loops=1) + Buffers: shared hit=265 + -> Sort (cost=604.70..604.85 rows=60 width=330) (actual time=0.099..0.100 rows=20 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: top-N heapsort Memory: 35kB + Buffers: shared hit=265 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..603.11 rows=60 width=330) (actual time=0.015..0.089 rows=52 loops=1) + Index Cond: ((customer_id IS NOT NULL) AND (customer_id = ''::uuid)) + Filter: ((payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=265 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=52) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=208 +Planning: + Buffers: shared hit=8 +Planning Time: 0.156 ms +Execution Time: 0.127 ms diff --git a/script/perf/payments_filters/plans/after/five_filter_common.count.txt b/script/perf/payments_filters/plans/after/five_filter_common.count.txt new file mode 100644 index 00000000000..cc4f76cab52 --- /dev/null +++ b/script/perf/payments_filters/plans/after/five_filter_common.count.txt @@ -0,0 +1,42 @@ +-- case: five_filter_common (count) phase: after selective: false +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2024-09-08","created_at_to":"2026-09-08","amount_from":100,"payment_provider_type":["stripe"]} search_term: nil page: 1 +-- runs_ms: [9227.8, 9715.4, 9742.8] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND (payments.amount_cents >= 100::bigint) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" = '' + +Aggregate (cost=8413211.89..8413211.90 rows=1 width=8) (actual time=9714.271..9714.273 rows=1 loops=1) + Buffers: shared hit=11945440 read=168752 + I/O Timings: shared/local read=527.808 + -> Bitmap Heap Scan on payments (cost=73555.66..8410255.71 rows=1182472 width=0) (actual time=277.420..9622.450 rows=3115566 loops=1) + Recheck Cond: ((payment_provider_id = ''::uuid) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '100'::bigint) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 885087 + Heap Blocks: exact=157639 + Buffers: shared hit=11945440 read=168752 + I/O Timings: shared/local read=527.808 + -> BitmapAnd (cost=73555.66..73555.66 rows=3023534 width=0) (actual time=203.336..203.337 rows=0 loops=1) + Buffers: shared hit=1 read=4096 + I/O Timings: shared/local read=9.045 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..33656.81 rows=3984450 width=0) (actual time=51.372..51.373 rows=4000653 loops=1) + Index Cond: (payment_provider_id = ''::uuid) + Buffers: shared read=3423 + I/O Timings: shared/local read=7.082 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=149.444..149.444 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=673 + I/O Timings: shared/local read=1.963 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=2988114) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=11945439 read=7017 + I/O Timings: shared/local read=197.440 +Planning: + Buffers: shared hit=7 read=1 + I/O Timings: shared/local read=0.065 +Planning Time: 0.285 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.672 ms, Inlining 5.000 ms, Optimization 29.354 ms, Emission 25.724 ms, Total 60.751 ms +Execution Time: 9715.416 ms diff --git a/script/perf/payments_filters/plans/after/five_filter_common.txt b/script/perf/payments_filters/plans/after/five_filter_common.txt new file mode 100644 index 00000000000..dab5d6761b7 --- /dev/null +++ b/script/perf/payments_filters/plans/after/five_filter_common.txt @@ -0,0 +1,22 @@ +-- case: five_filter_common (list) phase: after selective: false +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2024-09-08","created_at_to":"2026-09-08","amount_from":100,"payment_provider_type":["stripe"]} search_term: nil page: 1 +-- runs_ms: [1.9, 0.2, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND (payments.amount_cents >= 100::bigint) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" = '' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..230.94 rows=20 width=330) (actual time=0.041..0.154 rows=20 loops=1) + Buffers: shared hit=123 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13620972.85 rows=1182472 width=330) (actual time=0.041..0.153 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '100'::bigint) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND (payment_provider_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 19 + Buffers: shared hit=123 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.189 ms +Execution Time: 0.181 ms diff --git a/script/perf/payments_filters/plans/after/five_filter_rare.count.txt b/script/perf/payments_filters/plans/after/five_filter_rare.count.txt new file mode 100644 index 00000000000..b5dc6104d24 --- /dev/null +++ b/script/perf/payments_filters/plans/after/five_filter_rare.count.txt @@ -0,0 +1,29 @@ +-- case: five_filter_rare (count) phase: after selective: true +-- filters: {"payment_status":["failed"],"currency":"GBP","created_at_from":"2026-09-01","created_at_to":"2026-09-08","amount_from":4914,"payment_provider_type":["gocardless"]} search_term: nil page: 1 +-- runs_ms: [19.8, 18.8, 16.9] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" = '' + +Aggregate (cost=9686.95..9686.96 rows=1 width=8) (actual time=18.776..18.777 rows=1 loops=1) + Buffers: shared hit=1072 + -> Bitmap Heap Scan on payments (cost=6389.91..9686.95 rows=1 width=0) (actual time=18.775..18.776 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND (payment_provider_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND ((amount_currency)::text = 'GBP'::text) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 406 + Heap Blocks: exact=405 + Buffers: shared hit=1072 + -> BitmapAnd (cost=6389.91..6389.91 rows=872 width=0) (actual time=18.359..18.360 rows=0 loops=1) + Buffers: shared hit=667 + -> Bitmap Index Scan on index_payments_by_cursor (cost=0.00..159.07 rows=7753 width=0) (actual time=0.246..0.247 rows=2648 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..6230.59 rows=737554 width=0) (actual time=17.829..17.829 rows=749818 loops=1) + Index Cond: (payment_provider_id = ''::uuid) + Buffers: shared hit=644 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.221 ms +Execution Time: 18.833 ms diff --git a/script/perf/payments_filters/plans/after/five_filter_rare.txt b/script/perf/payments_filters/plans/after/five_filter_rare.txt new file mode 100644 index 00000000000..2254e0771a8 --- /dev/null +++ b/script/perf/payments_filters/plans/after/five_filter_rare.txt @@ -0,0 +1,33 @@ +-- case: five_filter_rare (list) phase: after selective: true +-- filters: {"payment_status":["failed"],"currency":"GBP","created_at_from":"2026-09-01","created_at_to":"2026-09-08","amount_from":4914,"payment_provider_type":["gocardless"]} search_term: nil page: 1 +-- runs_ms: [19.1, 18.3, 18.6] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" = '' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=9686.96..9686.97 rows=1 width=330) (actual time=18.550..18.551 rows=0 loops=1) + Buffers: shared hit=1072 + -> Sort (cost=9686.96..9686.97 rows=1 width=330) (actual time=18.549..18.550 rows=0 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=1072 + -> Bitmap Heap Scan on payments (cost=6389.91..9686.95 rows=1 width=330) (actual time=18.545..18.547 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND (payment_provider_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND ((amount_currency)::text = 'GBP'::text) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 406 + Heap Blocks: exact=405 + Buffers: shared hit=1072 + -> BitmapAnd (cost=6389.91..6389.91 rows=872 width=0) (actual time=18.040..18.041 rows=0 loops=1) + Buffers: shared hit=667 + -> Bitmap Index Scan on index_payments_by_cursor (cost=0.00..159.07 rows=7753 width=0) (actual time=0.225..0.226 rows=2648 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..6230.59 rows=737554 width=0) (actual time=17.437..17.437 rows=749818 loops=1) + Index Cond: (payment_provider_id = ''::uuid) + Buffers: shared hit=644 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.202 ms +Execution Time: 18.629 ms diff --git a/script/perf/payments_filters/plans/after/invoice_hit_direct.count.txt b/script/perf/payments_filters/plans/after/invoice_hit_direct.count.txt new file mode 100644 index 00000000000..248190a2482 --- /dev/null +++ b/script/perf/payments_filters/plans/after/invoice_hit_direct.count.txt @@ -0,0 +1,20 @@ +-- case: invoice_hit_direct (count) phase: after selective: true +-- filters: {"invoice_number":"her-1556-202609-000457624"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.0, 0.0] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND ((payments.payable_type = 'Invoice' AND payments.payable_id IN ('')) OR (payments.payable_type = 'PaymentRequest' AND payments.payable_id IN (NULL))) + +Aggregate (cost=5.32..5.33 rows=1 width=8) (actual time=0.011..0.011 rows=1 loops=1) + Buffers: shared hit=13 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments (cost=0.43..5.32 rows=1 width=0) (actual time=0.009..0.010 rows=2 loops=1) + Index Cond: ((payable_id IS NOT NULL) AND (payable_id = ''::uuid) AND ((payable_type)::text = 'Invoice'::text)) + Filter: ((customer_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=13 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=2) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=8 +Planning: + Buffers: shared hit=8 +Planning Time: 0.083 ms +Execution Time: 0.023 ms diff --git a/script/perf/payments_filters/plans/after/invoice_hit_direct.txt b/script/perf/payments_filters/plans/after/invoice_hit_direct.txt new file mode 100644 index 00000000000..f6d3bece144 --- /dev/null +++ b/script/perf/payments_filters/plans/after/invoice_hit_direct.txt @@ -0,0 +1,24 @@ +-- case: invoice_hit_direct (list) phase: after selective: true +-- filters: {"invoice_number":"her-1556-202609-000457624"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.0, 0.0] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND ((payments.payable_type = 'Invoice' AND payments.payable_id IN ('')) OR (payments.payable_type = 'PaymentRequest' AND payments.payable_id IN (NULL))) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=5.33..5.33 rows=1 width=330) (actual time=0.017..0.017 rows=2 loops=1) + Buffers: shared hit=13 + -> Sort (cost=5.33..5.33 rows=1 width=330) (actual time=0.016..0.017 rows=2 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 26kB + Buffers: shared hit=13 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments (cost=0.43..5.32 rows=1 width=330) (actual time=0.010..0.012 rows=2 loops=1) + Index Cond: ((payable_id IS NOT NULL) AND (payable_id = ''::uuid) AND ((payable_type)::text = 'Invoice'::text)) + Filter: ((customer_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=13 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=2) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=8 +Planning: + Buffers: shared hit=8 +Planning Time: 0.114 ms +Execution Time: 0.038 ms diff --git a/script/perf/payments_filters/plans/after/invoice_hit_request.count.txt b/script/perf/payments_filters/plans/after/invoice_hit_request.count.txt new file mode 100644 index 00000000000..381df9fead8 --- /dev/null +++ b/script/perf/payments_filters/plans/after/invoice_hit_request.count.txt @@ -0,0 +1,28 @@ +-- case: invoice_hit_request (count) phase: after selective: true +-- filters: {"invoice_number":"her-1556-202609-000765141"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.0, 0.0] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND ((payments.payable_type = 'Invoice' AND payments.payable_id IN ('')) OR (payments.payable_type = 'PaymentRequest' AND payments.payable_id IN (''))) + +Aggregate (cost=6.88..6.89 rows=1 width=8) (actual time=0.005..0.005 rows=1 loops=1) + Buffers: shared hit=7 + -> Bitmap Heap Scan on payments (cost=3.09..6.87 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=1) + Recheck Cond: (((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'Invoice'::text)) OR ((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'PaymentRequest'::text))) + Filter: ((customer_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Heap Blocks: exact=1 + Buffers: shared hit=7 + -> BitmapOr (cost=3.09..3.09 rows=1 width=0) (actual time=0.003..0.003 rows=0 loops=1) + Buffers: shared hit=6 + -> Bitmap Index Scan on index_payments_on_payable_id_and_payable_type_and_error_code (cost=0.00..1.55 rows=1 width=0) (actual time=0.002..0.002 rows=0 loops=1) + Index Cond: ((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'Invoice'::text)) + Buffers: shared hit=3 + -> Bitmap Index Scan on index_payments_on_payable_id_and_payable_type_and_error_code (cost=0.00..1.55 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1) + Index Cond: ((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'PaymentRequest'::text)) + Buffers: shared hit=3 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.114 ms +Execution Time: 0.029 ms diff --git a/script/perf/payments_filters/plans/after/invoice_hit_request.txt b/script/perf/payments_filters/plans/after/invoice_hit_request.txt new file mode 100644 index 00000000000..9b05f577b37 --- /dev/null +++ b/script/perf/payments_filters/plans/after/invoice_hit_request.txt @@ -0,0 +1,32 @@ +-- case: invoice_hit_request (list) phase: after selective: true +-- filters: {"invoice_number":"her-1556-202609-000765141"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.0, 0.0] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND ((payments.payable_type = 'Invoice' AND payments.payable_id IN ('')) OR (payments.payable_type = 'PaymentRequest' AND payments.payable_id IN (''))) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=6.88..6.89 rows=1 width=330) (actual time=0.004..0.005 rows=1 loops=1) + Buffers: shared hit=7 + -> Sort (cost=6.88..6.89 rows=1 width=330) (actual time=0.004..0.005 rows=1 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=7 + -> Bitmap Heap Scan on payments (cost=3.09..6.87 rows=1 width=330) (actual time=0.003..0.004 rows=1 loops=1) + Recheck Cond: (((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'Invoice'::text)) OR ((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'PaymentRequest'::text))) + Filter: ((customer_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Heap Blocks: exact=1 + Buffers: shared hit=7 + -> BitmapOr (cost=3.09..3.09 rows=1 width=0) (actual time=0.003..0.003 rows=0 loops=1) + Buffers: shared hit=6 + -> Bitmap Index Scan on index_payments_on_payable_id_and_payable_type_and_error_code (cost=0.00..1.55 rows=1 width=0) (actual time=0.001..0.001 rows=0 loops=1) + Index Cond: ((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'Invoice'::text)) + Buffers: shared hit=3 + -> Bitmap Index Scan on index_payments_on_payable_id_and_payable_type_and_error_code (cost=0.00..1.55 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1) + Index Cond: ((payable_id = ''::uuid) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'PaymentRequest'::text)) + Buffers: shared hit=3 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.109 ms +Execution Time: 0.030 ms diff --git a/script/perf/payments_filters/plans/after/invoice_miss.count.txt b/script/perf/payments_filters/plans/after/invoice_miss.count.txt new file mode 100644 index 00000000000..4697f16d3e4 --- /dev/null +++ b/script/perf/payments_filters/plans/after/invoice_miss.count.txt @@ -0,0 +1,7 @@ +-- case: invoice_miss (count) phase: after selective: true +-- filters: {"invoice_number":"PERF-NOPE-000000-000000001"} search_term: nil page: 1 +-- runs_ms: [0.0] +(no query) + +Result (no query: ActiveRecord short-circuits an empty IN list) (actual time=0.000..0.000 rows=0 loops=1) +Execution Time: 0.000 ms diff --git a/script/perf/payments_filters/plans/after/invoice_miss.txt b/script/perf/payments_filters/plans/after/invoice_miss.txt new file mode 100644 index 00000000000..b57238c428e --- /dev/null +++ b/script/perf/payments_filters/plans/after/invoice_miss.txt @@ -0,0 +1,13 @@ +-- case: invoice_miss (list) phase: after selective: true +-- filters: {"invoice_number":"PERF-NOPE-000000-000000001"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.0, 0.0] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (1=0) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.01..0.02 rows=1 width=448) (actual time=0.001..0.001 rows=0 loops=1) + -> Sort (cost=0.01..0.02 rows=0 width=448) (actual time=0.001..0.001 rows=0 loops=1) + Sort Key: created_at DESC, id + Sort Method: quicksort Memory: 25kB + -> Result (cost=0.00..0.00 rows=0 width=0) (actual time=0.000..0.000 rows=0 loops=1) + One-Time Filter: false +Planning Time: 0.028 ms +Execution Time: 0.010 ms diff --git a/script/perf/payments_filters/plans/after/payable_type_invoice.count.txt b/script/perf/payments_filters/plans/after/payable_type_invoice.count.txt new file mode 100644 index 00000000000..b089a207c8d --- /dev/null +++ b/script/perf/payments_filters/plans/after/payable_type_invoice.count.txt @@ -0,0 +1,42 @@ +-- case: payable_type_invoice (count) phase: after selective: false +-- filters: {"payable_type":["Invoice"]} search_term: nil page: 1 +-- runs_ms: [17310.6, 15608.7, 15783.2] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'Invoice' + +Aggregate (cost=12989383.32..12989383.33 rows=1 width=8) (actual time=15781.892..15781.894 rows=1 loops=1) + Buffers: shared hit=18958875 read=227837 + I/O Timings: shared/local read=1691.204 + -> Bitmap Heap Scan on payments (cost=135138.13..12983478.99 rows=2361732 width=0) (actual time=604.081..15652.259 rows=4702553 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((payable_type)::text = 'Invoice'::text) AND (payable_id IS NOT NULL)) + Filter: ((customer_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Heap Blocks: exact=157734 + Buffers: shared hit=18958875 read=227837 + I/O Timings: shared/local read=1691.204 + -> BitmapAnd (cost=135138.13..135138.13 rows=4723463 width=0) (actual time=533.333..533.334 rows=0 loops=1) + Buffers: shared hit=1 read=29641 + I/O Timings: shared/local read=69.463 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=148.217..148.218 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=673 + I/O Timings: shared/local read=1.570 + -> Bitmap Index Scan on index_payments_on_payable_type_and_payable_id (cost=0.00..94649.65 rows=6224639 width=0) (actual time=382.800..382.800 rows=6226744 loops=1) + Index Cond: (((payable_type)::text = 'Invoice'::text) AND (payable_id IS NOT NULL)) + Buffers: shared read=28968 + I/O Timings: shared/local read=67.893 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18958874 read=40462 + I/O Timings: shared/local read=1333.548 +Planning: + Buffers: shared hit=3 read=5 + I/O Timings: shared/local read=0.268 +Planning Time: 0.483 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.579 ms, Inlining 4.070 ms, Optimization 28.281 ms, Emission 24.228 ms, Total 57.158 ms +Execution Time: 15783.178 ms diff --git a/script/perf/payments_filters/plans/after/payable_type_invoice.txt b/script/perf/payments_filters/plans/after/payable_type_invoice.txt new file mode 100644 index 00000000000..65acf43975c --- /dev/null +++ b/script/perf/payments_filters/plans/after/payable_type_invoice.txt @@ -0,0 +1,22 @@ +-- case: payable_type_invoice (list) phase: after selective: false +-- filters: {"payable_type":["Invoice"]} search_term: nil page: 1 +-- runs_ms: [1.0, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'Invoice' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..115.68 rows=20 width=330) (actual time=0.022..0.055 rows=20 loops=1) + Buffers: shared hit=110 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=2361732 width=330) (actual time=0.022..0.054 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'Invoice'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=110 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.107 ms +Execution Time: 0.078 ms diff --git a/script/perf/payments_filters/plans/after/payable_type_request.count.txt b/script/perf/payments_filters/plans/after/payable_type_request.count.txt new file mode 100644 index 00000000000..9f25262c089 --- /dev/null +++ b/script/perf/payments_filters/plans/after/payable_type_request.count.txt @@ -0,0 +1,32 @@ +-- case: payable_type_request (count) phase: after selective: false +-- filters: {"payable_type":["PaymentRequest"]} search_term: nil page: 1 +-- runs_ms: [670.6, 375.3, 389.8] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'PaymentRequest' + +Aggregate (cost=871741.91..871741.92 rows=1 width=8) (actual time=388.767..388.769 rows=1 loops=1) + Buffers: shared hit=129481 + -> Bitmap Heap Scan on payments (cost=44381.20..871429.41 rows=125000 width=0) (actual time=244.932..383.043 rows=250166 loops=1) + Recheck Cond: (((payable_type)::text = 'PaymentRequest'::text) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Heap Blocks: exact=126954 + Buffers: shared hit=129481 + -> BitmapAnd (cost=44381.20..44381.20 rows=249999 width=0) (actual time=173.166..173.166 rows=0 loops=1) + Buffers: shared hit=2527 + -> Bitmap Index Scan on index_payments_on_payable_type_and_payable_id (cost=0.00..5011.08 rows=329452 width=0) (actual time=22.048..22.049 rows=327839 loops=1) + Index Cond: (((payable_type)::text = 'PaymentRequest'::text) AND (payable_id IS NOT NULL)) + Buffers: shared hit=1853 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=147.609..147.609 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=674 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.162 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.547 ms, Inlining 5.855 ms, Optimization 29.157 ms, Emission 24.695 ms, Total 60.254 ms +Execution Time: 389.780 ms diff --git a/script/perf/payments_filters/plans/after/payable_type_request.txt b/script/perf/payments_filters/plans/after/payable_type_request.txt new file mode 100644 index 00000000000..a9369d474d2 --- /dev/null +++ b/script/perf/payments_filters/plans/after/payable_type_request.txt @@ -0,0 +1,20 @@ +-- case: payable_type_request (list) phase: after selective: false +-- filters: {"payable_type":["PaymentRequest"]} search_term: nil page: 1 +-- runs_ms: [4.8, 0.3, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'PaymentRequest' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..2175.65 rows=20 width=330) (actual time=0.030..0.238 rows=20 loops=1) + Buffers: shared hit=427 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=125000 width=330) (actual time=0.029..0.237 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'PaymentRequest'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 400 + Buffers: shared hit=427 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.168 ms +Execution Time: 0.265 ms diff --git a/script/perf/payments_filters/plans/after/payment_type_manual.count.txt b/script/perf/payments_filters/plans/after/payment_type_manual.count.txt new file mode 100644 index 00000000000..26ad3c9e5f5 --- /dev/null +++ b/script/perf/payments_filters/plans/after/payment_type_manual.count.txt @@ -0,0 +1,41 @@ +-- case: payment_type_manual (count) phase: after selective: false +-- filters: {"payment_type":["manual"]} search_term: nil page: 1 +-- runs_ms: [6076.1, 6287.7, 6971.9] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'manual' + +Aggregate (cost=876117.55..876117.56 rows=1 width=8) (actual time=6286.363..6286.365 rows=1 loops=1) + Buffers: shared hit=891856 read=183147 + I/O Timings: shared/local read=4456.512 + -> Bitmap Heap Scan on payments (cost=42173.48..875802.35 rows=126077 width=0) (actual time=271.120..6267.689 rows=247155 loops=1) + Recheck Cond: ((payment_type = 'manual'::payment_type) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2374 + Heap Blocks: exact=126006 + Buffers: shared hit=891856 read=183147 + I/O Timings: shared/local read=4456.512 + -> BitmapAnd (cost=42173.48..42173.48 rows=252155 width=0) (actual time=199.567..199.568 rows=0 loops=1) + Buffers: shared hit=1 read=952 + I/O Timings: shared/local read=4.736 + -> Bitmap Index Scan on index_payments_on_payment_type (cost=0.00..2802.82 rows=332292 width=0) (actual time=16.188..16.188 rows=327794 loops=1) + Index Cond: (payment_type = 'manual'::payment_type) + Buffers: shared read=279 + I/O Timings: shared/local read=1.346 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=179.821..179.821 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=673 + I/O Timings: shared/local read=3.390 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.021..0.021 rows=1 loops=237011) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=891126 read=56918 + I/O Timings: shared/local read=4052.285 +Planning: + Buffers: shared hit=8 +Planning Time: 0.233 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.721 ms, Inlining 5.062 ms, Optimization 29.234 ms, Emission 25.691 ms, Total 60.708 ms +Execution Time: 6287.662 ms diff --git a/script/perf/payments_filters/plans/after/payment_type_manual.txt b/script/perf/payments_filters/plans/after/payment_type_manual.txt new file mode 100644 index 00000000000..1d50a279aef --- /dev/null +++ b/script/perf/payments_filters/plans/after/payment_type_manual.txt @@ -0,0 +1,22 @@ +-- case: payment_type_manual (list) phase: after selective: false +-- filters: {"payment_type":["manual"]} search_term: nil page: 1 +-- runs_ms: [1.4, 0.3, 0.2] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'manual' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..2157.07 rows=20 width=330) (actual time=0.018..0.243 rows=20 loops=1) + Buffers: shared hit=743 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=126077 width=330) (actual time=0.018..0.242 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payment_type = 'manual'::payment_type) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 631 + Buffers: shared hit=743 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.130 ms +Execution Time: 0.259 ms diff --git a/script/perf/payments_filters/plans/after/payment_type_provider.count.txt b/script/perf/payments_filters/plans/after/payment_type_provider.count.txt new file mode 100644 index 00000000000..af33cbda958 --- /dev/null +++ b/script/perf/payments_filters/plans/after/payment_type_provider.count.txt @@ -0,0 +1,42 @@ +-- case: payment_type_provider (count) phase: after selective: false +-- filters: {"payment_type":["provider"]} search_term: nil page: 1 +-- runs_ms: [15593.5, 15557.3, 14939.9] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'provider' + +Aggregate (cost=12941420.74..12941420.75 rows=1 width=8) (actual time=15555.570..15555.572 rows=1 loops=1) + Buffers: shared hit=18040957 read=173910 + I/O Timings: shared/local read=1032.587 + -> Bitmap Heap Scan on payments (cost=92945.57..12935519.11 rows=2360654 width=0) (actual time=325.875..15409.907 rows=4705564 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND (payment_type = 'provider'::payment_type)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 44907 + Heap Blocks: exact=157657 + Buffers: shared hit=18040957 read=173910 + I/O Timings: shared/local read=1032.587 + -> BitmapAnd (cost=92945.57..92945.57 rows=4721308 width=0) (actual time=249.867..249.868 rows=0 loops=1) + Buffers: shared hit=1 read=5917 + I/O Timings: shared/local read=18.269 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=154.338..154.338 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=673 + I/O Timings: shared/local read=3.176 + -> Bitmap Index Scan on index_payments_on_payment_type (cost=0.00..52457.62 rows=6221799 width=0) (actual time=93.050..93.050 rows=6226789 loops=1) + Index Cond: (payment_type = 'provider'::payment_type) + Buffers: shared read=5244 + I/O Timings: shared/local read=15.092 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4512823) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18040956 read=10336 + I/O Timings: shared/local read=495.841 +Planning: + Buffers: shared hit=7 read=1 + I/O Timings: shared/local read=0.084 +Planning Time: 0.356 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.958 ms, Inlining 5.822 ms, Optimization 29.442 ms, Emission 26.490 ms, Total 62.712 ms +Execution Time: 15557.262 ms diff --git a/script/perf/payments_filters/plans/after/payment_type_provider.txt b/script/perf/payments_filters/plans/after/payment_type_provider.txt new file mode 100644 index 00000000000..93cd598d426 --- /dev/null +++ b/script/perf/payments_filters/plans/after/payment_type_provider.txt @@ -0,0 +1,22 @@ +-- case: payment_type_provider (list) phase: after selective: false +-- filters: {"payment_type":["provider"]} search_term: nil page: 1 +-- runs_ms: [2.5, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'provider' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..115.73 rows=20 width=330) (actual time=0.045..0.084 rows=20 loops=1) + Buffers: shared hit=107 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=2360654 width=330) (actual time=0.044..0.083 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payment_type = 'provider'::payment_type) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 3 + Buffers: shared hit=107 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.324 ms +Execution Time: 0.115 ms diff --git a/script/perf/payments_filters/plans/after/provider_common.count.txt b/script/perf/payments_filters/plans/after/provider_common.count.txt new file mode 100644 index 00000000000..39866d4326c --- /dev/null +++ b/script/perf/payments_filters/plans/after/provider_common.count.txt @@ -0,0 +1,41 @@ +-- case: provider_common (count) phase: after selective: false +-- filters: {"payment_provider_type":["stripe"]} search_term: nil page: 1 +-- runs_ms: [12901.0, 14283.5, 12684.0] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" = '' + +Aggregate (cost=8376405.60..8376405.61 rows=1 width=8) (actual time=12898.217..12898.219 rows=1 loops=1) + Buffers: shared hit=15202828 read=161916 + I/O Timings: shared/local read=447.459 + -> Bitmap Heap Scan on payments (cost=73720.31..8372626.18 rows=1511767 width=0) (actual time=309.489..12784.729 rows=3962686 loops=1) + Recheck Cond: ((payment_provider_id = ''::uuid) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 37967 + Heap Blocks: exact=157639 + Buffers: shared hit=15202828 read=161916 + I/O Timings: shared/local read=447.459 + -> BitmapAnd (cost=73720.31..73720.31 rows=3023534 width=0) (actual time=229.576..229.577 rows=0 loops=1) + Buffers: shared hit=1 read=4096 + I/O Timings: shared/local read=14.888 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..33656.81 rows=3984450 width=0) (actual time=67.159..67.159 rows=4000653 loops=1) + Index Cond: (payment_provider_id = ''::uuid) + Buffers: shared read=3423 + I/O Timings: shared/local read=11.764 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=160.015..160.015 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=673 + I/O Timings: shared/local read=3.124 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=3800752) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=15202803 read=205 + I/O Timings: shared/local read=17.959 +Planning: + Buffers: shared hit=8 +Planning Time: 0.159 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 2.340 ms, Inlining 6.518 ms, Optimization 31.018 ms, Emission 27.818 ms, Total 67.694 ms +Execution Time: 12901.029 ms diff --git a/script/perf/payments_filters/plans/after/provider_common.txt b/script/perf/payments_filters/plans/after/provider_common.txt new file mode 100644 index 00000000000..98b86064aa7 --- /dev/null +++ b/script/perf/payments_filters/plans/after/provider_common.txt @@ -0,0 +1,22 @@ +-- case: provider_common (list) phase: after selective: false +-- filters: {"payment_provider_type":["stripe"]} search_term: nil page: 1 +-- runs_ms: [2.5, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" = '' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..180.40 rows=20 width=330) (actual time=0.034..0.126 rows=20 loops=1) + Buffers: shared hit=117 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=1511767 width=330) (actual time=0.033..0.125 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payment_provider_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 13 + Buffers: shared hit=117 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.193 ms +Execution Time: 0.147 ms diff --git a/script/perf/payments_filters/plans/after/provider_miss.count.txt b/script/perf/payments_filters/plans/after/provider_miss.count.txt new file mode 100644 index 00000000000..155b28941be --- /dev/null +++ b/script/perf/payments_filters/plans/after/provider_miss.count.txt @@ -0,0 +1,7 @@ +-- case: provider_miss (count) phase: after selective: true +-- filters: {"payment_provider_type":["cashfree"]} search_term: nil page: 1 +-- runs_ms: [0.0] +(no query) + +Result (no query: ActiveRecord short-circuits an empty IN list) (actual time=0.000..0.000 rows=0 loops=1) +Execution Time: 0.000 ms diff --git a/script/perf/payments_filters/plans/after/provider_miss.txt b/script/perf/payments_filters/plans/after/provider_miss.txt new file mode 100644 index 00000000000..2a49de6170c --- /dev/null +++ b/script/perf/payments_filters/plans/after/provider_miss.txt @@ -0,0 +1,13 @@ +-- case: provider_miss (list) phase: after selective: true +-- filters: {"payment_provider_type":["cashfree"]} search_term: nil page: 1 +-- runs_ms: [0.9, 0.0, 0.0] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND 1=0 ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.01..0.02 rows=1 width=448) (actual time=0.005..0.005 rows=0 loops=1) + -> Sort (cost=0.01..0.02 rows=0 width=448) (actual time=0.005..0.005 rows=0 loops=1) + Sort Key: created_at DESC, id + Sort Method: quicksort Memory: 25kB + -> Result (cost=0.00..0.00 rows=0 width=0) (actual time=0.000..0.000 rows=0 loops=1) + One-Time Filter: false +Planning Time: 0.042 ms +Execution Time: 0.022 ms diff --git a/script/perf/payments_filters/plans/after/provider_rare.count.txt b/script/perf/payments_filters/plans/after/provider_rare.count.txt new file mode 100644 index 00000000000..4eabfc3b62e --- /dev/null +++ b/script/perf/payments_filters/plans/after/provider_rare.count.txt @@ -0,0 +1,41 @@ +-- case: provider_rare (count) phase: after selective: false +-- filters: {"payment_provider_type":["gocardless"]} search_term: nil page: 1 +-- runs_ms: [7478.4, 9222.5, 10728.5] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" = '' + +Aggregate (cost=1751394.33..1751394.34 rows=1 width=8) (actual time=9221.002..9221.004 rows=1 loops=1) + Buffers: shared hit=2785257 read=220988 + I/O Timings: shared/local read=5615.132 + -> Bitmap Heap Scan on payments (cost=45678.13..1750694.73 rows=279840 width=0) (actual time=248.928..9186.494 rows=742878 loops=1) + Recheck Cond: ((payment_provider_id = ''::uuid) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 6940 + Heap Blocks: exact=156643 + Buffers: shared hit=2785257 read=220988 + I/O Timings: shared/local read=5615.132 + -> BitmapAnd (cost=45678.13..45678.13 rows=559680 width=0) (actual time=175.696..175.697 rows=0 loops=1) + Buffers: shared hit=1 read=1317 + I/O Timings: shared/local read=3.952 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..6230.59 rows=737554 width=0) (actual time=20.497..20.497 rows=749818 loops=1) + Index Cond: (payment_provider_id = ''::uuid) + Buffers: shared read=644 + I/O Timings: shared/local read=1.801 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39307.37 rows=4973463 width=0) (actual time=152.823..152.823 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=673 + I/O Timings: shared/local read=2.151 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.011..0.011 rows=1 loops=712071) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=2785236 read=63048 + I/O Timings: shared/local read=5090.951 +Planning: + Buffers: shared hit=8 +Planning Time: 0.211 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.748 ms, Inlining 4.389 ms, Optimization 29.000 ms, Emission 25.393 ms, Total 59.531 ms +Execution Time: 9222.463 ms diff --git a/script/perf/payments_filters/plans/after/provider_rare.txt b/script/perf/payments_filters/plans/after/provider_rare.txt new file mode 100644 index 00000000000..58d2f498b14 --- /dev/null +++ b/script/perf/payments_filters/plans/after/provider_rare.txt @@ -0,0 +1,21 @@ +-- case: provider_rare (list) phase: after selective: false +-- filters: {"payment_provider_type":["gocardless"]} search_term: nil page: 1 +-- runs_ms: [2.4, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" = '' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..972.14 rows=20 width=330) (actual time=0.015..0.072 rows=20 loops=1) + Buffers: shared hit=188 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=279840 width=330) (actual time=0.015..0.071 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payment_provider_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 88 + Buffers: shared hit=188 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=19) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=76 +Planning: + Buffers: shared hit=8 +Planning Time: 0.123 ms +Execution Time: 0.089 ms diff --git a/script/perf/payments_filters/plans/after/receipt_hit.count.txt b/script/perf/payments_filters/plans/after/receipt_hit.count.txt new file mode 100644 index 00000000000..be01e903e1a --- /dev/null +++ b/script/perf/payments_filters/plans/after/receipt_hit.count.txt @@ -0,0 +1,25 @@ +-- case: receipt_hit (count) phase: after selective: true +-- filters: {"receipt_number":"her-1556-335-rcpt-000123"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.1, 0.0] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payment_receipts"."payment_id" FROM "payment_receipts" WHERE "payment_receipts"."organization_id" = '' AND (lower(payment_receipts.number) = lower('her-1556-335-rcpt-000123'))) + +Aggregate (cost=151.13..151.14 rows=1 width=8) (actual time=0.022..0.022 rows=1 loops=1) + Buffers: shared hit=22 + -> Nested Loop (cost=0.99..151.11 rows=9 width=0) (actual time=0.017..0.021 rows=2 loops=1) + Buffers: shared hit=22 + -> Index Scan using index_payment_receipts_on_organization_id_lower_number on payment_receipts (cost=0.56..23.58 rows=24 width=16) (actual time=0.007..0.008 rows=2 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (lower((number)::text) = 'her-1556-335-rcpt-000123'::text)) + Buffers: shared hit=6 + -> Index Scan using payments_pkey on payments (cost=0.43..5.31 rows=1 width=16) (actual time=0.006..0.006 rows=1 loops=2) + Index Cond: (id = payment_receipts.payment_id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=16 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=2) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=8 +Planning: + Buffers: shared hit=24 +Planning Time: 0.192 ms +Execution Time: 0.043 ms diff --git a/script/perf/payments_filters/plans/after/receipt_hit.txt b/script/perf/payments_filters/plans/after/receipt_hit.txt new file mode 100644 index 00000000000..314b3137655 --- /dev/null +++ b/script/perf/payments_filters/plans/after/receipt_hit.txt @@ -0,0 +1,29 @@ +-- case: receipt_hit (list) phase: after selective: true +-- filters: {"receipt_number":"her-1556-335-rcpt-000123"} search_term: nil page: 1 +-- runs_ms: [0.1, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payment_receipts"."payment_id" FROM "payment_receipts" WHERE "payment_receipts"."organization_id" = '' AND (lower(payment_receipts.number) = lower('her-1556-335-rcpt-000123'))) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=151.25..151.27 rows=9 width=330) (actual time=0.024..0.024 rows=2 loops=1) + Buffers: shared hit=22 + -> Sort (cost=151.25..151.27 rows=9 width=330) (actual time=0.023..0.024 rows=2 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=22 + -> Nested Loop (cost=0.99..151.11 rows=9 width=330) (actual time=0.017..0.020 rows=2 loops=1) + Buffers: shared hit=22 + -> Index Scan using index_payment_receipts_on_organization_id_lower_number on payment_receipts (cost=0.56..23.58 rows=24 width=16) (actual time=0.007..0.008 rows=2 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (lower((number)::text) = 'her-1556-335-rcpt-000123'::text)) + Buffers: shared hit=6 + -> Index Scan using payments_pkey on payments (cost=0.43..5.31 rows=1 width=330) (actual time=0.006..0.006 rows=1 loops=2) + Index Cond: (id = payment_receipts.payment_id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=16 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=2) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=8 +Planning: + Buffers: shared hit=24 +Planning Time: 0.194 ms +Execution Time: 0.052 ms diff --git a/script/perf/payments_filters/plans/after/receipt_miss.count.txt b/script/perf/payments_filters/plans/after/receipt_miss.count.txt new file mode 100644 index 00000000000..5e536dcfa5c --- /dev/null +++ b/script/perf/payments_filters/plans/after/receipt_miss.count.txt @@ -0,0 +1,23 @@ +-- case: receipt_miss (count) phase: after selective: true +-- filters: {"receipt_number":"PERF-NOPE-RCPT-000001"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.0, 0.0] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payment_receipts"."payment_id" FROM "payment_receipts" WHERE "payment_receipts"."organization_id" = '' AND (lower(payment_receipts.number) = lower('PERF-NOPE-RCPT-000001'))) + +Aggregate (cost=151.13..151.14 rows=1 width=8) (actual time=0.005..0.005 rows=1 loops=1) + Buffers: shared hit=4 + -> Nested Loop (cost=0.99..151.11 rows=9 width=0) (actual time=0.005..0.005 rows=0 loops=1) + Buffers: shared hit=4 + -> Index Scan using index_payment_receipts_on_organization_id_lower_number on payment_receipts (cost=0.56..23.58 rows=24 width=16) (actual time=0.005..0.005 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (lower((number)::text) = 'perf-nope-rcpt-000001'::text)) + Buffers: shared hit=4 + -> Index Scan using payments_pkey on payments (cost=0.43..5.31 rows=1 width=16) (never executed) + Index Cond: (id = payment_receipts.payment_id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=24 +Planning Time: 0.175 ms +Execution Time: 0.025 ms diff --git a/script/perf/payments_filters/plans/after/receipt_miss.txt b/script/perf/payments_filters/plans/after/receipt_miss.txt new file mode 100644 index 00000000000..23a18c2c81d --- /dev/null +++ b/script/perf/payments_filters/plans/after/receipt_miss.txt @@ -0,0 +1,27 @@ +-- case: receipt_miss (list) phase: after selective: true +-- filters: {"receipt_number":"PERF-NOPE-RCPT-000001"} search_term: nil page: 1 +-- runs_ms: [0.0, 0.0, 0.0] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payment_receipts"."payment_id" FROM "payment_receipts" WHERE "payment_receipts"."organization_id" = '' AND (lower(payment_receipts.number) = lower('PERF-NOPE-RCPT-000001'))) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=151.25..151.27 rows=9 width=330) (actual time=0.008..0.008 rows=0 loops=1) + Buffers: shared hit=4 + -> Sort (cost=151.25..151.27 rows=9 width=330) (actual time=0.008..0.008 rows=0 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=4 + -> Nested Loop (cost=0.99..151.11 rows=9 width=330) (actual time=0.005..0.005 rows=0 loops=1) + Buffers: shared hit=4 + -> Index Scan using index_payment_receipts_on_organization_id_lower_number on payment_receipts (cost=0.56..23.58 rows=24 width=16) (actual time=0.005..0.005 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (lower((number)::text) = 'perf-nope-rcpt-000001'::text)) + Buffers: shared hit=4 + -> Index Scan using payments_pkey on payments (cost=0.43..5.31 rows=1 width=330) (never executed) + Index Cond: (id = payment_receipts.payment_id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=24 +Planning Time: 0.186 ms +Execution Time: 0.031 ms diff --git a/script/perf/payments_filters/plans/after/search_term.count.txt b/script/perf/payments_filters/plans/after/search_term.count.txt new file mode 100644 index 00000000000..bc715c360d0 --- /dev/null +++ b/script/perf/payments_filters/plans/after/search_term.count.txt @@ -0,0 +1,93 @@ +-- case: search_term (count) phase: after selective: true +-- filters: {} search_term: "_0_4031686" page: 1 +-- runs_ms: [56.8, 56.6, 52.0] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) + +Aggregate (cost=19334.33..19334.34 rows=1 width=8) (actual time=56.368..56.370 rows=1 loops=1) + Buffers: shared hit=4565 + -> Nested Loop (cost=5515.26..19331.85 rows=992 width=0) (actual time=56.366..56.368 rows=1 loops=1) + Buffers: shared hit=4565 + -> HashAggregate (cost=5514.83..5540.97 rows=2614 width=16) (actual time=56.336..56.338 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4557 + -> Append (cost=181.94..5508.29 rows=2614 width=16) (actual time=54.829..56.327 rows=1 loops=1) + Buffers: shared hit=4557 + -> Bitmap Heap Scan on payments payments_1 (cost=181.94..705.97 rows=472 width=16) (actual time=54.828..54.828 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4278 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..181.82 rows=472 width=0) (actual time=54.817..54.817 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4277 + -> Bitmap Heap Scan on payments payments_2 (cost=57.46..85.31 rows=25 width=16) (actual time=0.132..0.132 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..57.45 rows=25 width=0) (actual time=0.131..0.132 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Nested Loop (cost=251.44..1944.03 rows=360 width=16) (actual time=0.189..0.190 rows=0 loops=1) + Buffers: shared hit=40 + -> Bitmap Heap Scan on invoices (cost=251.01..749.64 rows=449 width=16) (actual time=0.189..0.189 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..250.89 rows=449 width=0) (actual time=0.188..0.188 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=118.62..2733.78 rows=1757 width=16) (actual time=1.174..1.175 rows=0 loops=1) + Buffers: shared hit=208 + -> HashAggregate (cost=118.19..118.32 rows=13 width=16) (actual time=1.173..1.174 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=208 + -> Append (cost=25.35..118.16 rows=13 width=16) (actual time=1.172..1.173 rows=0 loops=1) + Buffers: shared hit=208 + -> Bitmap Heap Scan on customers (cost=25.35..30.91 rows=5 width=16) (actual time=0.537..0.537 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.536..0.536 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_1 (cost=17.60..18.72 rows=1 width=16) (actual time=0.028..0.028 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.027..0.027 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_2 (cost=17.60..18.72 rows=1 width=16) (actual time=0.030..0.030 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.029..0.029 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_3 (cost=25.35..30.91 rows=5 width=16) (actual time=0.544..0.544 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.543..0.543 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_4 (cost=17.60..18.72 rows=1 width=16) (actual time=0.033..0.033 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.031..0.031 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..199.83 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=16) (actual time=0.027..0.028 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.015..0.015 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.985 ms +Execution Time: 56.574 ms diff --git a/script/perf/payments_filters/plans/after/search_term.txt b/script/perf/payments_filters/plans/after/search_term.txt new file mode 100644 index 00000000000..076e62fb943 --- /dev/null +++ b/script/perf/payments_filters/plans/after/search_term.txt @@ -0,0 +1,97 @@ +-- case: search_term (list) phase: after selective: true +-- filters: {} search_term: "_0_4031686" page: 1 +-- runs_ms: [54.0, 52.4, 51.8] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=19358.25..19358.30 rows=20 width=330) (actual time=52.256..52.259 rows=1 loops=1) + Buffers: shared hit=4565 + -> Sort (cost=19358.25..19360.73 rows=992 width=330) (actual time=52.256..52.258 rows=1 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=4565 + -> Nested Loop (cost=5515.26..19331.85 rows=992 width=330) (actual time=52.248..52.251 rows=1 loops=1) + Buffers: shared hit=4565 + -> HashAggregate (cost=5514.83..5540.97 rows=2614 width=16) (actual time=52.223..52.225 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4557 + -> Append (cost=181.94..5508.29 rows=2614 width=16) (actual time=50.677..52.214 rows=1 loops=1) + Buffers: shared hit=4557 + -> Bitmap Heap Scan on payments payments_1 (cost=181.94..705.97 rows=472 width=16) (actual time=50.676..50.677 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4278 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..181.82 rows=472 width=0) (actual time=50.669..50.669 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4277 + -> Bitmap Heap Scan on payments payments_2 (cost=57.46..85.31 rows=25 width=16) (actual time=0.123..0.123 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..57.45 rows=25 width=0) (actual time=0.122..0.122 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Nested Loop (cost=251.44..1944.03 rows=360 width=16) (actual time=0.195..0.196 rows=0 loops=1) + Buffers: shared hit=40 + -> Bitmap Heap Scan on invoices (cost=251.01..749.64 rows=449 width=16) (actual time=0.195..0.195 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..250.89 rows=449 width=0) (actual time=0.194..0.194 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=118.62..2733.78 rows=1757 width=16) (actual time=1.216..1.218 rows=0 loops=1) + Buffers: shared hit=208 + -> HashAggregate (cost=118.19..118.32 rows=13 width=16) (actual time=1.216..1.217 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=208 + -> Append (cost=25.35..118.16 rows=13 width=16) (actual time=1.215..1.216 rows=0 loops=1) + Buffers: shared hit=208 + -> Bitmap Heap Scan on customers (cost=25.35..30.91 rows=5 width=16) (actual time=0.533..0.534 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.533..0.533 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_1 (cost=17.60..18.72 rows=1 width=16) (actual time=0.028..0.028 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.028..0.028 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_2 (cost=17.60..18.72 rows=1 width=16) (actual time=0.030..0.030 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.030..0.030 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_3 (cost=25.35..30.91 rows=5 width=16) (actual time=0.595..0.595 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.594..0.594 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_4 (cost=17.60..18.72 rows=1 width=16) (actual time=0.027..0.028 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.027..0.027 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..199.83 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=330) (actual time=0.023..0.023 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.011..0.011 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.791 ms +Execution Time: 52.433 ms diff --git a/script/perf/payments_filters/plans/after/search_term_status.count.txt b/script/perf/payments_filters/plans/after/search_term_status.count.txt new file mode 100644 index 00000000000..31a2511c45e --- /dev/null +++ b/script/perf/payments_filters/plans/after/search_term_status.count.txt @@ -0,0 +1,93 @@ +-- case: search_term_status (count) phase: after selective: true +-- filters: {"payment_status":["succeeded"]} search_term: "_0_4031686" page: 1 +-- runs_ms: [53.4, 51.2, 50.9] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) AND "payments"."payable_payment_status" = 'succeeded' + +Aggregate (cost=19340.43..19340.44 rows=1 width=8) (actual time=51.047..51.050 rows=1 loops=1) + Buffers: shared hit=4565 + -> Nested Loop (cost=5515.26..19338.39 rows=819 width=0) (actual time=51.046..51.048 rows=1 loops=1) + Buffers: shared hit=4565 + -> HashAggregate (cost=5514.83..5540.97 rows=2614 width=16) (actual time=51.021..51.024 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4557 + -> Append (cost=181.94..5508.29 rows=2614 width=16) (actual time=49.391..51.013 rows=1 loops=1) + Buffers: shared hit=4557 + -> Bitmap Heap Scan on payments payments_1 (cost=181.94..705.97 rows=472 width=16) (actual time=49.390..49.391 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4278 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..181.82 rows=472 width=0) (actual time=49.382..49.382 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4277 + -> Bitmap Heap Scan on payments payments_2 (cost=57.46..85.31 rows=25 width=16) (actual time=0.127..0.127 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..57.45 rows=25 width=0) (actual time=0.127..0.127 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Nested Loop (cost=251.44..1944.03 rows=360 width=16) (actual time=0.191..0.191 rows=0 loops=1) + Buffers: shared hit=40 + -> Bitmap Heap Scan on invoices (cost=251.01..749.64 rows=449 width=16) (actual time=0.191..0.191 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..250.89 rows=449 width=0) (actual time=0.190..0.190 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=118.62..2733.78 rows=1757 width=16) (actual time=1.301..1.302 rows=0 loops=1) + Buffers: shared hit=208 + -> HashAggregate (cost=118.19..118.32 rows=13 width=16) (actual time=1.301..1.302 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=208 + -> Append (cost=25.35..118.16 rows=13 width=16) (actual time=1.300..1.301 rows=0 loops=1) + Buffers: shared hit=208 + -> Bitmap Heap Scan on customers (cost=25.35..30.91 rows=5 width=16) (actual time=0.583..0.583 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.582..0.582 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_1 (cost=17.60..18.72 rows=1 width=16) (actual time=0.029..0.029 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.029..0.029 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_2 (cost=17.60..18.72 rows=1 width=16) (actual time=0.040..0.040 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.040..0.040 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_3 (cost=25.35..30.91 rows=5 width=16) (actual time=0.617..0.617 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.616..0.616 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_4 (cost=17.60..18.72 rows=1 width=16) (actual time=0.030..0.030 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.029..0.029 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..199.83 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=16) (actual time=0.023..0.024 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.011..0.011 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.813 ms +Execution Time: 51.216 ms diff --git a/script/perf/payments_filters/plans/after/search_term_status.txt b/script/perf/payments_filters/plans/after/search_term_status.txt new file mode 100644 index 00000000000..26e90e03d32 --- /dev/null +++ b/script/perf/payments_filters/plans/after/search_term_status.txt @@ -0,0 +1,97 @@ +-- case: search_term_status (list) phase: after selective: true +-- filters: {"payment_status":["succeeded"]} search_term: "_0_4031686" page: 1 +-- runs_ms: [54.0, 52.7, 55.4] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) AND "payments"."payable_payment_status" = 'succeeded' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=19360.18..19360.23 rows=20 width=330) (actual time=53.813..53.816 rows=1 loops=1) + Buffers: shared hit=4565 + -> Sort (cost=19360.18..19362.23 rows=819 width=330) (actual time=53.812..53.815 rows=1 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=4565 + -> Nested Loop (cost=5515.26..19338.39 rows=819 width=330) (actual time=53.802..53.805 rows=1 loops=1) + Buffers: shared hit=4565 + -> HashAggregate (cost=5514.83..5540.97 rows=2614 width=16) (actual time=53.774..53.777 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4557 + -> Append (cost=181.94..5508.29 rows=2614 width=16) (actual time=52.232..53.762 rows=1 loops=1) + Buffers: shared hit=4557 + -> Bitmap Heap Scan on payments payments_1 (cost=181.94..705.97 rows=472 width=16) (actual time=52.232..52.232 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4278 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..181.82 rows=472 width=0) (actual time=52.223..52.224 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4277 + -> Bitmap Heap Scan on payments payments_2 (cost=57.46..85.31 rows=25 width=16) (actual time=0.119..0.119 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..57.45 rows=25 width=0) (actual time=0.119..0.119 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=31 + -> Nested Loop (cost=251.44..1944.03 rows=360 width=16) (actual time=0.193..0.194 rows=0 loops=1) + Buffers: shared hit=40 + -> Bitmap Heap Scan on invoices (cost=251.01..749.64 rows=449 width=16) (actual time=0.193..0.193 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..250.89 rows=449 width=0) (actual time=0.192..0.193 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=40 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=118.62..2733.78 rows=1757 width=16) (actual time=1.214..1.215 rows=0 loops=1) + Buffers: shared hit=208 + -> HashAggregate (cost=118.19..118.32 rows=13 width=16) (actual time=1.213..1.214 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=208 + -> Append (cost=25.35..118.16 rows=13 width=16) (actual time=1.212..1.213 rows=0 loops=1) + Buffers: shared hit=208 + -> Bitmap Heap Scan on customers (cost=25.35..30.91 rows=5 width=16) (actual time=0.550..0.550 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.549..0.549 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_1 (cost=17.60..18.72 rows=1 width=16) (actual time=0.031..0.031 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.031..0.031 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_2 (cost=17.60..18.72 rows=1 width=16) (actual time=0.038..0.038 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.038..0.038 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Bitmap Heap Scan on customers customers_3 (cost=25.35..30.91 rows=5 width=16) (actual time=0.559..0.559 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=68 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..25.35 rows=5 width=0) (actual time=0.558..0.558 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=68 + -> Bitmap Heap Scan on customers customers_4 (cost=17.60..18.72 rows=1 width=16) (actual time=0.032..0.032 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=24 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..17.60 rows=1 width=0) (actual time=0.032..0.032 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=24 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..199.83 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=330) (actual time=0.025..0.026 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.013..0.013 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.878 ms +Execution Time: 54.002 ms diff --git a/script/perf/payments_filters/plans/after/status_common_succeeded.count.txt b/script/perf/payments_filters/plans/after/status_common_succeeded.count.txt new file mode 100644 index 00000000000..297247634da --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_common_succeeded.count.txt @@ -0,0 +1,30 @@ +-- case: status_common_succeeded (count) phase: after selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 1 +-- runs_ms: [12041.6, 12572.8, 12473.4] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' + +Aggregate (cost=13558635.47..13558635.48 rows=1 width=8) (actual time=12472.216..12472.217 rows=1 loops=1) + Buffers: shared hit=15731507 read=162161 + I/O Timings: shared/local read=711.522 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13553501.19 rows=2053709 width=0) (actual time=64.932..12354.859 rows=4100643 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 899357 + Buffers: shared hit=15731507 read=162161 + I/O Timings: shared/local read=711.522 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=3932914) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=15731507 read=149 + I/O Timings: shared/local read=22.987 +Planning: + Buffers: shared hit=3 read=5 + I/O Timings: shared/local read=0.199 +Planning Time: 0.560 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.008 ms, Inlining 5.939 ms, Optimization 32.221 ms, Emission 26.644 ms, Total 65.813 ms +Execution Time: 12473.382 ms diff --git a/script/perf/payments_filters/plans/after/status_common_succeeded.txt b/script/perf/payments_filters/plans/after/status_common_succeeded.txt new file mode 100644 index 00000000000..12be66c5570 --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_common_succeeded.txt @@ -0,0 +1,22 @@ +-- case: status_common_succeeded (list) phase: after selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 1 +-- runs_ms: [2.3, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..132.95 rows=20 width=330) (actual time=0.018..0.055 rows=20 loops=1) + Buffers: shared hit=108 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=2053709 width=330) (actual time=0.018..0.054 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4 + Buffers: shared hit=108 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.125 ms +Execution Time: 0.071 ms diff --git a/script/perf/payments_filters/plans/after/status_common_succeeded_page50.count.txt b/script/perf/payments_filters/plans/after/status_common_succeeded_page50.count.txt new file mode 100644 index 00000000000..137ec866225 --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_common_succeeded_page50.count.txt @@ -0,0 +1,30 @@ +-- case: status_common_succeeded_page50 (count) phase: after selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 50 +-- runs_ms: [16027.1, 15925.3, 14960.4] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' + +Aggregate (cost=13558635.47..13558635.48 rows=1 width=8) (actual time=15923.331..15923.335 rows=1 loops=1) + Buffers: shared hit=15711524 read=182144 written=201 + I/O Timings: shared/local read=2402.205 write=1.611 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13553501.19 rows=2053709 width=0) (actual time=75.681..15791.985 rows=4100643 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 899357 + Buffers: shared hit=15711524 read=182144 written=201 + I/O Timings: shared/local read=2402.205 write=1.611 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=3932914) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=15711523 read=20133 written=98 + I/O Timings: shared/local read=1587.338 write=0.726 +Planning: + Buffers: shared hit=703 read=116 + I/O Timings: shared/local read=14.495 +Planning Time: 17.522 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.613 ms, Inlining 6.843 ms, Optimization 36.413 ms, Emission 32.406 ms, Total 77.274 ms +Execution Time: 15925.291 ms diff --git a/script/perf/payments_filters/plans/after/status_common_succeeded_page50.txt b/script/perf/payments_filters/plans/after/status_common_succeeded_page50.txt new file mode 100644 index 00000000000..dec3f49554f --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_common_succeeded_page50.txt @@ -0,0 +1,22 @@ +-- case: status_common_succeeded_page50 (list) phase: after selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 50 +-- runs_ms: [38.0, 3.8, 1.8] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 980 + +Limit (cost=6487.58..6619.96 rows=20 width=330) (actual time=3.711..3.752 rows=20 loops=1) + Buffers: shared hit=4980 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=2053709 width=330) (actual time=0.025..3.733 rows=1000 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 164 + Buffers: shared hit=4980 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=951) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=3804 +Planning: + Buffers: shared hit=8 +Planning Time: 0.196 ms +Execution Time: 3.776 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_failed.count.txt b/script/perf/payments_filters/plans/after/status_rare_failed.count.txt new file mode 100644 index 00000000000..cb543ea7c02 --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_failed.count.txt @@ -0,0 +1,30 @@ +-- case: status_rare_failed (count) phase: after selective: false +-- filters: {"payment_status":["failed"]} search_term: nil page: 1 +-- runs_ms: [9734.0, 11470.9, 10938.7] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' + +Aggregate (cost=13554417.97..13554417.98 rows=1 width=8) (actual time=10934.823..10934.824 rows=1 loops=1) + Buffers: shared hit=2687888 read=228988 + I/O Timings: shared/local read=7706.638 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13553501.19 rows=366710 width=0) (actual time=70.154..10900.385 rows=718336 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4281664 + Buffers: shared hit=2687888 read=228988 + I/O Timings: shared/local read=7706.638 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.014..0.014 rows=1 loops=688716) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=2687888 read=66976 + I/O Timings: shared/local read=7109.931 +Planning: + Buffers: shared hit=6 read=2 + I/O Timings: shared/local read=0.103 +Planning Time: 0.564 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 3.677 ms, Inlining 7.706 ms, Optimization 33.757 ms, Emission 28.523 ms, Total 73.662 ms +Execution Time: 10938.703 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_failed.txt b/script/perf/payments_filters/plans/after/status_rare_failed.txt new file mode 100644 index 00000000000..51ce100b4c2 --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_failed.txt @@ -0,0 +1,21 @@ +-- case: status_rare_failed (list) phase: after selective: false +-- filters: {"payment_status":["failed"]} search_term: nil page: 1 +-- runs_ms: [9.1, 0.3, 0.2] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..741.98 rows=20 width=330) (actual time=0.038..0.256 rows=20 loops=1) + Buffers: shared hit=409 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=366710 width=330) (actual time=0.037..0.256 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 303 + Buffers: shared hit=409 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.185 ms +Execution Time: 0.273 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_pending.count.txt b/script/perf/payments_filters/plans/after/status_rare_pending.count.txt new file mode 100644 index 00000000000..5736cb1f8f7 --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_pending.count.txt @@ -0,0 +1,26 @@ +-- case: status_rare_pending (count) phase: after selective: true +-- filters: {"payment_status":["pending"]} search_term: nil page: 1 +-- runs_ms: [398.1, 353.9, 330.5] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'pending' + +Aggregate (cost=317569.39..317569.40 rows=1 width=8) (actual time=352.572..352.573 rows=1 loops=1) + Buffers: shared hit=432875 + -> Index Scan using index_payments_on_org_pending_processing_created_at on payments (cost=0.42..317455.83 rows=45424 width=0) (actual time=6.230..349.371 rows=89176 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (payable_payment_status = 'pending'::payment_payable_payment_status)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 838 + Buffers: shared hit=432875 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=85530) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=342120 +Planning: + Buffers: shared hit=8 +Planning Time: 0.782 ms +JIT: + Functions: 14 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 1.248 ms, Inlining 0.000 ms, Optimization 0.683 ms, Emission 5.539 ms, Total 7.470 ms +Execution Time: 353.914 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_pending.txt b/script/perf/payments_filters/plans/after/status_rare_pending.txt new file mode 100644 index 00000000000..fad9263a34a --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_pending.txt @@ -0,0 +1,20 @@ +-- case: status_rare_pending (list) phase: after selective: true +-- filters: {"payment_status":["pending"]} search_term: nil page: 1 +-- runs_ms: [0.3, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'pending' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.42..140.19 rows=20 width=330) (actual time=0.012..0.043 rows=20 loops=1) + Buffers: shared hit=103 + -> Index Scan using index_payments_on_org_pending_processing_created_at on payments (cost=0.42..317455.83 rows=45424 width=330) (actual time=0.012..0.042 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (payable_payment_status = 'pending'::payment_payable_payment_status)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=103 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.116 ms +Execution Time: 0.063 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_pending_processing.count.txt b/script/perf/payments_filters/plans/after/status_rare_pending_processing.count.txt new file mode 100644 index 00000000000..2e4cccc8d2e --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_pending_processing.count.txt @@ -0,0 +1,26 @@ +-- case: status_rare_pending_processing (count) phase: after selective: true +-- filters: {"payment_status":["pending","processing"]} search_term: nil page: 1 +-- runs_ms: [646.4, 528.3, 498.9] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" IN ('pending', 'processing') + +Aggregate (cost=455164.25..455164.26 rows=1 width=8) (actual time=527.533..527.534 rows=1 loops=1) + Buffers: shared hit=648943 + -> Index Scan using index_payments_on_org_pending_processing_created_at on payments (cost=0.42..454998.47 rows=66313 width=0) (actual time=6.478..522.686 rows=133740 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1277 + Buffers: shared hit=648943 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=128204) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=512816 +Planning: + Buffers: shared hit=8 +Planning Time: 0.224 ms +JIT: + Functions: 14 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.706 ms, Inlining 0.000 ms, Optimization 0.477 ms, Emission 5.973 ms, Total 7.156 ms +Execution Time: 528.326 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_pending_processing.txt b/script/perf/payments_filters/plans/after/status_rare_pending_processing.txt new file mode 100644 index 00000000000..54b811e2ad8 --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_pending_processing.txt @@ -0,0 +1,22 @@ +-- case: status_rare_pending_processing (list) phase: after selective: true +-- filters: {"payment_status":["pending","processing"]} search_term: nil page: 1 +-- runs_ms: [12.2, 0.5, 0.3] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" IN ('pending', 'processing') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..4100.61 rows=20 width=330) (actual time=0.019..0.463 rows=20 loops=1) + Buffers: shared hit=770 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13594334.57 rows=66313 width=330) (actual time=0.019..0.462 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = ANY ('{pending,processing}'::payment_payable_payment_status[])) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 658 + Buffers: shared hit=770 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.173 ms +Execution Time: 0.484 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_processing.count.txt b/script/perf/payments_filters/plans/after/status_rare_processing.count.txt new file mode 100644 index 00000000000..252294ec159 --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_processing.count.txt @@ -0,0 +1,26 @@ +-- case: status_rare_processing (count) phase: after selective: true +-- filters: {"payment_status":["processing"]} search_term: nil page: 1 +-- runs_ms: [177.2, 178.9, 172.1] +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'processing' + +Aggregate (cost=149631.51..149631.52 rows=1 width=8) (actual time=176.497..176.498 rows=1 loops=1) + Buffers: shared hit=216071 + -> Index Scan using index_payments_on_org_pending_processing_created_at on payments (cost=0.42..149579.29 rows=20889 width=0) (actual time=5.316..174.923 rows=44564 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (payable_payment_status = 'processing'::payment_payable_payment_status)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 439 + Buffers: shared hit=216071 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=42674) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=170696 +Planning: + Buffers: shared hit=8 +Planning Time: 0.094 ms +JIT: + Functions: 14 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.636 ms, Inlining 0.000 ms, Optimization 0.422 ms, Emission 4.893 ms, Total 5.951 ms +Execution Time: 177.208 ms diff --git a/script/perf/payments_filters/plans/after/status_rare_processing.txt b/script/perf/payments_filters/plans/after/status_rare_processing.txt new file mode 100644 index 00000000000..b11d53a9deb --- /dev/null +++ b/script/perf/payments_filters/plans/after/status_rare_processing.txt @@ -0,0 +1,22 @@ +-- case: status_rare_processing (list) phase: after selective: true +-- filters: {"payment_status":["processing"]} search_term: nil page: 1 +-- runs_ms: [0.2, 0.1, 0.1] +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'processing' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.42..143.63 rows=20 width=330) (actual time=0.017..0.092 rows=20 loops=1) + Buffers: shared hit=108 + -> Index Scan using index_payments_on_org_pending_processing_created_at on payments (cost=0.42..149579.29 rows=20889 width=330) (actual time=0.017..0.091 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (payable_payment_status = 'processing'::payment_payable_payment_status)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=108 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.131 ms +Execution Time: 0.111 ms diff --git a/script/perf/payments_filters/plans/after/summary.json b/script/perf/payments_filters/plans/after/summary.json new file mode 100644 index 00000000000..fd3f2005597 --- /dev/null +++ b/script/perf/payments_filters/plans/after/summary.json @@ -0,0 +1,2028 @@ +{ + "phase": "after", + "rebuilt_from_plans": true, + "cases": [ + { + "name": "amount_common_from_p50", + "page": 1, + "selective": false, + "filters": { + "amount_from": 4914 + }, + "search_term": null, + "count": { + "ms": 9161.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 4851663, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 9497639, + "shared_read": 164193, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 9161.0, + 9264.5, + 8918.3 + ] + }, + "list": { + "ms": 0.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 58, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 117, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 4.1, + 0.3, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "amount_rare_from_p99", + "page": 1, + "selective": false, + "filters": { + "amount_from": 80467 + }, + "search_term": null, + "count": { + "ms": 1023.5, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 97188, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 352469, + "shared_read": 23, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 2176.3, + 1023.5, + 613.1 + ] + }, + "list": { + "ms": 2.7, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2738, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 21.1, + 2.7, + 1.7 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "amount_rare_range", + "page": 1, + "selective": false, + "filters": { + "amount_from": 80467, + "amount_to": 160934 + }, + "search_term": null, + "count": { + "ms": 523.1, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 78490, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 315860, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 523.1, + 514.1, + 543.2 + ] + }, + "list": { + "ms": 8.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 3209, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 11.1, + 8.0, + 8.5 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "combo_customer_status_date", + "page": 1, + "selective": true, + "filters": { + "external_customer_id": "perf-cust-0-1", + "payment_status": [ + "succeeded" + ], + "created_at_from": "2024-09-08", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 465.2, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5354374, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 520301, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 465.2, + 467.6, + 456.5 + ] + }, + "list": { + "ms": 0.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1191, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 7.4, + 0.5, + 0.4 + ] + }, + "flags": [] + }, + { + "name": "combo_provider_status", + "page": 1, + "selective": false, + "filters": { + "payment_provider_type": [ + "stripe" + ], + "payment_status": [ + "failed" + ] + }, + "search_term": null, + "count": { + "ms": 5853.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 10126301, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2143493, + "shared_read": 222535, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 6277.8, + 5853.3, + 5536.7 + ] + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 412, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 1.7, + 0.1, + 0.2 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "combo_status_amount", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "failed" + ], + "amount_from": 4914 + }, + "search_term": null, + "count": { + "ms": 4364.5, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 704109, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1327787, + "shared_read": 212285, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 4144.2, + 4364.5, + 4557.5 + ] + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 584, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.7, + 0.2, + 0.2 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "combo_status_currency_date", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "succeeded" + ], + "currency": "EUR", + "created_at_from": "2026-09-01", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 5.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 4138, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 10755, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 8.4, + 5.6, + 4.6 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 109, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.2, + 0.1, + 0.1 + ] + }, + "flags": [] + }, + { + "name": "control", + "page": 1, + "selective": false, + "filters": {}, + "search_term": null, + "count": { + "ms": 15417.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9702554, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18989927, + "shared_read": 171421, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 15417.3, + 14304.5, + 17266.3 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 105, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 1.6, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "control_page50", + "page": 50, + "selective": false, + "filters": {}, + "search_term": null, + "count": { + "ms": 14840.4, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9702554, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18991130, + "shared_read": 170218, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 14896.9, + 14584.6, + 14840.4 + ] + }, + "list": { + "ms": 3.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 1979, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4860, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 28.6, + 3.5, + 1.8 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "created_24m", + "page": 1, + "selective": false, + "filters": { + "created_at_from": "2024-09-08", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 14673.2, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9700367, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18991594, + "shared_read": 165462, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 14211.8, + 16234.0, + 14673.2 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 105, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 1.4, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "created_7d", + "page": 1, + "selective": true, + "filters": { + "created_at_from": "2026-09-01", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 9.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5123, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 12699, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 9.6, + 9.7, + 8.2 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 105, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.2, + 0.1, + 0.1 + ] + }, + "flags": [] + }, + { + "name": "currency_common", + "page": 1, + "selective": false, + "filters": { + "currency": "EUR" + }, + "search_term": null, + "count": { + "ms": 14084.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9217625, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18047083, + "shared_read": 164189, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 14084.0, + 14744.6, + 13934.5 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 106, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 2.0, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "currency_rare", + "page": 1, + "selective": true, + "filters": { + "currency": "GBP" + }, + "search_term": null, + "count": { + "ms": 636.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 97248, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 352466, + "shared_read": 18, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 2033.1, + 636.3, + 566.2 + ] + }, + "list": { + "ms": 2.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2554, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 22.6, + 2.5, + 1.4 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "customer_heavy", + "page": 1, + "selective": true, + "filters": { + "external_customer_id": "perf-cust-0-1" + }, + "search_term": null, + "count": { + "ms": 532.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5400131, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 609885, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 573.9, + 530.5, + 532.8 + ] + }, + "list": { + "ms": 1.8, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1191, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 10.4, + 1.8, + 0.6 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "customer_light", + "page": 1, + "selective": true, + "filters": { + "external_customer_id": "perf-cust-0-23830" + }, + "search_term": null, + "count": { + "ms": 0.1, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 105, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 265, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.1, + 0.1, + 0.1 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 144, + "nodes": [ + "Sort", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 20, + "shared_hit": 265, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": [ + 0.1, + 0.1, + 0.1 + ] + }, + "flags": [] + }, + { + "name": "five_filter_common", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "succeeded" + ], + "currency": "EUR", + "created_at_from": "2024-09-08", + "created_at_to": "2026-09-08", + "amount_from": 100, + "payment_provider_type": [ + "stripe" + ] + }, + "search_term": null, + "count": { + "ms": 9715.4, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 15104334, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 11945440, + "shared_read": 168752, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 9227.8, + 9715.4, + 9742.8 + ] + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 123, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 1.9, + 0.2, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "five_filter_rare", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "failed" + ], + "currency": "GBP", + "created_at_from": "2026-09-01", + "created_at_to": "2026-09-08", + "amount_from": 4914, + "payment_provider_type": [ + "gocardless" + ] + }, + "search_term": null, + "count": { + "ms": 18.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 752467, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1072, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 19.8, + 18.8, + 16.9 + ] + }, + "list": { + "ms": 18.6, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 752466, + "nodes": [ + "Sort", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1072, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": true, + "all_runs_ms": [ + 19.1, + 18.3, + 18.6 + ] + }, + "flags": [] + }, + { + "name": "invoice_hit_direct", + "page": 1, + "selective": true, + "filters": { + "invoice_number": "her-1556-202609-000457624" + }, + "search_term": null, + "count": { + "ms": 0.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 13, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.0, + 0.0, + 0.0 + ] + }, + "list": { + "ms": 0.0, + "timeout": false, + "rows_returned": 2, + "rows_scanned": 8, + "nodes": [ + "Sort", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 2, + "shared_hit": 13, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": [ + 0.0, + 0.0, + 0.0 + ] + }, + "flags": [] + }, + { + "name": "invoice_hit_request", + "page": 1, + "selective": true, + "filters": { + "invoice_number": "her-1556-202609-000765141" + }, + "search_term": null, + "count": { + "ms": 0.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 3, + "nodes": [ + "Bitmap Heap Scan", + "BitmapOr", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 7, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.0, + 0.0, + 0.0 + ] + }, + "list": { + "ms": 0.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 4, + "nodes": [ + "Sort", + "Bitmap Heap Scan", + "BitmapOr", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 1, + "shared_hit": 7, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": [ + 0.0, + 0.0, + 0.0 + ] + }, + "flags": [] + }, + { + "name": "invoice_miss", + "page": 1, + "selective": true, + "filters": { + "invoice_number": "PERF-NOPE-000000-000000001" + }, + "search_term": null, + "count": { + "ms": 0.0, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 0, + "nodes": [], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": null, + "shared_read": null, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.0 + ] + }, + "list": { + "ms": 0.0, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 0, + "nodes": [ + "Sort", + "Result" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": null, + "shared_read": null, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.0, + 0.0, + 0.0 + ] + }, + "flags": [] + }, + { + "name": "payable_type_invoice", + "page": 1, + "selective": false, + "filters": { + "payable_type": [ + "Invoice" + ] + }, + "search_term": null, + "count": { + "ms": 15783.2, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 20679132, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18958875, + "shared_read": 227837, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 17310.6, + 15608.7, + 15783.2 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 110, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 1.0, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "payable_type_request", + "page": 1, + "selective": false, + "filters": { + "payable_type": [ + "PaymentRequest" + ] + }, + "search_term": null, + "count": { + "ms": 389.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5578006, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 129481, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 670.6, + 375.3, + 389.8 + ] + }, + "list": { + "ms": 0.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 40, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 427, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 4.8, + 0.3, + 0.1 + ] + }, + "flags": [] + }, + { + "name": "payment_type_manual", + "page": 1, + "selective": false, + "filters": { + "payment_type": [ + "manual" + ] + }, + "search_term": null, + "count": { + "ms": 6287.7, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5811961, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 891856, + "shared_read": 183147, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 6076.1, + 6287.7, + 6971.9 + ] + }, + "list": { + "ms": 0.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 743, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 1.4, + 0.3, + 0.2 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "payment_type_provider", + "page": 1, + "selective": false, + "filters": { + "payment_type": [ + "provider" + ] + }, + "search_term": null, + "count": { + "ms": 15557.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 20445177, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18040957, + "shared_read": 173910, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 15593.5, + 15557.3, + 14939.9 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 107, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 2.5, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "provider_common", + "page": 1, + "selective": false, + "filters": { + "payment_provider_type": [ + "stripe" + ] + }, + "search_term": null, + "count": { + "ms": 12901.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 16764092, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 15202828, + "shared_read": 161916, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 12901.0, + 14283.5, + 12684.0 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 117, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 2.5, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "provider_miss", + "page": 1, + "selective": true, + "filters": { + "payment_provider_type": [ + "cashfree" + ] + }, + "search_term": null, + "count": { + "ms": 0.0, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 0, + "nodes": [], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": null, + "shared_read": null, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.0 + ] + }, + "list": { + "ms": 0.0, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 0, + "nodes": [ + "Sort", + "Result" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": null, + "shared_read": null, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.9, + 0.0, + 0.0 + ] + }, + "flags": [] + }, + { + "name": "provider_rare", + "page": 1, + "selective": false, + "filters": { + "payment_provider_type": [ + "gocardless" + ] + }, + "search_term": null, + "count": { + "ms": 9222.5, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 7204768, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2785257, + "shared_read": 220988, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 7478.4, + 9222.5, + 10728.5 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 59, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 188, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 2.4, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "receipt_hit", + "page": 1, + "selective": true, + "filters": { + "receipt_number": "her-1556-335-rcpt-000123" + }, + "search_term": null, + "count": { + "ms": 0.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9, + "nodes": [ + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 22, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.0, + 0.1, + 0.0 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 2, + "rows_scanned": 12, + "nodes": [ + "Sort", + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 2, + "shared_hit": 22, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": [ + 0.1, + 0.1, + 0.1 + ] + }, + "flags": [] + }, + { + "name": "receipt_miss", + "page": 1, + "selective": true, + "filters": { + "receipt_number": "PERF-NOPE-RCPT-000001" + }, + "search_term": null, + "count": { + "ms": 0.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 1, + "nodes": [ + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.0, + 0.0, + 0.0 + ] + }, + "list": { + "ms": 0.0, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 0, + "nodes": [ + "Sort", + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": [ + 0.0, + 0.0, + 0.0 + ] + }, + "flags": [] + }, + { + "name": "search_term", + "page": 1, + "selective": true, + "filters": {}, + "search_term": "_0_4031686", + "count": { + "ms": 56.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8, + "nodes": [ + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4565, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 56.8, + 56.6, + 52.0 + ] + }, + "list": { + "ms": 52.4, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9, + "nodes": [ + "Sort", + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 1, + "shared_hit": 4565, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": [ + 54.0, + 52.4, + 51.8 + ] + }, + "flags": [] + }, + { + "name": "search_term_status", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "succeeded" + ] + }, + "search_term": "_0_4031686", + "count": { + "ms": 51.2, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8, + "nodes": [ + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4565, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 53.4, + 51.2, + 50.9 + ] + }, + "list": { + "ms": 54.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9, + "nodes": [ + "Sort", + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 1, + "shared_hit": 4565, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": [ + 54.0, + 52.7, + 55.4 + ] + }, + "flags": [] + }, + { + "name": "status_common_succeeded", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "succeeded" + ] + }, + "search_term": null, + "count": { + "ms": 12473.4, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8033558, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 15731507, + "shared_read": 162161, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 12041.6, + 12572.8, + 12473.4 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 108, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 2.3, + 0.1, + 0.1 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_common_succeeded_page50", + "page": 50, + "selective": false, + "filters": { + "payment_status": [ + "succeeded" + ] + }, + "search_term": null, + "count": { + "ms": 15925.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8033558, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 15711524, + "shared_read": 182144, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 16027.1, + 15925.3, + 14960.4 + ] + }, + "list": { + "ms": 3.8, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 1971, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4980, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 38.0, + 3.8, + 1.8 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_rare_failed", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "failed" + ] + }, + "search_term": null, + "count": { + "ms": 10938.7, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 1407053, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2687888, + "shared_read": 228988, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 9734.0, + 11470.9, + 10938.7 + ] + }, + "list": { + "ms": 0.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 409, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 9.1, + 0.3, + 0.2 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_rare_pending", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "pending" + ] + }, + "search_term": null, + "count": { + "ms": 353.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 174707, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 432875, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 398.1, + 353.9, + 330.5 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 103, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.3, + 0.1, + 0.1 + ] + }, + "flags": [] + }, + { + "name": "status_rare_pending_processing", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "pending", + "processing" + ] + }, + "search_term": null, + "count": { + "ms": 528.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 261945, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 648943, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 646.4, + 528.3, + 498.9 + ] + }, + "list": { + "ms": 0.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 770, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": [ + 12.2, + 0.5, + 0.3 + ] + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_rare_processing", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "processing" + ] + }, + "search_term": null, + "count": { + "ms": 177.2, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 87239, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 216071, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 177.2, + 178.9, + 172.1 + ] + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 108, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": [ + 0.2, + 0.1, + 0.1 + ] + }, + "flags": [] + } + ] +} \ No newline at end of file diff --git a/script/perf/payments_filters/plans/after/summary.md b/script/perf/payments_filters/plans/after/summary.md new file mode 100644 index 00000000000..75bbd076417 --- /dev/null +++ b/script/perf/payments_filters/plans/after/summary.md @@ -0,0 +1,43 @@ +# Plans: after + +Median of n (see plan headers) EXPLAIN (ANALYZE, BUFFERS) runs per statement. Synthetic dataset. `ms` is nil when the statement hit the timeout. + +| case | page | selective | list ms | count ms | rows | list nodes | seq scan (watched) | max sort rows | shared read (list) | ordering by cursor index | flags | +|---|---|---|---|---|---|---|---|---|---|---|---| +| amount_common_from_p50 | 1 | false | 0.3 | 9161.0 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| amount_rare_from_p99 | 1 | false | 2.7 | 1023.5 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| amount_rare_range | 1 | false | 8.5 | 523.1 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| combo_customer_status_date | 1 | true | 0.5 | 465.2 | 20 | Index Scan | | 0 | 0 | true | | +| combo_provider_status | 1 | false | 0.2 | 5853.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| combo_status_amount | 1 | false | 0.2 | 4364.5 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| combo_status_currency_date | 1 | true | 0.1 | 5.6 | 20 | Index Scan | | 0 | 0 | true | | +| control | 1 | false | 0.1 | 15417.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| control_page50 | 50 | false | 3.5 | 14840.4 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| created_24m | 1 | false | 0.1 | 14673.2 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| created_7d | 1 | true | 0.1 | 9.6 | 20 | Index Scan | | 0 | 0 | true | | +| currency_common | 1 | false | 0.1 | 14084.0 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| currency_rare | 1 | true | 2.5 | 636.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| customer_heavy | 1 | true | 1.8 | 532.8 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| customer_light | 1 | true | 0.1 | 0.1 | 20 | Sort, Index Scan | | 20 | 0 | false | | +| five_filter_common | 1 | false | 0.2 | 9715.4 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| five_filter_rare | 1 | true | 18.6 | 18.8 | 0 | Sort, Bitmap Heap Scan, BitmapAnd, Bitmap Index Scan, Index Scan | | 0 | 0 | false | | +| invoice_hit_direct | 1 | true | 0.0 | 0.0 | 2 | Sort, Index Scan | | 2 | 0 | false | | +| invoice_hit_request | 1 | true | 0.0 | 0.0 | 1 | Sort, Bitmap Heap Scan, BitmapOr, Bitmap Index Scan, Index Scan | | 1 | 0 | false | | +| invoice_miss | 1 | true | 0.0 | 0.0 | 0 | Sort, Result | | 0 | | false | | +| payable_type_invoice | 1 | false | 0.1 | 15783.2 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| payable_type_request | 1 | false | 0.3 | 389.8 | 20 | Index Scan | | 0 | 0 | true | | +| payment_type_manual | 1 | false | 0.3 | 6287.7 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| payment_type_provider | 1 | false | 0.1 | 15557.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| provider_common | 1 | false | 0.1 | 12901.0 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| provider_miss | 1 | true | 0.0 | 0.0 | 0 | Sort, Result | | 0 | | false | | +| provider_rare | 1 | false | 0.1 | 9222.5 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| receipt_hit | 1 | true | 0.1 | 0.0 | 2 | Sort, Nested Loop, Index Scan | | 2 | 0 | false | | +| receipt_miss | 1 | true | 0.0 | 0.0 | 0 | Sort, Nested Loop, Index Scan | | 0 | 0 | false | | +| search_term | 1 | true | 52.4 | 56.6 | 1 | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | 1 | 0 | false | | +| search_term_status | 1 | true | 54.0 | 51.2 | 1 | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | 1 | 0 | false | | +| status_common_succeeded | 1 | false | 0.1 | 12473.4 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_common_succeeded_page50 | 50 | false | 3.8 | 15925.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_rare_failed | 1 | false | 0.3 | 10938.7 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_rare_pending | 1 | true | 0.1 | 353.9 | 20 | Index Scan | | 0 | 0 | false | | +| status_rare_pending_processing | 1 | true | 0.5 | 528.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_rare_processing | 1 | true | 0.1 | 177.2 | 20 | Index Scan | | 0 | 0 | false | | diff --git a/script/perf/payments_filters/plans/baseline/amount_common_from_p50.count.txt b/script/perf/payments_filters/plans/baseline/amount_common_from_p50.count.txt new file mode 100644 index 00000000000..b06e7f931a6 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/amount_common_from_p50.count.txt @@ -0,0 +1,29 @@ +-- case: amount_common_from_p50 (count) phase: baseline selective: false +-- filters: {"amount_from":4914} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 4914::bigint) + +Aggregate (cost=13591184.32..13591184.33 rows=1 width=8) (actual time=7615.003..7615.004 rows=1 loops=1) + Buffers: shared hit=9483936 read=177896 + I/O Timings: shared/local read=458.825 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=1236871 width=0) (actual time=62.469..7547.362 rows=2476707 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2523293 + Buffers: shared hit=9483936 read=177896 + I/O Timings: shared/local read=458.825 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=2374955) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=9483936 read=15884 + I/O Timings: shared/local read=53.049 +Planning: + Buffers: shared hit=6 read=2 + I/O Timings: shared/local read=0.011 +Planning Time: 0.192 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.638 ms, Inlining 3.879 ms, Optimization 32.176 ms, Emission 26.384 ms, Total 63.077 ms +Execution Time: 7615.752 ms diff --git a/script/perf/payments_filters/plans/baseline/amount_common_from_p50.txt b/script/perf/payments_filters/plans/baseline/amount_common_from_p50.txt new file mode 100644 index 00000000000..523085c600e --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/amount_common_from_p50.txt @@ -0,0 +1,20 @@ +-- case: amount_common_from_p50 (list) phase: baseline selective: false +-- filters: {"amount_from":4914} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 4914::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..220.94 rows=20 width=330) (actual time=0.069..0.195 rows=20 loops=1) + Buffers: shared hit=117 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=1236871 width=330) (actual time=0.067..0.192 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 21 + Buffers: shared hit=117 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.005..0.005 rows=1 loops=18) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=72 +Planning: + Buffers: shared hit=8 +Planning Time: 0.314 ms +Execution Time: 0.236 ms diff --git a/script/perf/payments_filters/plans/baseline/amount_rare_from_p99.count.txt b/script/perf/payments_filters/plans/baseline/amount_rare_from_p99.count.txt new file mode 100644 index 00000000000..78fa8cdc294 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/amount_rare_from_p99.count.txt @@ -0,0 +1,25 @@ +-- case: amount_rare_from_p99 (count) phase: baseline selective: false +-- filters: {"amount_from":80467} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) + +Aggregate (cost=13588155.27..13588155.28 rows=1 width=8) (actual time=625.860..625.861 rows=1 loops=1) + Buffers: shared hit=352492 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=25253 width=0) (actual time=74.975..623.301 rows=49567 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4950433 + Buffers: shared hit=352492 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=47620) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=190480 +Planning: + Buffers: shared hit=8 +Planning Time: 0.211 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.693 ms, Inlining 4.792 ms, Optimization 38.549 ms, Emission 31.587 ms, Total 75.621 ms +Execution Time: 626.648 ms diff --git a/script/perf/payments_filters/plans/baseline/amount_rare_from_p99.txt b/script/perf/payments_filters/plans/baseline/amount_rare_from_p99.txt new file mode 100644 index 00000000000..55eac370f6c --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/amount_rare_from_p99.txt @@ -0,0 +1,20 @@ +-- case: amount_rare_from_p99 (list) phase: baseline selective: false +-- filters: {"amount_from":80467} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..10794.66 rows=20 width=330) (actual time=0.078..1.898 rows=20 loops=1) + Buffers: shared hit=2738 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=25253 width=330) (actual time=0.078..1.896 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2616 + Buffers: shared hit=2738 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.160 ms +Execution Time: 1.920 ms diff --git a/script/perf/payments_filters/plans/baseline/amount_rare_range.count.txt b/script/perf/payments_filters/plans/baseline/amount_rare_range.count.txt new file mode 100644 index 00000000000..62e721aa95f --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/amount_rare_range.count.txt @@ -0,0 +1,25 @@ +-- case: amount_rare_range (count) phase: baseline selective: false +-- filters: {"amount_from":80467,"amount_to":160934} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) AND (payments.amount_cents <= 160934::bigint) + +Aggregate (cost=13600560.95..13600560.96 rows=1 width=8) (actual time=557.961..557.962 rows=1 loops=1) + Buffers: shared hit=315860 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13600558.37 rows=1033 width=0) (actual time=59.281..556.246 rows=40027 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND (amount_cents <= '160934'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4959973 + Buffers: shared hit=315860 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=38462) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=153848 +Planning: + Buffers: shared hit=8 +Planning Time: 0.161 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.566 ms, Inlining 3.931 ms, Optimization 29.878 ms, Emission 25.435 ms, Total 59.810 ms +Execution Time: 558.690 ms diff --git a/script/perf/payments_filters/plans/baseline/amount_rare_range.txt b/script/perf/payments_filters/plans/baseline/amount_rare_range.txt new file mode 100644 index 00000000000..8cc7e907c65 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/amount_rare_range.txt @@ -0,0 +1,24 @@ +-- case: amount_rare_range (list) phase: baseline selective: false +-- filters: {"amount_from":80467,"amount_to":160934} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (payments.amount_cents >= 80467::bigint) AND (payments.amount_cents <= 160934::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..264117.49 rows=20 width=330) (actual time=4.498..7.402 rows=20 loops=1) + Buffers: shared hit=3209 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13641640.14 rows=1033 width=330) (actual time=0.120..3.023 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '80467'::bigint) AND (amount_cents <= '160934'::bigint) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 3083 + Buffers: shared hit=3209 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.150 ms +JIT: + Functions: 14 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.511 ms, Inlining 0.000 ms, Optimization 0.322 ms, Emission 4.058 ms, Total 4.891 ms +Execution Time: 7.951 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_customer_status_date.count.txt b/script/perf/payments_filters/plans/baseline/combo_customer_status_date.count.txt new file mode 100644 index 00000000000..8c44ba0ea32 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_customer_status_date.count.txt @@ -0,0 +1,26 @@ +-- case: combo_customer_status_date (count) phase: baseline selective: true +-- filters: {"external_customer_id":"perf-cust-0-1","payment_status":["succeeded"],"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" INNER JOIN "customers" ON "customers"."id" = "payments"."customer_id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (customers.external_id = 'perf-cust-0-1') AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=1915.22..1915.23 rows=1 width=8) (actual time=311.835..311.836 rows=1 loops=1) + Buffers: shared hit=520197 + -> Nested Loop (cost=0.85..1915.16 rows=25 width=0) (actual time=0.226..308.240 rows=111437 loops=1) + Buffers: shared hit=520197 + -> Index Scan using index_customers_on_external_id on customers (cost=0.42..1242.05 rows=1 width=16) (actual time=0.216..1.001 rows=1 loops=1) + Index Cond: ((external_id)::text = 'perf-cust-0-1'::text) + Buffers: shared hit=570 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..672.55 rows=56 width=16) (actual time=0.009..300.953 rows=111437 loops=1) + Index Cond: ((customer_id = customers.id) AND (customer_id IS NOT NULL)) + Filter: ((payable_id IS NOT NULL) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND (organization_id = ''::uuid) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 24640 + Buffers: shared hit=519627 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=106859) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=427436 +Planning: + Buffers: shared hit=24 +Planning Time: 0.343 ms +Execution Time: 311.977 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_customer_status_date.txt b/script/perf/payments_filters/plans/baseline/combo_customer_status_date.txt new file mode 100644 index 00000000000..68c098bb91e --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_customer_status_date.txt @@ -0,0 +1,30 @@ +-- case: combo_customer_status_date (list) phase: baseline selective: true +-- filters: {"external_customer_id":"perf-cust-0-1","payment_status":["succeeded"],"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" INNER JOIN "customers" ON "customers"."id" = "payments"."customer_id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (customers.external_id = 'perf-cust-0-1') AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=1915.74..1915.79 rows=20 width=330) (actual time=367.561..367.565 rows=20 loops=1) + Buffers: shared hit=520197 + -> Sort (cost=1915.74..1915.81 rows=25 width=330) (actual time=367.560..367.563 rows=20 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: top-N heapsort Memory: 35kB + Buffers: shared hit=520197 + -> Nested Loop (cost=0.85..1915.16 rows=25 width=330) (actual time=0.270..349.132 rows=111437 loops=1) + Buffers: shared hit=520197 + -> Index Scan using index_customers_on_external_id on customers (cost=0.42..1242.05 rows=1 width=16) (actual time=0.253..1.006 rows=1 loops=1) + Index Cond: ((external_id)::text = 'perf-cust-0-1'::text) + Buffers: shared hit=570 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..672.55 rows=56 width=330) (actual time=0.015..339.362 rows=111437 loops=1) + Index Cond: ((customer_id = customers.id) AND (customer_id IS NOT NULL)) + Filter: ((payable_id IS NOT NULL) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND (organization_id = ''::uuid) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 24640 + Buffers: shared hit=519627 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=106859) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=427436 +Planning: + Buffers: shared hit=24 +Planning Time: 0.469 ms +Execution Time: 367.644 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_provider_status.count.txt b/script/perf/payments_filters/plans/baseline/combo_provider_status.count.txt new file mode 100644 index 00000000000..b0408db0bb5 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_provider_status.count.txt @@ -0,0 +1,47 @@ +-- case: combo_provider_status (count) phase: baseline selective: false +-- filters: {"payment_provider_type":["stripe"],"payment_status":["failed"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::StripeProvider') + +Aggregate (cost=10274552.92..10274552.93 rows=1 width=8) (actual time=11030.349..11030.353 rows=1 loops=1) + Buffers: shared hit=2234768 read=166203 + I/O Timings: shared/local read=926.988 + -> Nested Loop (cost=40136.19..10274005.91 rows=218807 width=0) (actual time=290.332..11012.217 rows=574574 loops=1) + Buffers: shared hit=2234768 read=166203 + I/O Timings: shared/local read=926.988 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=51 width=16) (actual time=64.332..67.770 rows=51 loops=1) + Filter: ((type)::text = 'PaymentProviders::StripeProvider'::text) + Rows Removed by Filter: 34 + Buffers: shared hit=2 read=3 + I/O Timings: shared/local read=3.270 + -> Bitmap Heap Scan on payments (cost=40136.19..201408.16 rows=4282 width=16) (actual time=159.089..213.920 rows=11266 loops=51) + Recheck Cond: ((payment_provider_id = payment_providers.id) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 67178 + Heap Blocks: exact=157639 + Buffers: shared hit=2234766 read=166200 + I/O Timings: shared/local read=923.718 + -> BitmapAnd (cost=40136.19..40136.19 rows=58557 width=0) (actual time=158.803..158.803 rows=0 loops=51) + Buffers: shared hit=34452 read=4583 + I/O Timings: shared/local read=12.238 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..634.21 rows=76961 width=0) (actual time=1.387..1.387 rows=104764 loops=51) + Index Cond: (payment_provider_id = payment_providers.id) + Buffers: shared hit=129 read=4583 + I/O Timings: shared/local read=12.238 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=156.986..156.986 rows=5000000 loops=51) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=34323 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=551073) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=2131139 read=73153 + I/O Timings: shared/local read=688.428 +Planning: + Buffers: shared hit=20 +Planning Time: 0.186 ms +JIT: + Functions: 18 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.364 ms, Inlining 4.687 ms, Optimization 31.486 ms, Emission 28.251 ms, Total 65.789 ms +Execution Time: 11031.889 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_provider_status.txt b/script/perf/payments_filters/plans/baseline/combo_provider_status.txt new file mode 100644 index 00000000000..a248b06e1d8 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_provider_status.txt @@ -0,0 +1,26 @@ +-- case: combo_provider_status (list) phase: baseline selective: false +-- filters: {"payment_provider_type":["stripe"],"payment_status":["failed"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::StripeProvider') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.70..1251.88 rows=20 width=330) (actual time=0.023..0.156 rows=20 loops=1) + Buffers: shared hit=456 + -> Nested Loop (cost=0.70..13688364.96 rows=218807 width=330) (actual time=0.022..0.155 rows=20 loops=1) + Buffers: shared hit=456 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=364679 width=330) (actual time=0.019..0.142 rows=21 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 305 + Buffers: shared hit=416 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=84 + -> Index Scan using payment_providers_pkey on payment_providers (cost=0.14..0.16 rows=1 width=16) (actual time=0.000..0.000 rows=1 loops=21) + Index Cond: (id = payments.payment_provider_id) + Filter: ((type)::text = 'PaymentProviders::StripeProvider'::text) + Buffers: shared hit=40 +Planning: + Buffers: shared hit=20 +Planning Time: 0.189 ms +Execution Time: 0.177 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_status_amount.count.txt b/script/perf/payments_filters/plans/baseline/combo_status_amount.count.txt new file mode 100644 index 00000000000..5f525513929 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_status_amount.count.txt @@ -0,0 +1,28 @@ +-- case: combo_status_amount (count) phase: baseline selective: false +-- filters: {"payment_status":["failed"],"amount_from":4914} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) + +Aggregate (cost=13601010.65..13601010.66 rows=1 width=8) (actual time=2952.009..2952.010 rows=1 loops=1) + Buffers: shared hit=1327419 read=212653 + I/O Timings: shared/local read=1435.967 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13600558.37 rows=180913 width=0) (actual time=62.025..2937.628 rows=359593 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4640407 + Buffers: shared hit=1327419 read=212653 + I/O Timings: shared/local read=1435.967 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.006..0.006 rows=1 loops=344515) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=1326573 read=51487 + I/O Timings: shared/local read=1027.259 +Planning: + Buffers: shared hit=8 +Planning Time: 0.308 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 3.460 ms, Inlining 5.115 ms, Optimization 29.972 ms, Emission 26.894 ms, Total 65.441 ms +Execution Time: 2955.571 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_status_amount.txt b/script/perf/payments_filters/plans/baseline/combo_status_amount.txt new file mode 100644 index 00000000000..b1a8d1097e1 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_status_amount.txt @@ -0,0 +1,20 @@ +-- case: combo_status_amount (list) phase: baseline selective: false +-- filters: {"payment_status":["failed"],"amount_from":4914} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..1508.65 rows=20 width=330) (actual time=0.034..0.213 rows=20 loops=1) + Buffers: shared hit=584 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13641640.14 rows=180913 width=330) (actual time=0.034..0.212 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 477 + Buffers: shared hit=584 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.147 ms +Execution Time: 0.234 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_status_currency_date.count.txt b/script/perf/payments_filters/plans/baseline/combo_status_currency_date.count.txt new file mode 100644 index 00000000000..bd88519c1d7 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_status_currency_date.count.txt @@ -0,0 +1,21 @@ +-- case: combo_status_currency_date (count) phase: baseline selective: true +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=21114.90..21114.91 rows=1 width=8) (actual time=4.561..4.561 rows=1 loops=1) + Buffers: shared hit=10755 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..21109.25 rows=2262 width=0) (actual time=0.018..4.494 rows=2116 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 532 + Buffers: shared hit=10755 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=2021) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=8084 +Planning: + Buffers: shared hit=8 +Planning Time: 0.102 ms +Execution Time: 4.580 ms diff --git a/script/perf/payments_filters/plans/baseline/combo_status_currency_date.txt b/script/perf/payments_filters/plans/baseline/combo_status_currency_date.txt new file mode 100644 index 00000000000..faeec935ddd --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/combo_status_currency_date.txt @@ -0,0 +1,21 @@ +-- case: combo_status_currency_date (list) phase: baseline selective: true +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..187.19 rows=20 width=330) (actual time=0.014..0.052 rows=20 loops=1) + Buffers: shared hit=109 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..21109.25 rows=2262 width=330) (actual time=0.014..0.051 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 5 + Buffers: shared hit=109 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.093 ms +Execution Time: 0.064 ms diff --git a/script/perf/payments_filters/plans/baseline/control.count.txt b/script/perf/payments_filters/plans/baseline/control.count.txt new file mode 100644 index 00000000000..61372de308b --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/control.count.txt @@ -0,0 +1,28 @@ +-- case: control (count) phase: baseline selective: false +-- filters: {} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) + +Aggregate (cost=13581859.02..13581859.03 rows=1 width=8) (actual time=14062.940..14062.940 rows=1 loops=1) + Buffers: shared hit=18999346 read=162002 + I/O Timings: shared/local read=869.185 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=0) (actual time=65.620..13933.008 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18999346 read=162002 + I/O Timings: shared/local read=869.185 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18999323 read=13 + I/O Timings: shared/local read=1.845 +Planning: + Buffers: shared hit=8 +Planning Time: 0.091 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.887 ms, Inlining 5.604 ms, Optimization 31.562 ms, Emission 27.991 ms, Total 67.044 ms +Execution Time: 14064.925 ms diff --git a/script/perf/payments_filters/plans/baseline/control.txt b/script/perf/payments_filters/plans/baseline/control.txt new file mode 100644 index 00000000000..4ae2d118b34 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/control.txt @@ -0,0 +1,21 @@ +-- case: control (list) phase: baseline selective: false +-- filters: {} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..109.79 rows=20 width=330) (actual time=0.016..0.049 rows=20 loops=1) + Buffers: shared hit=105 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.015..0.048 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=105 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.131 ms +Execution Time: 0.070 ms diff --git a/script/perf/payments_filters/plans/baseline/control_page50.count.txt b/script/perf/payments_filters/plans/baseline/control_page50.count.txt new file mode 100644 index 00000000000..070a46a582e --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/control_page50.count.txt @@ -0,0 +1,28 @@ +-- case: control_page50 (count) phase: baseline selective: false +-- filters: {} search_term: nil page: 50 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) + +Aggregate (cost=13581859.02..13581859.03 rows=1 width=8) (actual time=15893.612..15893.615 rows=1 loops=1) + Buffers: shared hit=18999998 read=161350 + I/O Timings: shared/local read=1439.081 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=0) (actual time=77.716..15749.460 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18999998 read=161350 + I/O Timings: shared/local read=1439.081 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18999315 read=21 + I/O Timings: shared/local read=0.415 +Planning: + Buffers: shared hit=8 +Planning Time: 0.189 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.839 ms, Inlining 6.254 ms, Optimization 35.983 ms, Emission 35.593 ms, Total 79.669 ms +Execution Time: 15895.714 ms diff --git a/script/perf/payments_filters/plans/baseline/control_page50.txt b/script/perf/payments_filters/plans/baseline/control_page50.txt new file mode 100644 index 00000000000..bec6ef97253 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/control_page50.txt @@ -0,0 +1,21 @@ +-- case: control_page50 (list) phase: baseline selective: false +-- filters: {} search_term: nil page: 50 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 980 + +Limit (cost=5352.77..5461.99 rows=20 width=330) (actual time=3.612..3.678 rows=20 loops=1) + Buffers: shared hit=4860 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.043..3.656 rows=1000 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 13 + Buffers: shared hit=4860 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=959) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=3836 +Planning: + Buffers: shared hit=8 +Planning Time: 0.217 ms +Execution Time: 3.728 ms diff --git a/script/perf/payments_filters/plans/baseline/created_24m.count.txt b/script/perf/payments_filters/plans/baseline/created_24m.count.txt new file mode 100644 index 00000000000..51f6032769e --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/created_24m.count.txt @@ -0,0 +1,29 @@ +-- case: created_24m (count) phase: baseline selective: false +-- filters: {"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=13606775.29..13606775.30 rows=1 width=8) (actual time=14546.448..14546.452 rows=1 loops=1) + Buffers: shared hit=18975846 read=181210 + I/O Timings: shared/local read=804.659 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13600558.37 rows=2486767 width=0) (actual time=60.025..14411.106 rows=4951605 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 48395 + Buffers: shared hit=18975846 read=181210 + I/O Timings: shared/local read=804.659 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4748761) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18975846 read=19198 + I/O Timings: shared/local read=99.832 +Planning: + Buffers: shared hit=6 read=2 + I/O Timings: shared/local read=0.011 +Planning Time: 0.232 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.748 ms, Inlining 4.951 ms, Optimization 29.922 ms, Emission 25.299 ms, Total 60.919 ms +Execution Time: 14547.415 ms diff --git a/script/perf/payments_filters/plans/baseline/created_24m.txt b/script/perf/payments_filters/plans/baseline/created_24m.txt new file mode 100644 index 00000000000..d251ba3aa1a --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/created_24m.txt @@ -0,0 +1,21 @@ +-- case: created_24m (list) phase: baseline selective: false +-- filters: {"created_at_from":"2024-09-08","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..109.99 rows=20 width=330) (actual time=0.015..0.045 rows=20 loops=1) + Buffers: shared hit=105 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13606712.94 rows=2486767 width=330) (actual time=0.015..0.044 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=105 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.107 ms +Execution Time: 0.061 ms diff --git a/script/perf/payments_filters/plans/baseline/created_7d.count.txt b/script/perf/payments_filters/plans/baseline/created_7d.count.txt new file mode 100644 index 00000000000..36311b7b1df --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/created_7d.count.txt @@ -0,0 +1,21 @@ +-- case: created_7d (count) phase: baseline selective: true +-- filters: {"created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' + +Aggregate (cost=21087.61..21087.62 rows=1 width=8) (actual time=10.004..10.005 rows=1 loops=1) + Buffers: shared hit=12699 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..21080.40 rows=2884 width=0) (actual time=0.040..9.894 rows=2615 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 33 + Buffers: shared hit=12699 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=2507) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=10028 +Planning: + Buffers: shared hit=8 +Planning Time: 0.173 ms +Execution Time: 10.052 ms diff --git a/script/perf/payments_filters/plans/baseline/created_7d.txt b/script/perf/payments_filters/plans/baseline/created_7d.txt new file mode 100644 index 00000000000..6c01632b5db --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/created_7d.txt @@ -0,0 +1,21 @@ +-- case: created_7d (list) phase: baseline selective: true +-- filters: {"created_at_from":"2026-09-01","created_at_to":"2026-09-08"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..146.74 rows=20 width=330) (actual time=0.024..0.072 rows=20 loops=1) + Buffers: shared hit=105 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..21080.40 rows=2884 width=330) (actual time=0.024..0.071 rows=20 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=105 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.164 ms +Execution Time: 0.098 ms diff --git a/script/perf/payments_filters/plans/baseline/currency_common.count.txt b/script/perf/payments_filters/plans/baseline/currency_common.count.txt new file mode 100644 index 00000000000..582b303ee56 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/currency_common.count.txt @@ -0,0 +1,29 @@ +-- case: currency_common (count) phase: baseline selective: false +-- filters: {"currency":"EUR"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' + +Aggregate (cost=13594002.17..13594002.18 rows=1 width=8) (actual time=15143.154..15143.180 rows=1 loops=1) + Buffers: shared hit=18046005 read=165267 + I/O Timings: shared/local read=1206.293 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=2364013 width=0) (actual time=70.494..14986.101 rows=4705309 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 294691 + Buffers: shared hit=18046005 read=165267 + I/O Timings: shared/local read=1206.293 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4512315) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18046005 read=3255 + I/O Timings: shared/local read=94.352 +Planning: + Buffers: shared hit=3 read=5 + I/O Timings: shared/local read=0.073 +Planning Time: 0.406 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.062 ms, Inlining 7.756 ms, Optimization 34.043 ms, Emission 28.868 ms, Total 71.729 ms +Execution Time: 15144.646 ms diff --git a/script/perf/payments_filters/plans/baseline/currency_common.txt b/script/perf/payments_filters/plans/baseline/currency_common.txt new file mode 100644 index 00000000000..dc2664d64bc --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/currency_common.txt @@ -0,0 +1,21 @@ +-- case: currency_common (list) phase: baseline selective: false +-- filters: {"currency":"EUR"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..115.86 rows=20 width=330) (actual time=0.026..0.088 rows=20 loops=1) + Buffers: shared hit=106 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=2364013 width=330) (actual time=0.026..0.087 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'EUR'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=106 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.174 ms +Execution Time: 0.114 ms diff --git a/script/perf/payments_filters/plans/baseline/currency_rare.count.txt b/script/perf/payments_filters/plans/baseline/currency_rare.count.txt new file mode 100644 index 00000000000..19c1a883dae --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/currency_rare.count.txt @@ -0,0 +1,25 @@ +-- case: currency_rare (count) phase: baseline selective: true +-- filters: {"currency":"GBP"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' + +Aggregate (cost=13588151.77..13588151.78 rows=1 width=8) (actual time=861.965..861.966 rows=1 loops=1) + Buffers: shared hit=352484 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=23852 width=0) (actual time=92.624..857.007 rows=49629 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'GBP'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4950371 + Buffers: shared hit=352484 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=47618) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=190472 +Planning: + Buffers: shared hit=8 +Planning Time: 0.579 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 2.221 ms, Inlining 10.229 ms, Optimization 48.192 ms, Emission 34.122 ms, Total 94.764 ms +Execution Time: 864.286 ms diff --git a/script/perf/payments_filters/plans/baseline/currency_rare.txt b/script/perf/payments_filters/plans/baseline/currency_rare.txt new file mode 100644 index 00000000000..2f22cfd2bd0 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/currency_rare.txt @@ -0,0 +1,20 @@ +-- case: currency_rare (list) phase: baseline selective: true +-- filters: {"currency":"GBP"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..11428.68 rows=20 width=330) (actual time=0.104..2.473 rows=20 loops=1) + Buffers: shared hit=2554 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=23852 width=330) (actual time=0.103..2.471 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((amount_currency)::text = 'GBP'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2433 + Buffers: shared hit=2554 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.197 ms +Execution Time: 2.514 ms diff --git a/script/perf/payments_filters/plans/baseline/customer_heavy.count.txt b/script/perf/payments_filters/plans/baseline/customer_heavy.count.txt new file mode 100644 index 00000000000..f89ce7118b9 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/customer_heavy.count.txt @@ -0,0 +1,26 @@ +-- case: customer_heavy (count) phase: baseline selective: true +-- filters: {"external_customer_id":"perf-cust-0-1"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" INNER JOIN "customers" ON "customers"."id" = "payments"."customer_id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (customers.external_id = 'perf-cust-0-1') + +Aggregate (cost=1914.03..1914.04 rows=1 width=8) (actual time=415.644..415.645 rows=1 loops=1) + Buffers: shared hit=609781 + -> Nested Loop (cost=0.85..1913.95 rows=31 width=0) (actual time=0.309..410.267 rows=134798 loops=1) + Buffers: shared hit=609781 + -> Index Scan using index_customers_on_external_id on customers (cost=0.42..1242.05 rows=1 width=16) (actual time=0.295..1.203 rows=1 loops=1) + Index Cond: ((external_id)::text = 'perf-cust-0-1'::text) + Buffers: shared hit=570 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..671.22 rows=68 width=16) (actual time=0.012..402.008 rows=134798 loops=1) + Index Cond: ((customer_id = customers.id) AND (customer_id IS NOT NULL)) + Filter: ((payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1279 + Buffers: shared hit=609211 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=129255) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=517020 +Planning: + Buffers: shared hit=24 +Planning Time: 0.450 ms +Execution Time: 415.729 ms diff --git a/script/perf/payments_filters/plans/baseline/customer_heavy.txt b/script/perf/payments_filters/plans/baseline/customer_heavy.txt new file mode 100644 index 00000000000..87e26da0d1b --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/customer_heavy.txt @@ -0,0 +1,30 @@ +-- case: customer_heavy (list) phase: baseline selective: true +-- filters: {"external_customer_id":"perf-cust-0-1"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" INNER JOIN "customers" ON "customers"."id" = "payments"."customer_id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (customers.external_id = 'perf-cust-0-1') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=1914.72..1914.77 rows=20 width=330) (actual time=399.288..399.292 rows=20 loops=1) + Buffers: shared hit=609781 + -> Sort (cost=1914.72..1914.80 rows=31 width=330) (actual time=399.287..399.289 rows=20 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: top-N heapsort Memory: 35kB + Buffers: shared hit=609781 + -> Nested Loop (cost=0.85..1913.95 rows=31 width=330) (actual time=0.328..379.867 rows=134798 loops=1) + Buffers: shared hit=609781 + -> Index Scan using index_customers_on_external_id on customers (cost=0.42..1242.05 rows=1 width=16) (actual time=0.312..1.193 rows=1 loops=1) + Index Cond: ((external_id)::text = 'perf-cust-0-1'::text) + Buffers: shared hit=570 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..671.22 rows=68 width=330) (actual time=0.014..369.386 rows=134798 loops=1) + Index Cond: ((customer_id = customers.id) AND (customer_id IS NOT NULL)) + Filter: ((payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1279 + Buffers: shared hit=609211 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=129255) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=517020 +Planning: + Buffers: shared hit=24 +Planning Time: 0.481 ms +Execution Time: 399.379 ms diff --git a/script/perf/payments_filters/plans/baseline/customer_light.count.txt b/script/perf/payments_filters/plans/baseline/customer_light.count.txt new file mode 100644 index 00000000000..54b6476a6da --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/customer_light.count.txt @@ -0,0 +1,26 @@ +-- case: customer_light (count) phase: baseline selective: true +-- filters: {"external_customer_id":"perf-cust-0-37952"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" INNER JOIN "customers" ON "customers"."id" = "payments"."customer_id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (customers.external_id = 'perf-cust-0-37952') + +Aggregate (cost=1914.03..1914.04 rows=1 width=8) (actual time=1.551..1.551 rows=1 loops=1) + Buffers: shared hit=843 + -> Nested Loop (cost=0.85..1913.95 rows=31 width=0) (actual time=0.908..1.547 rows=52 loops=1) + Buffers: shared hit=843 + -> Index Scan using index_customers_on_external_id on customers (cost=0.42..1242.05 rows=1 width=16) (actual time=0.885..1.366 rows=1 loops=1) + Index Cond: ((external_id)::text = 'perf-cust-0-37952'::text) + Buffers: shared hit=570 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..671.22 rows=68 width=16) (actual time=0.022..0.177 rows=52 loops=1) + Index Cond: ((customer_id = customers.id) AND (customer_id IS NOT NULL)) + Filter: ((payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=273 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=54) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=216 +Planning: + Buffers: shared hit=24 +Planning Time: 0.369 ms +Execution Time: 1.594 ms diff --git a/script/perf/payments_filters/plans/baseline/customer_light.txt b/script/perf/payments_filters/plans/baseline/customer_light.txt new file mode 100644 index 00000000000..b8088eab398 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/customer_light.txt @@ -0,0 +1,30 @@ +-- case: customer_light (list) phase: baseline selective: true +-- filters: {"external_customer_id":"perf-cust-0-37952"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" INNER JOIN "customers" ON "customers"."id" = "payments"."customer_id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (customers.external_id = 'perf-cust-0-37952') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=1914.72..1914.77 rows=20 width=330) (actual time=1.731..1.744 rows=20 loops=1) + Buffers: shared hit=843 + -> Sort (cost=1914.72..1914.80 rows=31 width=330) (actual time=1.731..1.732 rows=20 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: top-N heapsort Memory: 35kB + Buffers: shared hit=843 + -> Nested Loop (cost=0.85..1913.95 rows=31 width=330) (actual time=0.987..1.712 rows=52 loops=1) + Buffers: shared hit=843 + -> Index Scan using index_customers_on_external_id on customers (cost=0.42..1242.05 rows=1 width=16) (actual time=0.958..1.496 rows=1 loops=1) + Index Cond: ((external_id)::text = 'perf-cust-0-37952'::text) + Buffers: shared hit=570 + -> Index Scan using index_payments_on_customer_id on payments (cost=0.43..671.22 rows=68 width=330) (actual time=0.024..0.206 rows=52 loops=1) + Index Cond: ((customer_id = customers.id) AND (customer_id IS NOT NULL)) + Filter: ((payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=273 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=54) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=216 +Planning: + Buffers: shared hit=24 +Planning Time: 0.433 ms +Execution Time: 1.809 ms diff --git a/script/perf/payments_filters/plans/baseline/five_filter_common.count.txt b/script/perf/payments_filters/plans/baseline/five_filter_common.count.txt new file mode 100644 index 00000000000..52c97a19233 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/five_filter_common.count.txt @@ -0,0 +1,48 @@ +-- case: five_filter_common (count) phase: baseline selective: false +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2024-09-08","created_at_to":"2026-09-08","amount_from":100,"payment_provider_type":["stripe"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND (payments.amount_cents >= 100::bigint) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::StripeProvider') + +Aggregate (cost=10336642.41..10336642.42 rows=1 width=8) (actual time=16645.389..16645.397 rows=1 loops=1) + Buffers: shared hit=11970579 read=178556 + I/O Timings: shared/local read=581.601 + -> Nested Loop (cost=40535.97..10333724.59 rows=1167130 width=0) (actual time=306.109..16563.693 rows=3115566 loops=1) + Buffers: shared hit=11970579 read=178556 + I/O Timings: shared/local read=581.601 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=51 width=16) (actual time=70.305..70.513 rows=51 loops=1) + Filter: ((type)::text = 'PaymentProviders::StripeProvider'::text) + Rows Removed by Filter: 34 + Buffers: shared hit=1 read=4 + I/O Timings: shared/local read=0.050 + -> Bitmap Heap Scan on payments (cost=40535.97..202393.50 rows=22843 width=16) (actual time=157.925..320.365 rows=61090 loops=51) + Recheck Cond: ((payment_provider_id = payment_providers.id) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '100'::bigint) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 17355 + Heap Blocks: exact=157639 + Buffers: shared hit=11970578 read=178552 + I/O Timings: shared/local read=581.551 + -> BitmapAnd (cost=40535.97..40535.97 rows=58557 width=0) (actual time=157.640..157.640 rows=0 loops=51) + Buffers: shared hit=34449 read=4586 + I/O Timings: shared/local read=17.569 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..634.21 rows=76961 width=0) (actual time=1.582..1.582 rows=104764 loops=51) + Index Cond: (payment_provider_id = payment_providers.id) + Buffers: shared hit=126 read=4586 + I/O Timings: shared/local read=17.569 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=155.633..155.633 rows=5000000 loops=51) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=34323 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=2988114) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=11936129 read=16327 + I/O Timings: shared/local read=155.237 +Planning: + Buffers: shared hit=16 read=4 + I/O Timings: shared/local read=0.017 +Planning Time: 0.429 ms +JIT: + Functions: 18 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.488 ms, Inlining 7.497 ms, Optimization 33.336 ms, Emission 29.592 ms, Total 71.912 ms +Execution Time: 16647.050 ms diff --git a/script/perf/payments_filters/plans/baseline/five_filter_common.txt b/script/perf/payments_filters/plans/baseline/five_filter_common.txt new file mode 100644 index 00000000000..e4d042daf3d --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/five_filter_common.txt @@ -0,0 +1,33 @@ +-- case: five_filter_common (list) phase: baseline selective: false +-- filters: {"payment_status":["succeeded"],"currency":"EUR","created_at_from":"2024-09-08","created_at_to":"2026-09-08","amount_from":100,"payment_provider_type":["stripe"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'EUR' AND "payments"."payable_payment_status" = 'succeeded' AND (payments.amount_cents >= 100::bigint) AND "payments"."created_at" >= '2024-09-08 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::StripeProvider') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.71..235.34 rows=20 width=330) (actual time=0.025..0.091 rows=20 loops=1) + Buffers: shared hit=171 + -> Nested Loop (cost=0.71..13692344.19 rows=1167130 width=330) (actual time=0.025..0.089 rows=20 loops=1) + Buffers: shared hit=171 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13644014.45 rows=1945216 width=330) (actual time=0.017..0.075 rows=32 loops=1) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2024-09-08 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '100'::bigint) AND ((amount_currency)::text = 'EUR'::text) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 7 + Buffers: shared hit=167 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=31) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=124 + -> Memoize (cost=0.15..0.17 rows=1 width=16) (actual time=0.000..0.000 rows=1 loops=32) + Cache Key: payments.payment_provider_id + Cache Mode: logical + Hits: 29 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB + Buffers: shared hit=4 + -> Index Scan using payment_providers_pkey on payment_providers (cost=0.14..0.16 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=3) + Index Cond: (id = payments.payment_provider_id) + Filter: ((type)::text = 'PaymentProviders::StripeProvider'::text) + Rows Removed by Filter: 0 + Buffers: shared hit=4 +Planning: + Buffers: shared hit=20 +Planning Time: 0.266 ms +Execution Time: 0.129 ms diff --git a/script/perf/payments_filters/plans/baseline/five_filter_rare.count.txt b/script/perf/payments_filters/plans/baseline/five_filter_rare.count.txt new file mode 100644 index 00000000000..3b48768308c --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/five_filter_rare.count.txt @@ -0,0 +1,34 @@ +-- case: five_filter_rare (count) phase: baseline selective: true +-- filters: {"payment_status":["failed"],"currency":"GBP","created_at_from":"2026-09-01","created_at_to":"2026-09-08","amount_from":4914,"payment_provider_type":["gocardless"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::GocardlessProvider') + +Aggregate (cost=17367.37..17367.38 rows=1 width=8) (actual time=20.942..20.943 rows=1 loops=1) + Buffers: shared hit=1549 + -> Nested Loop (cost=763.73..17367.37 rows=1 width=0) (actual time=20.940..20.941 rows=0 loops=1) + Buffers: shared hit=1549 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=17 width=16) (actual time=0.001..0.013 rows=17 loops=1) + Filter: ((type)::text = 'PaymentProviders::GocardlessProvider'::text) + Rows Removed by Filter: 68 + Buffers: shared hit=5 + -> Bitmap Heap Scan on payments (cost=763.73..1021.24 rows=1 width=16) (actual time=1.227..1.227 rows=0 loops=17) + Recheck Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND (payment_provider_id = payment_providers.id)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND ((amount_currency)::text = 'GBP'::text) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 24 + Heap Blocks: exact=405 + Buffers: shared hit=1544 + -> BitmapAnd (cost=763.73..763.73 rows=68 width=0) (actual time=1.200..1.200 rows=0 loops=17) + Buffers: shared hit=1139 + -> Bitmap Index Scan on index_payments_by_cursor (cost=0.00..118.87 rows=5769 width=0) (actual time=0.121..0.121 rows=2648 loops=17) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Buffers: shared hit=391 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..644.61 rows=76961 width=0) (actual time=1.010..1.010 rows=48042 loops=17) + Index Cond: (payment_provider_id = payment_providers.id) + Buffers: shared hit=748 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=20 +Planning Time: 0.217 ms +Execution Time: 20.981 ms diff --git a/script/perf/payments_filters/plans/baseline/five_filter_rare.txt b/script/perf/payments_filters/plans/baseline/five_filter_rare.txt new file mode 100644 index 00000000000..4b489e75dba --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/five_filter_rare.txt @@ -0,0 +1,38 @@ +-- case: five_filter_rare (list) phase: baseline selective: true +-- filters: {"payment_status":["failed"],"currency":"GBP","created_at_from":"2026-09-01","created_at_to":"2026-09-08","amount_from":4914,"payment_provider_type":["gocardless"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."amount_currency" = 'GBP' AND "payments"."payable_payment_status" = 'failed' AND (payments.amount_cents >= 4914::bigint) AND "payments"."created_at" >= '2026-09-01 00:00:00' AND "payments"."created_at" <= '2026-09-08 23:59:59.999999' AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::GocardlessProvider') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=17367.38..17367.38 rows=1 width=330) (actual time=20.731..20.732 rows=0 loops=1) + Buffers: shared hit=1549 + -> Sort (cost=17367.38..17367.38 rows=1 width=330) (actual time=20.731..20.732 rows=0 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=1549 + -> Nested Loop (cost=763.73..17367.37 rows=1 width=330) (actual time=20.728..20.729 rows=0 loops=1) + Buffers: shared hit=1549 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=17 width=16) (actual time=0.002..0.015 rows=17 loops=1) + Filter: ((type)::text = 'PaymentProviders::GocardlessProvider'::text) + Rows Removed by Filter: 68 + Buffers: shared hit=5 + -> Bitmap Heap Scan on payments (cost=763.73..1021.24 rows=1 width=330) (actual time=1.214..1.214 rows=0 loops=17) + Recheck Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone) AND (payment_provider_id = payment_providers.id)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (amount_cents >= '4914'::bigint) AND ((amount_currency)::text = 'GBP'::text) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 24 + Heap Blocks: exact=405 + Buffers: shared hit=1544 + -> BitmapAnd (cost=763.73..763.73 rows=68 width=0) (actual time=1.191..1.191 rows=0 loops=17) + Buffers: shared hit=1139 + -> Bitmap Index Scan on index_payments_by_cursor (cost=0.00..118.87 rows=5769 width=0) (actual time=0.125..0.125 rows=2648 loops=17) + Index Cond: ((organization_id = ''::uuid) AND (created_at >= '2026-09-01 00:00:00'::timestamp without time zone) AND (created_at <= '2026-09-08 23:59:59.999999'::timestamp without time zone)) + Buffers: shared hit=391 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..644.61 rows=76961 width=0) (actual time=0.995..0.995 rows=48042 loops=17) + Index Cond: (payment_provider_id = payment_providers.id) + Buffers: shared hit=748 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=20 +Planning Time: 0.289 ms +Execution Time: 20.772 ms diff --git a/script/perf/payments_filters/plans/baseline/invoice_hit_direct.count.txt b/script/perf/payments_filters/plans/baseline/invoice_hit_direct.count.txt new file mode 100644 index 00000000000..151a8359f95 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/invoice_hit_direct.count.txt @@ -0,0 +1,51 @@ +-- case: invoice_hit_direct (count) phase: baseline selective: true +-- filters: {"invoice_number":"her-1556-202609-000457624"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '' AND LOWER(invoices.number) = LOWER('her-1556-202609-000457624') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) + +Aggregate (cost=85076132259.31..85076132259.32 rows=1 width=8) (actual time=18041.762..18042.088 rows=1 loops=1) + Buffers: shared hit=19170569 read=227499 + I/O Timings: shared/local read=2826.612 + -> Nested Loop Semi Join (cost=1000.43..85076132082.35 rows=70786 width=0) (actual time=2736.360..18041.135 rows=2 loops=1) + Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (hashed SubPlan 3))) + Rows Removed by Join Filter: 4952717 + Buffers: shared hit=19170569 read=227499 + I/O Timings: shared/local read=2826.612 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=24) (actual time=156.614..16095.687 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18997491 read=163857 + I/O Timings: shared/local read=2028.318 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18997427 read=1909 + I/O Timings: shared/local read=65.074 + -> Materialize (cost=1000.00..273258.07 rows=22437 width=16) (actual time=0.000..0.000 rows=1 loops=4952719) + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=763.971 + -> Gather (cost=1000.00..273145.88 rows=22437 width=16) (actual time=785.681..785.989 rows=1 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=763.971 + -> Parallel Seq Scan on invoices (cost=0.00..269902.18 rows=9349 width=16) (actual time=683.844..757.619 rows=0 loops=3) + Filter: ((organization_id = ''::uuid) AND (lower((number)::text) = 'her-1556-202609-000457624'::text)) + Rows Removed by Filter: 1966358 + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=763.971 + SubPlan 3 + -> Seq Scan on invoices_payment_requests (cost=0.00..17205.38 rows=737338 width=32) (actual time=9.738..78.204 rows=737338 loops=1) + Buffers: shared read=9832 + I/O Timings: shared/local read=34.323 +Planning: + Buffers: shared hit=6 read=5 + I/O Timings: shared/local read=0.694 +Planning Time: 1.673 ms +JIT: + Functions: 48 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 8.637 ms, Inlining 91.848 ms, Optimization 121.658 ms, Emission 95.605 ms, Total 317.748 ms +Execution Time: 18048.860 ms diff --git a/script/perf/payments_filters/plans/baseline/invoice_hit_direct.txt b/script/perf/payments_filters/plans/baseline/invoice_hit_direct.txt new file mode 100644 index 00000000000..eb11188a8ca --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/invoice_hit_direct.txt @@ -0,0 +1,51 @@ +-- case: invoice_hit_direct (list) phase: baseline selective: true +-- filters: {"invoice_number":"her-1556-202609-000457624"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '' AND LOWER(invoices.number) = LOWER('her-1556-202609-000457624') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=1000.56..24038570.74 rows=20 width=330) (actual time=1477.759..72519.610 rows=2 loops=1) + Buffers: shared hit=21201432 read=3070314 + I/O Timings: shared/local read=46128.706 + -> Nested Loop Semi Join (cost=1000.56..85076173164.12 rows=70786 width=330) (actual time=1307.955..72349.800 rows=2 loops=1) + Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (hashed SubPlan 3))) + Rows Removed by Join Filter: 4952717 + Buffers: shared hit=21201432 read=3070314 + I/O Timings: shared/local read=46128.706 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=3.055..69324.905 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=21087630 read=2947396 + I/O Timings: shared/local read=44189.872 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.010..0.010 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=17471297 read=1528039 + I/O Timings: shared/local read=31721.937 + -> Materialize (cost=1000.00..273258.07 rows=22437 width=16) (actual time=0.000..0.000 rows=1 loops=4952719) + Buffers: shared hit=113802 read=113086 + I/O Timings: shared/local read=1916.859 + -> Gather (cost=1000.00..273145.88 rows=22437 width=16) (actual time=1080.753..1080.991 rows=1 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=113802 read=113086 + I/O Timings: shared/local read=1916.859 + -> Parallel Seq Scan on invoices (cost=0.00..269902.18 rows=9349 width=16) (actual time=956.119..1062.731 rows=0 loops=3) + Filter: ((organization_id = ''::uuid) AND (lower((number)::text) = 'her-1556-202609-000457624'::text)) + Rows Removed by Filter: 1966358 + Buffers: shared hit=113802 read=113086 + I/O Timings: shared/local read=1916.859 + SubPlan 3 + -> Seq Scan on invoices_payment_requests (cost=0.00..17205.38 rows=737338 width=32) (actual time=8.572..61.464 rows=737338 loops=1) + Buffers: shared read=9832 + I/O Timings: shared/local read=21.974 +Planning: + Buffers: shared hit=6 read=5 + I/O Timings: shared/local read=0.399 +Planning Time: 1.257 ms +JIT: + Functions: 49 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 7.317 ms, Inlining 60.933 ms, Optimization 119.898 ms, Emission 105.838 ms, Total 293.985 ms +Execution Time: 72525.325 ms diff --git a/script/perf/payments_filters/plans/baseline/invoice_hit_request.count.txt b/script/perf/payments_filters/plans/baseline/invoice_hit_request.count.txt new file mode 100644 index 00000000000..49d85a476a4 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/invoice_hit_request.count.txt @@ -0,0 +1,51 @@ +-- case: invoice_hit_request (count) phase: baseline selective: true +-- filters: {"invoice_number":"her-1556-202609-000765141"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '' AND LOWER(invoices.number) = LOWER('her-1556-202609-000765141') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) + +Aggregate (cost=85076132259.31..85076132259.32 rows=1 width=8) (actual time=17070.225..17070.327 rows=1 loops=1) + Buffers: shared hit=19169990 read=228078 + I/O Timings: shared/local read=2693.793 + -> Nested Loop Semi Join (cost=1000.43..85076132082.35 rows=70786 width=0) (actual time=3497.788..17070.190 rows=1 loops=1) + Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (hashed SubPlan 3))) + Rows Removed by Join Filter: 4952718 + Buffers: shared hit=19169990 read=228078 + I/O Timings: shared/local read=2693.793 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=24) (actual time=141.273..15035.275 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18996912 read=164436 + I/O Timings: shared/local read=1623.282 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18996912 read=2424 + I/O Timings: shared/local read=69.487 + -> Materialize (cost=1000.00..273258.07 rows=22437 width=16) (actual time=0.000..0.000 rows=1 loops=4952719) + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=1042.835 + -> Gather (cost=1000.00..273145.88 rows=22437 width=16) (actual time=904.313..904.431 rows=1 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=1042.835 + -> Parallel Seq Scan on invoices (cost=0.00..269902.18 rows=9349 width=16) (actual time=891.108..891.112 rows=0 loops=3) + Filter: ((organization_id = ''::uuid) AND (lower((number)::text) = 'her-1556-202609-000765141'::text)) + Rows Removed by Filter: 1966358 + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=1042.835 + SubPlan 3 + -> Seq Scan on invoices_payment_requests (cost=0.00..17205.38 rows=737338 width=32) (actual time=11.637..73.762 rows=737338 loops=1) + Buffers: shared read=9832 + I/O Timings: shared/local read=27.677 +Planning: + Buffers: shared hit=6 read=5 + I/O Timings: shared/local read=0.488 +Planning Time: 1.187 ms +JIT: + Functions: 48 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 5.702 ms, Inlining 60.710 ms, Optimization 104.142 ms, Emission 90.830 ms, Total 261.384 ms +Execution Time: 17075.037 ms diff --git a/script/perf/payments_filters/plans/baseline/invoice_hit_request.txt b/script/perf/payments_filters/plans/baseline/invoice_hit_request.txt new file mode 100644 index 00000000000..17787f431a9 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/invoice_hit_request.txt @@ -0,0 +1,51 @@ +-- case: invoice_hit_request (list) phase: baseline selective: true +-- filters: {"invoice_number":"her-1556-202609-000765141"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '' AND LOWER(invoices.number) = LOWER('her-1556-202609-000765141') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=1000.56..24038570.74 rows=20 width=330) (actual time=1935.709..83840.740 rows=1 loops=1) + Buffers: shared hit=21201392 read=3070354 + I/O Timings: shared/local read=54826.375 + -> Nested Loop Semi Join (cost=1000.56..85076173164.12 rows=70786 width=330) (actual time=1761.283..83666.309 rows=1 loops=1) + Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (hashed SubPlan 3))) + Rows Removed by Join Filter: 4952718 + Buffers: shared hit=21201392 read=3070354 + I/O Timings: shared/local read=54826.375 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=1.082..80188.038 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=21087593 read=2947433 + I/O Timings: shared/local read=51805.554 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.012..0.012 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=17471319 read=1528017 + I/O Timings: shared/local read=36652.872 + -> Materialize (cost=1000.00..273258.07 rows=22437 width=16) (actual time=0.000..0.000 rows=1 loops=4952719) + Buffers: shared hit=113799 read=113089 + I/O Timings: shared/local read=2986.872 + -> Gather (cost=1000.00..273145.88 rows=22437 width=16) (actual time=1494.704..1494.910 rows=1 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=113799 read=113089 + I/O Timings: shared/local read=2986.872 + -> Parallel Seq Scan on invoices (cost=0.00..269902.18 rows=9349 width=16) (actual time=1477.632..1477.692 rows=0 loops=3) + Filter: ((organization_id = ''::uuid) AND (lower((number)::text) = 'her-1556-202609-000765141'::text)) + Rows Removed by Filter: 1966358 + Buffers: shared hit=113799 read=113089 + I/O Timings: shared/local read=2986.872 + SubPlan 3 + -> Seq Scan on invoices_payment_requests (cost=0.00..17205.38 rows=737338 width=32) (actual time=9.755..78.417 rows=737338 loops=1) + Buffers: shared read=9832 + I/O Timings: shared/local read=33.949 +Planning: + Buffers: shared hit=6 read=5 + I/O Timings: shared/local read=0.570 +Planning Time: 1.406 ms +JIT: + Functions: 49 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 7.542 ms, Inlining 64.079 ms, Optimization 118.912 ms, Emission 102.503 ms, Total 293.035 ms +Execution Time: 83846.898 ms diff --git a/script/perf/payments_filters/plans/baseline/invoice_miss.count.txt b/script/perf/payments_filters/plans/baseline/invoice_miss.count.txt new file mode 100644 index 00000000000..463dde7f222 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/invoice_miss.count.txt @@ -0,0 +1,48 @@ +-- case: invoice_miss (count) phase: baseline selective: true +-- filters: {"invoice_number":"PERF-NOPE-000000-000000001"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '' AND LOWER(invoices.number) = LOWER('PERF-NOPE-000000-000000001') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) + +Aggregate (cost=85076132259.31..85076132259.32 rows=1 width=8) (actual time=16469.838..16469.950 rows=1 loops=1) + Buffers: shared hit=19139517 read=248719 + I/O Timings: shared/local read=2594.394 + -> Nested Loop Semi Join (cost=1000.43..85076132082.35 rows=70786 width=0) (actual time=16469.692..16469.801 rows=0 loops=1) + Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (hashed SubPlan 3))) + Buffers: shared hit=19139517 read=248719 + I/O Timings: shared/local read=2594.394 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=24) (actual time=157.867..15073.525 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18997500 read=163848 + I/O Timings: shared/local read=1118.710 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18990429 read=8907 + I/O Timings: shared/local read=89.067 + -> Materialize (cost=1000.00..273258.07 rows=22437 width=16) (actual time=0.000..0.000 rows=0 loops=4952719) + Buffers: shared hit=142017 read=84871 + I/O Timings: shared/local read=1475.683 + -> Gather (cost=1000.00..273145.88 rows=22437 width=16) (actual time=974.359..974.465 rows=0 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=142017 read=84871 + I/O Timings: shared/local read=1475.683 + -> Parallel Seq Scan on invoices (cost=0.00..269902.18 rows=9349 width=16) (actual time=956.420..956.420 rows=0 loops=3) + Filter: ((organization_id = ''::uuid) AND (lower((number)::text) = 'perf-nope-000000-000000001'::text)) + Rows Removed by Filter: 1966359 + Buffers: shared hit=142017 read=84871 + I/O Timings: shared/local read=1475.683 + SubPlan 3 + -> Seq Scan on invoices_payment_requests (cost=0.00..17205.38 rows=737338 width=32) (never executed) +Planning: + Buffers: shared hit=6 read=5 + I/O Timings: shared/local read=0.017 +Planning Time: 0.810 ms +JIT: + Functions: 45 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 6.046 ms, Inlining 62.167 ms, Optimization 101.785 ms, Emission 89.845 ms, Total 259.843 ms +Execution Time: 16473.652 ms diff --git a/script/perf/payments_filters/plans/baseline/invoice_miss.txt b/script/perf/payments_filters/plans/baseline/invoice_miss.txt new file mode 100644 index 00000000000..d21182f6d9c --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/invoice_miss.txt @@ -0,0 +1,48 @@ +-- case: invoice_miss (list) phase: baseline selective: true +-- filters: {"invoice_number":"PERF-NOPE-000000-000000001"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (EXISTS ( SELECT 1 FROM invoices WHERE invoices.organization_id = '' AND LOWER(invoices.number) = LOWER('PERF-NOPE-000000-000000001') AND ( (payments.payable_type = 'Invoice' AND invoices.id = payments.payable_id) OR (payments.payable_type = 'PaymentRequest' AND EXISTS ( SELECT 1 FROM invoices_payment_requests WHERE invoices_payment_requests.payment_request_id = payments.payable_id AND invoices_payment_requests.invoice_id = invoices.id )) ) )) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=1000.56..24038570.74 rows=20 width=330) (actual time=60497.294..60497.631 rows=0 loops=1) + Buffers: shared hit=21194858 read=3067056 + I/O Timings: shared/local read=33017.826 + -> Nested Loop Semi Join (cost=1000.56..85076173164.12 rows=70786 width=330) (actual time=60314.450..60314.787 rows=0 loops=1) + Join Filter: ((((payments.payable_type)::text = 'Invoice'::text) AND (invoices.id = payments.payable_id)) OR (((payments.payable_type)::text = 'PaymentRequest'::text) AND (hashed SubPlan 3))) + Buffers: shared hit=21194858 read=3067056 + I/O Timings: shared/local read=33017.826 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=2.246..59073.367 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=21021780 read=3013246 + I/O Timings: shared/local read=32721.525 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.008..0.008 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=17859144 read=1140192 + I/O Timings: shared/local read=19422.737 + -> Materialize (cost=1000.00..273258.07 rows=22437 width=16) (actual time=0.000..0.000 rows=0 loops=4952719) + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=296.302 + -> Gather (cost=1000.00..273145.88 rows=22437 width=16) (actual time=594.010..594.336 rows=0 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=296.302 + -> Parallel Seq Scan on invoices (cost=0.00..269902.18 rows=9349 width=16) (actual time=579.615..579.615 rows=0 loops=3) + Filter: ((organization_id = ''::uuid) AND (lower((number)::text) = 'perf-nope-000000-000000001'::text)) + Rows Removed by Filter: 1966359 + Buffers: shared hit=173078 read=53810 + I/O Timings: shared/local read=296.302 + SubPlan 3 + -> Seq Scan on invoices_payment_requests (cost=0.00..17205.38 rows=737338 width=32) (never executed) +Planning: + Buffers: shared hit=6 read=5 + I/O Timings: shared/local read=0.064 +Planning Time: 1.075 ms +JIT: + Functions: 46 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 6.148 ms, Inlining 93.298 ms, Optimization 111.374 ms, Emission 109.271 ms, Total 320.091 ms +Execution Time: 60503.346 ms diff --git a/script/perf/payments_filters/plans/baseline/method_common_json.count.txt b/script/perf/payments_filters/plans/baseline/method_common_json.count.txt new file mode 100644 index 00000000000..27426bd1896 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_common_json.count.txt @@ -0,0 +1,41 @@ +-- case: method_common_json (count) phase: baseline selective: false +-- filters: {"payment_method_type":["card"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card')) + +Aggregate (cost=13585517.46..13585517.47 rows=1 width=8) (actual time=14712.277..14712.283 rows=1 loops=1) + Buffers: shared hit=18998944 read=163895 + I/O Timings: shared/local read=1015.517 + -> Hash Left Join (cost=3315.51..13585486.29 rows=12466 width=0) (actual time=174.376..14602.775 rows=3950520 loops=1) + Hash Cond: (payments.payment_method_id = payment_methods.id) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = 'card'::text) + Rows Removed by Filter: 1002199 + Buffers: shared hit=18998944 read=163895 + I/O Timings: shared/local read=1015.517 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=58) (actual time=161.102..13152.336 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18998923 read=162425 + I/O Timings: shared/local read=1012.483 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18998737 read=599 + I/O Timings: shared/local read=9.635 + -> Hash (cost=2301.70..2301.70 rows=81070 width=22) (actual time=13.098..13.099 rows=81070 loops=1) + Buckets: 131072 Batches: 1 Memory Usage: 5308kB + Buffers: shared hit=21 read=1470 + I/O Timings: shared/local read=3.034 + -> Seq Scan on payment_methods (cost=0.00..2301.70 rows=81070 width=22) (actual time=0.019..6.933 rows=81070 loops=1) + Buffers: shared hit=21 read=1470 + I/O Timings: shared/local read=3.034 +Planning: + Buffers: shared hit=24 +Planning Time: 0.179 ms +JIT: + Functions: 26 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.694 ms, Inlining 11.489 ms, Optimization 80.683 ms, Emission 69.028 ms, Total 162.893 ms +Execution Time: 14714.496 ms diff --git a/script/perf/payments_filters/plans/baseline/method_common_json.txt b/script/perf/payments_filters/plans/baseline/method_common_json.txt new file mode 100644 index 00000000000..41b9ae655dc --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_common_json.txt @@ -0,0 +1,28 @@ +-- case: method_common_json (list) phase: baseline selective: false +-- filters: {"payment_method_type":["card"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card')) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.97..23660.48 rows=20 width=330) (actual time=0.032..0.148 rows=20 loops=1) + Buffers: shared hit=227 + -> Nested Loop Left Join (cost=0.97..14746969.56 rows=12466 width=330) (actual time=0.031..0.146 rows=20 loops=1) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = 'card'::text) + Rows Removed by Filter: 6 + Buffers: shared hit=227 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.024..0.099 rows=26 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=135 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=26) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=104 + -> Index Scan using payment_methods_pkey on payment_methods (cost=0.42..0.44 rows=1 width=22) (actual time=0.002..0.002 rows=1 loops=26) + Index Cond: (id = payments.payment_method_id) + Buffers: shared hit=92 +Planning: + Buffers: shared hit=24 +Planning Time: 0.276 ms +Execution Time: 0.174 ms diff --git a/script/perf/payments_filters/plans/baseline/method_fallback_only.count.txt b/script/perf/payments_filters/plans/baseline/method_fallback_only.count.txt new file mode 100644 index 00000000000..a81de27eb5f --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_fallback_only.count.txt @@ -0,0 +1,41 @@ +-- case: method_fallback_only (count) phase: baseline selective: true +-- filters: {"payment_method_type":["bacs_debit"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('bacs_debit')) + +Aggregate (cost=13585517.46..13585517.47 rows=1 width=8) (actual time=14749.618..14749.621 rows=1 loops=1) + Buffers: shared hit=18985303 read=177536 + I/O Timings: shared/local read=680.462 + -> Hash Left Join (cost=3315.51..13585486.29 rows=12466 width=0) (actual time=166.666..14747.658 rows=11672 loops=1) + Hash Cond: (payments.payment_method_id = payment_methods.id) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = 'bacs_debit'::text) + Rows Removed by Filter: 4941047 + Buffers: shared hit=18985303 read=177536 + I/O Timings: shared/local read=680.462 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=58) (actual time=154.931..13278.799 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18984392 read=176956 + I/O Timings: shared/local read=679.060 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18978384 read=20952 + I/O Timings: shared/local read=128.359 + -> Hash (cost=2301.70..2301.70 rows=81070 width=22) (actual time=11.308..11.309 rows=81070 loops=1) + Buckets: 131072 Batches: 1 Memory Usage: 5308kB + Buffers: shared hit=911 read=580 + I/O Timings: shared/local read=1.402 + -> Seq Scan on payment_methods (cost=0.00..2301.70 rows=81070 width=22) (actual time=0.012..5.806 rows=81070 loops=1) + Buffers: shared hit=911 read=580 + I/O Timings: shared/local read=1.402 +Planning: + Buffers: shared hit=24 +Planning Time: 0.316 ms +JIT: + Functions: 26 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.649 ms, Inlining 6.964 ms, Optimization 81.094 ms, Emission 66.927 ms, Total 156.634 ms +Execution Time: 14751.758 ms diff --git a/script/perf/payments_filters/plans/baseline/method_fallback_only.txt b/script/perf/payments_filters/plans/baseline/method_fallback_only.txt new file mode 100644 index 00000000000..9aa11e86860 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_fallback_only.txt @@ -0,0 +1,28 @@ +-- case: method_fallback_only (list) phase: baseline selective: true +-- filters: {"payment_method_type":["bacs_debit"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('bacs_debit')) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.97..23660.48 rows=20 width=330) (actual time=2.370..37.347 rows=20 loops=1) + Buffers: shared hit=78120 + -> Nested Loop Left Join (cost=0.97..14746969.56 rows=12466 width=330) (actual time=2.370..37.345 rows=20 loops=1) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = 'bacs_debit'::text) + Rows Removed by Filter: 8998 + Buffers: shared hit=78120 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.022..27.964 rows=9018 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 93 + Buffers: shared hit=43728 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=8637) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=34548 + -> Index Scan using payment_methods_pkey on payment_methods (cost=0.42..0.44 rows=1 width=22) (actual time=0.001..0.001 rows=1 loops=9018) + Index Cond: (id = payments.payment_method_id) + Buffers: shared hit=34392 +Planning: + Buffers: shared hit=24 +Planning Time: 0.297 ms +Execution Time: 37.406 ms diff --git a/script/perf/payments_filters/plans/baseline/method_multi.count.txt b/script/perf/payments_filters/plans/baseline/method_multi.count.txt new file mode 100644 index 00000000000..b321bb4b43a --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_multi.count.txt @@ -0,0 +1,41 @@ +-- case: method_multi (count) phase: baseline selective: false +-- filters: {"payment_method_type":["card","crypto"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card', 'crypto')) + +Aggregate (cost=13585548.62..13585548.63 rows=1 width=8) (actual time=16666.387..16666.392 rows=1 loops=1) + Buffers: shared hit=18981090 read=181749 + I/O Timings: shared/local read=1236.718 + -> Hash Left Join (cost=3315.51..13585486.29 rows=24932 width=0) (actual time=168.048..16538.046 rows=3954022 loops=1) + Hash Cond: (payments.payment_method_id = payment_methods.id) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = ANY ('{card,crypto}'::text[])) + Rows Removed by Filter: 998697 + Buffers: shared hit=18981090 read=181749 + I/O Timings: shared/local read=1236.718 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=58) (actual time=152.306..14774.167 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18981069 read=180279 + I/O Timings: shared/local read=1232.072 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18981053 read=18283 + I/O Timings: shared/local read=157.486 + -> Hash (cost=2301.70..2301.70 rows=81070 width=22) (actual time=15.589..15.590 rows=81070 loops=1) + Buckets: 131072 Batches: 1 Memory Usage: 5308kB + Buffers: shared hit=21 read=1470 + I/O Timings: shared/local read=4.647 + -> Seq Scan on payment_methods (cost=0.00..2301.70 rows=81070 width=22) (actual time=0.013..8.707 rows=81070 loops=1) + Buffers: shared hit=21 read=1470 + I/O Timings: shared/local read=4.647 +Planning: + Buffers: shared hit=24 +Planning Time: 0.194 ms +JIT: + Functions: 26 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 2.801 ms, Inlining 8.095 ms, Optimization 78.945 ms, Emission 65.432 ms, Total 155.273 ms +Execution Time: 16669.774 ms diff --git a/script/perf/payments_filters/plans/baseline/method_multi.txt b/script/perf/payments_filters/plans/baseline/method_multi.txt new file mode 100644 index 00000000000..139b5e26347 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_multi.txt @@ -0,0 +1,28 @@ +-- case: method_multi (list) phase: baseline selective: false +-- filters: {"payment_method_type":["card","crypto"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('card', 'crypto')) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.97..11830.73 rows=20 width=330) (actual time=0.029..0.100 rows=20 loops=1) + Buffers: shared hit=227 + -> Nested Loop Left Join (cost=0.97..14746969.56 rows=24932 width=330) (actual time=0.029..0.099 rows=20 loops=1) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = ANY ('{card,crypto}'::text[])) + Rows Removed by Filter: 6 + Buffers: shared hit=227 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.017..0.059 rows=26 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=135 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=26) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=104 + -> Index Scan using payment_methods_pkey on payment_methods (cost=0.42..0.44 rows=1 width=22) (actual time=0.001..0.001 rows=1 loops=26) + Index Cond: (id = payments.payment_method_id) + Buffers: shared hit=92 +Planning: + Buffers: shared hit=24 +Planning Time: 0.259 ms +Execution Time: 0.123 ms diff --git a/script/perf/payments_filters/plans/baseline/method_rare_json.count.txt b/script/perf/payments_filters/plans/baseline/method_rare_json.count.txt new file mode 100644 index 00000000000..79c205801bd --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_rare_json.count.txt @@ -0,0 +1,42 @@ +-- case: method_rare_json (count) phase: baseline selective: true +-- filters: {"payment_method_type":["crypto"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('crypto')) + +Aggregate (cost=13585517.46..13585517.47 rows=1 width=8) (actual time=19368.226..19368.230 rows=1 loops=1) + Buffers: shared hit=18982823 read=180016 + I/O Timings: shared/local read=1655.672 + -> Hash Left Join (cost=3315.51..13585486.29 rows=12466 width=0) (actual time=224.599..19366.239 rows=3502 loops=1) + Hash Cond: (payments.payment_method_id = payment_methods.id) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = 'crypto'::text) + Rows Removed by Filter: 4949217 + Buffers: shared hit=18982823 read=180016 + I/O Timings: shared/local read=1655.672 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13575625.91 rows=2493246 width=58) (actual time=187.554..17371.283 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=18982823 read=178525 + I/O Timings: shared/local read=1641.866 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18982823 read=16513 + I/O Timings: shared/local read=209.629 + -> Hash (cost=2301.70..2301.70 rows=81070 width=22) (actual time=30.280..30.280 rows=81070 loops=1) + Buckets: 131072 Batches: 1 Memory Usage: 5308kB + Buffers: shared read=1491 + I/O Timings: shared/local read=13.806 + -> Seq Scan on payment_methods (cost=0.00..2301.70 rows=81070 width=22) (actual time=0.034..18.309 rows=81070 loops=1) + Buffers: shared read=1491 + I/O Timings: shared/local read=13.806 +Planning: + Buffers: shared hit=6 read=18 + I/O Timings: shared/local read=0.272 +Planning Time: 1.279 ms +JIT: + Functions: 26 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 5.939 ms, Inlining 14.447 ms, Optimization 93.685 ms, Emission 79.523 ms, Total 193.594 ms +Execution Time: 19374.841 ms diff --git a/script/perf/payments_filters/plans/baseline/method_rare_json.txt b/script/perf/payments_filters/plans/baseline/method_rare_json.txt new file mode 100644 index 00000000000..b94b9528aef --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/method_rare_json.txt @@ -0,0 +1,28 @@ +-- case: method_rare_json (list) phase: baseline selective: true +-- filters: {"payment_method_type":["crypto"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" LEFT JOIN payment_methods ON payment_methods.id = payments.payment_method_id WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (COALESCE(NULLIF(payments.provider_payment_method_data->>'type', ''), payment_methods.provider_method_type) IN ('crypto')) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.97..23660.48 rows=20 width=330) (actual time=0.426..145.006 rows=20 loops=1) + Buffers: shared hit=310101 + -> Nested Loop Left Join (cost=0.97..14746969.56 rows=12466 width=330) (actual time=0.426..145.002 rows=20 loops=1) + Filter: (COALESCE(NULLIF((payments.provider_payment_method_data ->> 'type'::text), ''::text), (payment_methods.provider_method_type)::text) = 'crypto'::text) + Rows Removed by Filter: 35785 + Buffers: shared hit=310101 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.024..109.515 rows=35805 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 351 + Buffers: shared hit=173941 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=34381) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=137524 + -> Index Scan using payment_methods_pkey on payment_methods (cost=0.42..0.44 rows=1 width=22) (actual time=0.001..0.001 rows=1 loops=35805) + Index Cond: (id = payments.payment_method_id) + Buffers: shared hit=136160 +Planning: + Buffers: shared hit=24 +Planning Time: 0.360 ms +Execution Time: 145.070 ms diff --git a/script/perf/payments_filters/plans/baseline/payable_type_invoice.count.txt b/script/perf/payments_filters/plans/baseline/payable_type_invoice.count.txt new file mode 100644 index 00000000000..42f12d7729d --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payable_type_invoice.count.txt @@ -0,0 +1,40 @@ +-- case: payable_type_invoice (count) phase: baseline selective: false +-- filters: {"payable_type":["Invoice"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'Invoice' + +Aggregate (cost=13029791.67..13029791.68 rows=1 width=8) (actual time=15349.363..15349.369 rows=1 loops=1) + Buffers: shared hit=18949531 read=237180 + I/O Timings: shared/local read=1778.634 + -> Bitmap Heap Scan on payments (cost=135293.95..13023868.55 rows=2369248 width=0) (actual time=769.084..15216.405 rows=4702553 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((payable_type)::text = 'Invoice'::text) AND (payable_id IS NOT NULL)) + Filter: ((customer_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Heap Blocks: exact=157734 + Buffers: shared hit=18949531 read=237180 + I/O Timings: shared/local read=1778.634 + -> BitmapAnd (cost=135293.95..135293.95 rows=4738497 width=0) (actual time=687.821..687.826 rows=0 loops=1) + Buffers: shared hit=4 read=29637 + I/O Timings: shared/local read=129.543 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=178.364..178.368 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=3 read=670 + I/O Timings: shared/local read=3.268 + -> Bitmap Index Scan on index_payments_on_payable_type_and_payable_id (cost=0.00..94699.59 rows=6227763 width=0) (actual time=505.785..505.786 rows=6226721 loops=1) + Index Cond: (((payable_type)::text = 'Invoice'::text) AND (payable_id IS NOT NULL)) + Buffers: shared hit=1 read=28967 + I/O Timings: shared/local read=126.275 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18949514 read=49822 + I/O Timings: shared/local read=853.979 +Planning: + Buffers: shared hit=8 +Planning Time: 0.133 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 4.190 ms, Inlining 7.849 ms, Optimization 31.720 ms, Emission 27.462 ms, Total 71.220 ms +Execution Time: 15354.056 ms diff --git a/script/perf/payments_filters/plans/baseline/payable_type_invoice.txt b/script/perf/payments_filters/plans/baseline/payable_type_invoice.txt new file mode 100644 index 00000000000..f2392c2573d --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payable_type_invoice.txt @@ -0,0 +1,21 @@ +-- case: payable_type_invoice (list) phase: baseline selective: false +-- filters: {"payable_type":["Invoice"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'Invoice' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..115.61 rows=20 width=330) (actual time=0.037..0.091 rows=20 loops=1) + Buffers: shared hit=110 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=2369248 width=330) (actual time=0.036..0.089 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'Invoice'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2 + Buffers: shared hit=110 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.389 ms +Execution Time: 0.135 ms diff --git a/script/perf/payments_filters/plans/baseline/payable_type_request.count.txt b/script/perf/payments_filters/plans/baseline/payable_type_request.count.txt new file mode 100644 index 00000000000..994722783d7 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payable_type_request.count.txt @@ -0,0 +1,31 @@ +-- case: payable_type_request (count) phase: baseline selective: false +-- filters: {"payable_type":["PaymentRequest"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'PaymentRequest' + +Aggregate (cost=865664.39..865664.40 rows=1 width=8) (actual time=478.809..478.811 rows=1 loops=1) + Buffers: shared hit=129480 + -> Bitmap Heap Scan on payments (cost=44428.96..865354.40 rows=123997 width=0) (actual time=313.797..471.968 rows=250166 loops=1) + Recheck Cond: (((payable_type)::text = 'PaymentRequest'::text) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Heap Blocks: exact=126954 + Buffers: shared hit=129480 + -> BitmapAnd (cost=44428.96..44428.96 rows=247995 width=0) (actual time=218.491..218.492 rows=0 loops=1) + Buffers: shared hit=2526 + -> Bitmap Index Scan on index_payments_on_payable_type_and_payable_id (cost=0.00..4957.23 rows=325937 width=0) (actual time=30.550..30.550 rows=327832 loops=1) + Index Cond: (((payable_type)::text = 'PaymentRequest'::text) AND (payable_id IS NOT NULL)) + Buffers: shared hit=1853 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=183.629..183.629 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=673 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.229 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.920 ms, Inlining 12.781 ms, Optimization 36.064 ms, Emission 33.771 ms, Total 83.536 ms +Execution Time: 480.459 ms diff --git a/script/perf/payments_filters/plans/baseline/payable_type_request.txt b/script/perf/payments_filters/plans/baseline/payable_type_request.txt new file mode 100644 index 00000000000..adf29772c6d --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payable_type_request.txt @@ -0,0 +1,19 @@ +-- case: payable_type_request (list) phase: baseline selective: false +-- filters: {"payable_type":["PaymentRequest"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_type" = 'PaymentRequest' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..2198.86 rows=20 width=330) (actual time=0.055..0.925 rows=20 loops=1) + Buffers: shared hit=427 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=123997 width=330) (actual time=0.054..0.923 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND ((payable_type)::text = 'PaymentRequest'::text) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 400 + Buffers: shared hit=427 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=8 +Planning Time: 0.351 ms +Execution Time: 0.987 ms diff --git a/script/perf/payments_filters/plans/baseline/payment_type_manual.count.txt b/script/perf/payments_filters/plans/baseline/payment_type_manual.count.txt new file mode 100644 index 00000000000..301052c7726 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payment_type_manual.count.txt @@ -0,0 +1,40 @@ +-- case: payment_type_manual (count) phase: baseline selective: false +-- filters: {"payment_type":["manual"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'manual' + +Aggregate (cost=849162.06..849162.07 rows=1 width=8) (actual time=2216.154..2216.155 rows=1 loops=1) + Buffers: shared hit=928050 read=146952 + I/O Timings: shared/local read=657.302 + -> Bitmap Heap Scan on payments (cost=42167.76..848857.89 rows=121670 width=0) (actual time=356.948..2201.665 rows=247155 loops=1) + Recheck Cond: ((payment_type = 'manual'::payment_type) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 2374 + Heap Blocks: exact=126006 + Buffers: shared hit=928050 read=146952 + I/O Timings: shared/local read=657.302 + -> BitmapAnd (cost=42167.76..42167.76 rows=243341 width=0) (actual time=220.266..220.267 rows=0 loops=1) + Buffers: shared hit=1 read=951 + I/O Timings: shared/local read=5.138 + -> Bitmap Index Scan on index_payments_on_payment_type (cost=0.00..2697.19 rows=319821 width=0) (actual time=19.913..19.913 rows=327784 loops=1) + Index Cond: (payment_type = 'manual'::payment_type) + Buffers: shared read=279 + I/O Timings: shared/local read=1.172 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=196.000..196.000 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=672 + I/O Timings: shared/local read=3.966 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=237011) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=920309 read=27735 + I/O Timings: shared/local read=182.033 +Planning: + Buffers: shared hit=8 +Planning Time: 0.264 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.254 ms, Inlining 8.952 ms, Optimization 64.044 ms, Emission 49.157 ms, Total 123.406 ms +Execution Time: 2217.887 ms diff --git a/script/perf/payments_filters/plans/baseline/payment_type_manual.txt b/script/perf/payments_filters/plans/baseline/payment_type_manual.txt new file mode 100644 index 00000000000..a6f7e4c3913 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payment_type_manual.txt @@ -0,0 +1,21 @@ +-- case: payment_type_manual (list) phase: baseline selective: false +-- filters: {"payment_type":["manual"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'manual' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..2240.91 rows=20 width=330) (actual time=0.043..0.663 rows=20 loops=1) + Buffers: shared hit=743 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=121670 width=330) (actual time=0.042..0.661 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payment_type = 'manual'::payment_type) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 631 + Buffers: shared hit=743 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.209 ms +Execution Time: 0.704 ms diff --git a/script/perf/payments_filters/plans/baseline/payment_type_provider.count.txt b/script/perf/payments_filters/plans/baseline/payment_type_provider.count.txt new file mode 100644 index 00000000000..face7fedebd --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payment_type_provider.count.txt @@ -0,0 +1,40 @@ +-- case: payment_type_provider (count) phase: baseline selective: false +-- filters: {"payment_type":["provider"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'provider' + +Aggregate (cost=13000114.65..13000114.66 rows=1 width=8) (actual time=12466.580..12466.582 rows=1 loops=1) + Buffers: shared hit=18036477 read=178389 + I/O Timings: shared/local read=720.852 + -> Bitmap Heap Scan on payments (cost=93155.85..12994185.71 rows=2371575 width=0) (actual time=346.325..12342.125 rows=4705564 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND (payment_type = 'provider'::payment_type)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 44907 + Heap Blocks: exact=157657 + Buffers: shared hit=18036477 read=178389 + I/O Timings: shared/local read=720.852 + -> BitmapAnd (cost=93155.85..93155.85 rows=4743151 width=0) (actual time=267.239..267.240 rows=0 loops=1) + Buffers: shared hit=1 read=5916 + I/O Timings: shared/local read=20.493 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=154.633..154.634 rows=5000000 loops=1) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=1 read=672 + I/O Timings: shared/local read=2.318 + -> Bitmap Index Scan on index_payments_on_payment_type (cost=0.00..52560.33 rows=6233879 width=0) (actual time=110.461..110.461 rows=6226769 loops=1) + Index Cond: (payment_type = 'provider'::payment_type) + Buffers: shared read=5244 + I/O Timings: shared/local read=18.176 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=4512823) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=18036460 read=14832 + I/O Timings: shared/local read=143.397 +Planning: + Buffers: shared hit=8 +Planning Time: 0.187 ms +JIT: + Functions: 15 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 2.424 ms, Inlining 8.301 ms, Optimization 30.329 ms, Emission 26.279 ms, Total 67.333 ms +Execution Time: 12469.502 ms diff --git a/script/perf/payments_filters/plans/baseline/payment_type_provider.txt b/script/perf/payments_filters/plans/baseline/payment_type_provider.txt new file mode 100644 index 00000000000..8403c603861 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/payment_type_provider.txt @@ -0,0 +1,21 @@ +-- case: payment_type_provider (list) phase: baseline selective: false +-- filters: {"payment_type":["provider"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_type" = 'provider' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..115.50 rows=20 width=330) (actual time=0.052..0.121 rows=20 loops=1) + Buffers: shared hit=107 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=2371575 width=330) (actual time=0.051..0.119 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payment_type = 'provider'::payment_type) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 3 + Buffers: shared hit=107 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.316 ms +Execution Time: 0.173 ms diff --git a/script/perf/payments_filters/plans/baseline/provider_common.count.txt b/script/perf/payments_filters/plans/baseline/provider_common.count.txt new file mode 100644 index 00000000000..0ee74cdc30b --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/provider_common.count.txt @@ -0,0 +1,49 @@ +-- case: provider_common (count) phase: baseline selective: false +-- filters: {"payment_provider_type":["stripe"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::StripeProvider') + +Aggregate (cost=10310486.17..10310486.18 rows=1 width=8) (actual time=23418.391..23418.393 rows=1 loops=1) + Buffers: shared hit=15222443 read=177244 + I/O Timings: shared/local read=1926.515 + -> Nested Loop (cost=40674.58..10306746.30 rows=1495948 width=0) (actual time=325.795..23282.201 rows=3962686 loops=1) + Buffers: shared hit=15222443 read=177244 + I/O Timings: shared/local read=1926.515 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=51 width=16) (actual time=74.349..74.595 rows=51 loops=1) + Filter: ((type)::text = 'PaymentProviders::StripeProvider'::text) + Rows Removed by Filter: 34 + Buffers: shared hit=1 read=4 + I/O Timings: shared/local read=0.031 + -> Bitmap Heap Scan on payments (cost=40674.58..201800.16 rows=29279 width=16) (actual time=183.153..450.675 rows=77700 loops=51) + Recheck Cond: ((payment_provider_id = payment_providers.id) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 744 + Heap Blocks: exact=157639 + Buffers: shared hit=15222442 read=177240 + I/O Timings: shared/local read=1926.484 + -> BitmapAnd (cost=40674.58..40674.58 rows=58557 width=0) (actual time=182.868..182.868 rows=0 loops=51) + Buffers: shared hit=33756 read=5279 + I/O Timings: shared/local read=30.058 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..634.21 rows=76961 width=0) (actual time=1.782..1.782 rows=104764 loops=51) + Index Cond: (payment_provider_id = payment_providers.id) + Buffers: shared hit=106 read=4606 + I/O Timings: shared/local read=26.728 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=180.599..180.599 rows=5000000 loops=51) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=33650 read=673 + I/O Timings: shared/local read=3.331 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=3800752) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=15177885 read=25123 + I/O Timings: shared/local read=953.838 +Planning: + Buffers: shared hit=16 read=4 + I/O Timings: shared/local read=0.038 +Planning Time: 0.703 ms +JIT: + Functions: 18 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.965 ms, Inlining 9.764 ms, Optimization 32.404 ms, Emission 32.281 ms, Total 76.415 ms +Execution Time: 23420.570 ms diff --git a/script/perf/payments_filters/plans/baseline/provider_common.txt b/script/perf/payments_filters/plans/baseline/provider_common.txt new file mode 100644 index 00000000000..73a3fdeee21 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/provider_common.txt @@ -0,0 +1,33 @@ +-- case: provider_common (list) phase: baseline selective: false +-- filters: {"payment_provider_type":["stripe"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::StripeProvider') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.71..183.59 rows=20 width=330) (actual time=0.052..0.220 rows=20 loops=1) + Buffers: shared hit=165 + -> Nested Loop (cost=0.71..13678649.72 rows=1495948 width=330) (actual time=0.051..0.218 rows=20 loops=1) + Buffers: shared hit=165 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.034..0.194 rows=32 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=161 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.005..0.005 rows=1 loops=31) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=124 + -> Memoize (cost=0.15..0.17 rows=1 width=16) (actual time=0.001..0.001 rows=1 loops=32) + Cache Key: payments.payment_provider_id + Cache Mode: logical + Hits: 29 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB + Buffers: shared hit=4 + -> Index Scan using payment_providers_pkey on payment_providers (cost=0.14..0.16 rows=1 width=16) (actual time=0.005..0.005 rows=0 loops=3) + Index Cond: (id = payments.payment_provider_id) + Filter: ((type)::text = 'PaymentProviders::StripeProvider'::text) + Rows Removed by Filter: 0 + Buffers: shared hit=4 +Planning: + Buffers: shared hit=20 +Planning Time: 0.353 ms +Execution Time: 0.258 ms diff --git a/script/perf/payments_filters/plans/baseline/provider_miss.count.txt b/script/perf/payments_filters/plans/baseline/provider_miss.count.txt new file mode 100644 index 00000000000..fe4d94a971c --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/provider_miss.count.txt @@ -0,0 +1,32 @@ +-- case: provider_miss (count) phase: baseline selective: true +-- filters: {"payment_provider_type":["cashfree"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::CashfreeProvider') + +Aggregate (cost=251661.48..251661.49 rows=1 width=8) (actual time=5.084..5.085 rows=1 loops=1) + Buffers: shared hit=5 + -> Nested Loop (cost=40691.71..251588.15 rows=29332 width=0) (actual time=5.082..5.082 rows=0 loops=1) + Buffers: shared hit=5 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=1 width=16) (actual time=5.081..5.082 rows=0 loops=1) + Filter: ((type)::text = 'PaymentProviders::CashfreeProvider'::text) + Rows Removed by Filter: 85 + Buffers: shared hit=5 + -> Bitmap Heap Scan on payments (cost=40691.71..251289.29 rows=29279 width=16) (never executed) + Recheck Cond: ((payment_provider_id = payment_providers.id) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + -> BitmapAnd (cost=40691.71..40691.71 rows=58557 width=0) (never executed) + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..651.34 rows=76961 width=0) (never executed) + Index Cond: (payment_provider_id = payment_providers.id) + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (never executed) + Index Cond: (organization_id = ''::uuid) + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=20 +Planning Time: 0.186 ms +JIT: + Functions: 18 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.536 ms, Inlining 0.000 ms, Optimization 0.381 ms, Emission 4.694 ms, Total 5.611 ms +Execution Time: 5.655 ms diff --git a/script/perf/payments_filters/plans/baseline/provider_miss.txt b/script/perf/payments_filters/plans/baseline/provider_miss.txt new file mode 100644 index 00000000000..2937a0e3a96 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/provider_miss.txt @@ -0,0 +1,37 @@ +-- case: provider_miss (list) phase: baseline selective: true +-- filters: {"payment_provider_type":["cashfree"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::CashfreeProvider') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..9310.60 rows=20 width=330) (actual time=66537.344..66537.347 rows=0 loops=1) + Buffers: shared hit=21097387 read=2937644 + I/O Timings: shared/local read=38438.801 + -> Nested Loop (cost=0.56..13654112.44 rows=29332 width=330) (actual time=66537.343..66537.344 rows=0 loops=1) + Join Filter: (payments.payment_provider_id = payment_providers.id) + Buffers: shared hit=21097387 read=2937644 + I/O Timings: shared/local read=38438.801 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.750..65892.996 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=21097385 read=2937641 + I/O Timings: shared/local read=38438.683 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.009..0.009 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=17469701 read=1529635 + I/O Timings: shared/local read=23166.824 + -> Materialize (cost=0.00..6.07 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=4952719) + Buffers: shared hit=2 read=3 + I/O Timings: shared/local read=0.118 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=1 width=16) (actual time=0.136..0.137 rows=0 loops=1) + Filter: ((type)::text = 'PaymentProviders::CashfreeProvider'::text) + Rows Removed by Filter: 85 + Buffers: shared hit=2 read=3 + I/O Timings: shared/local read=0.118 +Planning: + Buffers: shared hit=5 read=15 + I/O Timings: shared/local read=2.655 +Planning Time: 3.422 ms +Execution Time: 66537.518 ms diff --git a/script/perf/payments_filters/plans/baseline/provider_rare.count.txt b/script/perf/payments_filters/plans/baseline/provider_rare.count.txt new file mode 100644 index 00000000000..10d4be501c7 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/provider_rare.count.txt @@ -0,0 +1,47 @@ +-- case: provider_rare (count) phase: baseline selective: false +-- filters: {"payment_provider_type":["gocardless"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::GocardlessProvider') + +Aggregate (cost=3584870.67..3584870.68 rows=1 width=8) (actual time=9731.456..9731.462 rows=1 loops=1) + Buffers: shared hit=2817569 read=199552 + I/O Timings: shared/local read=3603.814 + -> Nested Loop (cost=40684.98..3583624.04 rows=498649 width=0) (actual time=269.906..9694.620 rows=742878 loops=1) + Buffers: shared hit=2817569 read=199552 + I/O Timings: shared/local read=3603.814 + -> Seq Scan on payment_providers (cost=0.00..6.06 rows=17 width=16) (actual time=67.740..67.898 rows=17 loops=1) + Filter: ((type)::text = 'PaymentProviders::GocardlessProvider'::text) + Rows Removed by Filter: 68 + Buffers: shared hit=2 read=3 + I/O Timings: shared/local read=0.064 + -> Bitmap Heap Scan on payments (cost=40684.98..210508.27 rows=29279 width=16) (actual time=170.780..562.594 rows=43699 loops=17) + Recheck Cond: ((payment_provider_id = payment_providers.id) AND (organization_id = ''::uuid)) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 408 + Heap Blocks: exact=156643 + Buffers: shared hit=2817567 read=199549 + I/O Timings: shared/local read=3603.749 + -> BitmapAnd (cost=40684.98..40684.98 rows=58557 width=0) (actual time=169.934..169.934 rows=0 loops=17) + Buffers: shared hit=11472 read=717 + I/O Timings: shared/local read=5.608 + -> Bitmap Index Scan on index_payments_on_payment_provider_id (cost=0.00..644.61 rows=76961 width=0) (actual time=1.573..1.573 rows=48042 loops=17) + Index Cond: (payment_provider_id = payment_providers.id) + Buffers: shared hit=31 read=717 + I/O Timings: shared/local read=5.608 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..39409.49 rows=4986492 width=0) (actual time=167.772..167.772 rows=5000000 loops=17) + Index Cond: (organization_id = ''::uuid) + Buffers: shared hit=11441 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.007..0.007 rows=1 loops=712071) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=2805987 read=42297 + I/O Timings: shared/local read=2724.460 +Planning: + Buffers: shared hit=20 +Planning Time: 0.165 ms +JIT: + Functions: 18 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.374 ms, Inlining 6.666 ms, Optimization 34.494 ms, Emission 26.722 ms, Total 69.257 ms +Execution Time: 9733.051 ms diff --git a/script/perf/payments_filters/plans/baseline/provider_rare.txt b/script/perf/payments_filters/plans/baseline/provider_rare.txt new file mode 100644 index 00000000000..959c0ce4190 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/provider_rare.txt @@ -0,0 +1,33 @@ +-- case: provider_rare (list) phase: baseline selective: false +-- filters: {"payment_provider_type":["gocardless"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payment_provider_id" IN (SELECT "payment_providers"."id" FROM "payment_providers" WHERE "payment_providers"."type" = 'PaymentProviders::GocardlessProvider') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.71..549.34 rows=20 width=330) (actual time=0.048..0.418 rows=20 loops=1) + Buffers: shared hit=512 + -> Nested Loop (cost=0.71..13678680.12 rows=498649 width=330) (actual time=0.047..0.416 rows=20 loops=1) + Buffers: shared hit=512 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.029..0.392 rows=107 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1 + Buffers: shared hit=508 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=99) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=396 + -> Memoize (cost=0.15..0.17 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=107) + Cache Key: payments.payment_provider_id + Cache Mode: logical + Hits: 104 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB + Buffers: shared hit=4 + -> Index Scan using payment_providers_pkey on payment_providers (cost=0.14..0.16 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=3) + Index Cond: (id = payments.payment_provider_id) + Filter: ((type)::text = 'PaymentProviders::GocardlessProvider'::text) + Rows Removed by Filter: 0 + Buffers: shared hit=4 +Planning: + Buffers: shared hit=20 +Planning Time: 0.511 ms +Execution Time: 0.453 ms diff --git a/script/perf/payments_filters/plans/baseline/receipt_hit.count.txt b/script/perf/payments_filters/plans/baseline/receipt_hit.count.txt new file mode 100644 index 00000000000..472a14ef00e --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/receipt_hit.count.txt @@ -0,0 +1,33 @@ +-- case: receipt_hit (count) phase: baseline selective: true +-- filters: {"receipt_number":"her-1556-335-rcpt-000123"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" INNER JOIN "payment_receipts" ON "payment_receipts"."payment_id" = "payments"."id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (LOWER(payment_receipts.number) = LOWER('her-1556-335-rcpt-000123')) + +Aggregate (cost=174495.02..174495.03 rows=1 width=8) (actual time=274.477..281.346 rows=1 loops=1) + Buffers: shared hit=60825 + -> Nested Loop (cost=1000.43..174478.24 rows=6709 width=0) (actual time=192.060..281.325 rows=2 loops=1) + Buffers: shared hit=60825 + -> Gather (cost=1000.00..85615.76 rows=17635 width=16) (actual time=191.993..281.233 rows=2 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=60809 + -> Parallel Seq Scan on payment_receipts (cost=0.00..82852.26 rows=7348 width=16) (actual time=157.023..263.749 rows=1 loops=3) + Filter: (lower((number)::text) = 'her-1556-335-rcpt-000123'::text) + Rows Removed by Filter: 1175624 + Buffers: shared hit=60809 + -> Index Scan using payments_pkey on payments (cost=0.43..5.04 rows=1 width=16) (actual time=0.039..0.039 rows=1 loops=2) + Index Cond: (id = payment_receipts.payment_id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=16 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.013..0.013 rows=1 loops=2) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=8 +Planning: + Buffers: shared hit=24 +Planning Time: 0.450 ms +JIT: + Functions: 28 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 2.320 ms, Inlining 0.000 ms, Optimization 0.981 ms, Emission 11.655 ms, Total 14.956 ms +Execution Time: 282.929 ms diff --git a/script/perf/payments_filters/plans/baseline/receipt_hit.txt b/script/perf/payments_filters/plans/baseline/receipt_hit.txt new file mode 100644 index 00000000000..238d8516c2d --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/receipt_hit.txt @@ -0,0 +1,34 @@ +-- case: receipt_hit (list) phase: baseline selective: true +-- filters: {"receipt_number":"her-1556-335-rcpt-000123"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" INNER JOIN "payment_receipts" ON "payment_receipts"."payment_id" = "payments"."id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (LOWER(payment_receipts.number) = LOWER('her-1556-335-rcpt-000123')) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.99..44213.94 rows=20 width=330) (actual time=42.672..105273.957 rows=2 loops=1) + Buffers: shared hit=36993897 read=4564673 + I/O Timings: shared/local read=63126.266 + -> Nested Loop (cost=0.99..14831236.10 rows=6709 width=330) (actual time=42.671..105273.946 rows=2 loops=1) + Buffers: shared hit=36993897 read=4564673 + I/O Timings: shared/local read=63126.266 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13616707.68 rows=2493246 width=330) (actual time=0.601..78855.122 rows=4952719 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 47281 + Buffers: shared hit=19808387 read=4226639 + I/O Timings: shared/local read=53220.747 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.011..0.011 rows=1 loops=4749834) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=16893990 read=2105346 + I/O Timings: shared/local read=36406.765 + -> Index Scan using index_payment_receipts_on_payment_id on payment_receipts (cost=0.43..0.49 rows=1 width=16) (actual time=0.005..0.005 rows=0 loops=4952719) + Index Cond: (payment_id = payments.id) + Filter: (lower((number)::text) = 'her-1556-335-rcpt-000123'::text) + Rows Removed by Filter: 1 + Buffers: shared hit=17185510 read=338034 + I/O Timings: shared/local read=9905.518 +Planning: + Buffers: shared hit=10 read=14 + I/O Timings: shared/local read=1.863 +Planning Time: 2.597 ms +Execution Time: 105274.087 ms diff --git a/script/perf/payments_filters/plans/baseline/receipt_miss.count.txt b/script/perf/payments_filters/plans/baseline/receipt_miss.count.txt new file mode 100644 index 00000000000..08e17ce96cd --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/receipt_miss.count.txt @@ -0,0 +1,31 @@ +-- case: receipt_miss (count) phase: baseline selective: true +-- filters: {"receipt_number":"PERF-NOPE-RCPT-000001"} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" INNER JOIN "payment_receipts" ON "payment_receipts"."payment_id" = "payments"."id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (LOWER(payment_receipts.number) = LOWER('PERF-NOPE-RCPT-000001')) + +Aggregate (cost=174495.02..174495.03 rows=1 width=8) (actual time=231.207..238.429 rows=1 loops=1) + Buffers: shared hit=60809 + -> Nested Loop (cost=1000.43..174478.24 rows=6709 width=0) (actual time=231.189..238.411 rows=0 loops=1) + Buffers: shared hit=60809 + -> Gather (cost=1000.00..85615.76 rows=17635 width=16) (actual time=231.188..238.409 rows=0 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=60809 + -> Parallel Seq Scan on payment_receipts (cost=0.00..82852.26 rows=7348 width=16) (actual time=224.103..224.103 rows=0 loops=3) + Filter: (lower((number)::text) = 'perf-nope-rcpt-000001'::text) + Rows Removed by Filter: 1175625 + Buffers: shared hit=60809 + -> Index Scan using payments_pkey on payments (cost=0.43..5.04 rows=1 width=16) (never executed) + Index Cond: (id = payment_receipts.payment_id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (never executed) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) +Planning: + Buffers: shared hit=24 +Planning Time: 0.302 ms +JIT: + Functions: 28 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 1.280 ms, Inlining 0.000 ms, Optimization 0.875 ms, Emission 7.943 ms, Total 10.097 ms +Execution Time: 239.285 ms diff --git a/script/perf/payments_filters/plans/baseline/receipt_miss.txt b/script/perf/payments_filters/plans/baseline/receipt_miss.txt new file mode 100644 index 00000000000..dfafa8dcd21 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/receipt_miss.txt @@ -0,0 +1,5 @@ +-- case: receipt_miss (list) phase: baseline selective: true +-- filters: {"receipt_number":"PERF-NOPE-RCPT-000001"} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" INNER JOIN "payment_receipts" ON "payment_receipts"."payment_id" = "payments"."id" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND (LOWER(payment_receipts.number) = LOWER('PERF-NOPE-RCPT-000001')) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +TIMEOUT after 120s diff --git a/script/perf/payments_filters/plans/baseline/search_term.count.txt b/script/perf/payments_filters/plans/baseline/search_term.count.txt new file mode 100644 index 00000000000..d21821db8a5 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/search_term.count.txt @@ -0,0 +1,92 @@ +-- case: search_term (count) phase: baseline selective: true +-- filters: {} search_term: "_0_4031686" page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) + +Aggregate (cost=19310.06..19310.07 rows=1 width=8) (actual time=52.576..52.579 rows=1 loops=1) + Buffers: shared hit=4555 + -> Nested Loop (cost=5490.99..19307.58 rows=994 width=0) (actual time=52.574..52.577 rows=1 loops=1) + Buffers: shared hit=4555 + -> HashAggregate (cost=5490.56..5516.70 rows=2614 width=16) (actual time=52.546..52.549 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4547 + -> Append (cost=180.86..5484.02 rows=2614 width=16) (actual time=51.071..52.536 rows=1 loops=1) + Buffers: shared hit=4547 + -> Bitmap Heap Scan on payments payments_1 (cost=180.86..707.10 rows=474 width=16) (actual time=51.070..51.071 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4277 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..180.74 rows=474 width=0) (actual time=51.059..51.059 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4276 + -> Bitmap Heap Scan on payments payments_2 (cost=56.35..83.08 rows=24 width=16) (actual time=0.128..0.128 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..56.34 rows=24 width=0) (actual time=0.127..0.127 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Nested Loop (cost=248.13..1940.72 rows=361 width=16) (actual time=0.203..0.203 rows=0 loops=1) + Buffers: shared hit=37 + -> Bitmap Heap Scan on invoices (cost=247.70..746.34 rows=449 width=16) (actual time=0.202..0.202 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..247.59 rows=449 width=0) (actual time=0.201..0.201 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=113.12..2713.90 rows=1755 width=16) (actual time=1.131..1.132 rows=0 loops=1) + Buffers: shared hit=203 + -> HashAggregate (cost=112.69..112.82 rows=13 width=16) (actual time=1.130..1.131 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=203 + -> Append (cost=24.25..112.66 rows=13 width=16) (actual time=1.129..1.130 rows=0 loops=1) + Buffers: shared hit=203 + -> Bitmap Heap Scan on customers (cost=24.25..29.81 rows=5 width=16) (actual time=0.524..0.524 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.523..0.523 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_1 (cost=16.50..17.62 rows=1 width=16) (actual time=0.030..0.031 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.030..0.030 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_2 (cost=16.50..17.62 rows=1 width=16) (actual time=0.030..0.030 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.029..0.029 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_3 (cost=24.25..29.81 rows=5 width=16) (actual time=0.516..0.516 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.515..0.515 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_4 (cost=16.50..17.62 rows=1 width=16) (actual time=0.027..0.027 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.025..0.025 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..198.72 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=16) (actual time=0.026..0.026 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.013..0.013 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.938 ms +Execution Time: 52.833 ms diff --git a/script/perf/payments_filters/plans/baseline/search_term.txt b/script/perf/payments_filters/plans/baseline/search_term.txt new file mode 100644 index 00000000000..9cc0efa24d8 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/search_term.txt @@ -0,0 +1,96 @@ +-- case: search_term (list) phase: baseline selective: true +-- filters: {} search_term: "_0_4031686" page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=19334.03..19334.08 rows=20 width=330) (actual time=55.855..55.858 rows=1 loops=1) + Buffers: shared hit=4555 + -> Sort (cost=19334.03..19336.51 rows=994 width=330) (actual time=55.854..55.857 rows=1 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=4555 + -> Nested Loop (cost=5490.99..19307.58 rows=994 width=330) (actual time=55.849..55.852 rows=1 loops=1) + Buffers: shared hit=4555 + -> HashAggregate (cost=5490.56..5516.70 rows=2614 width=16) (actual time=55.819..55.821 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4547 + -> Append (cost=180.86..5484.02 rows=2614 width=16) (actual time=54.174..55.808 rows=1 loops=1) + Buffers: shared hit=4547 + -> Bitmap Heap Scan on payments payments_1 (cost=180.86..707.10 rows=474 width=16) (actual time=54.173..54.174 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4277 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..180.74 rows=474 width=0) (actual time=54.160..54.161 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4276 + -> Bitmap Heap Scan on payments payments_2 (cost=56.35..83.08 rows=24 width=16) (actual time=0.151..0.151 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..56.34 rows=24 width=0) (actual time=0.151..0.151 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Nested Loop (cost=248.13..1940.72 rows=361 width=16) (actual time=0.192..0.192 rows=0 loops=1) + Buffers: shared hit=37 + -> Bitmap Heap Scan on invoices (cost=247.70..746.34 rows=449 width=16) (actual time=0.192..0.192 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..247.59 rows=449 width=0) (actual time=0.191..0.191 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=113.12..2713.90 rows=1755 width=16) (actual time=1.287..1.288 rows=0 loops=1) + Buffers: shared hit=203 + -> HashAggregate (cost=112.69..112.82 rows=13 width=16) (actual time=1.286..1.288 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=203 + -> Append (cost=24.25..112.66 rows=13 width=16) (actual time=1.285..1.286 rows=0 loops=1) + Buffers: shared hit=203 + -> Bitmap Heap Scan on customers (cost=24.25..29.81 rows=5 width=16) (actual time=0.573..0.573 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.573..0.573 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_1 (cost=16.50..17.62 rows=1 width=16) (actual time=0.032..0.032 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.031..0.031 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_2 (cost=16.50..17.62 rows=1 width=16) (actual time=0.027..0.027 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.026..0.026 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_3 (cost=24.25..29.81 rows=5 width=16) (actual time=0.592..0.592 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.591..0.591 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_4 (cost=16.50..17.62 rows=1 width=16) (actual time=0.060..0.060 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.059..0.059 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..198.72 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=330) (actual time=0.028..0.028 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.012..0.012 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.906 ms +Execution Time: 56.040 ms diff --git a/script/perf/payments_filters/plans/baseline/search_term_status.count.txt b/script/perf/payments_filters/plans/baseline/search_term_status.count.txt new file mode 100644 index 00000000000..f4bd55faa55 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/search_term_status.count.txt @@ -0,0 +1,92 @@ +-- case: search_term_status (count) phase: baseline selective: true +-- filters: {"payment_status":["succeeded"]} search_term: "_0_4031686" page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) AND "payments"."payable_payment_status" = 'succeeded' + +Aggregate (cost=19316.17..19316.18 rows=1 width=8) (actual time=50.416..50.419 rows=1 loops=1) + Buffers: shared hit=4555 + -> Nested Loop (cost=5490.99..19314.11 rows=822 width=0) (actual time=50.415..50.417 rows=1 loops=1) + Buffers: shared hit=4555 + -> HashAggregate (cost=5490.56..5516.70 rows=2614 width=16) (actual time=50.396..50.399 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4547 + -> Append (cost=180.86..5484.02 rows=2614 width=16) (actual time=48.908..50.388 rows=1 loops=1) + Buffers: shared hit=4547 + -> Bitmap Heap Scan on payments payments_1 (cost=180.86..707.10 rows=474 width=16) (actual time=48.908..48.909 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4277 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..180.74 rows=474 width=0) (actual time=48.903..48.903 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4276 + -> Bitmap Heap Scan on payments payments_2 (cost=56.35..83.08 rows=24 width=16) (actual time=0.117..0.117 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..56.34 rows=24 width=0) (actual time=0.117..0.117 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Nested Loop (cost=248.13..1940.72 rows=361 width=16) (actual time=0.173..0.173 rows=0 loops=1) + Buffers: shared hit=37 + -> Bitmap Heap Scan on invoices (cost=247.70..746.34 rows=449 width=16) (actual time=0.173..0.173 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..247.59 rows=449 width=0) (actual time=0.172..0.172 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=113.12..2713.90 rows=1755 width=16) (actual time=1.187..1.188 rows=0 loops=1) + Buffers: shared hit=203 + -> HashAggregate (cost=112.69..112.82 rows=13 width=16) (actual time=1.186..1.187 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=203 + -> Append (cost=24.25..112.66 rows=13 width=16) (actual time=1.186..1.187 rows=0 loops=1) + Buffers: shared hit=203 + -> Bitmap Heap Scan on customers (cost=24.25..29.81 rows=5 width=16) (actual time=0.543..0.543 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.542..0.542 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_1 (cost=16.50..17.62 rows=1 width=16) (actual time=0.026..0.026 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.026..0.026 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_2 (cost=16.50..17.62 rows=1 width=16) (actual time=0.025..0.026 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.025..0.025 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_3 (cost=24.25..29.81 rows=5 width=16) (actual time=0.566..0.566 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.565..0.565 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_4 (cost=16.50..17.62 rows=1 width=16) (actual time=0.025..0.025 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.023..0.023 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..198.72 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=16) (actual time=0.017..0.017 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.008..0.008 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.802 ms +Execution Time: 50.563 ms diff --git a/script/perf/payments_filters/plans/baseline/search_term_status.txt b/script/perf/payments_filters/plans/baseline/search_term_status.txt new file mode 100644 index 00000000000..2d059b0d214 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/search_term_status.txt @@ -0,0 +1,96 @@ +-- case: search_term_status (list) phase: baseline selective: true +-- filters: {"payment_status":["succeeded"]} search_term: "_0_4031686" page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."id" IN (SELECT "payments"."id" FROM (SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.provider_payment_id ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND (payments.reference ILIKE '%\_0\_4031686%') UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."payable_type" = 'Invoice' AND "payments"."payable_id" IN (SELECT "invoices"."id" FROM "invoices" WHERE "invoices"."organization_id" = '' AND (invoices.number ILIKE '%\_0\_4031686%')) UNION SELECT "payments"."id" FROM "payments" WHERE "payments"."organization_id" = '' AND "payments"."customer_id" IN (SELECT "customers"."id" FROM (SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.name ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.firstname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.lastname ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.external_id ILIKE '%\_0\_4031686%') UNION SELECT "customers"."id" FROM "customers" WHERE "customers"."deleted_at" IS NULL AND "customers"."organization_id" = '' AND (customers.email ILIKE '%\_0\_4031686%')) AS customers)) AS payments) AND "payments"."payable_payment_status" = 'succeeded' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=19335.99..19336.04 rows=20 width=330) (actual time=49.464..49.467 rows=1 loops=1) + Buffers: shared hit=4555 + -> Sort (cost=19335.99..19338.04 rows=822 width=330) (actual time=49.453..49.455 rows=1 loops=1) + Sort Key: payments.created_at DESC, payments.id + Sort Method: quicksort Memory: 25kB + Buffers: shared hit=4555 + -> Nested Loop (cost=5490.99..19314.11 rows=822 width=330) (actual time=49.447..49.450 rows=1 loops=1) + Buffers: shared hit=4555 + -> HashAggregate (cost=5490.56..5516.70 rows=2614 width=16) (actual time=49.426..49.429 rows=1 loops=1) + Group Key: payments_1.id + Batches: 1 Memory Usage: 121kB + Buffers: shared hit=4547 + -> Append (cost=180.86..5484.02 rows=2614 width=16) (actual time=48.065..49.418 rows=1 loops=1) + Buffers: shared hit=4547 + -> Bitmap Heap Scan on payments payments_1 (cost=180.86..707.10 rows=474 width=16) (actual time=48.064..48.065 rows=1 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Heap Blocks: exact=1 + Buffers: shared hit=4277 + -> Bitmap Index Scan on idx_on_organization_id_provider_payment_id_gin_trgm_2bcf073c0b (cost=0.00..180.74 rows=474 width=0) (actual time=48.058..48.059 rows=1 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((provider_payment_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=4276 + -> Bitmap Heap Scan on payments payments_2 (cost=56.35..83.08 rows=24 width=16) (actual time=0.104..0.104 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Bitmap Index Scan on index_payments_on_organization_id_reference_gin_trgm_ops (cost=0.00..56.34 rows=24 width=0) (actual time=0.104..0.104 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((reference)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=30 + -> Nested Loop (cost=248.13..1940.72 rows=361 width=16) (actual time=0.150..0.150 rows=0 loops=1) + Buffers: shared hit=37 + -> Bitmap Heap Scan on invoices (cost=247.70..746.34 rows=449 width=16) (actual time=0.149..0.149 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Bitmap Index Scan on index_invoices_on_organization_id_number_gin_trgm_ops (cost=0.00..247.59 rows=449 width=0) (actual time=0.149..0.149 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((number)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=37 + -> Index Scan using index_payments_on_payable_id_and_payable_type_and_error_code on payments payments_3 (cost=0.43..2.65 rows=1 width=32) (never executed) + Index Cond: ((payable_id = invoices.id) AND ((payable_type)::text = 'Invoice'::text)) + Filter: (organization_id = ''::uuid) + -> Nested Loop (cost=113.12..2713.90 rows=1755 width=16) (actual time=1.097..1.098 rows=0 loops=1) + Buffers: shared hit=203 + -> HashAggregate (cost=112.69..112.82 rows=13 width=16) (actual time=1.096..1.097 rows=0 loops=1) + Group Key: customers.id + Batches: 1 Memory Usage: 24kB + Buffers: shared hit=203 + -> Append (cost=24.25..112.66 rows=13 width=16) (actual time=1.096..1.097 rows=0 loops=1) + Buffers: shared hit=203 + -> Bitmap Heap Scan on customers (cost=24.25..29.81 rows=5 width=16) (actual time=0.518..0.519 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_name_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.518..0.518 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((name)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_1 (cost=16.50..17.62 rows=1 width=16) (actual time=0.022..0.022 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_firstname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.022..0.022 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((firstname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_2 (cost=16.50..17.62 rows=1 width=16) (actual time=0.021..0.021 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_lastname_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.021..0.021 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((lastname)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Bitmap Heap Scan on customers customers_3 (cost=24.25..29.81 rows=5 width=16) (actual time=0.513..0.513 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=67 + -> Bitmap Index Scan on index_customers_on_organization_id_external_id_gin_trgm_ops (cost=0.00..24.25 rows=5 width=0) (actual time=0.512..0.513 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((external_id)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=67 + -> Bitmap Heap Scan on customers customers_4 (cost=16.50..17.62 rows=1 width=16) (actual time=0.020..0.020 rows=0 loops=1) + Recheck Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text) AND (deleted_at IS NULL)) + Buffers: shared hit=23 + -> Bitmap Index Scan on index_customers_on_organization_id_email_gin_trgm_ops (cost=0.00..16.50 rows=1 width=0) (actual time=0.020..0.020 rows=0 loops=1) + Index Cond: ((organization_id = ''::uuid) AND ((email)::text ~~* '%\_0\_4031686%'::text)) + Buffers: shared hit=23 + -> Index Scan using index_payments_on_customer_id on payments payments_4 (cost=0.43..198.72 rows=135 width=32) (never executed) + Index Cond: (customer_id = customers.id) + Filter: (organization_id = ''::uuid) + -> Index Scan using payments_pkey on payments (cost=0.43..5.27 rows=1 width=330) (actual time=0.019..0.019 rows=1 loops=1) + Index Cond: (id = payments_1.id) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (organization_id = ''::uuid) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Buffers: shared hit=8 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices invoices_1 (cost=0.43..2.66 rows=1 width=0) (actual time=0.008..0.008 rows=1 loops=1) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=4 +Planning: + Buffers: shared hit=65 +Planning Time: 0.739 ms +Execution Time: 49.621 ms diff --git a/script/perf/payments_filters/plans/baseline/status_common_succeeded.count.txt b/script/perf/payments_filters/plans/baseline/status_common_succeeded.count.txt new file mode 100644 index 00000000000..9e65a01f006 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_common_succeeded.count.txt @@ -0,0 +1,29 @@ +-- case: status_common_succeeded (count) phase: baseline selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' + +Aggregate (cost=13593246.30..13593246.31 rows=1 width=8) (actual time=11372.918..11372.919 rows=1 loops=1) + Buffers: shared hit=15720820 read=172848 + I/O Timings: shared/local read=633.660 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=2061665 width=0) (actual time=67.806..11264.836 rows=4100643 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 899357 + Buffers: shared hit=15720820 read=172848 + I/O Timings: shared/local read=633.660 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=3932914) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=15720820 read=10836 + I/O Timings: shared/local read=60.466 +Planning: + Buffers: shared hit=6 read=2 + I/O Timings: shared/local read=0.036 +Planning Time: 0.697 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 2.946 ms, Inlining 6.048 ms, Optimization 31.276 ms, Emission 30.436 ms, Total 70.706 ms +Execution Time: 11376.025 ms diff --git a/script/perf/payments_filters/plans/baseline/status_common_succeeded.txt b/script/perf/payments_filters/plans/baseline/status_common_succeeded.txt new file mode 100644 index 00000000000..399e9608e86 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_common_succeeded.txt @@ -0,0 +1,21 @@ +-- case: status_common_succeeded (list) phase: baseline selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..132.77 rows=20 width=330) (actual time=0.018..0.057 rows=20 loops=1) + Buffers: shared hit=108 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=2061665 width=330) (actual time=0.018..0.056 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4 + Buffers: shared hit=108 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.123 ms +Execution Time: 0.073 ms diff --git a/script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.count.txt b/script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.count.txt new file mode 100644 index 00000000000..272cb0d5bcc --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.count.txt @@ -0,0 +1,29 @@ +-- case: status_common_succeeded_page50 (count) phase: baseline selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 50 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' + +Aggregate (cost=13593246.30..13593246.31 rows=1 width=8) (actual time=12760.280..12760.288 rows=1 loops=1) + Buffers: shared hit=15709835 read=183833 + I/O Timings: shared/local read=1172.597 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=2061665 width=0) (actual time=84.667..12643.222 rows=4100643 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 899357 + Buffers: shared hit=15709835 read=183833 + I/O Timings: shared/local read=1172.597 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=3932914) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=15709833 read=21823 + I/O Timings: shared/local read=308.134 +Planning: + Buffers: shared hit=6 read=2 + I/O Timings: shared/local read=0.027 +Planning Time: 0.499 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 1.390 ms, Inlining 5.936 ms, Optimization 45.694 ms, Emission 33.218 ms, Total 86.237 ms +Execution Time: 12762.035 ms diff --git a/script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.txt b/script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.txt new file mode 100644 index 00000000000..5d03bd5e03f --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_common_succeeded_page50.txt @@ -0,0 +1,21 @@ +-- case: status_common_succeeded_page50 (list) phase: baseline selective: false +-- filters: {"payment_status":["succeeded"]} search_term: nil page: 50 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'succeeded' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 980 + +Limit (cost=6479.10..6611.32 rows=20 width=330) (actual time=2.281..2.318 rows=20 loops=1) + Buffers: shared hit=4980 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=2061665 width=330) (actual time=0.018..2.300 rows=1000 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'succeeded'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 164 + Buffers: shared hit=4980 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=951) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=3804 +Planning: + Buffers: shared hit=8 +Planning Time: 0.131 ms +Execution Time: 2.336 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_failed.count.txt b/script/perf/payments_filters/plans/baseline/status_rare_failed.count.txt new file mode 100644 index 00000000000..aaa673a087e --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_failed.count.txt @@ -0,0 +1,28 @@ +-- case: status_rare_failed (count) phase: baseline selective: false +-- filters: {"payment_status":["failed"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' + +Aggregate (cost=13589003.84..13589003.85 rows=1 width=8) (actual time=3410.712..3410.713 rows=1 loops=1) + Buffers: shared hit=2691336 read=225540 + I/O Timings: shared/local read=743.183 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=364679 width=0) (actual time=69.504..3383.781 rows=718336 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4281664 + Buffers: shared hit=2691336 read=225540 + I/O Timings: shared/local read=743.183 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=688716) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=2691174 read=63690 + I/O Timings: shared/local read=314.779 +Planning: + Buffers: shared hit=8 +Planning Time: 0.206 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.575 ms, Inlining 4.468 ms, Optimization 35.012 ms, Emission 30.081 ms, Total 70.136 ms +Execution Time: 3411.468 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_failed.txt b/script/perf/payments_filters/plans/baseline/status_rare_failed.txt new file mode 100644 index 00000000000..94195924e26 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_failed.txt @@ -0,0 +1,20 @@ +-- case: status_rare_failed (list) phase: baseline selective: false +-- filters: {"payment_status":["failed"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'failed' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..748.02 rows=20 width=330) (actual time=0.047..0.311 rows=20 loops=1) + Buffers: shared hit=409 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=364679 width=330) (actual time=0.047..0.310 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'failed'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 303 + Buffers: shared hit=409 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.214 ms +Execution Time: 0.341 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_pending.count.txt b/script/perf/payments_filters/plans/baseline/status_rare_pending.count.txt new file mode 100644 index 00000000000..b3eeffaa206 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_pending.count.txt @@ -0,0 +1,28 @@ +-- case: status_rare_pending (count) phase: baseline selective: true +-- filters: {"payment_status":["pending"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'pending' + +Aggregate (cost=13588204.33..13588204.34 rows=1 width=8) (actual time=831.263..831.264 rows=1 loops=1) + Buffers: shared hit=412761 read=91371 + I/O Timings: shared/local read=169.175 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=44878 width=0) (actual time=58.547..827.601 rows=89176 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'pending'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4910824 + Buffers: shared hit=412761 read=91371 + I/O Timings: shared/local read=169.175 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.004..0.004 rows=1 loops=85530) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=326721 read=15399 + I/O Timings: shared/local read=45.366 +Planning: + Buffers: shared hit=8 +Planning Time: 0.166 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.516 ms, Inlining 3.626 ms, Optimization 30.170 ms, Emission 24.691 ms, Total 59.003 ms +Execution Time: 831.878 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_pending.txt b/script/perf/payments_filters/plans/baseline/status_rare_pending.txt new file mode 100644 index 00000000000..614051cea48 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_pending.txt @@ -0,0 +1,20 @@ +-- case: status_rare_pending (list) phase: baseline selective: true +-- filters: {"payment_status":["pending"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'pending' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..6074.43 rows=20 width=330) (actual time=0.017..0.491 rows=20 loops=1) + Buffers: shared hit=1156 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=44878 width=330) (actual time=0.017..0.490 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'pending'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1045 + Buffers: shared hit=1156 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=20) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Buffers: shared hit=80 +Planning: + Buffers: shared hit=8 +Planning Time: 0.151 ms +Execution Time: 0.510 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_pending_processing.count.txt b/script/perf/payments_filters/plans/baseline/status_rare_pending_processing.count.txt new file mode 100644 index 00000000000..c1755b51dd9 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_pending_processing.count.txt @@ -0,0 +1,28 @@ +-- case: status_rare_pending_processing (count) phase: baseline selective: true +-- filters: {"payment_status":["pending","processing"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" IN ('pending', 'processing') + +Aggregate (cost=13588259.39..13588259.40 rows=1 width=8) (actual time=1480.603..1480.604 rows=1 loops=1) + Buffers: shared hit=570633 read=104195 + I/O Timings: shared/local read=501.882 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=66902 width=0) (actual time=60.763..1473.103 rows=133740 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = ANY ('{pending,processing}'::payment_payable_payment_status[])) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4866260 + Buffers: shared hit=570633 read=104195 + I/O Timings: shared/local read=501.882 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.006..0.006 rows=1 loops=128204) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=463420 read=49396 + I/O Timings: shared/local read=311.413 +Planning: + Buffers: shared hit=8 +Planning Time: 0.088 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.595 ms, Inlining 4.313 ms, Optimization 30.417 ms, Emission 26.002 ms, Total 61.327 ms +Execution Time: 1481.299 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_pending_processing.txt b/script/perf/payments_filters/plans/baseline/status_rare_pending_processing.txt new file mode 100644 index 00000000000..1d25006aa4d --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_pending_processing.txt @@ -0,0 +1,21 @@ +-- case: status_rare_pending_processing (list) phase: baseline selective: true +-- filters: {"payment_status":["pending","processing"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" IN ('pending', 'processing') ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..4074.93 rows=20 width=330) (actual time=0.022..0.262 rows=20 loops=1) + Buffers: shared hit=770 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=66902 width=330) (actual time=0.022..0.261 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = ANY ('{pending,processing}'::payment_payable_payment_status[])) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 658 + Buffers: shared hit=770 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.126 ms +Execution Time: 0.280 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_processing.count.txt b/script/perf/payments_filters/plans/baseline/status_rare_processing.count.txt new file mode 100644 index 00000000000..d3c658865f5 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_processing.count.txt @@ -0,0 +1,25 @@ +-- case: status_rare_processing (count) phase: baseline selective: true +-- filters: {"payment_status":["processing"]} search_term: nil page: 1 +SELECT COUNT(*) FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'processing' + +Aggregate (cost=13588147.20..13588147.21 rows=1 width=8) (actual time=573.219..573.222 rows=1 loops=1) + Buffers: shared hit=332708 + -> Index Scan using index_payments_on_organization_id on payments (cost=0.43..13588092.14 rows=22024 width=0) (actual time=59.347..570.853 rows=44564 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'processing'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 4955436 + Buffers: shared hit=332708 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=42674) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=170696 +Planning: + Buffers: shared hit=8 +Planning Time: 0.176 ms +JIT: + Functions: 14 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 0.718 ms, Inlining 4.070 ms, Optimization 30.763 ms, Emission 24.632 ms, Total 60.183 ms +Execution Time: 574.199 ms diff --git a/script/perf/payments_filters/plans/baseline/status_rare_processing.txt b/script/perf/payments_filters/plans/baseline/status_rare_processing.txt new file mode 100644 index 00000000000..911511ccd71 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/status_rare_processing.txt @@ -0,0 +1,21 @@ +-- case: status_rare_processing (list) phase: baseline selective: true +-- filters: {"payment_status":["processing"]} search_term: nil page: 1 +SELECT "payments"."id", "payments"."invoice_id", "payments"."payment_provider_id", "payments"."payment_provider_customer_id", "payments"."amount_cents", "payments"."amount_currency", "payments"."provider_payment_id", "payments"."status", "payments"."created_at", "payments"."updated_at", "payments"."payable_type", "payments"."payable_id", "payments"."provider_payment_data", "payments"."payable_payment_status", "payments"."payment_type", "payments"."reference", "payments"."provider_payment_method_data", "payments"."provider_payment_method_id", "payments"."organization_id", "payments"."customer_id", "payments"."error_code", "payments"."payment_method_id" FROM "payments" WHERE "payments"."customer_id" IS NOT NULL AND "payments"."organization_id" = '' AND "payments"."payable_id" IS NOT NULL AND (CASE payments.payable_type WHEN 'Invoice' THEN EXISTS( SELECT 1 FROM invoices WHERE invoices.id = payments.payable_id AND invoices.status IN (0,1,2,4,7) AND organization_id = '' ) ELSE TRUE END) AND "payments"."payable_payment_status" = 'processing' ORDER BY "payments"."created_at" DESC, "payments"."id" ASC LIMIT 20 OFFSET 0 + +Limit (cost=0.56..12377.21 rows=20 width=330) (actual time=0.090..0.473 rows=20 loops=1) + Buffers: shared hit=1440 + -> Index Scan using index_payments_by_cursor on payments (cost=0.56..13629173.91 rows=22024 width=330) (actual time=0.090..0.472 rows=20 loops=1) + Index Cond: (organization_id = ''::uuid) + Filter: ((customer_id IS NOT NULL) AND (payable_id IS NOT NULL) AND (payable_payment_status = 'processing'::payment_payable_payment_status) AND CASE payable_type WHEN 'Invoice'::text THEN (SubPlan 1) ELSE true END) + Rows Removed by Filter: 1323 + Buffers: shared hit=1440 + SubPlan 1 + -> Index Scan using invoices_pkey on invoices (cost=0.43..2.66 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=21) + Index Cond: (id = payments.payable_id) + Filter: ((organization_id = ''::uuid) AND (status = ANY ('{0,1,2,4,7}'::integer[]))) + Rows Removed by Filter: 0 + Buffers: shared hit=84 +Planning: + Buffers: shared hit=8 +Planning Time: 0.098 ms +Execution Time: 0.489 ms diff --git a/script/perf/payments_filters/plans/baseline/summary.json b/script/perf/payments_filters/plans/baseline/summary.json new file mode 100644 index 00000000000..a5514e33225 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/summary.json @@ -0,0 +1,2032 @@ +{ + "phase": "baseline", + "rebuilt_from_plans": true, + "cases": [ + { + "name": "amount_common_from_p50", + "page": 1, + "selective": false, + "filters": { + "amount_from": 4914 + }, + "search_term": null, + "count": { + "ms": 7615.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 4851663, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 9483936, + "shared_read": 177896, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 58, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 117, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "amount_rare_from_p99", + "page": 1, + "selective": false, + "filters": { + "amount_from": 80467 + }, + "search_term": null, + "count": { + "ms": 626.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 97188, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 352492, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 1.9, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2738, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "amount_rare_range", + "page": 1, + "selective": false, + "filters": { + "amount_from": 80467, + "amount_to": 160934 + }, + "search_term": null, + "count": { + "ms": 558.7, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 78490, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 315860, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 8.0, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 3209, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "combo_customer_status_date", + "page": 1, + "selective": true, + "filters": { + "external_customer_id": "perf-cust-0-1", + "payment_status": [ + "succeeded" + ], + "created_at_from": "2024-09-08", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 312.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 329735, + "nodes": [ + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 520197, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 367.6, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 329774, + "nodes": [ + "Sort", + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 20, + "shared_hit": 520197, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": null + }, + "flags": [ + "SLOW" + ] + }, + { + "name": "combo_provider_status", + "page": 1, + "selective": false, + "filters": { + "payment_provider_type": [ + "stripe" + ], + "payment_status": [ + "failed" + ] + }, + "search_term": null, + "count": { + "ms": 11031.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 262043229, + "nodes": [ + "Nested Loop", + "Seq Scan", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2234768, + "shared_read": 166203, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 103, + "nodes": [ + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 456, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "combo_status_amount", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "failed" + ], + "amount_from": 4914 + }, + "search_term": null, + "count": { + "ms": 2955.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 704109, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1327419, + "shared_read": 212653, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 584, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "combo_status_currency_date", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "succeeded" + ], + "currency": "EUR", + "created_at_from": "2026-09-01", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 4.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 4138, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 10755, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 109, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [] + }, + { + "name": "control", + "page": 1, + "selective": false, + "filters": {}, + "search_term": null, + "count": { + "ms": 14064.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9702554, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18999346, + "shared_read": 162002, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 105, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "control_page50", + "page": 50, + "selective": false, + "filters": {}, + "search_term": null, + "count": { + "ms": 15895.7, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9702554, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18999998, + "shared_read": 161350, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 3.7, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 1979, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4860, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "created_24m", + "page": 1, + "selective": false, + "filters": { + "created_at_from": "2024-09-08", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 14547.4, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9700367, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18975846, + "shared_read": 181210, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 105, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "created_7d", + "page": 1, + "selective": true, + "filters": { + "created_at_from": "2026-09-01", + "created_at_to": "2026-09-08" + }, + "search_term": null, + "count": { + "ms": 10.1, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5123, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 12699, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 105, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [] + }, + { + "name": "currency_common", + "page": 1, + "selective": false, + "filters": { + "currency": "EUR" + }, + "search_term": null, + "count": { + "ms": 15144.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9217625, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18046005, + "shared_read": 165267, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 106, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "currency_rare", + "page": 1, + "selective": true, + "filters": { + "currency": "GBP" + }, + "search_term": null, + "count": { + "ms": 864.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 97248, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 352484, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 2.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2554, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "customer_heavy", + "page": 1, + "selective": true, + "filters": { + "external_customer_id": "perf-cust-0-1" + }, + "search_term": null, + "count": { + "ms": 415.7, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 398853, + "nodes": [ + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 609781, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 399.4, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 398892, + "nodes": [ + "Sort", + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 20, + "shared_hit": 609781, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": null + }, + "flags": [ + "SLOW" + ] + }, + { + "name": "customer_light", + "page": 1, + "selective": true, + "filters": { + "external_customer_id": "perf-cust-0-37952" + }, + "search_term": null, + "count": { + "ms": 1.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 160, + "nodes": [ + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 843, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 1.8, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 199, + "nodes": [ + "Sort", + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 20, + "shared_hit": 843, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": null + }, + "flags": [] + }, + { + "name": "five_filter_common", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "succeeded" + ], + "currency": "EUR", + "created_at_from": "2024-09-08", + "created_at_to": "2026-09-08", + "amount_from": 100, + "payment_provider_type": [ + "stripe" + ] + }, + "search_term": null, + "count": { + "ms": 16647.1, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 269562286, + "nodes": [ + "Nested Loop", + "Seq Scan", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 11970579, + "shared_read": 178556, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 135, + "nodes": [ + "Nested Loop", + "Index Scan", + "Memoize" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 171, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "five_filter_rare", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "failed" + ], + "currency": "GBP", + "created_at_from": "2026-09-01", + "created_at_to": "2026-09-08", + "amount_from": 4914, + "payment_provider_type": [ + "gocardless" + ] + }, + "search_term": null, + "count": { + "ms": 21.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 861748, + "nodes": [ + "Nested Loop", + "Seq Scan", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1549, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 20.8, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 861747, + "nodes": [ + "Sort", + "Nested Loop", + "Seq Scan", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1549, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": true, + "all_runs_ms": null + }, + "flags": [] + }, + { + "name": "invoice_hit_direct", + "page": 1, + "selective": true, + "filters": { + "invoice_number": "her-1556-202609-000457624" + }, + "search_term": null, + "count": { + "ms": 18048.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 15392614, + "nodes": [ + "Nested Loop Semi Join", + "Index Scan", + "Materialize", + "Gather", + "Parallel Seq Scan", + "Seq Scan" + ], + "seq_scan_watched": [ + "invoices" + ], + "sort_rows_max": 0, + "shared_hit": 19170569, + "shared_read": 227499, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 72525.3, + "timeout": false, + "rows_returned": 2, + "rows_scanned": 15392615, + "nodes": [ + "Nested Loop Semi Join", + "Index Scan", + "Materialize", + "Gather", + "Parallel Seq Scan", + "Seq Scan" + ], + "seq_scan_watched": [ + "invoices" + ], + "sort_rows_max": 0, + "shared_hit": 21201432, + "shared_read": 3070314, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "SLOW", + "COUNT>500", + "SEQ:invoices" + ] + }, + { + "name": "invoice_hit_request", + "page": 1, + "selective": true, + "filters": { + "invoice_number": "her-1556-202609-000765141" + }, + "search_term": null, + "count": { + "ms": 17075.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 15392613, + "nodes": [ + "Nested Loop Semi Join", + "Index Scan", + "Materialize", + "Gather", + "Parallel Seq Scan", + "Seq Scan" + ], + "seq_scan_watched": [ + "invoices" + ], + "sort_rows_max": 0, + "shared_hit": 19169990, + "shared_read": 228078, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 83846.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 15392613, + "nodes": [ + "Nested Loop Semi Join", + "Index Scan", + "Materialize", + "Gather", + "Parallel Seq Scan", + "Seq Scan" + ], + "seq_scan_watched": [ + "invoices" + ], + "sort_rows_max": 0, + "shared_hit": 21201392, + "shared_read": 3070354, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "SLOW", + "COUNT>500", + "SEQ:invoices" + ] + }, + { + "name": "invoice_miss", + "page": 1, + "selective": true, + "filters": { + "invoice_number": "PERF-NOPE-000000-000000001" + }, + "search_term": null, + "count": { + "ms": 16473.7, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9702554, + "nodes": [ + "Nested Loop Semi Join", + "Index Scan", + "Materialize", + "Gather", + "Parallel Seq Scan", + "Seq Scan" + ], + "seq_scan_watched": [ + "invoices" + ], + "sort_rows_max": 0, + "shared_hit": 19139517, + "shared_read": 248719, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 60503.3, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 9702553, + "nodes": [ + "Nested Loop Semi Join", + "Index Scan", + "Materialize", + "Gather", + "Parallel Seq Scan", + "Seq Scan" + ], + "seq_scan_watched": [ + "invoices" + ], + "sort_rows_max": 0, + "shared_hit": 21194858, + "shared_read": 3067056, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "SLOW", + "COUNT>500", + "SEQ:invoices" + ] + }, + { + "name": "method_common_json", + "page": 1, + "selective": false, + "filters": { + "payment_method_type": [ + "card" + ] + }, + "search_term": null, + "count": { + "ms": 14714.5, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 13815214, + "nodes": [ + "Hash Left Join", + "Index Scan", + "Hash", + "Seq Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18998944, + "shared_read": 163895, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 118, + "nodes": [ + "Nested Loop Left Join", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 227, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "method_fallback_only", + "page": 1, + "selective": true, + "filters": { + "payment_method_type": [ + "bacs_debit" + ] + }, + "search_term": null, + "count": { + "ms": 14751.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9876366, + "nodes": [ + "Hash Left Join", + "Index Scan", + "Hash", + "Seq Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18985303, + "shared_read": 177536, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 37.4, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 26713, + "nodes": [ + "Nested Loop Left Join", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 78120, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "method_multi", + "page": 1, + "selective": false, + "filters": { + "payment_method_type": [ + "card", + "crypto" + ] + }, + "search_term": null, + "count": { + "ms": 16669.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 13818716, + "nodes": [ + "Hash Left Join", + "Index Scan", + "Hash", + "Seq Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18981090, + "shared_read": 181749, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 118, + "nodes": [ + "Nested Loop Left Join", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 227, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "method_rare_json", + "page": 1, + "selective": true, + "filters": { + "payment_method_type": [ + "crypto" + ] + }, + "search_term": null, + "count": { + "ms": 19374.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9868196, + "nodes": [ + "Hash Left Join", + "Index Scan", + "Hash", + "Seq Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18982823, + "shared_read": 180016, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 145.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 106031, + "nodes": [ + "Nested Loop Left Join", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 310101, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "payable_type_invoice", + "page": 1, + "selective": false, + "filters": { + "payable_type": [ + "Invoice" + ] + }, + "search_term": null, + "count": { + "ms": 15354.1, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 20679109, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18949531, + "shared_read": 237180, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 110, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "payable_type_request", + "page": 1, + "selective": false, + "filters": { + "payable_type": [ + "PaymentRequest" + ] + }, + "search_term": null, + "count": { + "ms": 480.5, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5577999, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 129480, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 1.0, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 40, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 427, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [] + }, + { + "name": "payment_type_manual", + "page": 1, + "selective": false, + "filters": { + "payment_type": [ + "manual" + ] + }, + "search_term": null, + "count": { + "ms": 2217.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 5811951, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 928050, + "shared_read": 146952, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.7, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 743, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "payment_type_provider", + "page": 1, + "selective": false, + "filters": { + "payment_type": [ + "provider" + ] + }, + "search_term": null, + "count": { + "ms": 12469.5, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 20445157, + "nodes": [ + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 18036477, + "shared_read": 178389, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.2, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 107, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "provider_common", + "page": 1, + "selective": false, + "filters": { + "payment_provider_type": [ + "stripe" + ] + }, + "search_term": null, + "count": { + "ms": 23420.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 272069154, + "nodes": [ + "Nested Loop", + "Seq Scan", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 15222443, + "shared_read": 177244, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 135, + "nodes": [ + "Nested Loop", + "Index Scan", + "Memoize" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 165, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "provider_miss", + "page": 1, + "selective": true, + "filters": { + "payment_provider_type": [ + "cashfree" + ] + }, + "search_term": null, + "count": { + "ms": 5.7, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 1, + "nodes": [ + "Nested Loop", + "Seq Scan", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 5, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 66537.5, + "timeout": false, + "rows_returned": 0, + "rows_scanned": 9702553, + "nodes": [ + "Nested Loop", + "Index Scan", + "Materialize", + "Seq Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 21097387, + "shared_read": 2937644, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "SLOW" + ] + }, + { + "name": "provider_rare", + "page": 1, + "selective": false, + "filters": { + "payment_provider_type": [ + "gocardless" + ] + }, + "search_term": null, + "count": { + "ms": 9733.1, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 88014564, + "nodes": [ + "Nested Loop", + "Seq Scan", + "Bitmap Heap Scan", + "BitmapAnd", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2817569, + "shared_read": 199552, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 246, + "nodes": [ + "Nested Loop", + "Index Scan", + "Memoize" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 512, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "receipt_hit", + "page": 1, + "selective": true, + "filters": { + "receipt_number": "her-1556-335-rcpt-000123" + }, + "search_term": null, + "count": { + "ms": 282.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 12, + "nodes": [ + "Nested Loop", + "Gather", + "Parallel Seq Scan", + "Index Scan" + ], + "seq_scan_watched": [ + "payment_receipts" + ], + "sort_rows_max": 0, + "shared_hit": 60825, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 105274.1, + "timeout": false, + "rows_returned": 2, + "rows_scanned": 9702557, + "nodes": [ + "Nested Loop", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 36993897, + "shared_read": 4564673, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "SLOW", + "SEQ:payment_receipts" + ] + }, + { + "name": "receipt_miss", + "page": 1, + "selective": true, + "filters": { + "receipt_number": "PERF-NOPE-RCPT-000001" + }, + "search_term": null, + "count": { + "ms": 239.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 1, + "nodes": [ + "Nested Loop", + "Gather", + "Parallel Seq Scan", + "Index Scan" + ], + "seq_scan_watched": [ + "payment_receipts" + ], + "sort_rows_max": 0, + "shared_hit": 60809, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": null, + "timeout": true, + "rows_returned": 0, + "rows_scanned": 0, + "nodes": [], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": null, + "shared_read": null, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "SLOW", + "SEQ:payment_receipts", + "TIMEOUT" + ] + }, + { + "name": "search_term", + "page": 1, + "selective": true, + "filters": {}, + "search_term": "_0_4031686", + "count": { + "ms": 52.8, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8, + "nodes": [ + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4555, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 56.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9, + "nodes": [ + "Sort", + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 1, + "shared_hit": 4555, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": null + }, + "flags": [] + }, + { + "name": "search_term_status", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "succeeded" + ] + }, + "search_term": "_0_4031686", + "count": { + "ms": 50.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8, + "nodes": [ + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4555, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 49.6, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 9, + "nodes": [ + "Sort", + "Nested Loop", + "HashAggregate", + "Append", + "Bitmap Heap Scan", + "Bitmap Index Scan", + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 1, + "shared_hit": 4555, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": true, + "all_runs_ms": null + }, + "flags": [] + }, + { + "name": "status_common_succeeded", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "succeeded" + ] + }, + "search_term": null, + "count": { + "ms": 11376.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8033558, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 15720820, + "shared_read": 172848, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.1, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 108, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_common_succeeded_page50", + "page": 50, + "selective": false, + "filters": { + "payment_status": [ + "succeeded" + ] + }, + "search_term": null, + "count": { + "ms": 12762.0, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 8033558, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 15709835, + "shared_read": 183833, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 2.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 1971, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 4980, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_rare_failed", + "page": 1, + "selective": false, + "filters": { + "payment_status": [ + "failed" + ] + }, + "search_term": null, + "count": { + "ms": 3411.5, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 1407053, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 2691336, + "shared_read": 225540, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 409, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_rare_pending", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "pending" + ] + }, + "search_term": null, + "count": { + "ms": 831.9, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 174707, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 412761, + "shared_read": 91371, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 60, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1156, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_rare_pending_processing", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "pending", + "processing" + ] + }, + "search_term": null, + "count": { + "ms": 1481.3, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 261945, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 570633, + "shared_read": 104195, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.3, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 770, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + }, + { + "name": "status_rare_processing", + "page": 1, + "selective": true, + "filters": { + "payment_status": [ + "processing" + ] + }, + "search_term": null, + "count": { + "ms": 574.2, + "timeout": false, + "rows_returned": 1, + "rows_scanned": 87239, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 332708, + "shared_read": 0, + "cursor_index": false, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "list": { + "ms": 0.5, + "timeout": false, + "rows_returned": 20, + "rows_scanned": 61, + "nodes": [ + "Index Scan" + ], + "seq_scan_watched": [], + "sort_rows_max": 0, + "shared_hit": 1440, + "shared_read": 0, + "cursor_index": true, + "sort_on_created_at": false, + "all_runs_ms": null + }, + "flags": [ + "COUNT>500" + ] + } + ] +} \ No newline at end of file diff --git a/script/perf/payments_filters/plans/baseline/summary.md b/script/perf/payments_filters/plans/baseline/summary.md new file mode 100644 index 00000000000..e5b31d81ca5 --- /dev/null +++ b/script/perf/payments_filters/plans/baseline/summary.md @@ -0,0 +1,47 @@ +# Plans: baseline + +Median of n (see plan headers) EXPLAIN (ANALYZE, BUFFERS) runs per statement. Synthetic dataset. `ms` is nil when the statement hit the timeout. + +| case | page | selective | list ms | count ms | rows | list nodes | seq scan (watched) | max sort rows | shared read (list) | ordering by cursor index | flags | +|---|---|---|---|---|---|---|---|---|---|---|---| +| amount_common_from_p50 | 1 | false | 0.2 | 7615.8 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| amount_rare_from_p99 | 1 | false | 1.9 | 626.6 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| amount_rare_range | 1 | false | 8.0 | 558.7 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| combo_customer_status_date | 1 | true | 367.6 | 312.0 | 20 | Sort, Nested Loop, Index Scan | | 20 | 0 | false | SLOW | +| combo_provider_status | 1 | false | 0.2 | 11031.9 | 20 | Nested Loop, Index Scan | | 0 | 0 | true | COUNT>500 | +| combo_status_amount | 1 | false | 0.2 | 2955.6 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| combo_status_currency_date | 1 | true | 0.1 | 4.6 | 20 | Index Scan | | 0 | 0 | true | | +| control | 1 | false | 0.1 | 14064.9 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| control_page50 | 50 | false | 3.7 | 15895.7 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| created_24m | 1 | false | 0.1 | 14547.4 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| created_7d | 1 | true | 0.1 | 10.1 | 20 | Index Scan | | 0 | 0 | true | | +| currency_common | 1 | false | 0.1 | 15144.6 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| currency_rare | 1 | true | 2.5 | 864.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| customer_heavy | 1 | true | 399.4 | 415.7 | 20 | Sort, Nested Loop, Index Scan | | 20 | 0 | false | SLOW | +| customer_light | 1 | true | 1.8 | 1.6 | 20 | Sort, Nested Loop, Index Scan | | 20 | 0 | false | | +| five_filter_common | 1 | false | 0.1 | 16647.1 | 20 | Nested Loop, Index Scan, Memoize | | 0 | 0 | true | COUNT>500 | +| five_filter_rare | 1 | true | 20.8 | 21.0 | 0 | Sort, Nested Loop, Seq Scan, Bitmap Heap Scan, BitmapAnd, Bitmap Index Scan, Index Scan | | 0 | 0 | false | | +| invoice_hit_direct | 1 | true | 72525.3 | 18048.9 | 2 | Nested Loop Semi Join, Index Scan, Materialize, Gather, Parallel Seq Scan, Seq Scan | invoices | 0 | 3070314 | true | SLOW COUNT>500 SEQ:invoices | +| invoice_hit_request | 1 | true | 83846.9 | 17075.0 | 1 | Nested Loop Semi Join, Index Scan, Materialize, Gather, Parallel Seq Scan, Seq Scan | invoices | 0 | 3070354 | true | SLOW COUNT>500 SEQ:invoices | +| invoice_miss | 1 | true | 60503.3 | 16473.7 | 0 | Nested Loop Semi Join, Index Scan, Materialize, Gather, Parallel Seq Scan, Seq Scan | invoices | 0 | 3067056 | true | SLOW COUNT>500 SEQ:invoices | +| method_common_json | 1 | false | 0.2 | 14714.5 | 20 | Nested Loop Left Join, Index Scan | | 0 | 0 | true | COUNT>500 | +| method_fallback_only | 1 | true | 37.4 | 14751.8 | 20 | Nested Loop Left Join, Index Scan | | 0 | 0 | true | COUNT>500 | +| method_multi | 1 | false | 0.1 | 16669.8 | 20 | Nested Loop Left Join, Index Scan | | 0 | 0 | true | COUNT>500 | +| method_rare_json | 1 | true | 145.1 | 19374.8 | 20 | Nested Loop Left Join, Index Scan | | 0 | 0 | true | COUNT>500 | +| payable_type_invoice | 1 | false | 0.1 | 15354.1 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| payable_type_request | 1 | false | 1.0 | 480.5 | 20 | Index Scan | | 0 | 0 | true | | +| payment_type_manual | 1 | false | 0.7 | 2217.9 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| payment_type_provider | 1 | false | 0.2 | 12469.5 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| provider_common | 1 | false | 0.3 | 23420.6 | 20 | Nested Loop, Index Scan, Memoize | | 0 | 0 | true | COUNT>500 | +| provider_miss | 1 | true | 66537.5 | 5.7 | 0 | Nested Loop, Index Scan, Materialize, Seq Scan | | 0 | 2937644 | true | SLOW | +| provider_rare | 1 | false | 0.5 | 9733.1 | 20 | Nested Loop, Index Scan, Memoize | | 0 | 0 | true | COUNT>500 | +| receipt_hit | 1 | true | 105274.1 | 282.9 | 2 | Nested Loop, Index Scan | payment_receipts | 0 | 4564673 | true | SLOW SEQ:payment_receipts | +| receipt_miss | 1 | true | timeout | 239.3 | 0 | | payment_receipts | 0 | | false | SLOW SEQ:payment_receipts TIMEOUT | +| search_term | 1 | true | 56.0 | 52.8 | 1 | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | 1 | 0 | false | | +| search_term_status | 1 | true | 49.6 | 50.6 | 1 | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | 1 | 0 | false | | +| status_common_succeeded | 1 | false | 0.1 | 11376.0 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_common_succeeded_page50 | 50 | false | 2.3 | 12762.0 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_rare_failed | 1 | false | 0.3 | 3411.5 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_rare_pending | 1 | true | 0.5 | 831.9 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_rare_pending_processing | 1 | true | 0.3 | 1481.3 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | +| status_rare_processing | 1 | true | 0.5 | 574.2 | 20 | Index Scan | | 0 | 0 | true | COUNT>500 | diff --git a/script/perf/payments_filters/summarize.rb b/script/perf/payments_filters/summarize.rb new file mode 100644 index 00000000000..c6162704add --- /dev/null +++ b/script/perf/payments_filters/summarize.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Rebuilds plans//summary.json and summary.md from the saved plan files. +# Plain Ruby, no Rails: ruby script/perf/payments_filters/summarize.rb +# Useful when explain.rb was interrupted, or to re-derive the tables from the +# committed plans. + +require "json" +require_relative "plan_stats" + +phase = ARGV.first or abort "usage: ruby summarize.rb " +dir = File.join(__dir__, "plans", phase) +files = Dir[File.join(dir, "*.txt")] +abort "no plans in #{dir}" if files.empty? + +entries = {} +files.sort.each do |file| + text = File.read(file) + header = text[/\A-- case: (\S+) \((\w+)\) phase: \S+(?: selective: (\w+))?\n-- filters: (.*) search_term: (.*) page: (\d+)\n(?:-- runs_ms: (.*)\n)?/] + next unless header + name, kind, selective, filters, search_term, page, runs = Regexp.last_match.captures + plan = text.split("\n\n", 2).last + entry = (entries[name] ||= {name:, page: page.to_i, selective: selective == "true", filters: JSON.parse(filters), search_term: (search_term == "nil") ? nil : search_term.delete('"')}) + stats = PaymentsFiltersPerf::PlanStats.analyse(plan) + stats[:all_runs_ms] = begin + runs && JSON.parse(runs) + rescue + nil + end + entry[kind.to_sym] = stats +end + +summary = entries.values.select { |e| e[:list] && e[:count] } +summary.each { |e| e[:flags] = PaymentsFiltersPerf::PlanStats.flags(e[:list], e[:count]) } +variants = summary.any? { |e| e[:count_capped] } +File.write(File.join(dir, "summary.json"), JSON.pretty_generate({phase:, rebuilt_from_plans: true, cases: summary})) +File.write(File.join(dir, "summary.md"), PaymentsFiltersPerf::PlanStats.summary_markdown(phase, summary, runs: "n (see plan headers)", variants:)) +puts "#{summary.size} cases -> #{dir}/summary.{json,md}" +summary.each { |e| puts format("%-34s list %10s ms count %10s ms %s", e[:name], e[:list][:ms] || "timeout", e[:count][:ms] || "timeout", e[:flags].join(" ")) } From 2421001cbe063e39c4511b847c6c5f6bc943b8c8 Mon Sep 17 00:00:00 2001 From: Raffi Date: Tue, 8 Sep 2026 21:52:01 -0700 Subject: [PATCH 7/9] perf(payments): record after-phase measurements for the list filters Plans, DB-only and HTTP load-test results captured on the synthetic dataset after the rewrites and indexes, plus the before/after comparison. --- .../payments_filters/bench/after.http.json | 549 +++++++++++++++++ .../perf/payments_filters/bench/after.http.md | 43 ++ .../payments_filters/bench/after.sql.json | 552 ++++++++++++++++++ .../perf/payments_filters/bench/after.sql.md | 43 ++ .../bench/after_1client.http.json | 528 +++++++++++++++++ .../bench/after_1client.http.md | 43 ++ .../compare/baseline_vs_after.md | 68 +-- 7 files changed, 1792 insertions(+), 34 deletions(-) create mode 100644 script/perf/payments_filters/bench/after.http.json create mode 100644 script/perf/payments_filters/bench/after.http.md create mode 100644 script/perf/payments_filters/bench/after.sql.json create mode 100644 script/perf/payments_filters/bench/after.sql.md create mode 100644 script/perf/payments_filters/bench/after_1client.http.json create mode 100644 script/perf/payments_filters/bench/after_1client.http.md diff --git a/script/perf/payments_filters/bench/after.http.json b/script/perf/payments_filters/bench/after.http.json new file mode 100644 index 00000000000..16a0eeaa35f --- /dev/null +++ b/script/perf/payments_filters/bench/after.http.json @@ -0,0 +1,549 @@ +{ + "phase": "after", + "mode": "http", + "clients": 20, + "duration_s": 30.0, + "generated_at": "2026-09-09T04:26:08Z", + "target": "http://localhost:3000", + "cases": [ + { + "name": "control", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "control_page50", + "selective": false, + "page": 50, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 48240.9, + "p95_ms": 48240.9, + "p99_ms": 48240.9, + "max_ms": 48240.9, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "status_common_succeeded", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "status_common_succeeded_page50", + "selective": false, + "page": 50, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 46098.2, + "p95_ms": 46098.2, + "p99_ms": 46098.2, + "max_ms": 46098.2, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "status_rare_failed", + "selective": false, + "page": 1, + "requests": 39, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 18238.5, + "p95_ms": 33977.9, + "p99_ms": 35132.2, + "max_ms": 35132.2, + "error_samples": [] + }, + { + "name": "status_rare_pending", + "selective": true, + "page": 1, + "requests": 244, + "rps": 8.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2235.8, + "p95_ms": 5887.5, + "p99_ms": 5921.7, + "max_ms": 5924.1, + "error_samples": [] + }, + { + "name": "status_rare_processing", + "selective": true, + "page": 1, + "requests": 294, + "rps": 9.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1855.6, + "p95_ms": 4431.9, + "p99_ms": 4466.9, + "max_ms": 4470.0, + "error_samples": [] + }, + { + "name": "status_rare_pending_processing", + "selective": true, + "page": 1, + "requests": 252, + "rps": 8.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2513.1, + "p95_ms": 3246.7, + "p99_ms": 3713.5, + "max_ms": 5027.2, + "error_samples": [] + }, + { + "name": "amount_common_from_p50", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 31208.0, + "p95_ms": 31256.3, + "p99_ms": 31257.5, + "max_ms": 31257.5, + "error_samples": [] + }, + { + "name": "amount_rare_from_p99", + "selective": false, + "page": 1, + "requests": 209, + "rps": 7.0, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2567.7, + "p95_ms": 6089.1, + "p99_ms": 6127.1, + "max_ms": 8259.8, + "error_samples": [] + }, + { + "name": "amount_rare_range", + "selective": false, + "page": 1, + "requests": 263, + "rps": 8.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2333.8, + "p95_ms": 3102.6, + "p99_ms": 3985.1, + "max_ms": 4577.7, + "error_samples": [] + }, + { + "name": "created_7d", + "selective": true, + "page": 1, + "requests": 341, + "rps": 11.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1768.6, + "p95_ms": 2435.5, + "p99_ms": 2812.2, + "max_ms": 3088.1, + "error_samples": [] + }, + { + "name": "created_24m", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "currency_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "currency_rare", + "selective": true, + "page": 1, + "requests": 206, + "rps": 6.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2667.8, + "p95_ms": 5648.0, + "p99_ms": 5684.3, + "max_ms": 5689.5, + "error_samples": [] + }, + { + "name": "provider_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "provider_rare", + "selective": true, + "page": 1, + "requests": 39, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 18893.8, + "p95_ms": 32078.4, + "p99_ms": 34350.3, + "max_ms": 34350.3, + "error_samples": [] + }, + { + "name": "provider_miss", + "selective": true, + "page": 1, + "requests": 392, + "rps": 13.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1515.6, + "p95_ms": 1962.6, + "p99_ms": 2572.6, + "max_ms": 2952.8, + "error_samples": [] + }, + { + "name": "receipt_hit", + "selective": true, + "page": 1, + "requests": 384, + "rps": 12.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1554.9, + "p95_ms": 1898.9, + "p99_ms": 2462.3, + "max_ms": 2847.2, + "error_samples": [] + }, + { + "name": "receipt_miss", + "selective": true, + "page": 1, + "requests": 359, + "rps": 12.0, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1560.7, + "p95_ms": 2649.9, + "p99_ms": 3830.3, + "max_ms": 4331.2, + "error_samples": [] + }, + { + "name": "invoice_hit_direct", + "selective": true, + "page": 1, + "requests": 378, + "rps": 12.6, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1552.0, + "p95_ms": 2095.4, + "p99_ms": 2566.2, + "max_ms": 2996.9, + "error_samples": [] + }, + { + "name": "invoice_hit_request", + "selective": true, + "page": 1, + "requests": 384, + "rps": 12.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1553.9, + "p95_ms": 2018.9, + "p99_ms": 2267.7, + "max_ms": 2434.8, + "error_samples": [] + }, + { + "name": "invoice_miss", + "selective": true, + "page": 1, + "requests": 404, + "rps": 13.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1499.2, + "p95_ms": 1810.9, + "p99_ms": 2447.0, + "max_ms": 2734.4, + "error_samples": [] + }, + { + "name": "customer_heavy", + "selective": true, + "page": 1, + "requests": 215, + "rps": 7.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2641.9, + "p95_ms": 4289.4, + "p99_ms": 5381.0, + "max_ms": 6165.6, + "error_samples": [] + }, + { + "name": "customer_light", + "selective": true, + "page": 1, + "requests": 352, + "rps": 11.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1723.1, + "p95_ms": 2112.5, + "p99_ms": 2458.0, + "max_ms": 2774.7, + "error_samples": [] + }, + { + "name": "payment_type_manual", + "selective": false, + "page": 1, + "requests": 56, + "rps": 1.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 11503.1, + "p95_ms": 22119.8, + "p99_ms": 22122.4, + "max_ms": 22122.4, + "error_samples": [] + }, + { + "name": "payment_type_provider", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 19, + "error_rate": 0.95, + "p50_ms": 48484.3, + "p95_ms": 48484.3, + "p99_ms": 48484.3, + "max_ms": 48484.3, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "payable_type_request", + "selective": false, + "page": 1, + "requests": 248, + "rps": 8.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2274.6, + "p95_ms": 4251.5, + "p99_ms": 4578.1, + "max_ms": 4613.8, + "error_samples": [] + }, + { + "name": "payable_type_invoice", + "selective": false, + "page": 1, + "requests": 40, + "rps": 1.3, + "errors": 40, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::ConnectionFailed:\\\"PQconsumeInput() serv", + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "search_term", + "selective": true, + "page": 1, + "requests": 362, + "rps": 12.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1639.2, + "p95_ms": 2517.3, + "p99_ms": 3282.0, + "max_ms": 3528.1, + "error_samples": [] + }, + { + "name": "search_term_status", + "selective": true, + "page": 1, + "requests": 363, + "rps": 12.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1622.4, + "p95_ms": 2052.3, + "p99_ms": 2977.3, + "max_ms": 3164.3, + "error_samples": [] + }, + { + "name": "combo_status_currency_date", + "selective": true, + "page": 1, + "requests": 347, + "rps": 11.6, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1745.4, + "p95_ms": 2168.6, + "p99_ms": 3039.4, + "max_ms": 3281.9, + "error_samples": [] + }, + { + "name": "combo_status_amount", + "selective": false, + "page": 1, + "requests": 55, + "rps": 1.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 14607.1, + "p95_ms": 28787.9, + "p99_ms": 28790.7, + "max_ms": 28790.7, + "error_samples": [] + }, + { + "name": "combo_customer_status_date", + "selective": true, + "page": 1, + "requests": 246, + "rps": 8.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 2335.7, + "p95_ms": 3668.6, + "p99_ms": 4595.2, + "max_ms": 6103.9, + "error_samples": [] + }, + { + "name": "combo_provider_status", + "selective": false, + "page": 1, + "requests": 40, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 18771.0, + "p95_ms": 29351.3, + "p99_ms": 29357.7, + "max_ms": 29357.7, + "error_samples": [] + }, + { + "name": "five_filter_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 0.7, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "HTTP 500: {\"status\":500,\"error\":\"Internal Server Error\",\"exception\":\"#\\u003cActiveRecord::QueryCanceled:\\\"PG::QueryCanceled: ERROR" + ] + }, + { + "name": "five_filter_rare", + "selective": true, + "page": 1, + "requests": 384, + "rps": 12.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1561.0, + "p95_ms": 1968.2, + "p99_ms": 2674.6, + "max_ms": 2964.3, + "error_samples": [] + } + ] +} \ No newline at end of file diff --git a/script/perf/payments_filters/bench/after.http.md b/script/perf/payments_filters/bench/after.http.md new file mode 100644 index 00000000000..35517014cd4 --- /dev/null +++ b/script/perf/payments_filters/bench/after.http.md @@ -0,0 +1,43 @@ +# Load test: after (http) + +20 concurrent clients, 30s per case, synthetic dataset. + +| case | selective | requests | req/s | p50 ms | p95 ms | p99 ms | max ms | errors | +|---|---|---|---|---|---|---|---|---| +| control | false | 20 | 0.7 | | | | | 20 | +| control_page50 | false | 20 | 0.7 | 48240.9 | 48240.9 | 48240.9 | 48240.9 | 19 | +| status_common_succeeded | false | 20 | 0.7 | | | | | 20 | +| status_common_succeeded_page50 | false | 20 | 0.7 | 46098.2 | 46098.2 | 46098.2 | 46098.2 | 19 | +| status_rare_failed | false | 39 | 1.3 | 18238.5 | 33977.9 | 35132.2 | 35132.2 | 0 | +| status_rare_pending | true | 244 | 8.1 | 2235.8 | 5887.5 | 5921.7 | 5924.1 | 0 | +| status_rare_processing | true | 294 | 9.8 | 1855.6 | 4431.9 | 4466.9 | 4470.0 | 0 | +| status_rare_pending_processing | true | 252 | 8.4 | 2513.1 | 3246.7 | 3713.5 | 5027.2 | 0 | +| amount_common_from_p50 | false | 20 | 0.7 | 31208.0 | 31256.3 | 31257.5 | 31257.5 | 0 | +| amount_rare_from_p99 | false | 209 | 7.0 | 2567.7 | 6089.1 | 6127.1 | 8259.8 | 0 | +| amount_rare_range | false | 263 | 8.8 | 2333.8 | 3102.6 | 3985.1 | 4577.7 | 0 | +| created_7d | true | 341 | 11.4 | 1768.6 | 2435.5 | 2812.2 | 3088.1 | 0 | +| created_24m | false | 20 | 0.7 | | | | | 20 | +| currency_common | false | 20 | 0.7 | | | | | 20 | +| currency_rare | true | 206 | 6.9 | 2667.8 | 5648.0 | 5684.3 | 5689.5 | 0 | +| provider_common | false | 20 | 0.7 | | | | | 20 | +| provider_rare | true | 39 | 1.3 | 18893.8 | 32078.4 | 34350.3 | 34350.3 | 0 | +| provider_miss | true | 392 | 13.1 | 1515.6 | 1962.6 | 2572.6 | 2952.8 | 0 | +| receipt_hit | true | 384 | 12.8 | 1554.9 | 1898.9 | 2462.3 | 2847.2 | 0 | +| receipt_miss | true | 359 | 12.0 | 1560.7 | 2649.9 | 3830.3 | 4331.2 | 0 | +| invoice_hit_direct | true | 378 | 12.6 | 1552.0 | 2095.4 | 2566.2 | 2996.9 | 0 | +| invoice_hit_request | true | 384 | 12.8 | 1553.9 | 2018.9 | 2267.7 | 2434.8 | 0 | +| invoice_miss | true | 404 | 13.5 | 1499.2 | 1810.9 | 2447.0 | 2734.4 | 0 | +| customer_heavy | true | 215 | 7.2 | 2641.9 | 4289.4 | 5381.0 | 6165.6 | 0 | +| customer_light | true | 352 | 11.7 | 1723.1 | 2112.5 | 2458.0 | 2774.7 | 0 | +| payment_type_manual | false | 56 | 1.9 | 11503.1 | 22119.8 | 22122.4 | 22122.4 | 0 | +| payment_type_provider | false | 20 | 0.7 | 48484.3 | 48484.3 | 48484.3 | 48484.3 | 19 | +| payable_type_request | false | 248 | 8.3 | 2274.6 | 4251.5 | 4578.1 | 4613.8 | 0 | +| payable_type_invoice | false | 40 | 1.3 | | | | | 40 | +| search_term | true | 362 | 12.1 | 1639.2 | 2517.3 | 3282.0 | 3528.1 | 0 | +| search_term_status | true | 363 | 12.1 | 1622.4 | 2052.3 | 2977.3 | 3164.3 | 0 | +| combo_status_currency_date | true | 347 | 11.6 | 1745.4 | 2168.6 | 3039.4 | 3281.9 | 0 | +| combo_status_amount | false | 55 | 1.8 | 14607.1 | 28787.9 | 28790.7 | 28790.7 | 0 | +| combo_customer_status_date | true | 246 | 8.2 | 2335.7 | 3668.6 | 4595.2 | 6103.9 | 0 | +| combo_provider_status | false | 40 | 1.3 | 18771.0 | 29351.3 | 29357.7 | 29357.7 | 0 | +| five_filter_common | false | 20 | 0.7 | | | | | 20 | +| five_filter_rare | true | 384 | 12.8 | 1561.0 | 1968.2 | 2674.6 | 2964.3 | 0 | diff --git a/script/perf/payments_filters/bench/after.sql.json b/script/perf/payments_filters/bench/after.sql.json new file mode 100644 index 00000000000..805e5ec9cfe --- /dev/null +++ b/script/perf/payments_filters/bench/after.sql.json @@ -0,0 +1,552 @@ +{ + "phase": "after", + "mode": "sql", + "clients": 20, + "duration_s": 15.0, + "generated_at": "2026-09-09T04:39:15Z", + "target": "sql", + "cases": [ + { + "name": "control", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "control_page50", + "selective": false, + "page": 50, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "status_common_succeeded", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "status_common_succeeded_page50", + "selective": false, + "page": 50, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "status_rare_failed", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 17941.8, + "p95_ms": 17946.1, + "p99_ms": 17950.3, + "max_ms": 17950.3, + "error_samples": [] + }, + { + "name": "status_rare_pending", + "selective": true, + "page": 1, + "requests": 235, + "rps": 15.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 785.9, + "p95_ms": 6378.9, + "p99_ms": 6384.0, + "max_ms": 6384.9, + "error_samples": [] + }, + { + "name": "status_rare_processing", + "selective": true, + "page": 1, + "requests": 596, + "rps": 39.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 380.5, + "p95_ms": 700.8, + "p99_ms": 3006.5, + "max_ms": 3007.7, + "error_samples": [] + }, + { + "name": "status_rare_pending_processing", + "selective": true, + "page": 1, + "requests": 246, + "rps": 16.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1091.1, + "p95_ms": 1860.5, + "p99_ms": 2234.3, + "max_ms": 2262.0, + "error_samples": [] + }, + { + "name": "amount_common_from_p50", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "amount_rare_from_p99", + "selective": false, + "page": 1, + "requests": 189, + "rps": 12.6, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1254.2, + "p95_ms": 4979.8, + "p99_ms": 4985.2, + "max_ms": 4985.6, + "error_samples": [] + }, + { + "name": "amount_rare_range", + "selective": false, + "page": 1, + "requests": 266, + "rps": 17.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1053.4, + "p95_ms": 1805.9, + "p99_ms": 2057.6, + "max_ms": 2231.5, + "error_samples": [] + }, + { + "name": "created_7d", + "selective": true, + "page": 1, + "requests": 12513, + "rps": 834.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 23.1, + "p95_ms": 32.4, + "p99_ms": 39.2, + "max_ms": 222.0, + "error_samples": [] + }, + { + "name": "created_24m", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "currency_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "currency_rare", + "selective": true, + "page": 1, + "requests": 196, + "rps": 13.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1229.2, + "p95_ms": 4373.1, + "p99_ms": 4378.3, + "max_ms": 4378.8, + "error_samples": [] + }, + { + "name": "provider_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "provider_rare", + "selective": false, + "page": 1, + "requests": 40, + "rps": 2.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 14290.5, + "p95_ms": 20420.1, + "p99_ms": 20420.7, + "max_ms": 20420.7, + "error_samples": [] + }, + { + "name": "provider_miss", + "selective": true, + "page": 1, + "requests": 28266, + "rps": 1884.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10.4, + "p95_ms": 11.8, + "p99_ms": 14.5, + "max_ms": 24.1, + "error_samples": [] + }, + { + "name": "receipt_hit", + "selective": true, + "page": 1, + "requests": 26399, + "rps": 1759.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 11.0, + "p95_ms": 14.2, + "p99_ms": 18.1, + "max_ms": 34.6, + "error_samples": [] + }, + { + "name": "receipt_miss", + "selective": true, + "page": 1, + "requests": 26969, + "rps": 1797.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10.9, + "p95_ms": 12.6, + "p99_ms": 15.3, + "max_ms": 35.7, + "error_samples": [] + }, + { + "name": "invoice_hit_direct", + "selective": true, + "page": 1, + "requests": 27083, + "rps": 1805.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10.7, + "p95_ms": 13.7, + "p99_ms": 16.6, + "max_ms": 31.6, + "error_samples": [] + }, + { + "name": "invoice_hit_request", + "selective": true, + "page": 1, + "requests": 27472, + "rps": 1831.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10.7, + "p95_ms": 12.4, + "p99_ms": 15.2, + "max_ms": 34.5, + "error_samples": [] + }, + { + "name": "invoice_miss", + "selective": true, + "page": 1, + "requests": 27913, + "rps": 1860.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10.5, + "p95_ms": 12.1, + "p99_ms": 14.8, + "max_ms": 35.2, + "error_samples": [] + }, + { + "name": "customer_heavy", + "selective": true, + "page": 1, + "requests": 207, + "rps": 13.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1309.8, + "p95_ms": 3061.0, + "p99_ms": 3090.6, + "max_ms": 3102.2, + "error_samples": [] + }, + { + "name": "customer_light", + "selective": true, + "page": 1, + "requests": 27024, + "rps": 1801.6, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10.6, + "p95_ms": 14.0, + "p99_ms": 16.4, + "max_ms": 30.3, + "error_samples": [] + }, + { + "name": "payment_type_manual", + "selective": false, + "page": 1, + "requests": 40, + "rps": 2.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10143.4, + "p95_ms": 12971.9, + "p99_ms": 12973.6, + "max_ms": 12973.6, + "error_samples": [] + }, + { + "name": "payment_type_provider", + "selective": false, + "page": 1, + "requests": 84, + "rps": 5.6, + "errors": 84, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::ConnectionFailed: PQconsumeInput() server closed the connection unexpectedly\n\tThis probably means the server terminated abnormally\n\tbefore", + "ActiveRecord::ConnectionNotEstablished: connection to server at \"172.21.0.3\", port 5432 failed: FATAL: the database system is in recovery mode\n", + "ActiveRecord::ConnectionNotEstablished: connection to server at \"172.21.0.3\", port 5432 failed: FATAL: the database system is not yet accepting connections\nDET" + ] + }, + { + "name": "payable_type_request", + "selective": false, + "page": 1, + "requests": 289, + "rps": 19.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 911.9, + "p95_ms": 2230.6, + "p99_ms": 3097.7, + "max_ms": 3332.7, + "error_samples": [] + }, + { + "name": "payable_type_invoice", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "search_term", + "selective": true, + "page": 1, + "requests": 1009, + "rps": 67.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 245.6, + "p95_ms": 551.6, + "p99_ms": 650.0, + "max_ms": 785.0, + "error_samples": [] + }, + { + "name": "search_term_status", + "selective": true, + "page": 1, + "requests": 1034, + "rps": 68.9, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 231.9, + "p95_ms": 546.5, + "p99_ms": 664.5, + "max_ms": 762.7, + "error_samples": [] + }, + { + "name": "combo_status_currency_date", + "selective": true, + "page": 1, + "requests": 14792, + "rps": 986.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 19.6, + "p95_ms": 27.0, + "p99_ms": 31.5, + "max_ms": 227.7, + "error_samples": [] + }, + { + "name": "combo_status_amount", + "selective": false, + "page": 1, + "requests": 40, + "rps": 2.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 12505.7, + "p95_ms": 14061.5, + "p99_ms": 14062.4, + "max_ms": 14062.4, + "error_samples": [] + }, + { + "name": "combo_customer_status_date", + "selective": true, + "page": 1, + "requests": 259, + "rps": 17.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 1077.2, + "p95_ms": 1811.1, + "p99_ms": 2029.4, + "max_ms": 2194.4, + "error_samples": [] + }, + { + "name": "combo_provider_status", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 17969.2, + "p95_ms": 17971.2, + "p99_ms": 17971.5, + "max_ms": 17971.5, + "error_samples": [] + }, + { + "name": "five_filter_common", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 20, + "error_rate": 1.0, + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "max_ms": null, + "error_samples": [ + "ActiveRecord::QueryCanceled: PG::QueryCanceled: ERROR: canceling statement due to statement timeout\n" + ] + }, + { + "name": "five_filter_rare", + "selective": true, + "page": 1, + "requests": 2636, + "rps": 175.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 105.0, + "p95_ms": 193.2, + "p99_ms": 245.7, + "max_ms": 435.3, + "error_samples": [] + } + ] +} \ No newline at end of file diff --git a/script/perf/payments_filters/bench/after.sql.md b/script/perf/payments_filters/bench/after.sql.md new file mode 100644 index 00000000000..23d42b4d5fe --- /dev/null +++ b/script/perf/payments_filters/bench/after.sql.md @@ -0,0 +1,43 @@ +# Load test: after (sql) + +20 concurrent clients, 15s per case, synthetic dataset. + +| case | selective | requests | req/s | p50 ms | p95 ms | p99 ms | max ms | errors | +|---|---|---|---|---|---|---|---|---| +| control | false | 20 | 1.3 | | | | | 20 | +| control_page50 | false | 20 | 1.3 | | | | | 20 | +| status_common_succeeded | false | 20 | 1.3 | | | | | 20 | +| status_common_succeeded_page50 | false | 20 | 1.3 | | | | | 20 | +| status_rare_failed | false | 20 | 1.3 | 17941.8 | 17946.1 | 17950.3 | 17950.3 | 0 | +| status_rare_pending | true | 235 | 15.7 | 785.9 | 6378.9 | 6384.0 | 6384.9 | 0 | +| status_rare_processing | true | 596 | 39.7 | 380.5 | 700.8 | 3006.5 | 3007.7 | 0 | +| status_rare_pending_processing | true | 246 | 16.4 | 1091.1 | 1860.5 | 2234.3 | 2262.0 | 0 | +| amount_common_from_p50 | false | 20 | 1.3 | | | | | 20 | +| amount_rare_from_p99 | false | 189 | 12.6 | 1254.2 | 4979.8 | 4985.2 | 4985.6 | 0 | +| amount_rare_range | false | 266 | 17.7 | 1053.4 | 1805.9 | 2057.6 | 2231.5 | 0 | +| created_7d | true | 12513 | 834.2 | 23.1 | 32.4 | 39.2 | 222.0 | 0 | +| created_24m | false | 20 | 1.3 | | | | | 20 | +| currency_common | false | 20 | 1.3 | | | | | 20 | +| currency_rare | true | 196 | 13.1 | 1229.2 | 4373.1 | 4378.3 | 4378.8 | 0 | +| provider_common | false | 20 | 1.3 | | | | | 20 | +| provider_rare | false | 40 | 2.7 | 14290.5 | 20420.1 | 20420.7 | 20420.7 | 0 | +| provider_miss | true | 28266 | 1884.4 | 10.4 | 11.8 | 14.5 | 24.1 | 0 | +| receipt_hit | true | 26399 | 1759.9 | 11.0 | 14.2 | 18.1 | 34.6 | 0 | +| receipt_miss | true | 26969 | 1797.9 | 10.9 | 12.6 | 15.3 | 35.7 | 0 | +| invoice_hit_direct | true | 27083 | 1805.5 | 10.7 | 13.7 | 16.6 | 31.6 | 0 | +| invoice_hit_request | true | 27472 | 1831.5 | 10.7 | 12.4 | 15.2 | 34.5 | 0 | +| invoice_miss | true | 27913 | 1860.9 | 10.5 | 12.1 | 14.8 | 35.2 | 0 | +| customer_heavy | true | 207 | 13.8 | 1309.8 | 3061.0 | 3090.6 | 3102.2 | 0 | +| customer_light | true | 27024 | 1801.6 | 10.6 | 14.0 | 16.4 | 30.3 | 0 | +| payment_type_manual | false | 40 | 2.7 | 10143.4 | 12971.9 | 12973.6 | 12973.6 | 0 | +| payment_type_provider | false | 84 | 5.6 | | | | | 84 | +| payable_type_request | false | 289 | 19.3 | 911.9 | 2230.6 | 3097.7 | 3332.7 | 0 | +| payable_type_invoice | false | 20 | 1.3 | | | | | 20 | +| search_term | true | 1009 | 67.3 | 245.6 | 551.6 | 650.0 | 785.0 | 0 | +| search_term_status | true | 1034 | 68.9 | 231.9 | 546.5 | 664.5 | 762.7 | 0 | +| combo_status_currency_date | true | 14792 | 986.1 | 19.6 | 27.0 | 31.5 | 227.7 | 0 | +| combo_status_amount | false | 40 | 2.7 | 12505.7 | 14061.5 | 14062.4 | 14062.4 | 0 | +| combo_customer_status_date | true | 259 | 17.3 | 1077.2 | 1811.1 | 2029.4 | 2194.4 | 0 | +| combo_provider_status | false | 20 | 1.3 | 17969.2 | 17971.2 | 17971.5 | 17971.5 | 0 | +| five_filter_common | false | 20 | 1.3 | | | | | 20 | +| five_filter_rare | true | 2636 | 175.7 | 105.0 | 193.2 | 245.7 | 435.3 | 0 | diff --git a/script/perf/payments_filters/bench/after_1client.http.json b/script/perf/payments_filters/bench/after_1client.http.json new file mode 100644 index 00000000000..86caf538eba --- /dev/null +++ b/script/perf/payments_filters/bench/after_1client.http.json @@ -0,0 +1,528 @@ +{ + "phase": "after_1client", + "mode": "http", + "clients": 1, + "duration_s": 15.0, + "generated_at": "2026-09-09T04:50:22Z", + "target": "http://localhost:3000", + "cases": [ + { + "name": "control", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 21086.2, + "p95_ms": 21086.2, + "p99_ms": 21086.2, + "max_ms": 21086.2, + "error_samples": [] + }, + { + "name": "control_page50", + "selective": false, + "page": 50, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 13621.1, + "p95_ms": 13676.5, + "p99_ms": 13676.5, + "max_ms": 13676.5, + "error_samples": [] + }, + { + "name": "status_common_succeeded", + "selective": false, + "page": 1, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 10877.7, + "p95_ms": 10992.5, + "p99_ms": 10992.5, + "max_ms": 10992.5, + "error_samples": [] + }, + { + "name": "status_common_succeeded_page50", + "selective": false, + "page": 50, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 11234.8, + "p95_ms": 12210.8, + "p99_ms": 12210.8, + "max_ms": 12210.8, + "error_samples": [] + }, + { + "name": "status_rare_failed", + "selective": false, + "page": 1, + "requests": 3, + "rps": 0.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 6159.2, + "p95_ms": 7273.0, + "p99_ms": 7273.0, + "max_ms": 7273.0, + "error_samples": [] + }, + { + "name": "status_rare_pending", + "selective": true, + "page": 1, + "requests": 23, + "rps": 1.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 498.0, + "p95_ms": 889.2, + "p99_ms": 3177.8, + "max_ms": 3177.8, + "error_samples": [] + }, + { + "name": "status_rare_processing", + "selective": true, + "page": 1, + "requests": 40, + "rps": 2.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 339.3, + "p95_ms": 438.8, + "p99_ms": 1945.1, + "max_ms": 1945.1, + "error_samples": [] + }, + { + "name": "status_rare_pending_processing", + "selective": true, + "page": 1, + "requests": 24, + "rps": 1.6, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 624.6, + "p95_ms": 695.3, + "p99_ms": 859.0, + "max_ms": 859.0, + "error_samples": [] + }, + { + "name": "amount_common_from_p50", + "selective": false, + "page": 1, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 7283.9, + "p95_ms": 14594.9, + "p99_ms": 14594.9, + "max_ms": 14594.9, + "error_samples": [] + }, + { + "name": "amount_rare_from_p99", + "selective": false, + "page": 1, + "requests": 18, + "rps": 1.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 741.4, + "p95_ms": 1700.0, + "p99_ms": 1700.0, + "max_ms": 1700.0, + "error_samples": [] + }, + { + "name": "amount_rare_range", + "selective": false, + "page": 1, + "requests": 20, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 738.6, + "p95_ms": 807.5, + "p99_ms": 989.2, + "max_ms": 989.2, + "error_samples": [] + }, + { + "name": "created_7d", + "selective": true, + "page": 1, + "requests": 78, + "rps": 5.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 187.8, + "p95_ms": 223.4, + "p99_ms": 322.1, + "max_ms": 322.1, + "error_samples": [] + }, + { + "name": "created_24m", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 23804.0, + "p95_ms": 23804.0, + "p99_ms": 23804.0, + "max_ms": 23804.0, + "error_samples": [] + }, + { + "name": "currency_common", + "selective": false, + "page": 1, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 12793.4, + "p95_ms": 13373.0, + "p99_ms": 13373.0, + "max_ms": 13373.0, + "error_samples": [] + }, + { + "name": "currency_rare", + "selective": true, + "page": 1, + "requests": 18, + "rps": 1.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 753.1, + "p95_ms": 1489.3, + "p99_ms": 1489.3, + "max_ms": 1489.3, + "error_samples": [] + }, + { + "name": "provider_common", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 21034.6, + "p95_ms": 21034.6, + "p99_ms": 21034.6, + "max_ms": 21034.6, + "error_samples": [] + }, + { + "name": "provider_rare", + "selective": false, + "page": 1, + "requests": 3, + "rps": 0.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 5839.5, + "p95_ms": 6801.4, + "p99_ms": 6801.4, + "max_ms": 6801.4, + "error_samples": [] + }, + { + "name": "provider_miss", + "selective": true, + "page": 1, + "requests": 97, + "rps": 6.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 151.9, + "p95_ms": 182.1, + "p99_ms": 209.2, + "max_ms": 209.2, + "error_samples": [] + }, + { + "name": "receipt_hit", + "selective": true, + "page": 1, + "requests": 94, + "rps": 6.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 156.1, + "p95_ms": 185.0, + "p99_ms": 209.2, + "max_ms": 209.2, + "error_samples": [] + }, + { + "name": "receipt_miss", + "selective": true, + "page": 1, + "requests": 98, + "rps": 6.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 150.0, + "p95_ms": 168.5, + "p99_ms": 231.4, + "max_ms": 231.4, + "error_samples": [] + }, + { + "name": "invoice_hit_direct", + "selective": true, + "page": 1, + "requests": 96, + "rps": 6.4, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 154.6, + "p95_ms": 178.4, + "p99_ms": 206.4, + "max_ms": 206.4, + "error_samples": [] + }, + { + "name": "invoice_hit_request", + "selective": true, + "page": 1, + "requests": 95, + "rps": 6.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 156.0, + "p95_ms": 189.8, + "p99_ms": 216.5, + "max_ms": 216.5, + "error_samples": [] + }, + { + "name": "invoice_miss", + "selective": true, + "page": 1, + "requests": 100, + "rps": 6.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 148.2, + "p95_ms": 178.6, + "p99_ms": 204.8, + "max_ms": 332.4, + "error_samples": [] + }, + { + "name": "customer_heavy", + "selective": true, + "page": 1, + "requests": 19, + "rps": 1.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 702.0, + "p95_ms": 2518.2, + "p99_ms": 2518.2, + "max_ms": 2518.2, + "error_samples": [] + }, + { + "name": "customer_light", + "selective": true, + "page": 1, + "requests": 87, + "rps": 5.8, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 172.1, + "p95_ms": 193.6, + "p99_ms": 225.5, + "max_ms": 225.5, + "error_samples": [] + }, + { + "name": "payment_type_manual", + "selective": false, + "page": 1, + "requests": 4, + "rps": 0.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 3205.0, + "p95_ms": 5611.6, + "p99_ms": 5611.6, + "max_ms": 5611.6, + "error_samples": [] + }, + { + "name": "payment_type_provider", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 19388.5, + "p95_ms": 19388.5, + "p99_ms": 19388.5, + "max_ms": 19388.5, + "error_samples": [] + }, + { + "name": "payable_type_request", + "selective": false, + "page": 1, + "requests": 24, + "rps": 1.6, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 573.4, + "p95_ms": 994.2, + "p99_ms": 1374.9, + "max_ms": 1374.9, + "error_samples": [] + }, + { + "name": "payable_type_invoice", + "selective": false, + "page": 1, + "requests": 1, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 23866.8, + "p95_ms": 23866.8, + "p99_ms": 23866.8, + "max_ms": 23866.8, + "error_samples": [] + }, + { + "name": "search_term", + "selective": true, + "page": 1, + "requests": 54, + "rps": 3.6, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 273.0, + "p95_ms": 324.9, + "p99_ms": 331.8, + "max_ms": 331.8, + "error_samples": [] + }, + { + "name": "search_term_status", + "selective": true, + "page": 1, + "requests": 55, + "rps": 3.7, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 266.5, + "p95_ms": 314.9, + "p99_ms": 320.7, + "max_ms": 320.7, + "error_samples": [] + }, + { + "name": "combo_status_currency_date", + "selective": true, + "page": 1, + "requests": 80, + "rps": 5.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 184.6, + "p95_ms": 207.7, + "p99_ms": 282.9, + "max_ms": 282.9, + "error_samples": [] + }, + { + "name": "combo_status_amount", + "selective": false, + "page": 1, + "requests": 4, + "rps": 0.3, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 3931.4, + "p95_ms": 4111.0, + "p99_ms": 4111.0, + "max_ms": 4111.0, + "error_samples": [] + }, + { + "name": "combo_customer_status_date", + "selective": true, + "page": 1, + "requests": 22, + "rps": 1.5, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 665.2, + "p95_ms": 800.0, + "p99_ms": 1077.1, + "max_ms": 1077.1, + "error_samples": [] + }, + { + "name": "combo_provider_status", + "selective": false, + "page": 1, + "requests": 3, + "rps": 0.2, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 5611.8, + "p95_ms": 6869.1, + "p99_ms": 6869.1, + "max_ms": 6869.1, + "error_samples": [] + }, + { + "name": "five_filter_common", + "selective": false, + "page": 1, + "requests": 2, + "rps": 0.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 11442.1, + "p95_ms": 12911.2, + "p99_ms": 12911.2, + "max_ms": 12911.2, + "error_samples": [] + }, + { + "name": "five_filter_rare", + "selective": true, + "page": 1, + "requests": 76, + "rps": 5.1, + "errors": 0, + "error_rate": 0.0, + "p50_ms": 194.3, + "p95_ms": 222.8, + "p99_ms": 241.0, + "max_ms": 241.0, + "error_samples": [] + } + ] +} \ No newline at end of file diff --git a/script/perf/payments_filters/bench/after_1client.http.md b/script/perf/payments_filters/bench/after_1client.http.md new file mode 100644 index 00000000000..02c91e96759 --- /dev/null +++ b/script/perf/payments_filters/bench/after_1client.http.md @@ -0,0 +1,43 @@ +# Load test: after_1client (http) + +1 concurrent clients, 15s per case, synthetic dataset. + +| case | selective | requests | req/s | p50 ms | p95 ms | p99 ms | max ms | errors | +|---|---|---|---|---|---|---|---|---| +| control | false | 1 | 0.1 | 21086.2 | 21086.2 | 21086.2 | 21086.2 | 0 | +| control_page50 | false | 2 | 0.1 | 13621.1 | 13676.5 | 13676.5 | 13676.5 | 0 | +| status_common_succeeded | false | 2 | 0.1 | 10877.7 | 10992.5 | 10992.5 | 10992.5 | 0 | +| status_common_succeeded_page50 | false | 2 | 0.1 | 11234.8 | 12210.8 | 12210.8 | 12210.8 | 0 | +| status_rare_failed | false | 3 | 0.2 | 6159.2 | 7273.0 | 7273.0 | 7273.0 | 0 | +| status_rare_pending | true | 23 | 1.5 | 498.0 | 889.2 | 3177.8 | 3177.8 | 0 | +| status_rare_processing | true | 40 | 2.7 | 339.3 | 438.8 | 1945.1 | 1945.1 | 0 | +| status_rare_pending_processing | true | 24 | 1.6 | 624.6 | 695.3 | 859.0 | 859.0 | 0 | +| amount_common_from_p50 | false | 2 | 0.1 | 7283.9 | 14594.9 | 14594.9 | 14594.9 | 0 | +| amount_rare_from_p99 | false | 18 | 1.2 | 741.4 | 1700.0 | 1700.0 | 1700.0 | 0 | +| amount_rare_range | false | 20 | 1.3 | 738.6 | 807.5 | 989.2 | 989.2 | 0 | +| created_7d | true | 78 | 5.2 | 187.8 | 223.4 | 322.1 | 322.1 | 0 | +| created_24m | false | 1 | 0.1 | 23804.0 | 23804.0 | 23804.0 | 23804.0 | 0 | +| currency_common | false | 2 | 0.1 | 12793.4 | 13373.0 | 13373.0 | 13373.0 | 0 | +| currency_rare | true | 18 | 1.2 | 753.1 | 1489.3 | 1489.3 | 1489.3 | 0 | +| provider_common | false | 1 | 0.1 | 21034.6 | 21034.6 | 21034.6 | 21034.6 | 0 | +| provider_rare | false | 3 | 0.2 | 5839.5 | 6801.4 | 6801.4 | 6801.4 | 0 | +| provider_miss | true | 97 | 6.5 | 151.9 | 182.1 | 209.2 | 209.2 | 0 | +| receipt_hit | true | 94 | 6.3 | 156.1 | 185.0 | 209.2 | 209.2 | 0 | +| receipt_miss | true | 98 | 6.5 | 150.0 | 168.5 | 231.4 | 231.4 | 0 | +| invoice_hit_direct | true | 96 | 6.4 | 154.6 | 178.4 | 206.4 | 206.4 | 0 | +| invoice_hit_request | true | 95 | 6.3 | 156.0 | 189.8 | 216.5 | 216.5 | 0 | +| invoice_miss | true | 100 | 6.7 | 148.2 | 178.6 | 204.8 | 332.4 | 0 | +| customer_heavy | true | 19 | 1.3 | 702.0 | 2518.2 | 2518.2 | 2518.2 | 0 | +| customer_light | true | 87 | 5.8 | 172.1 | 193.6 | 225.5 | 225.5 | 0 | +| payment_type_manual | false | 4 | 0.3 | 3205.0 | 5611.6 | 5611.6 | 5611.6 | 0 | +| payment_type_provider | false | 1 | 0.1 | 19388.5 | 19388.5 | 19388.5 | 19388.5 | 0 | +| payable_type_request | false | 24 | 1.6 | 573.4 | 994.2 | 1374.9 | 1374.9 | 0 | +| payable_type_invoice | false | 1 | 0.1 | 23866.8 | 23866.8 | 23866.8 | 23866.8 | 0 | +| search_term | true | 54 | 3.6 | 273.0 | 324.9 | 331.8 | 331.8 | 0 | +| search_term_status | true | 55 | 3.7 | 266.5 | 314.9 | 320.7 | 320.7 | 0 | +| combo_status_currency_date | true | 80 | 5.3 | 184.6 | 207.7 | 282.9 | 282.9 | 0 | +| combo_status_amount | false | 4 | 0.3 | 3931.4 | 4111.0 | 4111.0 | 4111.0 | 0 | +| combo_customer_status_date | true | 22 | 1.5 | 665.2 | 800.0 | 1077.1 | 1077.1 | 0 | +| combo_provider_status | false | 3 | 0.2 | 5611.8 | 6869.1 | 6869.1 | 6869.1 | 0 | +| five_filter_common | false | 2 | 0.1 | 11442.1 | 12911.2 | 12911.2 | 12911.2 | 0 | +| five_filter_rare | true | 76 | 5.1 | 194.3 | 222.8 | 241.0 | 241.0 | 0 | diff --git a/script/perf/payments_filters/compare/baseline_vs_after.md b/script/perf/payments_filters/compare/baseline_vs_after.md index 0ed044a7beb..2dbbe4a5aa8 100644 --- a/script/perf/payments_filters/compare/baseline_vs_after.md +++ b/script/perf/payments_filters/compare/baseline_vs_after.md @@ -4,54 +4,54 @@ Plans: median EXPLAIN (ANALYZE, BUFFERS) execution time. HTTP: p95 of GET /api/v | case | sel. | list ms before | list ms after | count ms before | count ms after | http p95 before | http p95 after | sql p95 before | sql p95 after | list nodes after | flags before | flags after | |---|---|---|---|---|---|---|---|---|---|---|---|---| -| amount_common_from_p50 | false | 0.2 | 0.3 | 7615.8 | 9161.0 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| amount_rare_from_p99 | false | 1.9 | 2.7 | 626.6 | 1023.5 | 6906.6 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| amount_rare_range | false | 8.0 | 8.5 | 558.7 | 523.1 | 3963.4 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| combo_customer_status_date | true | 367.6 | 0.5 | 312.0 | 465.2 | 5876.8 | - | - | - | Index Scan | SLOW | | -| combo_provider_status | false | 0.2 | 0.2 | 11031.9 | 5853.3 | 48962.4 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| combo_status_amount | false | 0.2 | 0.2 | 2955.6 | 4364.5 | 20840.6 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| combo_status_currency_date | true | 0.1 | 0.1 | 4.6 | 5.6 | 2500.8 | - | - | - | Index Scan | | | +| amount_common_from_p50 | false | 0.2 | 0.3 | 7615.8 | 9161.0 | - | 31256.3 | - | - | Index Scan | COUNT>500 | COUNT>500 | +| amount_rare_from_p99 | false | 1.9 | 2.7 | 626.6 | 1023.5 | 6906.6 | 6089.1 | - | 4979.8 | Index Scan | COUNT>500 | COUNT>500 | +| amount_rare_range | false | 8.0 | 8.5 | 558.7 | 523.1 | 3963.4 | 3102.6 | - | 1805.9 | Index Scan | COUNT>500 | COUNT>500 | +| combo_customer_status_date | true | 367.6 | 0.5 | 312.0 | 465.2 | 5876.8 | 3668.6 | - | 1811.1 | Index Scan | SLOW | | +| combo_provider_status | false | 0.2 | 0.2 | 11031.9 | 5853.3 | 48962.4 | 29351.3 | - | 17971.2 | Index Scan | COUNT>500 | COUNT>500 | +| combo_status_amount | false | 0.2 | 0.2 | 2955.6 | 4364.5 | 20840.6 | 28787.9 | - | 14061.5 | Index Scan | COUNT>500 | COUNT>500 | +| combo_status_currency_date | true | 0.1 | 0.1 | 4.6 | 5.6 | 2500.8 | 2168.6 | - | 27.0 | Index Scan | | | | control | false | 0.1 | 0.1 | 14064.9 | 15417.3 | 51394.8 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| control_page50 | false | 3.7 | 3.5 | 15895.7 | 14840.4 | 51379.6 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| control_page50 | false | 3.7 | 3.5 | 15895.7 | 14840.4 | 51379.6 | 48240.9 | - | - | Index Scan | COUNT>500 | COUNT>500 | | created_24m | false | 0.1 | 0.1 | 14547.4 | 14673.2 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| created_7d | true | 0.1 | 0.1 | 10.1 | 9.6 | 2392.3 | - | - | - | Index Scan | | | +| created_7d | true | 0.1 | 0.1 | 10.1 | 9.6 | 2392.3 | 2435.5 | - | 32.4 | Index Scan | | | | currency_common | false | 0.1 | 0.1 | 15144.6 | 14084.0 | 56285.7 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| currency_rare | true | 2.5 | 2.5 | 864.3 | 636.3 | 6290.5 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| customer_heavy | true | 399.4 | 1.8 | 415.7 | 532.8 | 5860.3 | - | - | - | Index Scan | SLOW | COUNT>500 | -| customer_light | true | 1.8 | 0.1 | 1.6 | 0.1 | 2204.5 | - | - | - | Sort, Index Scan | | | +| currency_rare | true | 2.5 | 2.5 | 864.3 | 636.3 | 6290.5 | 5648.0 | - | 4373.1 | Index Scan | COUNT>500 | COUNT>500 | +| customer_heavy | true | 399.4 | 1.8 | 415.7 | 532.8 | 5860.3 | 4289.4 | - | 3061.0 | Index Scan | SLOW | COUNT>500 | +| customer_light | true | 1.8 | 0.1 | 1.6 | 0.1 | 2204.5 | 2112.5 | - | 14.0 | Sort, Index Scan | | | | five_filter_common | false | 0.1 | 0.2 | 16647.1 | 9715.4 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| five_filter_rare | true | 20.8 | 18.6 | 21.0 | 18.8 | 1997.5 | - | - | - | Sort, Bitmap Heap Scan, BitmapAnd, Bitmap Index Scan, Index Scan | | | -| invoice_hit_direct | true | 72525.3 | 0.0 | 18048.9 | 0.0 | - | - | - | - | Sort, Index Scan | SLOW COUNT>500 SEQ:invoices | | -| invoice_hit_request | true | 83846.9 | 0.0 | 17075.0 | 0.0 | - | - | - | - | Sort, Bitmap Heap Scan, BitmapOr, Bitmap Index Scan, Index Scan | SLOW COUNT>500 SEQ:invoices | | -| invoice_miss | true | 60503.3 | 0.0 | 16473.7 | 0.0 | - | - | - | - | Sort, Result | SLOW COUNT>500 SEQ:invoices | | +| five_filter_rare | true | 20.8 | 18.6 | 21.0 | 18.8 | 1997.5 | 1968.2 | - | 193.2 | Sort, Bitmap Heap Scan, BitmapAnd, Bitmap Index Scan, Index Scan | | | +| invoice_hit_direct | true | 72525.3 | 0.0 | 18048.9 | 0.0 | - | 2095.4 | - | 13.7 | Sort, Index Scan | SLOW COUNT>500 SEQ:invoices | | +| invoice_hit_request | true | 83846.9 | 0.0 | 17075.0 | 0.0 | - | 2018.9 | - | 12.4 | Sort, Bitmap Heap Scan, BitmapOr, Bitmap Index Scan, Index Scan | SLOW COUNT>500 SEQ:invoices | | +| invoice_miss | true | 60503.3 | 0.0 | 16473.7 | 0.0 | - | 1810.9 | - | 12.1 | Sort, Result | SLOW COUNT>500 SEQ:invoices | | | payable_type_invoice | false | 0.1 | 0.1 | 15354.1 | 15783.2 | 54239.9 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| payable_type_request | false | 1.0 | 0.3 | 480.5 | 389.8 | 5076.7 | - | - | - | Index Scan | | | -| payment_type_manual | false | 0.7 | 0.3 | 2217.9 | 6287.7 | 26829.8 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| payment_type_provider | false | 0.2 | 0.1 | 12469.5 | 15557.3 | 45414.8 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | +| payable_type_request | false | 1.0 | 0.3 | 480.5 | 389.8 | 5076.7 | 4251.5 | - | 2230.6 | Index Scan | | | +| payment_type_manual | false | 0.7 | 0.3 | 2217.9 | 6287.7 | 26829.8 | 22119.8 | - | 12971.9 | Index Scan | COUNT>500 | COUNT>500 | +| payment_type_provider | false | 0.2 | 0.1 | 12469.5 | 15557.3 | 45414.8 | 48484.3 | - | - | Index Scan | COUNT>500 | COUNT>500 | | provider_common | false | 0.3 | 0.1 | 23420.6 | 12901.0 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| provider_miss | true | 66537.5 | 0.0 | 5.7 | 0.0 | - | - | - | - | Sort, Result | SLOW | | -| provider_rare | false | 0.5 | 0.1 | 9733.1 | 9222.5 | 46707.1 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| receipt_hit | true | 105274.1 | 0.1 | 282.9 | 0.0 | - | - | - | - | Sort, Nested Loop, Index Scan | SLOW SEQ:payment_receipts | | -| receipt_miss | true | - | 0.0 | 239.3 | 0.0 | - | - | - | - | Sort, Nested Loop, Index Scan | SLOW SEQ:payment_receipts TIMEOUT | | -| search_term | true | 56.0 | 52.4 | 52.8 | 56.6 | 2119.3 | - | - | - | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | | -| search_term_status | true | 49.6 | 54.0 | 50.6 | 51.2 | 2913.3 | - | - | - | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | | +| provider_miss | true | 66537.5 | 0.0 | 5.7 | 0.0 | - | 1962.6 | - | 11.8 | Sort, Result | SLOW | | +| provider_rare | false | 0.5 | 0.1 | 9733.1 | 9222.5 | 46707.1 | 32078.4 | - | 20420.1 | Index Scan | COUNT>500 | COUNT>500 | +| receipt_hit | true | 105274.1 | 0.1 | 282.9 | 0.0 | - | 1898.9 | - | 14.2 | Sort, Nested Loop, Index Scan | SLOW SEQ:payment_receipts | | +| receipt_miss | true | - | 0.0 | 239.3 | 0.0 | - | 2649.9 | - | 12.6 | Sort, Nested Loop, Index Scan | SLOW SEQ:payment_receipts TIMEOUT | | +| search_term | true | 56.0 | 52.4 | 52.8 | 56.6 | 2119.3 | 2517.3 | - | 551.6 | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | | +| search_term_status | true | 49.6 | 54.0 | 50.6 | 51.2 | 2913.3 | 2052.3 | - | 546.5 | Sort, Nested Loop, HashAggregate, Append, Bitmap Heap Scan, Bitmap Index Scan, Index Scan | | | | status_common_succeeded | false | 0.1 | 0.1 | 11376.0 | 12473.4 | - | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| status_common_succeeded_page50 | false | 2.3 | 3.8 | 12762.0 | 15925.3 | 50229.5 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| status_rare_failed | false | 0.3 | 0.3 | 3411.5 | 10938.7 | 43340.0 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| status_rare_pending | true | 0.5 | 0.1 | 831.9 | 353.9 | 9685.4 | - | - | - | Index Scan | COUNT>500 | | -| status_rare_pending_processing | true | 0.3 | 0.5 | 1481.3 | 528.3 | 24907.5 | - | - | - | Index Scan | COUNT>500 | COUNT>500 | -| status_rare_processing | true | 0.5 | 0.1 | 574.2 | 177.2 | 8238.0 | - | - | - | Index Scan | COUNT>500 | | +| status_common_succeeded_page50 | false | 2.3 | 3.8 | 12762.0 | 15925.3 | 50229.5 | 46098.2 | - | - | Index Scan | COUNT>500 | COUNT>500 | +| status_rare_failed | false | 0.3 | 0.3 | 3411.5 | 10938.7 | 43340.0 | 33977.9 | - | 17946.1 | Index Scan | COUNT>500 | COUNT>500 | +| status_rare_pending | true | 0.5 | 0.1 | 831.9 | 353.9 | 9685.4 | 5887.5 | - | 6378.9 | Index Scan | COUNT>500 | | +| status_rare_pending_processing | true | 0.3 | 0.5 | 1481.3 | 528.3 | 24907.5 | 3246.7 | - | 1860.5 | Index Scan | COUNT>500 | COUNT>500 | +| status_rare_processing | true | 0.5 | 0.1 | 574.2 | 177.2 | 8238.0 | 4431.9 | - | 700.8 | Index Scan | COUNT>500 | | ## Scoreboard (after) | # | target | result | |---|---|---| -| G1 | single filter p95 < 300 ms | n/a (no http bench) | -| G2 | five-filter p95 < 800 ms | n/a (no http bench) | +| G1 | single filter p95 < 300 ms | RED worst payment_type_provider 48484.3 ms | +| G2 | five-filter p95 < 800 ms | RED worst five_filter_rare 1968.2 ms | | G3 | COUNT(*) < 500 ms (selective cases, new filters) | RED worst status_rare_pending_processing 528.3 ms; non-selective worst status_common_succeeded_page50 15925.3 ms and pre-existing filters over 500 ms (currency_common 14084.0 ms, currency_rare 636.3 ms, customer_heavy 532.8 ms) reported separately | | G4 | control p95 within +10 % | n/a (no http bench on both phases) | | G5 | selective plans: no watched Seq Scan, no Sort > 10k | GREEN all 18 selective cases clean | -| G8 | zero errors in the load test | n/a (no http bench) | -| G9 | page 50 < 2x page 1 | GREEN control_page50 0.1 -> 3.5 ms; status_common_succeeded_page50 0.1 -> 3.8 ms | +| G8 | zero errors in the load test | RED 217 errors over 7096 requests | +| G9 | page 50 < 2x page 1 | RED control_page50 0.1 -> 48240.9 ms; status_common_succeeded_page50 0.1 -> 46098.2 ms | G6 (<= 3 new payments indexes) and G7 (build < 15 min, no INVALID) are graded from the migration run log. From e8f15e9a53b5a40a2154ea3570e63189cb142658 Mon Sep 17 00:00:00 2001 From: Raffi Date: Tue, 8 Sep 2026 22:05:05 -0700 Subject: [PATCH 8/9] perf(payments): cap the GraphQL payments total count like invoices Counting every matching payment of a large organization is the slow half of the payments query. Reuse BaseQuery::CappedTotalCount and expose PaymentCollectionMetadata with totalCountCapped and hasNextPage, exactly as the invoices collection does, so the client shows "10,000+" and paginates on hasNextPage. --- app/graphql/resolvers/payments_resolver.rb | 8 +- .../types/payments/collection_metadata.rb | 25 ++++ schema.graphql | 37 +++++- schema.json | 109 +++++++++++++++++- .../resolvers/payments_resolver_spec.rb | 63 ++++++++++ 5 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 app/graphql/types/payments/collection_metadata.rb diff --git a/app/graphql/resolvers/payments_resolver.rb b/app/graphql/resolvers/payments_resolver.rb index 51c1c2690fc..d42c62da8fb 100644 --- a/app/graphql/resolvers/payments_resolver.rb +++ b/app/graphql/resolvers/payments_resolver.rb @@ -26,7 +26,7 @@ class PaymentsResolver < Resolvers::BaseResolver argument :receipt_number, String, required: false argument :search_term, String, required: false - type Types::Payments::Object.collection_type, null: false + type Types::Payments::Object.collection_type(metadata_type: Types::Payments::CollectionMetadata), null: false def resolve(page: nil, limit: nil, search_term: nil, **filters) result = PaymentsQuery.call( @@ -39,7 +39,11 @@ def resolve(page: nil, limit: nil, search_term: nil, **filters) } ) - result.success? ? result.payments : result_error(result) + return result_error(result) unless result.success? + + # Counting every matching payment of a large organization is the slow half of the + # request; cap it like invoices do and let the client show "10,000+". + result.payments.without_count.extend(BaseQuery::CappedTotalCount) end end end diff --git a/app/graphql/types/payments/collection_metadata.rb b/app/graphql/types/payments/collection_metadata.rb new file mode 100644 index 00000000000..d6569841a27 --- /dev/null +++ b/app/graphql/types/payments/collection_metadata.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module Payments + class CollectionMetadata < GraphqlPagination::CollectionMetadataType + graphql_name "PaymentCollectionMetadata" + description "Pagination metadata for a collection of payments" + + field :has_next_page, Boolean, null: false, + description: "True when another page follows, even when `totalCount` is capped" + field :total_count_capped, Boolean, null: false, + description: "True when `totalCount` hit the counting limit and is a lower bound, not the exact total" + + def has_next_page + return object.has_next_page? if object.respond_to?(:has_next_page?) + + object.current_page < object.total_pages + end + + def total_count_capped + object.respond_to?(:capped_total_count?) && object.capped_total_count? + end + end + end +end diff --git a/schema.graphql b/schema.graphql index ace794b5b75..091be5fcb89 100644 --- a/schema.graphql +++ b/schema.graphql @@ -10544,7 +10544,42 @@ type PaymentCollection { """ Pagination Metadata for navigating the Pagination """ - metadata: CollectionMetadata! + metadata: PaymentCollectionMetadata! +} + +""" +Pagination metadata for a collection of payments +""" +type PaymentCollectionMetadata { + """ + Current Page of loaded data + """ + currentPage: Int! + + """ + True when another page follows, even when `totalCount` is capped + """ + hasNextPage: Boolean! + + """ + The number of items per page + """ + limitValue: Int! + + """ + The total number of items to be paginated + """ + totalCount: Int! + + """ + True when `totalCount` hit the counting limit and is a lower bound, not the exact total + """ + totalCountCapped: Boolean! + + """ + The total number of pages in the pagination + """ + totalPages: Int! } type PaymentMethod { diff --git a/schema.json b/schema.json index 566e1f9f218..7dcdc9ed0e6 100644 --- a/schema.json +++ b/schema.json @@ -50933,7 +50933,114 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "CollectionMetadata", + "name": "PaymentCollectionMetadata", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PaymentCollectionMetadata", + "description": "Pagination metadata for a collection of payments", + "fields": [ + { + "name": "currentPage", + "description": "Current Page of loaded data", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasNextPage", + "description": "True when another page follows, even when `totalCount` is capped", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "limitValue", + "description": "The number of items per page", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "The total number of items to be paginated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCountCapped", + "description": "True when `totalCount` hit the counting limit and is a lower bound, not the exact total", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalPages", + "description": "The total number of pages in the pagination", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", "ofType": null } }, diff --git a/spec/graphql/resolvers/payments_resolver_spec.rb b/spec/graphql/resolvers/payments_resolver_spec.rb index 98b3c205afc..c9655cce0d4 100644 --- a/spec/graphql/resolvers/payments_resolver_spec.rb +++ b/spec/graphql/resolvers/payments_resolver_spec.rb @@ -225,4 +225,67 @@ end end end + + describe "total count" do + let(:page) { 1 } + let(:query) do + <<~GQL + query { + payments(page: #{page}, limit: 1) { + collection { id } + metadata { currentPage totalCount totalPages totalCountCapped hasNextPage } + } + } + GQL + end + + let(:metadata) do + execute_graphql( + current_user: membership.user, + current_organization: organization, + permissions: required_permission, + query: + )["data"]["payments"]["metadata"] + end + + it "returns the exact total" do + expect(metadata).to eq( + "currentPage" => 1, + "totalCount" => 2, + "totalPages" => 2, + "totalCountCapped" => false, + "hasNextPage" => true + ) + end + + context "when the result set exceeds the cap" do + before { stub_const("BaseQuery::CappedTotalCount::MAX_COUNTED_RECORDS", 1) } + + it "caps the total and keeps advertising the next page" do + expect(metadata).to eq( + "currentPage" => 1, + "totalCount" => 1, + "totalPages" => 1, + "totalCountCapped" => true, + "hasNextPage" => true + ) + end + + context "when on the last page" do + let(:page) { 2 } + + it "reports no next page" do + expect(metadata).to include("totalCountCapped" => true, "hasNextPage" => false) + end + end + end + + context "when the results land exactly on the limit" do + before { stub_const("BaseQuery::CappedTotalCount::MAX_COUNTED_RECORDS", 2) } + + it "reports the total as exact" do + expect(metadata).to include("totalCount" => 2, "totalCountCapped" => false, "hasNextPage" => true) + end + end + end end From e723f7eb02997c722dd5db81b20267fd3096342a Mon Sep 17 00:00:00 2001 From: Raffi Date: Tue, 8 Sep 2026 22:14:40 -0700 Subject: [PATCH 9/9] fix(perf): keep synthetic customer slugs unique past 999 lpad truncates longer strings, so customers 1000+ shared the slugs (and receipt numbers) of customers 1-999. Pad like the receipt trigger does. --- script/perf/payments_filters/generate.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/perf/payments_filters/generate.rb b/script/perf/payments_filters/generate.rb index b30ab5be72a..2c14b364ff4 100644 --- a/script/perf/payments_filters/generate.rb +++ b/script/perf/payments_filters/generate.rb @@ -129,7 +129,7 @@ INSERT INTO customers (id, organization_id, billing_entity_id, external_id, name, slug, sequential_id, currency, created_at, updated_at) SELECT gen_random_uuid(), o.organization_id, o.billing_entity_id, 'perf-cust-' || o.idx || '-' || c, 'Perf Customer ' || o.idx || '-' || c, - o.prefix || '-' || lpad(c::text, 3, '0'), c, 'EUR', + o.prefix || '-' || lpad(c::text, greatest(3, length(c::text)), '0'), c, 'EUR', now() - interval '1 month' * #{MONTHS}, now() FROM perf_orgs o CROSS JOIN LATERAL generate_series(1, o.n_customers) AS c SQL