Speed up Transactions, the Transfers pill, Reconcile and the transactions export: database update 3.5.4 indexes transfers.transfer_expense_id so the ledger no longer checks every expense against every transfer, accountLedgerSql() takes a with_balance flag so the running balance is only worked out where it shows (one account in date order and its export), and the Transfers summary cards come from the same pass as the pill counts and totals

This commit is contained in:
johnnyq
2026-11-02 15:59:11 -05:00
parent 87d8fdcdf8
commit c24d13bfe8
7 changed files with 65 additions and 27 deletions

View File

@@ -0,0 +1,27 @@
<?php
/*
* ITFlow - Database update to version 3.5.4 (from 3.5.3)
* Included by admin/database_updates.php - do not access directly
*
* Index transfers.transfer_expense_id - the out side of a transfer. The
* account ledger (accountLedgerSql(), behind Transactions, its Transfers
* pill and export, and Reconcile) joins every expense to the transfer it
* may belong to. With nothing to look it up by, each expense was checked
* against every transfer - expenses times transfers - which is what made
* those pages crawl as the books grew. transfer_revenue_id, the in side,
* has had its index since 2.7.5.
*
* Re-runnable: skipped where the index is already there.
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
$itflow_index_exists = mysqli_query($mysqli, "SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'transfers' AND INDEX_NAME = 'transfer_expense_id' LIMIT 1");
if (!$itflow_index_exists || mysqli_num_rows($itflow_index_exists) === 0) {
mysqli_query($mysqli, "ALTER TABLE `transfers` ADD KEY `transfer_expense_id` (`transfer_expense_id`)");
}
unset($itflow_index_exists);

View File

@@ -30,7 +30,8 @@ if (isExportRequest('export_transactions')) {
}
$kinds = accountLedgerKinds();
$sql = mysqli_query($mysqli, "SELECT ledger.* FROM (" . accountLedgerSql($account_id) . ") AS ledger WHERE $where
// The balance after each line is in the export of one account only
$sql = mysqli_query($mysqli, "SELECT ledger.* FROM (" . accountLedgerSql($account_id, (bool) $account) . ") AS ledger WHERE $where
ORDER BY ledger.txn_date ASC, ledger.txn_created_at ASC, ledger.txn_order ASC, ledger.txn_id ASC");
$num_rows = mysqli_num_rows($sql);

View File

@@ -78,7 +78,7 @@ if ($open) {
$lines = reconcileLines($open, $later);
$statement_date = $open['reconciliation_statement_date'];
} else {
$uncleared = $account ? mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*), COALESCE(SUM(ledger.txn_amount), 0) FROM (" . accountLedgerSql($account_id) . ") AS ledger WHERE ledger.txn_cleared_id IS NULL")) : [0, 0];
$uncleared = $account ? mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*), COALESCE(SUM(ledger.txn_amount), 0) FROM (" . accountLedgerSql($account_id, false) . ") AS ledger WHERE ledger.txn_cleared_id IS NULL")) : [0, 0];
}
$statement_form = function ($row) use ($mysqli, $account, $is_card, $suggest, $last, $starting, $money, $statement_money) {

View File

@@ -72,7 +72,8 @@ $balance_visible = !$all && $sort === 'txn_date';
$filter_src = array_merge($_GET, ['dtf' => $dtf, 'dtt' => $dtt, 'kind' => $kind_filter, 'show' => $show]);
$where = accountLedgerFiltersSql($filter_src, $summary, false, $all);
$show_query = $show !== '' ? 'AND ' . $shows[$show]['where'] : '';
$ledger = accountLedgerSql($account_filter);
// The balance after each line only where it shows - one account, in date order
$ledger = accountLedgerSql($account_filter, $balance_visible);
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS ledger.* FROM ($ledger) AS ledger WHERE $where $show_query ORDER BY $order_sql LIMIT $record_from, $record_to");
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
@@ -81,21 +82,35 @@ while ($row = mysqli_fetch_assoc($sql)) {
$rows[] = accountLedgerVisible($row);
}
// The pills, and the money in and out of what is listed
// The pills, and the money in and out of what is listed - and, on every account's
// transfers, what was moved, what paid off cards and what payment processors paid out.
// One pass over the ledger, without the balance after each line.
$transfer_cards = $all && $show === 'transfers';
$count_sql = [];
foreach ($shows as $key => $s) {
$count_sql[] = $key === '' ? 'COUNT(*)' : "COALESCE(SUM({$s['where']}), 0)";
}
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . ",
COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.txn_amount > 0 THEN ledger.txn_amount END), 0),
COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.txn_amount < 0 THEN -ledger.txn_amount END), 0)
FROM ($ledger) AS ledger WHERE $where"));
$count_sql[] = "COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.txn_amount > 0 THEN ledger.txn_amount END), 0)";
$count_sql[] = "COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.txn_amount < 0 THEN -ledger.txn_amount END), 0)";
if ($transfer_cards) {
$count_sql[] = "COALESCE(SUM(CASE WHEN 1 $show_query THEN -ledger.txn_amount END), 0)";
$count_sql[] = "COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.other_account_type = 3 THEN -ledger.txn_amount END), 0)";
$count_sql[] = "COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.other_account_type = 3 THEN 1 END), 0)";
$count_sql[] = "COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.account_type = 5 THEN -ledger.txn_amount END), 0)";
$count_sql[] = "COALESCE(SUM(CASE WHEN 1 $show_query AND ledger.account_type = 5 THEN 1 END), 0)";
}
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . " FROM (" . accountLedgerSql($account_filter, false) . ") AS ledger WHERE $where"));
$counts = [];
foreach (array_keys($shows) as $i => $key) {
$counts[$key] = intval($row[$i]);
}
$total_in = round(floatval($row[count($shows)]), 2);
$total_out = round(floatval($row[count($shows) + 1]), 2);
$n = count($shows);
$total_in = round(floatval($row[$n]), 2);
$total_out = round(floatval($row[$n + 1]), 2);
if ($transfer_cards) {
$moved = ['count' => $counts['transfers'], 'total' => round(floatval($row[$n + 2]), 2), 'cards' => round(floatval($row[$n + 3]), 2), 'card_count' => intval($row[$n + 4]),
'payouts' => round(floatval($row[$n + 5]), 2), 'payout_count' => intval($row[$n + 6])];
}
$currency = $account ? (string) $account['account_currency_code'] : (string) $session_company_currency;
$money = function ($amount, $code = null) use ($currency) {
return escapeHtml(billingMoney($amount, $code ?? $currency));
@@ -117,17 +132,6 @@ foreach ($open_accounts as $open) {
}
$is_card = $account && $account['type'] === 3;
// Every account's transfers: what was moved, what paid off cards and what payment processors paid out
$transfer_cards = $all && $show === 'transfers';
if ($transfer_cards) {
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*), COALESCE(SUM(-ledger.txn_amount), 0),
COALESCE(SUM(CASE WHEN ledger.other_account_type = 3 THEN -ledger.txn_amount END), 0), COALESCE(SUM(CASE WHEN ledger.other_account_type = 3 THEN 1 END), 0),
COALESCE(SUM(CASE WHEN ledger.account_type = 5 THEN -ledger.txn_amount END), 0), COALESCE(SUM(CASE WHEN ledger.account_type = 5 THEN 1 END), 0)
FROM ($ledger) AS ledger
WHERE $where $show_query"));
$moved = ['count' => intval($row[0]), 'total' => round(floatval($row[1]), 2), 'cards' => round(floatval($row[2]), 2), 'card_count' => intval($row[3]),
'payouts' => round(floatval($row[4]), 2), 'payout_count' => intval($row[5])];
}
?>

3
db.sql
View File

@@ -4013,7 +4013,8 @@ CREATE TABLE `transfers` (
`transfer_expense_id` int(11) NOT NULL,
`transfer_revenue_id` int(11) NOT NULL,
PRIMARY KEY (`transfer_id`),
KEY `transfer_revenue_id` (`transfer_revenue_id`)
KEY `transfer_revenue_id` (`transfer_revenue_id`),
KEY `transfer_expense_id` (`transfer_expense_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

View File

@@ -358,8 +358,13 @@ function accountLedgerKinds() {
* txn_method, txn_reference, txn_invoice_id, txn_currency, txn_imported and
* txn_balance - the account's balance after the line, in date order. Select
* from it as a derived table.
*
* The balance after each line means sorting the account's whole history, so
* it is only worked out with $with_balance - where it is shown. Without it,
* txn_balance is NULL: counts, totals and lists across every account don't
* need it.
*/
function accountLedgerSql($account_id = 0) {
function accountLedgerSql($account_id = 0, $with_balance = true) {
$account_id = intval($account_id);
$on = function ($column) use ($account_id) {
return $account_id ? "$column = $account_id" : "$column > 0";
@@ -408,8 +413,8 @@ function accountLedgerSql($account_id = 0) {
return "SELECT ledger_lines.*, accounts.account_name, accounts.account_type, accounts.account_currency_code, category_name, category_color, category_icon, client_name,
ledger_lines.txn_other_name AS other_account_name, other_account.account_type AS other_account_type,
cleared.reconciliation_id AS txn_cleared_id, cleared.reconciliation_statement_date AS txn_cleared_date, cleared.reconciliation_opening AS txn_cleared_opening,
accounts.opening_balance + SUM(ledger_lines.txn_amount) OVER (PARTITION BY ledger_lines.txn_account_id ORDER BY ledger_lines.txn_date ASC, ledger_lines.txn_created_at ASC,
ledger_lines.txn_order ASC, ledger_lines.txn_id ASC) AS txn_balance
" . ($with_balance ? "accounts.opening_balance + SUM(ledger_lines.txn_amount) OVER (PARTITION BY ledger_lines.txn_account_id ORDER BY ledger_lines.txn_date ASC, ledger_lines.txn_created_at ASC,
ledger_lines.txn_order ASC, ledger_lines.txn_id ASC)" : "NULL") . " AS txn_balance
FROM ($rows) AS ledger_lines
JOIN accounts ON accounts.account_id = ledger_lines.txn_account_id
LEFT JOIN accounts AS other_account ON ledger_lines.txn_kind IN ('transfer_in', 'transfer_out') AND other_account.account_id = ledger_lines.txn_other_account_id

View File

@@ -151,7 +151,7 @@ function reconcileLines(array $row, &$later = 0) {
global $mysqli;
$id = intval($row['reconciliation_id']);
$date = escapeSql($row['reconciliation_statement_date']);
$sql = mysqli_query($mysqli, "SELECT ledger.*, IF(ledger.txn_reconciliation_id = $id, 1, 0) AS ticked FROM (" . accountLedgerSql($row['reconciliation_account_id']) . ") AS ledger
$sql = mysqli_query($mysqli, "SELECT ledger.*, IF(ledger.txn_reconciliation_id = $id, 1, 0) AS ticked FROM (" . accountLedgerSql($row['reconciliation_account_id'], false) . ") AS ledger
WHERE ledger.txn_reconciliation_id = $id OR (ledger.txn_reconciliation_id IS NULL AND ledger.txn_date <= '$date')
ORDER BY ledger.txn_date ASC, ledger.txn_created_at ASC, ledger.txn_order ASC, ledger.txn_id ASC");
$lines = [];
@@ -159,7 +159,7 @@ function reconcileLines(array $row, &$later = 0) {
$line['ticked'] = intval($line['ticked']) === 1;
$lines[] = $line;
}
$later = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM (" . accountLedgerSql($row['reconciliation_account_id']) . ") AS ledger
$later = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM (" . accountLedgerSql($row['reconciliation_account_id'], false) . ") AS ledger
WHERE ledger.txn_reconciliation_id IS NULL AND ledger.txn_date > '$date'"))[0]);
return $lines;
}