<pre lang=”sql”>Tempos atrás me deparei com um problema que era criar uma função para gerar uma CURVA ABC. Eu não tinha a menor idéia do que ser tratava, mas resumindo, uma Curva ABC é uma separação entre faixas de valores, como os 20% mais ricos e o 20% mais pobres. Se quiser mais detalhes, veja a definição do Wikipédia. Apesar do conceito ser simples, fazer isso com SQL convencional não é. E quem me conhece, sabe que eu só utilizo PL no banco de dados, quando é absolutamente necessário. O motivo é simples, o PL não deixa muita margem para o otimizador do banco de dados encontrar a melhor forma de agrupar os dados. E eu sei que o otimizador é muito mais inteligente que eu.
Bom, resumindo a história, eu precisei lançar mão das novas funções de janela, incorporadas na versão 8.4 do PostgreSQL. Para exemplificar aqui, vou utilizar uma base de exemplo conhecida de todos, o PAGILA. Eu tentei o DELLSTORE, mas os dados não apareceram tão bem distribuídos para efeito de visualização.
Bom, depois de criar uma base e importar os dados do PAGILA, eu criei uma visão para facilitar a minha vida e simplificar a análise, deixando de lado detalhes desnecessários:
CREATE OR REPLACE VIEW sales_pgday AS
SELECT
f.title,
c.name AS category,
SUM(p.amount) AS total_sales
FROM
(((((payment p JOIN rental r ON ((p.rental_id = r.rental_id)))
JOIN inventory i ON ((r.inventory_id = i.inventory_id)))
JOIN film f ON ((i.film_id = f.film_id)))
JOIN film_category fc ON ((f.film_id = fc.film_id)))
JOIN category c ON ((fc.category_id = c.category_id)))
GROUP BY c.name, f.title
;
Com um select você verá os dados mais ou menos assim:
title | category | total_sales
-----------------------------+-------------+-------------
AMADEUS HOLY | Action | 33.79
AMERICAN CIRCUS | Action | 167.78
ANTITRUST TOMATOES | Action | 37.90
BAREFOOT MANCHURIAN | Action | 66.82
BERETS AGENT | Action | 78.78
BRIDE INTRIGUE | Action | 21.81
BULL SHAWSHANK | Action | 21.84
CADDYSHACK JEDI | Action | 51.84
CAMPUS REMEMBER | Action | 90.81
CASUALTIES ENCINO | Action | 72.91
CELEBRITY HORN | Action | 32.76
CLUELESS BUCKET | Action | 112.75
CROW GREASE | Action | 18.88
DANCES NONE | Action | 31.86
DARKO DORADO | Action | 82.89
DARN FORRESTER | Action | 93.82
DEVIL DESIRE | Action | 83.85
DRAGON SQUAD | Action | 27.89
DREAM PICKUP | Action | 81.78
DRIFTER COMMANDMENTS | Action | 141.76
...
O primeiro passo para calcular a curva ABC sobre as vendas de cada filme, para cada categoria de filme (os filmes mais vendidos e os menos vendidos em cada categoria) é calcular o total de vendas. Uma vez que queremos os 20% mais vendidos e os 20% menos vendidos, precisamos do total (100%) para calcular a porcentagem:
SELECT category, SUM(total_sales)
FROM sales_pgday
GROUP BY category;
E assim teremos:
category | sum
-------------+---------
Action | 4375.85
Animation | 4656.30
Children | 3655.55
Classics | 3639.59
Comedy | 4383.58
Documentary | 4217.52
Drama | 4587.39
Family | 4226.07
Foreign | 4270.67
Games | 4281.33
Horror | 3722.54
Music | 3417.72
New | 4361.57
Sci-Fi | 4756.98
Sports | 5314.21
Travel | 3549.64
(16 linhas)
Agora vejamos uma função de janela em ação aqui. Ainda não é a que vamos utilizar, é apenas um exemplo:
SELECT
title,
category,
total_sales,
rank() OVER (ORDER BY total_sales DESC) AS rank
FROM sales_pgday;
Agora vejamos o resultado:
title | category | total_sales | rank
-----------------------------+-------------+-------------+------
TELEGRAPH VOYAGE | Music | 231.73 | 1
WIFE TURN | Documentary | 223.69 | 2
ZORRO ARK | Comedy | 214.69 | 3
GOODFELLAS SALUTE | Sci-Fi | 209.69 | 4
SATURDAY LAMBS | Sports | 204.72 | 5
TITANS JERK | Sci-Fi | 201.71 | 6
TORQUE BOUND | Drama | 198.72 | 7
HARRY IDAHO | Drama | 195.70 | 8
INNOCENT USUAL | Foreign | 191.74 | 9
HUSTLER PARTY | Comedy | 190.78 | 10
PELICAN COMFORTS | Documentary | 188.74 | 11
CAT CONEHEADS | Comedy | 181.70 | 12
ENEMY ODDS | Music | 180.71 | 13
BUCKET BROTHERHOOD | Travel | 180.66 | 14
RANGE MOONWALKER | Family | 179.73 | 15
MASSACRE USUAL | Games | 179.70 | 16
VIDEOTAPE ARSENIC | Games | 178.71 | 17
DOGMA FAMILY | Animation | 178.70 | 18
APACHE DIVINE | Family | 178.69 | 19
VELVET TERMINATOR | Comedy | 177.74 | 20
...
Agora vamos olhar com calma os dados (tirei aqui apenas as 20 primeiras linhas) e a parte do SELECT que diz:
rank() OVER (ORDER BY total_sales DESC) AS rank
Veja, o campo está utilizando a função RANK, sobre uma janela de dados ordenados pelo total de vendas. O campo RANK simplesmente diz qual é mais significativo, sobre a amostra (OVER) determinada. Agora queremos saber quais são os mais vendidos por categoria. O curioso é que ao invés de utilizar o GROUP BY que afetaria todas as colunas, vamos utilizar o PARTITION BY dentro da definição da janela, assim, isto afetará apenas a nossa última coluna:
SELECT
title,
category,
total_sales,
rank() OVER (PARTITION BY category ORDER BY total_sales DESC) AS rank
FROM sales_pgday;
Assim teremos:
title | category | total_sales | rank
-----------------------------+-------------+-------------+------
FOOL MOCKINGBIRD | Action | 175.77 | 1
AMERICAN CIRCUS | Action | 167.78 | 2
STAGECOACH ARMAGEDDON | Action | 154.74 | 3
EASY GLADIATOR | Action | 150.77 | 4
MINDS TRUMAN | Action | 149.80 | 5
KISSING DOLLS | Action | 147.80 | 6
TRIP NEWTON | Action | 145.72 | 7
DRIFTER COMMANDMENTS | Action | 141.76 | 8
SUSPECTS QUILLS | Action | 133.70 | 9
WATERFRONT DELIVERANCE | Action | 121.83 | 10
...
DRAGON SQUAD | Action | 27.89 | 56
BULL SHAWSHANK | Action | 21.84 | 57
BRIDE INTRIGUE | Action | 21.81 | 58
CROW GREASE | Action | 18.88 | 59
LAWRENCE LOVE | Action | 15.87 | 60
MONTEZUMA COMMAND | Action | 11.91 | 61
DOGMA FAMILY | Animation | 178.70 | 1
SUNRISE LEAGUE | Animation | 170.76 | 2
TITANIC BOONDOCK | Animation | 154.77 | 3
FORRESTER COMANCHEROS | Animation | 146.73 | 4
FALCON VOLUME | Animation | 127.77 | 5
...
Notem que agora o rank é realizado para cada categoria, o rank é ordenado pelas vendas. Vale a pena executar o SQL no seu computador para verificar os dados com mais calma.
Bom, mas eu quero saber dos 20% mais vendidos. O problema aqui é que você precisaria ir somando o total de vendas de cada produto até chegar em 20% do total. Ocorre que você pode utilizar as funções de agregação como SUM, COUNT e outras junto com a sua janela. Vejamos como isso funciona:
SELECT
title,
category,
total_sales,
rank() OVER (PARTITION BY category ORDER BY total_sales DESC) AS rank,
SUM(total_sales) OVER (PARTITION BY category ORDER BY total_sales DESC) AS SUM
FROM sales_pgday;
E teremos os seguintes dados:
title | category | total_sales | rank | sum
-----------------------------+-------------+-------------+------+---------
FOOL MOCKINGBIRD | Action | 175.77 | 1 | 175.77
AMERICAN CIRCUS | Action | 167.78 | 2 | 343.55
STAGECOACH ARMAGEDDON | Action | 154.74 | 3 | 498.29
EASY GLADIATOR | Action | 150.77 | 4 | 649.06
MINDS TRUMAN | Action | 149.80 | 5 | 798.86
KISSING DOLLS | Action | 147.80 | 6 | 946.66
TRIP NEWTON | Action | 145.72 | 7 | 1092.38
DRIFTER COMMANDMENTS | Action | 141.76 | 8 | 1234.14
SUSPECTS QUILLS | Action | 133.70 | 9 | 1367.84
WATERFRONT DELIVERANCE | Action | 121.83 | 10 | 1489.67
TRUMAN CRAZY | Action | 121.77 | 11 | 1611.44
CLUELESS BUCKET | Action | 112.75 | 12 | 1724.19
SOUTH WAIT | Action | 107.78 | 13 | 1831.97
DARN FORRESTER | Action | 93.82 | 14 | 1925.79
CAMPUS REMEMBER | Action | 90.81 | 15 | 2016.60
FORREST SONS | Action | 87.82 | 16 | 2104.42
DEVIL DESIRE | Action | 83.85 | 17 | 2188.27
DARKO DORADO | Action | 82.89 | 18 | 2271.16
DREAM PICKUP | Action | 81.78 | 19 | 2352.94
WEREWOLF LOLA | Action | 78.86 | 20 | 2431.80
...
REAR TRADING | Action | 32.83 | 52 | 4163.15
CELEBRITY HORN | Action | 32.76 | 53 | 4195.91
DANCES NONE | Action | 31.86 | 54 | 4227.77
SIDE ARK | Action | 29.88 | 55 | 4257.65
DRAGON SQUAD | Action | 27.89 | 56 | 4285.54
BULL SHAWSHANK | Action | 21.84 | 57 | 4307.38
BRIDE INTRIGUE | Action | 21.81 | 58 | 4329.19
CROW GREASE | Action | 18.88 | 59 | 4348.07
LAWRENCE LOVE | Action | 15.87 | 60 | 4363.94
MONTEZUMA COMMAND | Action | 11.91 | 61 | 4375.85
DOGMA FAMILY | Animation | 178.70 | 1 | 178.70
SUNRISE LEAGUE | Animation | 170.76 | 2 | 349.46
TITANIC BOONDOCK | Animation | 154.77 | 3 | 504.23
FORRESTER COMANCHEROS | Animation | 146.73 | 4 | 650.96
FALCON VOLUME | Animation | 127.77 | 5 | 778.73
MISSION ZOOLANDER | Animation | 126.82 | 6 | 905.55
DOORS PRESIDENT | Animation | 123.81 | 7 | 1029.36
SLEEPLESS MONSOON | Animation | 121.80 | 8 | 1151.16
THIEF PELICAN | Animation | 117.81 | 9 | 1268.97
HORN WORKING | Animation | 112.76 | 10 | 1381.73
...
Note que o valor da soma (SUM) vai acumulando registro a registro até que uma nova categoria apareça. Isto é uma coisa muito difícil de se fazer com SQL puro, você não consegue fazer cálculos com base nos registros anteriores.
Agora nós podemos juntar esta última consulta com a primeira para calcular a porcentagem de vendas sobre o total da categoria. Vamos colocar cada uma das consultas como uma entrada em FROM:
SELECT
sales.category,
title,
somatorio,
total,
(somatorio / total) * 100 AS porcentagem
FROM
(SELECT
category,
SUM(total_sales) total
FROM sales_pgday
GROUP BY category) AS tot,
(SELECT
title,
category,
SUM(total_sales) OVER (PARTITION BY category ORDER BY total_sales DESC) AS somatorio
FROM sales_pgday) sales
WHERE tot.category = sales.category;
O resultado será:
category | title | somatorio | total | porcentagem
-------------+-----------------------------+-----------+---------+--------------------------
Action | FOOL MOCKINGBIRD | 175.77 | 4375.85 | 4.01681958933692882500
Action | AMERICAN CIRCUS | 343.55 | 4375.85 | 7.85104608247540477900
Action | STAGECOACH ARMAGEDDON | 498.29 | 4375.85 | 11.38727332975307654500
Action | EASY GLADIATOR | 649.06 | 4375.85 | 14.83277534650410777300
Action | MINDS TRUMAN | 798.86 | 4375.85 | 18.25611024143880617500
Action | KISSING DOLLS | 946.66 | 4375.85 | 21.63373973056663276800
Action | TRIP NEWTON | 1092.38 | 4375.85 | 24.96383559765531268200
Action | DRIFTER COMMANDMENTS | 1234.14 | 4375.85 | 28.20343476124638641600
Action | SUSPECTS QUILLS | 1367.84 | 4375.85 | 31.25884113943576676500
Action | WATERFRONT DELIVERANCE | 1489.67 | 4375.85 | 34.04298593416136293500
Action | TRUMAN CRAZY | 1611.44 | 4375.85 | 36.82575956671275295100
Action | CLUELESS BUCKET | 1724.19 | 4375.85 | 39.40240181907515111300
Action | SOUTH WAIT | 1831.97 | 4375.85 | 41.86546613800747283400
Action | DARN FORRESTER | 1925.79 | 4375.85 | 44.00950672440782933600
Action | CAMPUS REMEMBER | 2016.60 | 4375.85 | 46.08476067506884376700
...
Action | REAR TRADING | 4163.15 | 4375.85 | 95.13923009243918324400
Action | CELEBRITY HORN | 4195.91 | 4375.85 | 95.88788463955574345600
Action | DANCES NONE | 4227.77 | 4375.85 | 96.61597175405921135300
Action | SIDE ARK | 4257.65 | 4375.85 | 97.29881051681387616100
Action | DRAGON SQUAD | 4285.54 | 4375.85 | 97.93617240079070352000
Action | BULL SHAWSHANK | 4307.38 | 4375.85 | 98.43527543220174366100
Action | BRIDE INTRIGUE | 4329.19 | 4375.85 | 98.93369288252568072500
Action | CROW GREASE | 4348.07 | 4375.85 | 99.36515191334255059000
Action | LAWRENCE LOVE | 4363.94 | 4375.85 | 99.72782430842007838500
Action | MONTEZUMA COMMAND | 4375.85 | 4375.85 | 100.00000000000000000000
Animation | DOGMA FAMILY | 178.70 | 4656.30 | 3.83781113759852243200
Animation | SUNRISE LEAGUE | 349.46 | 4656.30 | 7.50510061636922019600
Animation | TITANIC BOONDOCK | 504.23 | 4656.30 | 10.82898438674484032400
Animation | FORRESTER COMANCHEROS | 650.96 | 4656.30 | 13.98019887034770096400
Animation | FALCON VOLUME | 778.73 | 4656.30 | 16.72422309559091982900
Animation | MISSION ZOOLANDER | 905.55 | 4656.30 | 19.44784485535725790900
Animation | DOORS PRESIDENT | 1029.36 | 4656.30 | 22.10682301398105792200
Animation | SLEEPLESS MONSOON | 1151.16 | 4656.30 | 24.72263385091166806300
...
O resultado é a porcentagem do total de vendas sendo acumulado até 100% e depois começando novamente para outra categoria. Com isso fica fácil atribuir A para os 20% mais significativos, B para os próximos 60% e C para os 20% menos significativos. Aqui, irei utilizar uma função CASE, que embora ocupe um bom espaço no nosso SQL é algo bastante trivial:
SELECT
sales.category,
title,
TRUNC((somatorio / total) * 100) AS porcentagem,
CASE
WHEN (somatorio / total) <= 0.2 THEN 'A'
WHEN (somatorio / total) > 0.2 AND (somatorio / total) <= 0.8 THEN 'B'
ELSE 'C' END AS curva_abc
FROM
(SELECT
category,
SUM(total_sales) total
FROM sales_pgday
GROUP BY category) AS tot,
(SELECT
title,
category,
SUM(total_sales) OVER (PARTITION BY category ORDER BY total_sales DESC) AS somatorio
FROM sales_pgday) sales
WHERE tot.category = sales.category;
E finalmente temos a nossa curva ABC:
category | title | porcentagem | curva_abc
-------------+-----------------------------+-------------+-----------
Action | FOOL MOCKINGBIRD | 4 | A
Action | AMERICAN CIRCUS | 7 | A
Action | STAGECOACH ARMAGEDDON | 11 | A
Action | EASY GLADIATOR | 14 | A
Action | MINDS TRUMAN | 18 | A
Action | KISSING DOLLS | 21 | B
Action | TRIP NEWTON | 24 | B
Action | DRIFTER COMMANDMENTS | 28 | B
Action | SUSPECTS QUILLS | 31 | B
Action | WATERFRONT DELIVERANCE | 34 | B
Action | TRUMAN CRAZY | 36 | B
...
Action | EXCITEMENT EVE | 69 | B
Action | BAREFOOT MANCHURIAN | 70 | B
Action | HANDICAP BOONDOCK | 72 | B
Action | PARK CITIZEN | 73 | B
Action | UPRISING UPTOWN | 74 | B
Action | MOCKINGBIRD HOLLYWOOD | 76 | B
Action | PATRIOT ROMAN | 77 | B
Action | GRAIL FRANKENSTEIN | 78 | B
Action | SHRUNK DIVINE | 79 | B
Action | CADDYSHACK JEDI | 81 | C
Action | GOSFORD DONNIE | 82 | C
Action | ENTRAPMENT SATISFACTION | 83 | C
Action | SPEAKEASY DATE | 84 | C
Action | MIDNIGHT WESTWARD | 85 | C
Action | FANTASY TROOPERS | 86 | C
Action | WOMEN DORADO | 87 | C
...
Action | DRAGON SQUAD | 97 | C
Action | BULL SHAWSHANK | 98 | C
Action | BRIDE INTRIGUE | 98 | C
Action | CROW GREASE | 99 | C
Action | LAWRENCE LOVE | 99 | C
Action | MONTEZUMA COMMAND | 100 | C
Animation | DOGMA FAMILY | 3 | A
Animation | SUNRISE LEAGUE | 7 | A
Animation | TITANIC BOONDOCK | 10 | A
Animation | FORRESTER COMANCHEROS | 13 | A
Animation | FALCON VOLUME | 16 | A
Animation | MISSION ZOOLANDER | 19 | A
Animation | DOORS PRESIDENT | 22 | B
Animation | SLEEPLESS MONSOON | 24 | B
Animation | THIEF PELICAN | 27 | B
Animation | HORN WORKING | 29 | B
Animation | GANGS PRIDE | 32 | B
...
É claro que existem N coisas que você pode fazer com funções de janela. A documentação possui algumas explicações sobre este recurso e também uma lista com várias funções que você pode testar com a sua base, aproveitando como ponto de partida os exemplos mostrados aqui.
Boa diversão!