Counter that shows 0 when it no longer has true items Laravel - php

I will try to be a little more specific my problem is that when there are 2 articles of the same category and one is active and the other inactive it shows me 0 and tells me the one that is active I want it to show me 0 when there is no active item of that category of rest I do not hope to have explained myself better sorry for the inconvenience
$items = facturacion::select('clients_id', 'Status')
->selectRaw('count(CASE WHEN Status THEN 1 END) as c')
->groupBy('clients_id', 'Status')
->orderBy('clients_id', 'asc')
->orderBy('Status', 'asc')
->get();
$itemsA = [];
foreach ($items as $key => $value) {
$var = $value['c'];
if($value['Status'] == false){
if( 1 <= $var){
$var = '0';
array_push($itemsA, $var);
}
}else{
$var;
array_push($itemsA, $var);
}
}
I leave here an image how it shows me 1 and 0 at the same time when a category has an active and deactivated item
enter image description here

Your query groups by both clients_id and Status. That is why you get two result rows per client_id when the base table contains at least one row with each Status for a given client_id. Moreover, since Status is among your grouping columns, it must be the case that every base table row in each group has the same value of Status. Therefore, your counts will always be either 0 or the number of rows in the group.
It sounds like including Status as a grouping column is simply wrong for your purposes. You don't want to have separate results for separate statuses. Rather, you want a single result for each client_id that reports on (in general) a mix of statuses. And that being the case, it doesn't make sense to ask for a single status for each group, either, as the whole point is that there will not ordinarily be a consensus within the group.
So you probably want something more like this:
$items = facturacion::select('clients_id')
->selectRaw('count(CASE WHEN Status THEN 1 END) as c')
->groupBy('clients_id')
->orderBy('clients_id', 'asc')
->get();
That will give you one result row per client_id, with the c column conveying how many base-table rows with that client_id have Status equal to true.
Also, if your plan is to choose from among the results only those categories that have at least one active row (alternatively: only those with no active rows) then you are making extra work for yourself by leaving that discrimination for the PHP side. The database can easily do that for you. For example, to select only those client_ids for which there is at least one active row, you might do this:
$items = facturacion::select('clients_id')
->groupBy('clients_id')
->orderBy('clients_id', 'asc')
->havingRaw('count(CASE WHEN Status THEN 1 END) > 0')
->get();

Related

It is possible in cursor-based pagination to get the prev and next cursor in the same query

Let's see, I'm making a mess with the cursor pagination, based on an Id in my case ULID, I want to return an array with the results, next_cursor and prev_cursor.
To obtain the NextCursor is very easy, I only have to add one more to the Limit, that is to say, if I have a limit of 10, I request 11 records and if I get 11 records then the NextCursor is the result 11. But for the PrevCursor the only thing I can think of is to do an additional Query to the one I am already doing. Example:
$limit = 10;
$result = 'SELECT * FROM Table WHERE id <= $cursor ORDER BY id DESC LIMIT $limit+1'
$results = array_slice($result, 0, $limit);
$nextCursor = array_slice($result, $limit, 1);
And now to get the Prev Cursor, I do as I said before an additional query
$prevCursor = 'SELECT * FROM Table WHERE id > $cursor ORDER BY id ASC LIMIT 1'
That way my API can return the following array to the frontend
return [
'data' => $results,
'next_cursor' => $nextCursor,
'prev_cursor' => $prevCursor
];
Now I rephrase the same question again, is there any way to do this without having to do additional Mysql query to get the Prev Cursor, I mean in a same Query or in some other way, I don't know, it's the first time I do this, and I'm a bit lost.
Thanks very much!
Indexing column allows you to quickly find specific row by its ULID and scan nearest rows forward and backward, but obviously, scanning forward and backward internally are two different operations, so if you insist on having the result of the both, you are doomed to perform two different operations.
There's some SQL syntactic sugar to help you hide those two internally executed operations inside one query, but first let me clarify around your objective a little.
What you are trying to build here is actually a window, not a page.
Pagination is when you have an ordered list of rows split over in pages of some size and user references an index of a page which she wants to browse. E.g. page #0, page #1, ... etc. Last page might have less items than a page size, and also if the total number of rows is less than a page, then first page and last page would be the same page and it's OK.
LIMIT and OFFSET operators are here to support that use case. A link to previous page is simply min(0,current_page-1) and a link to next page is min(max_pages,current_page+1).
On the opposite side, windowing is when you have an ordered list of rows, and when user references some specific row by its ULID, you fetch him a few rows behind and/or after queried row. It's like grep -C 10 in bash.
You can emulate window using sub-selects and UNION.
$limit = 10;
// Fetch a limit+1 of results after and including id AND
// a limit of results before id
$result = 'SELECT * FROM (
(
SELECT * from Table
WHERE id >= $cursor ORDER BY id ASC LIMIT $limit+1
) UNION (
SELECT * from Table
WHERE id < $cursor ORDER BY id DESC LIMIT $limit
)
) TableAlias ORDER by id;'
// as we actually fetching some rows before cursor
// we should find its position in the result set ...
$cursor_index = array_search($cursor, array_column($result, 'id'));
// ... and throw away the rest rows
$results = array_slice($result, $cursor_index, $limit);
// here cursors are always first and last items of the result set
$prevCursor = array_slice($result, 0, 1)['id'];
$nextCursor = array_slice($result, -1, 1)['id'];
return [
'data' => $results,
'prev_cursor' => $prevCursor,
'next_cursor' => $nextCursor
];
Since MySQL 8.0 you have a whole set of windowing functions, for your case, LEAD() and LAG() can help you move away all cursor calculations and slicing to your MySQL server.
$limit = 10;
// Wrap same query as above into sub-select, because LAG/LEAD work after WHERE, so we still need UNION to fetch previous cursor
$result = '
WITH
tab1 AS (SELECT * FROM Table where id >= $cursor ORDER BY id ASC LIMIT $limit+1),
tab2 AS (SELECT * FROM Table WHERE id < $cursor ORDER BY id DESC LIMIT $limit),
tab3 AS (SELECT * FROM tab1
UNION ALL
SELECT * FROM tab2 ORDER BY id),
tab4 AS (SELECT
*,
LAG(id, 4) OVER (order by id) as prevcursor,
LEAD(id, 4) OVER (order by id) as nextcursor
FROM tab3)
SELECT * FROM tab4
WHERE id >= $cursor LIMIT $limit'
// here calculated cursor ids are always on first row of result set
$prevCursor = $result[0]['prevcursor'];
$nextCursor = $result[0]['nextcursor'];
// (optionally) strip unwanted columns from result
$results = array_map(function ($a) { return array_diff_key($a, array_flip(array('prevcursor', 'nextcursor'))); }, $result);
return [
'data' => $results,
'prev_cursor' => $prevCursor,
'next_cursor' => $nextCursor
];
That should work well if you don't ever delete rows.
Now, consider following. Both pagination and windowing do not work well with unstable lists.
E.g. if new rows are sequentially adding to the end of the set, then the last page is constantly moving forward. So when one user is opening 'last' page and sees, say, three items there, another user might add another bunch of items and his view of what 'last' page would be different.
What's worse is that if your table usage allows deleting rows, the whole set of pages after and including the page where deleted row was is now rearranged. This leads to very nasty user experience when user is clicking 'next' page and accidentally skips some items, or is clicking 'previous' and sees some of the items he has already seen before.
To overcome those deficiencies you might want to redesign your API such that querying 'previous' page and 'next' page be semantically clearly different from querying 'current' page.
That is, you would need three API endpoints:
query a row by ULID (and a set of up to N rows after it) - initial user entry point from, e.g. search or catalog tree.
query a set of N rows before specific ULID. You pass ULID of the first row in the window user is currently looking at. If there are no rows before given one, result set is empty, you might either display a notification message to user or silently redirect them to first endpoint.
query a set of N rows after specific ULID. You pass ULID of the last row in the window user is currently looking at. If there are no rows after given one, result set is empty, you might either display a notification message to user or silently redirect them back.
If you design your API that way, you would have following benefits:
all three API implemented by only simple ORDER and LIMIT
no need in second query neither explicit nor implicit
your next/previous window results would not ever have any misses or duplicates comparing to previously seen windows.
The only drawback here is that original row user is referring to can be deleted as well. To overcome this, you might want to add a boolean deleted flag to your table schema and set it to false instead of actual row deletion.
After reading #shomeax 's comment and thinking a little more I can suggest to encode cursor in base-64 and make it to contain prev cursor additionally. For example:
[$prevCursor, $curCursor] = explode(':', base64_decode($request['cursor']));
$limit = 10;
$prevPlusCurPagesLimit = $limit * 2;
$ulids = 'SELECT ulid FROM Table WHERE ulid <= $prevCursor ORDER BY ulid DESC LIMIT $prevPlusCurPagesLimit+1';
$resultUlids = array_slice($ulids, $limit, $limit);
$nextCursor = array_slice($ulids, $prevPlusCurPagesLimit, 1);
$prevPrevCursor = reset($ulids);
$response = [
'data' => $resultUlids,
'prevCursor' => base64_encode("$curCursor:$nextCursor"),
'nextCursor' => base64_encode("$prevPrevCursor:$prevCursor"),
];
I didn't try such approach myself but it is partially based on this article https://slack.engineering/evolving-api-pagination-at-slack/ and looks like working
You don’t need to request more, only use < and > rather than <= and >=.
Then you can use the last id in $results for next and the first id for previous.
Assuming that
the "cursor" is the ID from which the next portion starts
and IDs are sequential and without gaps,
and limit is not changed from one query to another
you could get the prev cursor by substracting/adding (depending on sorting) $limit from/to current cursor: $prevCursor = $results[0]['id'] - $limit
And if the ID column has gaps, I suppose there is no reasonable way to implement ability to get prev cursor without additional query. You could only turn it into sub-query or use UNION, but this does not make a big difference.
Consider this...
You fetch the 5 rows for the current page. Then the "previous" page ends before the first id on this page and the "next" page starts after the last id on the current page.
Example: The current page contains ids 65, 67, 71, 82, 91. This finds the 5 rows for the previous page:
SELECT ...
WHERE id < 65
ORDER BY id DESC
LIMIT 5;
(They will be in reverse order, but that is easy to fix.) For the "next" page (in proper order):
SELECT ...
WHERE id > 91
ORDER BY id ASC
LIMIT 5;
As another tip: fetching an extra row (6 instead of 5), lets you cheaply discover whether you are at the "end", thereby being able to suppress the [Next] or [Prev] button.
More: Pagination
Granted, this technique does not deliver the 5 rows for the next/previous page, but do you really need that? Since the Selects should be quite efficient, I don't necessarily see a drawback of doing more than one Select or combining selects with UNION.
I am going to delete my previous answer, despite its upvote, because it is clearly the wrong approach.
Update
Given that you are retrieving on a column, id, that is presumably unique and indexed, then when a "page" of N rows is returned (where N is 10), you need to pass up either the id of the first row (the one with the greater value since we are sorting by descending id) or the last row as query parameter lastId along with a direction flag parameter directionFlag that is either F for forward or B for backward to give the direction of "paging." It should then be possible to directly seek to the correct rows as follows (I am assuming that we are using PDO for MySql Access):
define(PAGE_SIZE, 10);
$limit = PAGE_SIZE;
// lastId parameter specified? It will not be present on the initial request:
if (isset($_REQUEST('lastId')) {
$lastId = $_REQUEST['lastId'];
$direction = $_REQUEST['direction']; 'F'orward or 'B'ackward
if ($direction == 'F') {
$sql = "SELECT * FROM Table ORDER BY id DESC where id < :id LIMIT $limit";
}
else {
// paging backward:
$sql = "SELECT * from (SELECT * FROM Table ORDER BY id ASC where id > :id LIMIT $limit) sq ORDER BY id DESC";
}
$params = [':id' => $lastId];
}
else {
// this is the initial request:
$sql = "SELECT * FROM Table ORDER BY id DESC LIMIT $limit";
$params = [];
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// The id from $rows[0] will be passed back as lastId with direction flag 'B' for paging backward
// And the id from $rows[PAGE_SIZE-1] will be passed back as lastId with direction flag 'F' for paging forward

Symfony 2 Multiple Selects with counts on the same table?

Ok I have a flag field on one table, open or closed which is boolean. I am trying to build one query that would take that field and count them based on that flag. Then I will need to group them by account ID
Here is what I am working with now,
$GetTest1 = $GetRepo->createQueryBuilder('s') <- I had 'w' in here but all that did was add an index and not a second alias?
->select(' (count(s.open_close)) AS ClosedCount, (count(w.open_close)) AS OpenCount ')
->where('s.open_close = ?1')
//->andWhere('w.open_close = ?2')
->groupBy('s.AccountID')
->setParameter('1', true)
//->setParameter('2', false)
->getQuery();
Is what I want do-able? I know (or at lest think) that I can build a query with multiple table alias? - Please correct me if I am wrong.
All help most welcome.
Thanks
This DQL query will group the rows in table by accountId and for each of them it will give you count for yes (and you can get count for no by substracting that from total).
BTW I found writing straight DQL queries much more straightforward than writing QueryBuilder queries (which i use only when i need to dynamically construct the query)
$results = $this->get("doctrine")->getManager()
->createQuery("
SELECT t.accountId, SUM(t.openClose) as count_yes, COUNT(t.accountId) as total
FROM AppBundle:Table t
GROUP BY t.accountId
")
->getResult();
foreach ($results as $result) {
//echo print_r($result);
//you can get count_no as $result["total"] - $result["count_yes"];
}

Codeigniter Join Table to Showing Different Content Results

I have 7 tables to store user data such posts, images, updates, comments, likes, reposts and user itself.
And here is my questions: How to using right query to execute join table?
I'm using this query:
if ( ! function_exists('getTimeline'))
{
function getTimelines($contributor = null, $limit = 10, $offset = 0)
{
$CI =& get_instance();
$CI->db->select('
abycms_posts.*,
abycms_images.imageID,
abycms_images.contributor as owner,
abycms_images.imageContent,
abycms_images.imageFile,
abycms_images.imageSlug,
abycms_images.timestamp as date,
abycms_images.visits_count as visits,
abycms_updates.updateID,
abycms_updates.userID as updater,
abycms_updates.updateContent,
abycms_updates.visibility,
abycms_updates.timestamp as update_time,
abycms_likes.likeID,
abycms_likes.userID as userLike,
abycms_likes.type as likeType,
abycms_likes.timestamp as like_time,
abycms_comments.commentID,
abycms_comments.userID as commentUser,
abycms_comments.type as commentType,
abycms_comments.timestamp as comment_time,
abycms_reposts.repostID,
abycms_reposts.userID as repostUser,
abycms_reposts.type as repostType,
abycms_reposts.timestamp as repost_time
');
$CI->db->from('abycms_users');
$CI->db->join('abycms_posts', 'abycms_posts.contributor = abycms_users.userID', 'left');
$CI->db->join('abycms_images', 'abycms_images.contributor = abycms_users.userID', 'left');
$CI->db->join('abycms_updates', 'abycms_updates.userID = abycms_users.userID', 'left');
$CI->db->join('abycms_likes', 'abycms_likes.userID = abycms_users.userID', 'left');
$CI->db->join('abycms_comments', 'abycms_comments.userID = abycms_users.userID', 'left');
$CI->db->join('abycms_reposts', 'abycms_reposts.userID = abycms_users.userID', 'left');
$CI->db->where('abycms_users.userID', $contributor);
$CI->db->limit($limit, $offset);
// How to order results by newest `timestamp` for posts, images, updates, comments, likes or reposts?
$CI->db->order_by('abycms_posts.timestamp', 'desc');
// How to handle not duplicate `postID` or `imageID` also group it by different `type`s?
$CI->db->group_by('abycms_posts.postID, abycms_images.imageID');
$query = $CI->db->get();
if($query->num_rows() > 0)
{
return $query->result_array();
}
else
{
return array();
}
}
}
And there is my view to handle results in different type:
foreach(getTimelines($page['userID'], $limit, $offset) as $row)
{
if($row['updateID'] != null) // Updating Status
{
// This status updates
}
elseif($row['postID'] != null) // Writing Article
{
// This is posts
}
elseif($row['imageID'] != null) // Uploading Image
{
// This is images
}
elseif($row['commentID'] != null) // Commented on Post
{
// This is comments
}
elseif($row['likeID'] != null) // Liking User Post
{
// This is likes
}
elseif($row['repostID'] != null) // Reposting User Post
{
// This is reposts
}
}
When i'm using above query, results is showing up but i have no idea to separate content types. It always shown as status updates, and all unique id such postID, imageID, updateID, repostID, likeID and commentID have same value.
The query is generating a partial cross product.
For every row returned from _users, MySQL is getting all of the matching rows from _likes.
For sake of an example, we'll assume that there is one row being returned from _users, and there are four matching rows in _likes, returning (so far) a total of four rows. The row from _users gets matched to each of the four rows from _likes. All of the columns from the row from _users is duplicated into each of the four rows.
And from the _posts table, for the sake of an example, we'll assume that there are two rows that match. So each of those two rows returned from _posts is going to matched to each of the four rows we already have, giving us a total of eight rows. (Every row returned from _posts is matched with every row returned from _likes.)
From the _comments table, for this example, let's say there are six rows returned. Each of those rows gets matched with the eight rows we already have, giving us a total of 48 rows. And a lot of values from the columns of each table is getting "duplicated" into new rows, as multiple rows from the new tables are joined in.
And so on, with each additional joined table.
It's a partial "cross product" of the tables. (A semi-Cartesian product?)
If you want to return distinct list of _posts, a distinct list of _likes, and distinct list of _comments, etc. then you could run a separate query for each table. That would avoid the "duplication" that happens due to the join operation. That's probably the simplest approach.
Otherwise, if you want to get a distinct list of _posts, _likes, _comments, et al. out of the resultset the current query is returning, you'd need the client to sift through the rows to filter out the duplicated _posts, _likes, _comments. You'd need to have a unique identifier for each of those tables included in the returned rows.
Basically, your code would need to build separate arrays for _posts, _likes, _comments. For each row from the resultset, you'd need to check whether the values from the _posts columns were from a row from _posts you've already processed. If it's one you've already processed, discard it, otherwise, add it to the array. Essentially de-duplicating the rows down into separate results from each table, in the form that you'd get from a separate query of each table.

Laravel - Merge two Queries?

I am building a Twitter-like application which has a feed in it. In this feed I need to display shares with this properties:
-Shares from user, who I am following
-Shares from user, which are sorted by the positive rating, but only the top10%
These two queries I need somehow to merge, so it will become an array in the end, which has all the shares which are applying to this criteria, without any duplicates and ordered by ID, desc
My tables are looking like this:
User, Shares, Follows
Shares:
-user_id
-positive
Follows:
-follower_id
-user_id
-level
What I already tried:
$follower_shares = Share::join('follows', 'shares.user_id', '=', 'follows.user_id')
->where('follows.follower_id', Auth::user()->id)
->where('follows.level', 1)
->get(array('shares.*'));
//count = 10% of total shares
$count = Share::count()/10;
$count = round($count);
$top10_shares = Share::orderBy('positive', 'DESC')
->take($count)
->get();
//sorts by id - descending
$top10_shares = $top10_shares->sortBy(function($top)
{
return -($top->id);
});
//merges shares
$shares = $top10_shares->merge($follower_shares);
The problem is now, that I was told that there is a better way to solve this.
Also, $shares is giving me the result which applies to the criteria, but the shares have duplicates (rows, which are applying to both criteria) and arent ordered by id desc in total.
I would be very happy, if you could tell me, how to do this the right way.
Thanks very much!
I found this to be a pretty clean solution:
// Instead of getting the count, we get the lowest rating we want to gather
$lowestRating = Share::orderBy('positive', 'DESC')
->skip(floor(Share::count() * 0.1))
->take(1)->lists('positive');
// Also get all followed ids
$desiredFollow = Auth::user()->follows()->lists('user_id');
// Select where followed or rating higher than minimal
// This lets the database take care of making them distinct
$shares = Share::whereIn('user_id', $desiredFollow)
->orWhere('positive', '>=', $lowestRating[0])
->get();

query to count MySQL rows

Trying to get a count (to later multiply with a rate) of names in upto (if data is present in one or more) four rows. In other words, there is a price per person occupying a room. So, I want to count the number of persons in the room by names entered on the form and saved in the database table.
I could simply add a field where the user also selects the number of people in addition to completeing the name fields but this seems redundant (and prone to error).
Setup
Table: 1.clients which has columns:
id,
tourbk_id,
tourstart,
roomtype1,
client1_name,
client2_name,
client3_name,
client4_name
Question
I have a query which currently checks the roomtype to the per person price for that room type and is working to produce the result but, of course, it is only returning (for two people in a room) the price person for double occupancy.
E.g.: (per person prices)... single = $10; double = $20; triple = $30; quad = $40
My current result for double room is $20 (which echo's next to "Price per person". I need a query to count the total persons in this double and multiple times the rate ... "Total: query[$20 * 2]"
How do I code a query to count the "client_name" entries in a table?
Here I've added a virtual column named ClientCount which is the count of clients in the room. I like to use client1_name > '' because it works whether you use blanks or NULLs, and saves me from asking a question.
SELECT
id,
tourbk_id,
tourstart,
roomtype1,
client1_name,
client2_name,
client3_name,
client4_name,
CASE WHEN client1_name > '' THEN 1 ELSE 0 end +
CASE WHEN client2_name > '' THEN 1 ELSE 0 end +
CASE WHEN client3_name > '' THEN 1 ELSE 0 end +
CASE WHEN client4_name > '' THEN 1 ELSE 0 end ClientCount
FROM TBL
You ought to consider normalising your schema by having a separate client_names table that relates a single name column to a single booking identifier: multiple clients would then be represented by multiple records in that new table. You would count clients with:
SELECT COUNT(*) FROM client_names WHERE booking_id = ...
However, with your current structure (and assuming that the name columns are NULL if there is no such client), you could do:
SELECT (client1_name IS NOT NULL)
+ (client2_name IS NOT NULL)
+ (client3_name IS NOT NULL)
+ (client4_name IS NOT NULL)
AS client_count
FROM clients
-- etc.
and, as a round-about way of doing it I thought of the following which also works (will just need to make a case for the 1, 2, 3, and 4 ["if roomtype1='sgl', then $mult=1, if roomtype='dbl', then $mult=2...] multiplyer to check for the other room types):
$total = ($roomrate['roomprice'] * 2);
echo "Total this booking : ";
echo $total;
thanks to all for the help!

Categories