The GridView loads all rows. As far as I know it does not optimize anything. And if you do not disable ViewState for the GridView (or the page it is on) all those rows are added to the ViewState, resulting in a HUGE page size of way too much MB's.
The paging in the GridView is nothing more than just showing the rows 100 - 200, while the rest still is loaded.
So I would do something like this (more info). Note that order by is required in this case.
SELECT [column list]
FROM [table]
WHERE [bla]
ORDER BY [Bla]
OFFSET [Offset] ROWS FETCH NEXT [Pagesize] ROWS ONLY
Now, how to implement this for a GridView. First, create a Stored Procedure. You could do it without one, but because you also would need the total number of rows you would need two requests to the database. In this example the data is required by a userID as the WHERE parameter.
CREATE PROCEDURE [dbo].[GridView_Paging]
    @offset int,
    @pagesize int,
    @userID int 
AS
BEGIN
    DECLARE @totalrows INT;
    -- you will need the total amount of rows for paging to work
    SELECT @totalrows = COUNT(itemID) FROM MyTable WHERE userid = @userID;
    --select the data and add the total rows also
    SELECT ID, name, @totalrows AS totalrows
    FROM MyTable
    WHERE userID = @userID 
    ORDER BY ID
    OFFSET @offset ROWS FETCH NEXT @pagesize ROWS ONLY;
END
GO
Then add a GridView to the aspx page and make sure ViewState is off (this saves network traffic). And add a Repeater that will function as the Pager.
<asp:GridView ID="GridView1" runat="server" EnableViewState="false"></asp:GridView>
<asp:Repeater ID="Pager_GridView" runat="server">
    <ItemTemplate>
        <asp:LinkButton ID="LinkButton1" runat="server" OnCommand="Pager_GridView_Command" 
            CommandArgument='<%# Container.ItemIndex %>'><%# Container.ItemIndex + 1 %>
        </asp:LinkButton>
    </ItemTemplate>
</asp:Repeater>
Then a function to call the stored procedure and add the data to the GridView and create the pager. I add the data to a DataTable first instead of directly to the GridView1.DataSource because I need the total number of rows.
public void LoadGridView(int page, int userID)
{
    int pagesize = 10;
    double totalrows = 0;
    int offset = page * pagesize;
    //create a connection
    using (var connection = new SqlConnection(Common.connectionString))
    using (var command = new SqlCommand("GridView_Paging @offset, @pagesize, @userID", connection))
    {
        //add the parameters
        command.Parameters.Add("@offset", SqlDbType.Int).Value = offset;
        command.Parameters.Add("@pagesize", SqlDbType.Int).Value = pagesize;
        command.Parameters.Add("@userID", SqlDbType.Int).Value = userID;
        //open the connection
        connection.Open();
        using (var reader = command.ExecuteReader())
        {
            var dt = new DataTable();
            dt.Load(reader);
            //add the data to the gridview
            GridView1.DataSource = dt;
            GridView1.DataBind();
            //get the total rows from the datatable
            if (dt.Rows.Count > 0)
                totalrows = (int)dt.Rows[0]["totalrows"];
        }
    }
    //calculate the number of pages
    var pages = Math.Ceiling(totalrows / pagesize);
    //create the pager by binding an array of the correct size to the repeater
    Pager_GridView.DataSource = new string[(int)pages];
    Pager_GridView.DataBind();
}
Then add the Repeater LinkButton command
protected void Pager_GridView_Command(object sender, CommandEventArgs e)
{
    LoadGridView(Convert.ToInt32(e.CommandArgument), 12345);
}
And finally Page_Load. Because I've disabled the ViewState, the data has to be bound on every page load and PostBack (which is also the same page load basically). If you would just put LoadGridView(0, 12345); then you would be double-hitting the database. Once in Page_Load and once you click a link in the pager. To prevent this, check if the __EVENTTARGET is from the Repeater
protected void Page_Load(object sender, EventArgs e)
{
    string eventTarget = Request.Form["__EVENTTARGET"];
    //check the eventtarget contains the repeater container to prevent double hitting
    if (string.IsNullOrEmpty(eventTarget) || !eventTarget.Contains(Pager_GridView.UniqueID))
    {
        LoadGridView(0, 12345);
    }
}