Calculating balance sheet with a single query
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{ margin-bottom:0;
}
I have this schema for double entry bookkeeping system:
CREATE TABLE account (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES account(id),
UNIQUE (name, parent_id)
);
CREATE TABLE journal (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
date DATE NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE posting (
id SERIAL PRIMARY KEY,
account_id INTEGER NOT NULL REFERENCES account(id),
journal_id INTEGER NOT NULL REFERENCES journal(id),
amount NUMERIC (12, 2) NOT NULL,
-- debit and credit are described by positive and negative value, therefore
-- zero value is not allowed
CHECK (amount <> 0)
);
So, basically each transaction will be recorded in journal
table and it's amount that is affecting account
table will be recorded in posting
table.
Since the structure of account is a hierarchical tree, the amount of each node must be calculated with all of it's descendant.
This is the best I can come up with:
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree(id, parent_id, name, path) AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path -- path from root
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
),
-- make a flat table. each account will appear as whenever it's child is
-- appeared
flatten AS (
SELECT unnest(tree.path) AS id,
tree.id AS account_id
FROM tree
ORDER BY unnest(tree.path), tree.id
)
SELECT tree.id AS id,
tree.name AS name,
coalesce(abs(sum(posting.amount)), 0) AS amount
FROM tree
LEFT OUTER JOIN flatten ON tree.id = flatten.id
LEFT OUTER JOIN posting ON posting.account_id = flatten.account_id
GROUP BY tree.id, tree.name
ORDER BY tree.id
My question is, Can this be improved? Will it perform well?
PS: Some data for testing code
INSERT INTO account VALUES (100, 'ASSET', NULL);
INSERT INTO account VALUES (101, 'Cash', 100);
INSERT INTO account VALUES (102, 'Bank', 100);
INSERT INTO account VALUES (200, 'LIABILITY', NULL);
INSERT INTO account VALUES (201, 'Loan', 200);
INSERT INTO account VALUES (300, 'EQUITY', NULL);
INSERT INTO account VALUES (301, 'Opening Balance', 300);
INSERT INTO journal VALUES (1, 'opening balance', '2018-12-05');
INSERT INTO posting VALUES (1, 101, 1, 1000000.00);
INSERT INTO posting VALUES (2, 301, 1, -1000000.00);
INSERT INTO journal VALUES (3, 'save to bank', '2018-12-05');
INSERT INTO posting VALUES (5, 102, 3, 1250000.00);
INSERT INTO posting VALUES (6, 101, 3, -1250000.00);
INSERT INTO journal VALUES (2, 'loan money', '2018-12-05');
INSERT INTO posting VALUES (3, 101, 2, 500000.00);
INSERT INTO posting VALUES (4, 201, 2, -500000.00);
The output (for running the query above) should be something like this:
id | name | amount
-----+-----------------+------------
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
(7 rows)
postgresql
bumped to the homepage by Community♦ 16 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
add a comment |
I have this schema for double entry bookkeeping system:
CREATE TABLE account (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES account(id),
UNIQUE (name, parent_id)
);
CREATE TABLE journal (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
date DATE NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE posting (
id SERIAL PRIMARY KEY,
account_id INTEGER NOT NULL REFERENCES account(id),
journal_id INTEGER NOT NULL REFERENCES journal(id),
amount NUMERIC (12, 2) NOT NULL,
-- debit and credit are described by positive and negative value, therefore
-- zero value is not allowed
CHECK (amount <> 0)
);
So, basically each transaction will be recorded in journal
table and it's amount that is affecting account
table will be recorded in posting
table.
Since the structure of account is a hierarchical tree, the amount of each node must be calculated with all of it's descendant.
This is the best I can come up with:
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree(id, parent_id, name, path) AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path -- path from root
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
),
-- make a flat table. each account will appear as whenever it's child is
-- appeared
flatten AS (
SELECT unnest(tree.path) AS id,
tree.id AS account_id
FROM tree
ORDER BY unnest(tree.path), tree.id
)
SELECT tree.id AS id,
tree.name AS name,
coalesce(abs(sum(posting.amount)), 0) AS amount
FROM tree
LEFT OUTER JOIN flatten ON tree.id = flatten.id
LEFT OUTER JOIN posting ON posting.account_id = flatten.account_id
GROUP BY tree.id, tree.name
ORDER BY tree.id
My question is, Can this be improved? Will it perform well?
PS: Some data for testing code
INSERT INTO account VALUES (100, 'ASSET', NULL);
INSERT INTO account VALUES (101, 'Cash', 100);
INSERT INTO account VALUES (102, 'Bank', 100);
INSERT INTO account VALUES (200, 'LIABILITY', NULL);
INSERT INTO account VALUES (201, 'Loan', 200);
INSERT INTO account VALUES (300, 'EQUITY', NULL);
INSERT INTO account VALUES (301, 'Opening Balance', 300);
INSERT INTO journal VALUES (1, 'opening balance', '2018-12-05');
INSERT INTO posting VALUES (1, 101, 1, 1000000.00);
INSERT INTO posting VALUES (2, 301, 1, -1000000.00);
INSERT INTO journal VALUES (3, 'save to bank', '2018-12-05');
INSERT INTO posting VALUES (5, 102, 3, 1250000.00);
INSERT INTO posting VALUES (6, 101, 3, -1250000.00);
INSERT INTO journal VALUES (2, 'loan money', '2018-12-05');
INSERT INTO posting VALUES (3, 101, 2, 500000.00);
INSERT INTO posting VALUES (4, 201, 2, -500000.00);
The output (for running the query above) should be something like this:
id | name | amount
-----+-----------------+------------
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
(7 rows)
postgresql
bumped to the homepage by Community♦ 16 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
1
This is really well prepared, I wish all newcomers would do that. However, some sample data (e.g. asinsert into
) and the expected output would be helpful as well.
– a_horse_with_no_name
Dec 5 '18 at 11:00
Update with data for testing.
– Mas Bagol
Dec 5 '18 at 11:58
add a comment |
I have this schema for double entry bookkeeping system:
CREATE TABLE account (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES account(id),
UNIQUE (name, parent_id)
);
CREATE TABLE journal (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
date DATE NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE posting (
id SERIAL PRIMARY KEY,
account_id INTEGER NOT NULL REFERENCES account(id),
journal_id INTEGER NOT NULL REFERENCES journal(id),
amount NUMERIC (12, 2) NOT NULL,
-- debit and credit are described by positive and negative value, therefore
-- zero value is not allowed
CHECK (amount <> 0)
);
So, basically each transaction will be recorded in journal
table and it's amount that is affecting account
table will be recorded in posting
table.
Since the structure of account is a hierarchical tree, the amount of each node must be calculated with all of it's descendant.
This is the best I can come up with:
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree(id, parent_id, name, path) AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path -- path from root
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
),
-- make a flat table. each account will appear as whenever it's child is
-- appeared
flatten AS (
SELECT unnest(tree.path) AS id,
tree.id AS account_id
FROM tree
ORDER BY unnest(tree.path), tree.id
)
SELECT tree.id AS id,
tree.name AS name,
coalesce(abs(sum(posting.amount)), 0) AS amount
FROM tree
LEFT OUTER JOIN flatten ON tree.id = flatten.id
LEFT OUTER JOIN posting ON posting.account_id = flatten.account_id
GROUP BY tree.id, tree.name
ORDER BY tree.id
My question is, Can this be improved? Will it perform well?
PS: Some data for testing code
INSERT INTO account VALUES (100, 'ASSET', NULL);
INSERT INTO account VALUES (101, 'Cash', 100);
INSERT INTO account VALUES (102, 'Bank', 100);
INSERT INTO account VALUES (200, 'LIABILITY', NULL);
INSERT INTO account VALUES (201, 'Loan', 200);
INSERT INTO account VALUES (300, 'EQUITY', NULL);
INSERT INTO account VALUES (301, 'Opening Balance', 300);
INSERT INTO journal VALUES (1, 'opening balance', '2018-12-05');
INSERT INTO posting VALUES (1, 101, 1, 1000000.00);
INSERT INTO posting VALUES (2, 301, 1, -1000000.00);
INSERT INTO journal VALUES (3, 'save to bank', '2018-12-05');
INSERT INTO posting VALUES (5, 102, 3, 1250000.00);
INSERT INTO posting VALUES (6, 101, 3, -1250000.00);
INSERT INTO journal VALUES (2, 'loan money', '2018-12-05');
INSERT INTO posting VALUES (3, 101, 2, 500000.00);
INSERT INTO posting VALUES (4, 201, 2, -500000.00);
The output (for running the query above) should be something like this:
id | name | amount
-----+-----------------+------------
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
(7 rows)
postgresql
I have this schema for double entry bookkeeping system:
CREATE TABLE account (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES account(id),
UNIQUE (name, parent_id)
);
CREATE TABLE journal (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
date DATE NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE posting (
id SERIAL PRIMARY KEY,
account_id INTEGER NOT NULL REFERENCES account(id),
journal_id INTEGER NOT NULL REFERENCES journal(id),
amount NUMERIC (12, 2) NOT NULL,
-- debit and credit are described by positive and negative value, therefore
-- zero value is not allowed
CHECK (amount <> 0)
);
So, basically each transaction will be recorded in journal
table and it's amount that is affecting account
table will be recorded in posting
table.
Since the structure of account is a hierarchical tree, the amount of each node must be calculated with all of it's descendant.
This is the best I can come up with:
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree(id, parent_id, name, path) AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path -- path from root
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
),
-- make a flat table. each account will appear as whenever it's child is
-- appeared
flatten AS (
SELECT unnest(tree.path) AS id,
tree.id AS account_id
FROM tree
ORDER BY unnest(tree.path), tree.id
)
SELECT tree.id AS id,
tree.name AS name,
coalesce(abs(sum(posting.amount)), 0) AS amount
FROM tree
LEFT OUTER JOIN flatten ON tree.id = flatten.id
LEFT OUTER JOIN posting ON posting.account_id = flatten.account_id
GROUP BY tree.id, tree.name
ORDER BY tree.id
My question is, Can this be improved? Will it perform well?
PS: Some data for testing code
INSERT INTO account VALUES (100, 'ASSET', NULL);
INSERT INTO account VALUES (101, 'Cash', 100);
INSERT INTO account VALUES (102, 'Bank', 100);
INSERT INTO account VALUES (200, 'LIABILITY', NULL);
INSERT INTO account VALUES (201, 'Loan', 200);
INSERT INTO account VALUES (300, 'EQUITY', NULL);
INSERT INTO account VALUES (301, 'Opening Balance', 300);
INSERT INTO journal VALUES (1, 'opening balance', '2018-12-05');
INSERT INTO posting VALUES (1, 101, 1, 1000000.00);
INSERT INTO posting VALUES (2, 301, 1, -1000000.00);
INSERT INTO journal VALUES (3, 'save to bank', '2018-12-05');
INSERT INTO posting VALUES (5, 102, 3, 1250000.00);
INSERT INTO posting VALUES (6, 101, 3, -1250000.00);
INSERT INTO journal VALUES (2, 'loan money', '2018-12-05');
INSERT INTO posting VALUES (3, 101, 2, 500000.00);
INSERT INTO posting VALUES (4, 201, 2, -500000.00);
The output (for running the query above) should be something like this:
id | name | amount
-----+-----------------+------------
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
(7 rows)
postgresql
postgresql
edited Dec 5 '18 at 11:58
Mas Bagol
asked Dec 5 '18 at 10:33
Mas BagolMas Bagol
1213
1213
bumped to the homepage by Community♦ 16 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
bumped to the homepage by Community♦ 16 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
1
This is really well prepared, I wish all newcomers would do that. However, some sample data (e.g. asinsert into
) and the expected output would be helpful as well.
– a_horse_with_no_name
Dec 5 '18 at 11:00
Update with data for testing.
– Mas Bagol
Dec 5 '18 at 11:58
add a comment |
1
This is really well prepared, I wish all newcomers would do that. However, some sample data (e.g. asinsert into
) and the expected output would be helpful as well.
– a_horse_with_no_name
Dec 5 '18 at 11:00
Update with data for testing.
– Mas Bagol
Dec 5 '18 at 11:58
1
1
This is really well prepared, I wish all newcomers would do that. However, some sample data (e.g. as
insert into
) and the expected output would be helpful as well.– a_horse_with_no_name
Dec 5 '18 at 11:00
This is really well prepared, I wish all newcomers would do that. However, some sample data (e.g. as
insert into
) and the expected output would be helpful as well.– a_horse_with_no_name
Dec 5 '18 at 11:00
Update with data for testing.
– Mas Bagol
Dec 5 '18 at 11:58
Update with data for testing.
– Mas Bagol
Dec 5 '18 at 11:58
add a comment |
1 Answer
1
active
oldest
votes
If your account
table doesn't change often, you could take advantage of Postgres Materialized Views.
CREATE MATERIALIZED VIEW account_tree
AS
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree
AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
)
SELECT
t.id,
t.account_id,
CASE WHEN t.parent_id = t.account_id
THEN a.name ELSE t.name END AS name
FROM
(SELECT id, parent_id, name, unnest(path) as account_id
FROM tree) t
JOIN
account a
ON a.id = t.parent_id;
Materialized views allows the use of index:
CREATE INDEX account_tree_ix
ON account_tree (id, account_id);
This view returns next result:
id | account_id | name
--: | ---------: | :--------------
101 | 100 | ASSET
101 | 101 | Cash
102 | 100 | ASSET
102 | 102 | Bank
201 | 200 | LIABILITY
201 | 201 | Loan
301 | 300 | EQUITY
301 | 301 | Opening Balance
Now you can use this view to obtain the balance sheet avoiding the recursive CTE:
SELECT at.account_id,
at.name,
coalesce(abs(sum(p.amount)), 0) AS amount
FROM account_tree at
LEFT JOIN posting p
ON p.account_id = at.id
GROUP BY at.account_id,
at.name
ORDER BY at.account_id;
account_id | name | amount
---------: | :-------------- | ---------:
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
db<>fiddle here
add a comment |
Your Answer
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "182"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fdba.stackexchange.com%2fquestions%2f224184%2fcalculating-balance-sheet-with-a-single-query%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
If your account
table doesn't change often, you could take advantage of Postgres Materialized Views.
CREATE MATERIALIZED VIEW account_tree
AS
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree
AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
)
SELECT
t.id,
t.account_id,
CASE WHEN t.parent_id = t.account_id
THEN a.name ELSE t.name END AS name
FROM
(SELECT id, parent_id, name, unnest(path) as account_id
FROM tree) t
JOIN
account a
ON a.id = t.parent_id;
Materialized views allows the use of index:
CREATE INDEX account_tree_ix
ON account_tree (id, account_id);
This view returns next result:
id | account_id | name
--: | ---------: | :--------------
101 | 100 | ASSET
101 | 101 | Cash
102 | 100 | ASSET
102 | 102 | Bank
201 | 200 | LIABILITY
201 | 201 | Loan
301 | 300 | EQUITY
301 | 301 | Opening Balance
Now you can use this view to obtain the balance sheet avoiding the recursive CTE:
SELECT at.account_id,
at.name,
coalesce(abs(sum(p.amount)), 0) AS amount
FROM account_tree at
LEFT JOIN posting p
ON p.account_id = at.id
GROUP BY at.account_id,
at.name
ORDER BY at.account_id;
account_id | name | amount
---------: | :-------------- | ---------:
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
db<>fiddle here
add a comment |
If your account
table doesn't change often, you could take advantage of Postgres Materialized Views.
CREATE MATERIALIZED VIEW account_tree
AS
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree
AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
)
SELECT
t.id,
t.account_id,
CASE WHEN t.parent_id = t.account_id
THEN a.name ELSE t.name END AS name
FROM
(SELECT id, parent_id, name, unnest(path) as account_id
FROM tree) t
JOIN
account a
ON a.id = t.parent_id;
Materialized views allows the use of index:
CREATE INDEX account_tree_ix
ON account_tree (id, account_id);
This view returns next result:
id | account_id | name
--: | ---------: | :--------------
101 | 100 | ASSET
101 | 101 | Cash
102 | 100 | ASSET
102 | 102 | Bank
201 | 200 | LIABILITY
201 | 201 | Loan
301 | 300 | EQUITY
301 | 301 | Opening Balance
Now you can use this view to obtain the balance sheet avoiding the recursive CTE:
SELECT at.account_id,
at.name,
coalesce(abs(sum(p.amount)), 0) AS amount
FROM account_tree at
LEFT JOIN posting p
ON p.account_id = at.id
GROUP BY at.account_id,
at.name
ORDER BY at.account_id;
account_id | name | amount
---------: | :-------------- | ---------:
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
db<>fiddle here
add a comment |
If your account
table doesn't change often, you could take advantage of Postgres Materialized Views.
CREATE MATERIALIZED VIEW account_tree
AS
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree
AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
)
SELECT
t.id,
t.account_id,
CASE WHEN t.parent_id = t.account_id
THEN a.name ELSE t.name END AS name
FROM
(SELECT id, parent_id, name, unnest(path) as account_id
FROM tree) t
JOIN
account a
ON a.id = t.parent_id;
Materialized views allows the use of index:
CREATE INDEX account_tree_ix
ON account_tree (id, account_id);
This view returns next result:
id | account_id | name
--: | ---------: | :--------------
101 | 100 | ASSET
101 | 101 | Cash
102 | 100 | ASSET
102 | 102 | Bank
201 | 200 | LIABILITY
201 | 201 | Loan
301 | 300 | EQUITY
301 | 301 | Opening Balance
Now you can use this view to obtain the balance sheet avoiding the recursive CTE:
SELECT at.account_id,
at.name,
coalesce(abs(sum(p.amount)), 0) AS amount
FROM account_tree at
LEFT JOIN posting p
ON p.account_id = at.id
GROUP BY at.account_id,
at.name
ORDER BY at.account_id;
account_id | name | amount
---------: | :-------------- | ---------:
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
db<>fiddle here
If your account
table doesn't change often, you could take advantage of Postgres Materialized Views.
CREATE MATERIALIZED VIEW account_tree
AS
WITH RECURSIVE
-- recursively build tree table, assigning path for each row
tree
AS (
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
ARRAY[account.id] AS path
FROM account
WHERE account.parent_id IS NULL
UNION ALL
SELECT account.id AS id,
account.parent_id AS parent_id,
account.name AS name,
tree.path || ARRAY[account.id] AS path
FROM account, tree
WHERE account.parent_id = tree.id
)
SELECT
t.id,
t.account_id,
CASE WHEN t.parent_id = t.account_id
THEN a.name ELSE t.name END AS name
FROM
(SELECT id, parent_id, name, unnest(path) as account_id
FROM tree) t
JOIN
account a
ON a.id = t.parent_id;
Materialized views allows the use of index:
CREATE INDEX account_tree_ix
ON account_tree (id, account_id);
This view returns next result:
id | account_id | name
--: | ---------: | :--------------
101 | 100 | ASSET
101 | 101 | Cash
102 | 100 | ASSET
102 | 102 | Bank
201 | 200 | LIABILITY
201 | 201 | Loan
301 | 300 | EQUITY
301 | 301 | Opening Balance
Now you can use this view to obtain the balance sheet avoiding the recursive CTE:
SELECT at.account_id,
at.name,
coalesce(abs(sum(p.amount)), 0) AS amount
FROM account_tree at
LEFT JOIN posting p
ON p.account_id = at.id
GROUP BY at.account_id,
at.name
ORDER BY at.account_id;
account_id | name | amount
---------: | :-------------- | ---------:
100 | ASSET | 1500000.00
101 | Cash | 250000.00
102 | Bank | 1250000.00
200 | LIABILITY | 500000.00
201 | Loan | 500000.00
300 | EQUITY | 1000000.00
301 | Opening Balance | 1000000.00
db<>fiddle here
edited Dec 17 '18 at 7:51
answered Dec 16 '18 at 0:42
McNetsMcNets
16.4k42261
16.4k42261
add a comment |
add a comment |
Thanks for contributing an answer to Database Administrators Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fdba.stackexchange.com%2fquestions%2f224184%2fcalculating-balance-sheet-with-a-single-query%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
1
This is really well prepared, I wish all newcomers would do that. However, some sample data (e.g. as
insert into
) and the expected output would be helpful as well.– a_horse_with_no_name
Dec 5 '18 at 11:00
Update with data for testing.
– Mas Bagol
Dec 5 '18 at 11:58