How to calculate income tax for a slab based taxation system?

  • Hi guys,

    I am stuck with a task & not getting there? 

    I want to calculate income tax based on a taxation system. The thing is for a specific amount say $45000 for example, the tax would be $6895.00 as per below : 

                              Income                                                                                     Amount                                                                                               Income Tax

    Income up to $14000, taxed at 10.5%                                                              $14,000.00                                                                                          = $1470.00
    Income over $14000 and up to $48000, taxed at 17.5%                                 $31,000.00                                                                                          = $5425.00
    Income over $48000 and up to $70000, taxed at 30% ($0.00 in this case)
    Remaining income over $70000, taxed at 33% 
    ($0.00 in this case)

    Total tax for $45000 is $6895.00

    Can someone please help me on how can I implement it in SQL query? Thanks a lot in advance. 🙂

  • I'd strongly urge you to make this table driven rather than hard-coding any of this.  If we know anything, it's that taxes always go up!


    DROP TABLE IF EXISTS #tax_brackets;

    CREATE TABLE #tax_brackets (
        income_minimum decimal(9, 2) NULL,
        income decimal(9, 2) NOT NULL,
        tax_rate decimal(5, 4) NOT NULL
      )
    INSERT INTO #tax_brackets (income, tax_rate)
    VALUES(14000, 0.1050),(48000, 0.1750),(70000, 0.3000),(9999999.99,0.3300)

    --dynamically determine the lower range for each tax bracket based on the values
    --in the table, i.e. NOT hard-coded.
    UPDATE tb
    SET income_minimum = ISNULL(tb_min.income, 0)
    --SELECT *
    FROM #tax_brackets tb
    OUTER APPLY (
        SELECT TOP (1) tb_min.*
       FROM #tax_brackets tb_min
        WHERE tb_min.income < tb.income
        ORDER BY tb_min.income DESC
    ) AS tb_min
    SELECT 'just to see the tax brackets' AS msg, * FROM #tax_brackets

    SELECT 'actual tax calcs' AS msg, id,
        MAX(data.income) AS income, SUM(taxable_income * tax_rate) AS total_tax_due
    FROM ( VALUES(1, 12000),(2, 36500),(3, 67200),(4, 112000) ) AS data(id, income) /*sample income amounts*/
    INNER JOIN #tax_brackets tb ON data.income > tb.income_minimum
    CROSS APPLY (
        SELECT CASE WHEN data.income >= tb.income THEN tb.income - tb.income_minimum
                    ELSE data.income - income_minimum END AS taxable_income
    ) AS alias1
    GROUP BY id

    SQL DBA,SQL Server MVP(07, 08, 09) "It's a dog-eat-dog world, and I'm wearing Milk-Bone underwear." "Norm", on "Cheers". Also from "Cheers", from "Carla": "You need to know 3 things about Tortelli men: Tortelli men draw women like flies; Tortelli men treat women like flies; Tortelli men's brains are in their flies".

  • CREATE TABLE #tax_brackets (
    income_minimum decimal(9, 2) NULL,
    income decimal(9, 2) NOT NULL,
    tax_rate decimal(5, 4) NOT NULL
    )
    INSERT INTO #tax_brackets (income, tax_rate)
    -- Values for 2019
    VALUES(20711, 0.3665),(68507, 0.3810),(1000000,0.5175)
    -- Values for 2020
    -- VALUES(20711, 0.3735),(68507, 0.3735),(1000000,0.5175)
    -- Values for 2021
    -- VALUES(20711, 0.3710),(68507, 0.3710),(1000000,0.5175)

    These are the tax brackets for the Netherlands. They are based on the Euro, but they are about the same value as the US dollar.

    As can be seen taxes from 2021 (compared to 2020) go down for everybody.

    As can be seen taxes from 2020 (compared to 2019) go down for a number of people. But up for lower incomes.

    And the number of 'slabs' (???) is reduced between 2019 and 2020. First two slabs are combined.

    So taxes do not always go up. (One shouldn't limit yourself to only the US and only a limited amount of time 🙂   )

    As can be seen taxes in the Netherlands are higher than in the US. (Taxes on income)

    Thanks for your code and as can be seen inserting different numbers, the system can be used in other parts of the world as wel. This shows that the code is more universal (and still simple) so it is better. (I do realize that this is a response to an older response, but maybe tax questions come up this time of year again).

    Thanks,

    Ben

     

     

     

    a

     

  • If you add just one more column to the table, you can avoid the Triangular Joins that are created to calculate things that will not change for a given year.  And another column and the table can hold multiple years worth of tax rates and be just as effective.

    Here's the code I used to build the table with the two extra columns...

    --=====================================================================================================================
    -- Create the tax bracket table.
    -- Note that this should be a permanent table. We''re creating it as a Temp Table just for demo purposes.
    -- Be cautious because we drop this table to make reruns in SSMS easier.
    --=====================================================================================================================
    --===== If the table exists, drop the table to make reruns in SSMS easier.
    DROP TABLE IF EXISTS #TaxBracket
    ;
    GO
    --===== Create the table
    CREATE TABLE #TaxBracket
    (
    TaxYear INT NOT NULL
    ,IncomeCutoffLo DECIMAL(19,4) NOT NULL -- 15 digits to the left of the decimal point should handle anything.
    ,IncomeCutoffHi DECIMAL(19,4) NOT NULL -- 15 digits to the left of the decimal point should handle anything.
    ,BracketTaxRate DECIMAL( 5,4) NOT NULL
    --,IncomeBracketRange DECIMAL(19,4) NOT NULL -- For sanity checks only. Can remove for Prod or leave.
    --,MaxBracketRangeTax DECIMAL(19,4) NOT NULL -- For sanity checks only. Can remove for Prod or leave.
    --,CumeMaxBracketRangeTax DECIMAL(19,4) NOT NULL -- For sanity checks only. Can remove for Prod or leave.
    ,PrevCumeMaxBracketRangeTax DECIMAL(19,4) NOT NULL
    ,CONSTRAINT PK_TaxBracket PRIMARY KEY CLUSTERED (IncomeCutoffLo,TaxYear)
    ,CONSTRAINT AK_TaxBracket UNIQUE (IncomeCutoffHi,TaxYear)
    )
    ;
    --===== Populate the table with new info for the Tax Year of 2019
    WITH ctePreAgg1 AS
    (
    SELECT v.*
    ,IncomeCutoffLo = LAG(v.IncomeCutoffHi,1,0) OVER (ORDER BY v.IncomeCutoffHi)
    ,IncomeBracketRange = (v.IncomeCutoffHi-LAG(v.IncomeCutoffHi,1,0) OVER (ORDER BY v.IncomeCutoffHi))
    ,MaxBracketRangeTax = (v.IncomeCutoffHi-LAG(v.IncomeCutoffHi,1,0) OVER (ORDER BY v.IncomeCutoffHi))
    * v.BracketTaxRate
    FROM (VALUES
    (2019,14000, 0.1050)
    ,(2019,48000, 0.1750)
    ,(2019,70000, 0.3000)
    ,(2019,9999999.99,0.3300) --999999999999999.9999 should always be the final IncomeCutoff for this table
    ) v (TaxYear,IncomeCutoffHi,BracketTaxRate)
    )
    ,
    ctePreAgg2 AS
    (
    SELECT pa1.*
    ,CumeMaxBracketRangeTax = SUM(pa1.MaxBracketRangeTax) OVER (ORDER BY pa1.IncomeCutoffHi)
    FROM ctePreAgg1 pa1
    )
    INSERT INTO #TaxBracket WITH (TABLOCK)
    (
    TaxYear
    ,IncomeCutoffLo
    ,IncomeCutoffHi
    ,BracketTaxRate
    --,IncomeBracketRange -- For sanity checks only. Can remove for Prod or leave.
    --,MaxBracketRangeTax -- For sanity checks only. Can remove for Prod or leave.
    --,CumeMaxBracketRangeTax -- For sanity checks only. Can remove for Prod or leave.
    ,PrevCumeMaxBracketRangeTax
    )
    SELECT TaxYear
    ,IncomeCutoffLo
    ,IncomeCutoffHi
    ,BracketTaxRate
    --,IncomeBracketRange -- For sanity checks only. Can remove for Prod or leave.
    --,MaxBracketRangeTax -- For sanity checks only. Can remove for Prod or leave.
    --,CumeMaxBracketRangeTax -- For sanity checks only. Can remove for Prod or leave.
    ,PrevCumeMaxBracketRangeTax = LAG(pa2.CumeMaxBracketRangeTax,1,0.00) OVER (ORDER BY pa2.IncomeCutoffHi)
    FROM ctePreAgg2 pa2
    ;
    --===== Let''s see what we''ve created
    SELECT * FROM #TaxBracket
    ;

    Here's some code to build a 10 million row test table (not to worry... it takes less than 5 seconds on a decent system)

    DROP TABLE #TestData
    ;
    SELECT TOP (10000000)
    ID = IDENTITY(INT,1,1)
    ,TaxYear = 2019
    ,Income = CONVERT(DECIMAL(19,4),RAND(CHECKSUM(NEWID()))*100000)
    INTO #TestData
    FROM sys.all_columns ac1
    CROSS JOIN sys.all_columns ac2
    ;

    ... and here's the code that uses the two new columns.  I frequently get beat by others' follow-up improvements but this code uses 10 times less CPU, more than 3733 times less in Logical Reads, and runs in almost 10 times less Duration than the code that does the accidental Triangular Joins.  It also makes the code a shedload easier to write and read.   It's all due to just precalculating that one extra column.

     SELECT  td.ID,td.Income,TaxDue = (td.Income-tb.IncomeCutoffLo) * tb.BracketTaxRate + tb.PrevCumeMaxBracketRangeTax
    --,* -- For sanity checks only. Can remove for Prod or leave.
    --,(@Income-IncomeCutoffLo) -- For sanity checks only. Can remove for Prod or leave.
    --,(@Income-IncomeCutoffLo) * BracketTaxRate -- For sanity checks only. Can remove for Prod or leave.
    --,PrevCumeMaxBracketRangeTax -- For sanity checks only. Can remove for Prod or leave.
    INTO #Results
    FROM #TaxBracket tb
    JOIN #TestData td
    ON tb.TaxYear = td.TaxYear
    AND td.Income > tb.IncomeCutoffLo
    AND td.Income <= tb.IncomeCutoffHI
    ;

    p.s. I didn't do any rounding because I don't know what the rounding rules are.  They can also vary in the U.S.A. if you round to the nearest dollar, which is allowed.

    • This reply was modified 4 years, 12 months ago by  Jeff Moden. Reason: Double up on the single quotes so that the awful forum software doesn't make it all look like dynamic SQL (and none of it is)

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

Viewing 4 posts - 1 through 3 (of 3 total)

You must be logged in to reply to this topic. Login to reply